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

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