The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

398 lines
13KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2013 - Raw Material Software Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. /* Note: there's a bit of light obfuscation in this code, just to make things
  18. a bit more annoying for crackers who try to reverse-engineer your binaries, but
  19. nothing particularly foolproof.
  20. */
  21. struct KeyFileUtils
  22. {
  23. static String encryptXML (const XmlElement& xml, RSAKey privateKey)
  24. {
  25. MemoryOutputStream text;
  26. text << xml.createDocument (String::empty, true);
  27. BigInteger val;
  28. val.loadFromMemoryBlock (text.getMemoryBlock());
  29. privateKey.applyToValue (val);
  30. return val.toString (16);
  31. }
  32. static String createKeyFile (String comment,
  33. const XmlElement& xml,
  34. RSAKey rsaPrivateKey)
  35. {
  36. String asHex ("#" + encryptXML (xml, rsaPrivateKey));
  37. StringArray lines;
  38. lines.add (comment);
  39. lines.add (String::empty);
  40. const int charsPerLine = 70;
  41. while (asHex.length() > 0)
  42. {
  43. lines.add (asHex.substring (0, charsPerLine));
  44. asHex = asHex.substring (charsPerLine);
  45. }
  46. lines.add (String::empty);
  47. return lines.joinIntoString ("\r\n");
  48. }
  49. //==============================================================================
  50. static XmlElement decryptXML (String hexData, RSAKey rsaPublicKey)
  51. {
  52. BigInteger val;
  53. val.parseString (hexData, 16);
  54. RSAKey key (rsaPublicKey);
  55. key.applyToValue (val);
  56. ScopedPointer<XmlElement> xml (XmlDocument::parse (val.toMemoryBlock().toString()));
  57. return xml != nullptr ? *xml : XmlElement("key");
  58. }
  59. static XmlElement getXmlFromKeyFile (String keyFileText, RSAKey rsaPublicKey)
  60. {
  61. return decryptXML (keyFileText.fromLastOccurrenceOf ("#", false, false).trim(), rsaPublicKey);
  62. }
  63. static StringArray getMachineNumbers (XmlElement xml)
  64. {
  65. StringArray numbers;
  66. numbers.addTokens (xml.getStringAttribute ("mach"), ",; ", String::empty);
  67. numbers.trim();
  68. numbers.removeEmptyStrings();
  69. return numbers;
  70. }
  71. static String getLicensee (const XmlElement& xml) { return xml.getStringAttribute ("user"); }
  72. static String getEmail (const XmlElement& xml) { return xml.getStringAttribute ("email"); }
  73. static String getAppID (const XmlElement& xml) { return xml.getStringAttribute ("app"); }
  74. struct KeyFileData
  75. {
  76. String licensee, email, appID;
  77. StringArray machineNumbers;
  78. };
  79. static KeyFileData getDataFromKeyFile (XmlElement xml)
  80. {
  81. KeyFileData data;
  82. data.licensee = getLicensee (xml);
  83. data.email = getEmail (xml);
  84. data.appID = getAppID (xml);
  85. data.machineNumbers.addArray (getMachineNumbers (xml));
  86. return data;
  87. }
  88. };
  89. //==============================================================================
  90. const char* TracktionMarketplaceStatus::unlockedProp = "u";
  91. static const char* stateTagName = "REG";
  92. static const char* userNameProp = "user";
  93. static const char* passwordProp = "pw";
  94. static var machineNumberAllowed (StringArray numbersFromKeyFile,
  95. StringArray localMachineNumbers)
  96. {
  97. var result;
  98. for (int i = 0; i < localMachineNumbers.size(); ++i)
  99. {
  100. String localNumber (localMachineNumbers[i].trim());
  101. if (localNumber.isNotEmpty())
  102. {
  103. for (int j = numbersFromKeyFile.size(); --j >= 0;)
  104. {
  105. var ok (localNumber.trim().equalsIgnoreCase (numbersFromKeyFile[j].trim()));
  106. result.swapWith (ok);
  107. if (result)
  108. break;
  109. }
  110. }
  111. }
  112. return result;
  113. }
  114. //==============================================================================
  115. TracktionMarketplaceStatus::TracktionMarketplaceStatus() : status (stateTagName)
  116. {
  117. }
  118. TracktionMarketplaceStatus::~TracktionMarketplaceStatus()
  119. {
  120. }
  121. void TracktionMarketplaceStatus::load()
  122. {
  123. MemoryBlock mb;
  124. mb.fromBase64Encoding (getState());
  125. if (mb.getSize() > 0)
  126. status = ValueTree::readFromGZIPData (mb.getData(), mb.getSize());
  127. else
  128. status = ValueTree (stateTagName);
  129. if (machineNumberAllowed (StringArray ("1234"), getLocalMachineIDs()))
  130. status.removeProperty (unlockedProp, nullptr);
  131. }
  132. void TracktionMarketplaceStatus::save()
  133. {
  134. MemoryOutputStream mo;
  135. {
  136. GZIPCompressorOutputStream gzipStream (&mo, 9);
  137. status.writeToStream (gzipStream);
  138. }
  139. saveState (mo.getMemoryBlock().toBase64Encoding());
  140. }
  141. static String getEncodedIDString (const String& input)
  142. {
  143. static const char* const platform =
  144. #if JUCE_MAC
  145. "M";
  146. #elif JUCE_WINDOWS
  147. "W";
  148. #elif JUCE_LINUX
  149. "L";
  150. #elif JUCE_IOS
  151. "I";
  152. #elif JUCE_ANDROID
  153. "A";
  154. #endif
  155. return platform + MD5 ((input + "salt_1" + platform).toUTF8())
  156. .toHexString().substring (0, 9).toUpperCase();
  157. }
  158. StringArray TracktionMarketplaceStatus::getLocalMachineIDs()
  159. {
  160. StringArray nums;
  161. // First choice for an ID number is a filesystem ID for the user's home
  162. // folder or windows directory.
  163. #if JUCE_WINDOWS
  164. uint64 num = File::getSpecialLocation (File::windowsSystemDirectory).getFileIdentifier();
  165. #else
  166. uint64 num = File ("~").getFileIdentifier();
  167. #endif
  168. if (num != 0)
  169. {
  170. nums.add (getEncodedIDString (String::toHexString ((int64) num)));
  171. return nums;
  172. }
  173. // ..if that fails, use the MAC addresses..
  174. Array<MACAddress> addresses;
  175. MACAddress::findAllAddresses (addresses);
  176. for (int i = 0; i < addresses.size(); ++i)
  177. nums.add (getEncodedIDString (addresses[i].toString()));
  178. jassert (nums.size() > 0); // failed to create any IDs!
  179. return nums;
  180. }
  181. URL TracktionMarketplaceStatus::getServerAuthenticationURL()
  182. {
  183. return URL ("https://www.tracktion.com/marketplace/authenticate.php");
  184. }
  185. String TracktionMarketplaceStatus::getWebsiteName()
  186. {
  187. return "tracktion.com";
  188. }
  189. void TracktionMarketplaceStatus::setUserEmail (const String& usernameOrEmail)
  190. {
  191. status.setProperty (userNameProp, usernameOrEmail, nullptr);
  192. }
  193. String TracktionMarketplaceStatus::getUserEmail() const
  194. {
  195. return status[userNameProp].toString();
  196. }
  197. bool TracktionMarketplaceStatus::applyKeyFile (String keyFileContent)
  198. {
  199. KeyFileUtils::KeyFileData data;
  200. data = KeyFileUtils::getDataFromKeyFile (KeyFileUtils::getXmlFromKeyFile (keyFileContent, getPublicKey()));
  201. if (data.licensee.isNotEmpty() && data.email.isNotEmpty() && data.appID == getMarketplaceProductID())
  202. {
  203. setUserEmail (data.email);
  204. if (! isUnlocked())
  205. {
  206. var actualResult (0), dummyResult (1.0);
  207. var v (machineNumberAllowed (data.machineNumbers, getLocalMachineIDs()));
  208. actualResult.swapWith (v);
  209. v = machineNumberAllowed (StringArray ("01"), getLocalMachineIDs());
  210. dummyResult.swapWith (v);
  211. jassert (! dummyResult);
  212. if ((! dummyResult) && actualResult)
  213. status.setProperty (unlockedProp, actualResult, nullptr);
  214. }
  215. return true;
  216. }
  217. return false;
  218. }
  219. static bool canConnectToWebsite (const URL& url)
  220. {
  221. ScopedPointer<InputStream> in (url.createInputStream (false, nullptr, nullptr, String(), 2000, nullptr));
  222. return in != nullptr;
  223. }
  224. static bool areMajorWebsitesAvailable()
  225. {
  226. const char* urlsToTry[] = { "http://google.com", "http://bing.com", "http://amazon.com", nullptr};
  227. for (const char** url = urlsToTry; *url != nullptr; ++url)
  228. if (canConnectToWebsite (URL (*url)))
  229. return true;
  230. return false;
  231. }
  232. TracktionMarketplaceStatus::UnlockResult TracktionMarketplaceStatus::handleXmlReply (XmlElement xml)
  233. {
  234. UnlockResult r;
  235. if (const XmlElement* keyNode = xml.getChildByName ("KEY"))
  236. {
  237. const String keyText (keyNode->getAllSubText().trim());
  238. r.succeeded = keyText.length() > 10 && applyKeyFile (keyText);
  239. }
  240. if (xml.hasTagName ("MESSAGE"))
  241. r.informativeMessage = xml.getStringAttribute ("message").trim();
  242. if (xml.hasTagName ("ERROR"))
  243. r.errorMessage = xml.getStringAttribute ("error").trim();
  244. if (xml.getStringAttribute ("url").isNotEmpty())
  245. r.urlToLaunch = xml.getStringAttribute ("url").trim();
  246. if (r.errorMessage.isEmpty() && r.informativeMessage.isEmpty() && r.urlToLaunch.isEmpty() && ! r.succeeded)
  247. r.errorMessage = TRANS ("Unexpected or corrupted reply from XYZ").replace ("XYZ", getWebsiteName()) + "...\n\n"
  248. + TRANS("Please try again in a few minutes, and contact us for support if this message appears again.");
  249. return r;
  250. }
  251. TracktionMarketplaceStatus::UnlockResult TracktionMarketplaceStatus::handleFailedConnection()
  252. {
  253. UnlockResult r;
  254. r.errorMessage = TRANS("Couldn't connect to XYZ").replace ("XYZ", getWebsiteName()) + "...\n\n";
  255. if (areMajorWebsitesAvailable())
  256. r.errorMessage << TRANS("Your internet connection seems to be OK, but our webserver "
  257. "didn't respond... This is most likely a temporary problem, so try "
  258. "again in a few minutes, but if it persists, please contact us for support!");
  259. else
  260. r.errorMessage << TRANS("No internet sites seem to be accessible from your computer.. Before trying again, "
  261. "please check that your network is working correctly, and make sure "
  262. "that any firewall/security software installed on your machine isn't "
  263. "blocking your web connection.");
  264. return r;
  265. }
  266. TracktionMarketplaceStatus::UnlockResult TracktionMarketplaceStatus::attemptWebserverUnlock (const String& email,
  267. const String& password)
  268. {
  269. // This method will block while it contacts the server, so you must run it on a background thread!
  270. jassert (! MessageManager::getInstance()->isThisTheMessageThread());
  271. URL url (getServerAuthenticationURL()
  272. .withParameter ("product", getMarketplaceProductID())
  273. .withParameter ("email", email)
  274. .withParameter ("pw", password)
  275. .withParameter ("os", SystemStats::getOperatingSystemName())
  276. .withParameter ("mach", getLocalMachineIDs()[0]));
  277. DBG ("Trying to unlock via URL: " << url.toString (true));
  278. const String reply (url.readEntireTextStream());
  279. DBG ("Reply from server: " << reply);
  280. ScopedPointer<XmlElement> xml (XmlDocument::parse (reply));
  281. if (xml != nullptr)
  282. return handleXmlReply (*xml);
  283. return handleFailedConnection();
  284. }
  285. //==============================================================================
  286. String TracktionMarketplaceKeyGeneration::generateKeyFile (const String& appName,
  287. const String& userEmail,
  288. const String& userName,
  289. const String& machineNumbers,
  290. const RSAKey& privateKey)
  291. {
  292. XmlElement xml ("key");
  293. xml.setAttribute ("user", userName);
  294. xml.setAttribute ("email", userEmail);
  295. xml.setAttribute ("mach", machineNumbers);
  296. xml.setAttribute ("app", appName);
  297. xml.setAttribute ("date", String::toHexString (Time::getCurrentTime().toMilliseconds()));
  298. String comment;
  299. comment << "Keyfile for " << appName << newLine;
  300. if (userName.isNotEmpty())
  301. comment << "User: " << userName << newLine;
  302. comment << "Email: " << userEmail << newLine
  303. << "Machine numbers: " << machineNumbers << newLine
  304. << "Created: " << Time::getCurrentTime().toString (true, true);
  305. return KeyFileUtils::createKeyFile (comment, xml, privateKey);
  306. }