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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  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.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 && getExpiredEpoch().before(
  131. 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.getMessage() != null) {
  211. msg = info.getMessage().replace("\\n", "\n");
  212. } else if (info != null && info.isLicenseExpired()) {
  213. String type = "evaluation".equals(info.getType()) ? "Evaluation license"
  214. : "License";
  215. msg = getErrorMessage("expired", title, majorVers, type);
  216. } else if (key == null) {
  217. msg = getErrorMessage("none", title, majorVers);
  218. } else {
  219. msg = getErrorMessage("invalid", title, majorVers);
  220. }
  221. return msg;
  222. }
  223. }
  224. /**
  225. * Exception thrown when the license server is unreachable
  226. */
  227. public static class UnreachableCvalServerException extends Exception {
  228. private static final long serialVersionUID = 1L;
  229. public final String name;
  230. public UnreachableCvalServerException(String name, Exception e) {
  231. super(e);
  232. this.name = name;
  233. }
  234. }
  235. public static final String LINE = "----------------------------------------------------------------------------------------------------------------------";
  236. static final int GRACE_DAYS_MSECS = 2 * 24 * 60 * 60 * 1000;
  237. private static final String LICENSE_URL_PROD = "https://tools.vaadin.com/vaadin-license-server/licenses/";
  238. /*
  239. * used in tests
  240. */
  241. static void cacheLicenseInfo(CvalInfo info) {
  242. if (info != null) {
  243. Preferences p = Preferences.userNodeForPackage(CvalInfo.class);
  244. if (info.toString().length() > Preferences.MAX_VALUE_LENGTH) {
  245. // This should never happen since MAX_VALUE_LENGTH is big
  246. // enough.
  247. // But server could eventually send a very big message, so we
  248. // discard it in cache and would use hard-coded messages.
  249. info.setMessage(null);
  250. }
  251. p.put(info.getProduct().getName(), info.toString());
  252. }
  253. }
  254. /*
  255. * used in tests
  256. */
  257. static void deleteCache(String productName) {
  258. Preferences p = Preferences.userNodeForPackage(CvalInfo.class);
  259. p.remove(productName);
  260. }
  261. /**
  262. * Given a product name returns the name of the file with the license key.
  263. *
  264. * Traditionally we have delivered license keys with a name like
  265. * 'vaadin.touchkit.developer.license' but our database product name is
  266. * 'vaadin-touchkit' so we have to replace '-' by '.' to maintain
  267. * compatibility.
  268. */
  269. static final String computeLicenseName(String productName) {
  270. return productName.replace("-", ".") + ".developer.license";
  271. }
  272. static final int computeMajorVersion(String productVersion) {
  273. return productVersion == null || productVersion.isEmpty() ? 0
  274. : parseInt(productVersion.replaceFirst("[^\\d]+.*$", ""));
  275. }
  276. /*
  277. * used in tests
  278. */
  279. static CvalInfo parseJson(String json) {
  280. if (json == null) {
  281. return null;
  282. }
  283. try {
  284. JsonObject o = JsonUtil.parse(json);
  285. return new CvalInfo(o);
  286. } catch (JsonException e) {
  287. return null;
  288. }
  289. }
  290. private CvalServer provider;
  291. /**
  292. * The constructor.
  293. */
  294. public CvalChecker() {
  295. setLicenseProvider(new CvalServer());
  296. }
  297. /**
  298. * Validate whether there is a valid license key for a product.
  299. *
  300. * @param productName
  301. * for example vaadin-touchkit
  302. * @param productVersion
  303. * for instance 4.0.1
  304. * @return CvalInfo Server response or cache response if server is offline
  305. * @throws InvalidCvalException
  306. * when there is no a valid license for the product
  307. * @throws UnreachableCvalServerException
  308. * when we have license key but server is unreachable
  309. */
  310. public CvalInfo validateProduct(String productName, String productVersion,
  311. String productTitle) throws InvalidCvalException,
  312. UnreachableCvalServerException {
  313. String key = getDeveloperLicenseKey(productName, productVersion,
  314. productTitle);
  315. CvalInfo info = null;
  316. if (key != null && !key.isEmpty()) {
  317. info = getCachedLicenseInfo(productName);
  318. if (info != null && !info.isValidInfo(productName, key)) {
  319. deleteCache(productName);
  320. info = null;
  321. }
  322. info = askLicenseServer(productName, key, productVersion, info);
  323. if (info != null && info.isValidInfo(productName, key)
  324. && info.isValidVersion(computeMajorVersion(productVersion))
  325. && !info.isLicenseExpired()) {
  326. return info;
  327. }
  328. }
  329. throw new InvalidCvalException(productName, productVersion,
  330. productTitle, key, info);
  331. }
  332. /*
  333. * Change the license provider, only used in tests.
  334. */
  335. final CvalChecker setLicenseProvider(CvalServer p) {
  336. provider = p;
  337. return this;
  338. }
  339. private CvalInfo askLicenseServer(String productName, String productKey,
  340. String productVersion, CvalInfo info)
  341. throws UnreachableCvalServerException {
  342. int majorVersion = computeMajorVersion(productVersion);
  343. // If we have a valid license info here, it means that we got it from
  344. // cache.
  345. // We add a grace time when so as if the server is unreachable
  346. // we allow the user to use the product.
  347. if (info != null && info.getExpiredEpoch() != null
  348. && !"evaluation".equals(info.getType())) {
  349. long ts = info.getExpiredEpoch().getTime() + GRACE_DAYS_MSECS;
  350. info.setExpiredEpoch(new Date(ts));
  351. }
  352. boolean validCache = info != null
  353. && info.isValidInfo(productName, productKey)
  354. && info.isValidVersion(majorVersion)
  355. && !info.isLicenseExpired();
  356. // if we have a validCache we set the timeout smaller
  357. int timeout = validCache ? 2000 : 10000;
  358. try {
  359. CvalInfo srvinfo = parseJson(provider.askServer(productName + "-"
  360. + productVersion, productKey, timeout));
  361. if (srvinfo != null && srvinfo.isValidInfo(productName, productKey)
  362. && srvinfo.isValidVersion(majorVersion)) {
  363. // We always cache the info if it is valid although it is
  364. // expired
  365. cacheLicenseInfo(srvinfo);
  366. info = srvinfo;
  367. }
  368. } catch (FileNotFoundException e) {
  369. // 404
  370. return null;
  371. } catch (Exception e) {
  372. if (info == null) {
  373. throw new UnreachableCvalServerException(productName, e);
  374. }
  375. }
  376. return info;
  377. }
  378. private CvalInfo getCachedLicenseInfo(String productName) {
  379. Preferences p = Preferences.userNodeForPackage(CvalInfo.class);
  380. String json = p.get(productName, "");
  381. if (!json.isEmpty()) {
  382. CvalInfo info = parseJson(json);
  383. if (info != null) {
  384. return info;
  385. }
  386. }
  387. return null;
  388. }
  389. private String getDeveloperLicenseKey(String productName,
  390. String productVersion, String productTitle)
  391. throws InvalidCvalException {
  392. String licenseName = computeLicenseName(productName);
  393. String key = System.getProperty(licenseName);
  394. if (key != null && !key.isEmpty()) {
  395. return key;
  396. }
  397. try {
  398. String dotLicenseName = "." + licenseName;
  399. String userHome = System.getProperty("user.home");
  400. for (URL url : new URL[] {
  401. new File(userHome, dotLicenseName).toURI().toURL(),
  402. new File(userHome, licenseName).toURI().toURL(),
  403. URL.class.getResource("/" + dotLicenseName),
  404. URL.class.getResource("/" + licenseName) }) {
  405. if (url != null) {
  406. try {
  407. key = readKeyFromFile(url,
  408. computeMajorVersion(productVersion));
  409. if (key != null && !(key = key.trim()).isEmpty()) {
  410. return key;
  411. }
  412. } catch (IOException ignored) {
  413. }
  414. }
  415. }
  416. } catch (Exception e) {
  417. e.printStackTrace();
  418. }
  419. throw new InvalidCvalException(productName, productVersion,
  420. productTitle, null, null);
  421. }
  422. String readKeyFromFile(URL url, int majorVersion) throws IOException {
  423. String majorVersionStr = String.valueOf(majorVersion);
  424. List<String> lines = IOUtils.readLines(url.openStream());
  425. String defaultKey = null;
  426. for (String line : lines) {
  427. String[] parts = line.split("\\s*=\\s*");
  428. if (parts.length < 2) {
  429. defaultKey = parts[0].trim();
  430. }
  431. if (parts[0].equals(majorVersionStr)) {
  432. return parts[1].trim();
  433. }
  434. }
  435. return defaultKey;
  436. }
  437. static String getErrorMessage(String key, Object... pars) {
  438. Locale loc = Locale.getDefault();
  439. ResourceBundle res = ResourceBundle.getBundle(
  440. CvalChecker.class.getName(), loc);
  441. String msg = res.getString(key);
  442. return new MessageFormat(msg, loc).format(pars);
  443. }
  444. }