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.

CvalChecker.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.tools;
  17. import static java.lang.Integer.parseInt;
  18. import java.io.FileNotFoundException;
  19. import java.io.IOException;
  20. import java.net.MalformedURLException;
  21. import java.net.URL;
  22. import java.net.URLConnection;
  23. import java.text.MessageFormat;
  24. import java.util.Arrays;
  25. import java.util.Date;
  26. import java.util.List;
  27. import java.util.Locale;
  28. import java.util.ResourceBundle;
  29. import java.util.prefs.Preferences;
  30. import elemental.json.JsonException;
  31. import elemental.json.JsonObject;
  32. import elemental.json.impl.JsonUtil;
  33. import org.apache.commons.io.IOUtils;
  34. /**
  35. * This class is able to validate the vaadin CVAL license.
  36. *
  37. * It reads the developer license file and asks the server to validate the
  38. * licenseKey. If the license is invalid it throws an exception with the
  39. * information about the problem and the server response.
  40. *
  41. * @since 7.3
  42. */
  43. public final class CvalChecker {
  44. /*
  45. * Class used for binding the JSON gotten from server.
  46. *
  47. * It is not in a separate f le, so as it is easier to copy into any product
  48. * which does not depend on vaadin core.
  49. *
  50. * We are using org.json in order not to use additional dependency like
  51. * auto-beans, gson, etc.
  52. */
  53. public static class CvalInfo {
  54. public static class Product {
  55. private JsonObject o;
  56. public Product(JsonObject o) {
  57. this.o = o;
  58. }
  59. public String getName() {
  60. return get(o, "name", String.class);
  61. }
  62. public Integer getVersion() {
  63. return get(o, "version", Integer.class);
  64. }
  65. }
  66. @SuppressWarnings("unchecked")
  67. private static <T> T get(JsonObject o, String k, Class<T> clz) {
  68. Object ret = null;
  69. try {
  70. if (clz == String.class) {
  71. ret = o.getString(k);
  72. } else if (clz == JsonObject.class) {
  73. ret = o.getObject(k);
  74. } else if (clz == Integer.class) {
  75. ret = Integer.valueOf((int) o.getNumber(k));
  76. } else if (clz == Date.class) {
  77. ret = new Date((long) o.getNumber(k));
  78. } else if (clz == Boolean.class) {
  79. ret = o.getBoolean(k);
  80. }
  81. } catch (JsonException e) {
  82. }
  83. return (T) ret;
  84. }
  85. private JsonObject o;
  86. private Product product;
  87. public CvalInfo(JsonObject o) {
  88. this.o = o;
  89. product = new Product(get(o, "product", JsonObject.class));
  90. }
  91. public Boolean getExpired() {
  92. return get(o, "expired", Boolean.class);
  93. }
  94. public Date getExpiredEpoch() {
  95. return get(o, "expiredEpoch", Date.class);
  96. }
  97. public String getLicensee() {
  98. return get(o, "licensee", String.class);
  99. }
  100. public String getLicenseKey() {
  101. return get(o, "licenseKey", String.class);
  102. }
  103. public String getMessage() {
  104. return get(o, "message", String.class);
  105. }
  106. public Product getProduct() {
  107. return product;
  108. }
  109. public String getType() {
  110. return get(o, "type", String.class);
  111. }
  112. public void setExpiredEpoch(Date expiredEpoch) {
  113. o.put("expiredEpoch", expiredEpoch.getTime());
  114. }
  115. public void setMessage(String msg) {
  116. o.put("message", msg);
  117. }
  118. @Override
  119. public String toString() {
  120. return o.toString();
  121. }
  122. public boolean isLicenseExpired() {
  123. return (getExpired() != null && getExpired())
  124. || (getExpiredEpoch() != null && getExpiredEpoch().before(
  125. new Date()));
  126. }
  127. public boolean isValidVersion(int majorVersion) {
  128. return getProduct().getVersion() == null
  129. || getProduct().getVersion() >= majorVersion;
  130. }
  131. private boolean isValidInfo(String name, String key) {
  132. return getProduct() != null && getProduct().getName() != null
  133. && getLicenseKey() != null
  134. && getProduct().getName().equals(name)
  135. && getLicenseKey().equals(key);
  136. }
  137. }
  138. /*
  139. * The class with the method for getting json from server side. It is here
  140. * and protected just for replacing it in tests.
  141. */
  142. public static class CvalServer {
  143. protected String licenseUrl = LICENSE_URL_PROD;
  144. String askServer(String productName, String productKey, int timeoutMs)
  145. throws IOException {
  146. String url = licenseUrl + productKey;
  147. URLConnection con;
  148. try {
  149. // Send some additional info in the User-Agent string.
  150. String ua = "Cval " + productName + " " + productKey + " "
  151. + getFirstLaunch();
  152. for (String prop : Arrays.asList("java.vendor.url",
  153. "java.version", "os.name", "os.version", "os.arch")) {
  154. ua += " " + System.getProperty(prop, "-").replace(" ", "_");
  155. }
  156. con = new URL(url).openConnection();
  157. con.setRequestProperty("User-Agent", ua);
  158. con.setConnectTimeout(timeoutMs);
  159. con.setReadTimeout(timeoutMs);
  160. String r = IOUtils.toString(con.getInputStream());
  161. return r;
  162. } catch (MalformedURLException e) {
  163. e.printStackTrace();
  164. return null;
  165. }
  166. }
  167. /*
  168. * Get the GWT firstLaunch timestamp.
  169. */
  170. String getFirstLaunch() {
  171. try {
  172. Class<?> clz = Class
  173. .forName("com.google.gwt.dev.shell.CheckForUpdates");
  174. return Preferences.userNodeForPackage(clz).get("firstLaunch",
  175. "-");
  176. } catch (ClassNotFoundException e) {
  177. return "-";
  178. }
  179. }
  180. }
  181. /**
  182. * Exception thrown when the user does not have a valid cval license.
  183. */
  184. public static class InvalidCvalException extends Exception {
  185. private static final long serialVersionUID = 1L;
  186. public final CvalInfo info;
  187. public final String name;
  188. public final String key;
  189. public final String version;
  190. public final String title;
  191. public InvalidCvalException(String name, String version, String title,
  192. String key, CvalInfo info) {
  193. super(composeMessage(title, version, key, info));
  194. this.info = info;
  195. this.name = name;
  196. this.key = key;
  197. this.version = version;
  198. this.title = title;
  199. }
  200. static String composeMessage(String title, String version, String key,
  201. CvalInfo info) {
  202. String msg = "";
  203. int majorVers = computeMajorVersion(version);
  204. if (info != null && info.getMessage() != null) {
  205. msg = info.getMessage().replace("\\n", "\n");
  206. } else if (info != null && info.isLicenseExpired()) {
  207. String type = "evaluation".equals(info.getType()) ? "Evaluation license"
  208. : "License";
  209. msg = getErrorMessage("expired", title, majorVers, type);
  210. } else if (key == null) {
  211. msg = getErrorMessage("none", title, majorVers);
  212. } else {
  213. msg = getErrorMessage("invalid", title, majorVers);
  214. }
  215. return msg;
  216. }
  217. }
  218. /**
  219. * Exception thrown when the license server is unreachable
  220. */
  221. public static class UnreachableCvalServerException extends Exception {
  222. private static final long serialVersionUID = 1L;
  223. public final String name;
  224. public UnreachableCvalServerException(String name, Exception e) {
  225. super(e);
  226. this.name = name;
  227. }
  228. }
  229. public static final String LINE = "----------------------------------------------------------------------------------------------------------------------";
  230. static final int GRACE_DAYS_MSECS = 2 * 24 * 60 * 60 * 1000;
  231. private static final String LICENSE_URL_PROD = "https://tools.vaadin.com/vaadin-license-server/licenses/";
  232. /*
  233. * used in tests
  234. */
  235. static void cacheLicenseInfo(CvalInfo info) {
  236. if (info != null) {
  237. Preferences p = Preferences.userNodeForPackage(CvalInfo.class);
  238. if (info.toString().length() > Preferences.MAX_VALUE_LENGTH) {
  239. // This should never happen since MAX_VALUE_LENGTH is big
  240. // enough.
  241. // But server could eventually send a very big message, so we
  242. // discard it in cache and would use hard-coded messages.
  243. info.setMessage(null);
  244. }
  245. p.put(info.getProduct().getName(), info.toString());
  246. }
  247. }
  248. /*
  249. * used in tests
  250. */
  251. static void deleteCache(String productName) {
  252. Preferences p = Preferences.userNodeForPackage(CvalInfo.class);
  253. p.remove(productName);
  254. }
  255. /**
  256. * Given a product name returns the name of the file with the license key.
  257. *
  258. * Traditionally we have delivered license keys with a name like
  259. * 'vaadin.touchkit.developer.license' but our database product name is
  260. * 'vaadin-touchkit' so we have to replace '-' by '.' to maintain
  261. * compatibility.
  262. */
  263. static final String computeLicenseName(String productName) {
  264. return productName.replace("-", ".") + ".developer.license";
  265. }
  266. static final int computeMajorVersion(String productVersion) {
  267. return productVersion == null || productVersion.isEmpty() ? 0
  268. : parseInt(productVersion.replaceFirst("[^\\d]+.*$", ""));
  269. }
  270. /*
  271. * used in tests
  272. */
  273. static CvalInfo parseJson(String json) {
  274. if (json == null) {
  275. return null;
  276. }
  277. try {
  278. JsonObject o = JsonUtil.parse(json);
  279. return new CvalInfo(o);
  280. } catch (JsonException e) {
  281. return null;
  282. }
  283. }
  284. private CvalServer provider;
  285. /**
  286. * The constructor.
  287. */
  288. public CvalChecker() {
  289. setLicenseProvider(new CvalServer());
  290. }
  291. /**
  292. * Validate whether there is a valid license key for a product.
  293. *
  294. * @param productName
  295. * for example vaadin-touchkit
  296. * @param productVersion
  297. * for instance 4.0.1
  298. * @return CvalInfo Server response or cache response if server is offline
  299. * @throws InvalidCvalException
  300. * when there is no a valid license for the product
  301. * @throws UnreachableCvalServerException
  302. * when we have license key but server is unreachable
  303. */
  304. public CvalInfo validateProduct(String productName, String productVersion,
  305. String productTitle) throws InvalidCvalException,
  306. UnreachableCvalServerException {
  307. String key = getDeveloperLicenseKey(productName, productVersion,
  308. productTitle);
  309. CvalInfo info = null;
  310. if (key != null && !key.isEmpty()) {
  311. info = getCachedLicenseInfo(productName);
  312. if (info != null && !info.isValidInfo(productName, key)) {
  313. deleteCache(productName);
  314. info = null;
  315. }
  316. info = askLicenseServer(productName, key, productVersion, info);
  317. if (info != null && info.isValidInfo(productName, key)
  318. && info.isValidVersion(computeMajorVersion(productVersion))
  319. && !info.isLicenseExpired()) {
  320. return info;
  321. }
  322. }
  323. throw new InvalidCvalException(productName, productVersion,
  324. productTitle, key, info);
  325. }
  326. /*
  327. * Change the license provider, only used in tests.
  328. */
  329. final CvalChecker setLicenseProvider(CvalServer p) {
  330. provider = p;
  331. return this;
  332. }
  333. private CvalInfo askLicenseServer(String productName, String productKey,
  334. String productVersion, CvalInfo info)
  335. throws UnreachableCvalServerException {
  336. int majorVersion = computeMajorVersion(productVersion);
  337. // If we have a valid license info here, it means that we got it from
  338. // cache.
  339. // We add a grace time when so as if the server is unreachable
  340. // we allow the user to use the product.
  341. if (info != null && info.getExpiredEpoch() != null
  342. && !"evaluation".equals(info.getType())) {
  343. long ts = info.getExpiredEpoch().getTime() + GRACE_DAYS_MSECS;
  344. info.setExpiredEpoch(new Date(ts));
  345. }
  346. boolean validCache = info != null
  347. && info.isValidInfo(productName, productKey)
  348. && info.isValidVersion(majorVersion)
  349. && !info.isLicenseExpired();
  350. // if we have a validCache we set the timeout smaller
  351. int timeout = validCache ? 2000 : 10000;
  352. try {
  353. CvalInfo srvinfo = parseJson(provider.askServer(productName + "-"
  354. + productVersion, productKey, timeout));
  355. if (srvinfo != null && srvinfo.isValidInfo(productName, productKey)
  356. && srvinfo.isValidVersion(majorVersion)) {
  357. // We always cache the info if it is valid although it is
  358. // expired
  359. cacheLicenseInfo(srvinfo);
  360. info = srvinfo;
  361. }
  362. } catch (FileNotFoundException e) {
  363. // 404
  364. return null;
  365. } catch (Exception e) {
  366. if (info == null) {
  367. throw new UnreachableCvalServerException(productName, e);
  368. }
  369. }
  370. return info;
  371. }
  372. private CvalInfo getCachedLicenseInfo(String productName) {
  373. Preferences p = Preferences.userNodeForPackage(CvalInfo.class);
  374. String json = p.get(productName, "");
  375. if (!json.isEmpty()) {
  376. CvalInfo info = parseJson(json);
  377. if (info != null) {
  378. return info;
  379. }
  380. }
  381. return null;
  382. }
  383. private String getDeveloperLicenseKey(String productName,
  384. String productVersion, String productTitle)
  385. throws InvalidCvalException {
  386. String licenseName = computeLicenseName(productName);
  387. String key = System.getProperty(licenseName);
  388. if (key != null && !key.isEmpty()) {
  389. return key;
  390. }
  391. try {
  392. String dotLicenseName = "." + licenseName;
  393. String userHome = "file://" + System.getProperty("user.home") + "/";
  394. for (URL url : new URL[] { new URL(userHome + dotLicenseName),
  395. new URL(userHome + licenseName),
  396. URL.class.getResource("/" + dotLicenseName),
  397. URL.class.getResource("/" + licenseName) }) {
  398. if (url != null) {
  399. try {
  400. key = readKeyFromFile(url,
  401. computeMajorVersion(productVersion));
  402. if (key != null && !(key = key.trim()).isEmpty()) {
  403. return key;
  404. }
  405. } catch (IOException ignored) {
  406. }
  407. }
  408. }
  409. } catch (Exception e) {
  410. e.printStackTrace();
  411. }
  412. throw new InvalidCvalException(productName, productVersion,
  413. productTitle, null, null);
  414. }
  415. String readKeyFromFile(URL url, int majorVersion) throws IOException {
  416. String majorVersionStr = String.valueOf(majorVersion);
  417. List<String> lines = IOUtils.readLines(url.openStream());
  418. String defaultKey = null;
  419. for (String line : lines) {
  420. String[] parts = line.split("\\s*=\\s*");
  421. if (parts.length < 2) {
  422. defaultKey = parts[0].trim();
  423. }
  424. if (parts[0].equals(majorVersionStr)) {
  425. return parts[1].trim();
  426. }
  427. }
  428. return defaultKey;
  429. }
  430. static String getErrorMessage(String key, Object... pars) {
  431. Locale loc = Locale.getDefault();
  432. ResourceBundle res = ResourceBundle.getBundle(
  433. CvalChecker.class.getName(), loc);
  434. String msg = res.getString(key);
  435. return new MessageFormat(msg, loc).format(pars);
  436. }
  437. }