diff options
Diffstat (limited to 'client-compiler/src/com/vaadin/tools/CvalChecker.java')
-rw-r--r-- | client-compiler/src/com/vaadin/tools/CvalChecker.java | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/client-compiler/src/com/vaadin/tools/CvalChecker.java b/client-compiler/src/com/vaadin/tools/CvalChecker.java new file mode 100644 index 0000000000..5ad44a70f8 --- /dev/null +++ b/client-compiler/src/com/vaadin/tools/CvalChecker.java @@ -0,0 +1,488 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.tools; + +import static java.lang.Integer.parseInt; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.prefs.Preferences; + +import org.apache.commons.io.IOUtils; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * This class is able to validate the vaadin CVAL license. + * + * It reads the developer license file and asks the server to validate the + * licenseKey. If the license is invalid it throws an exception with the + * information about the problem and the server response. + */ +public final class CvalChecker { + + /* + * Class used for binding the JSON gotten from server. + * + * It is not in a separate f le, so as it is easier to copy into any product + * which does not depend on vaadin core. + * + * We are using org.json in order not to use additional dependency like + * auto-beans, gson, etc. + */ + public static class CvalInfo { + + public static class Product { + private JSONObject o; + + public Product(JSONObject o) { + this.o = o; + } + + public String getName() { + return get(o, "name", String.class); + } + + public Integer getVersion() { + return get(o, "version", Integer.class); + } + } + + @SuppressWarnings("unchecked") + private static <T> T get(JSONObject o, String k, Class<T> clz) { + Object ret = null; + try { + if (clz == String.class) { + ret = o.getString(k); + } else if (clz == JSONObject.class) { + ret = o.getJSONObject(k); + } else if (clz == Integer.class) { + ret = o.getInt(k); + } else if (clz == Date.class) { + ret = new Date(o.getLong(k)); + } else if (clz == Boolean.class) { + ret = o.getBoolean(k); + } + } catch (JSONException e) { + } + return (T) ret; + } + + private static <T> T put(JSONObject o, String k, Object v) { + try { + o.put(k, v); + } catch (JSONException e) { + } + return null; + } + + private JSONObject o; + + private Product product; + + public CvalInfo(JSONObject o) { + this.o = o; + product = new Product(get(o, "product", JSONObject.class)); + } + + public Boolean getExpired() { + return get(o, "expired", Boolean.class); + } + + public Date getExpiredEpoch() { + return get(o, "expiredEpoch", Date.class); + } + + public String getLicensee() { + return get(o, "licensee", String.class); + } + + public String getLicenseKey() { + return get(o, "licenseKey", String.class); + } + + public String getMessage() { + return get(o, "message", String.class); + } + + public Product getProduct() { + return product; + } + + public String getType() { + return get(o, "type", String.class); + } + + public void setExpiredEpoch(Date expiredEpoch) { + put(o, "expiredEpoch", expiredEpoch.getTime()); + } + + public void setMessage(String msg) { + put(o, "message", msg); + } + + @Override + public String toString() { + return o.toString(); + } + + public boolean isLicenseExpired() { + return (getExpired() != null && getExpired()) + || (getExpiredEpoch() != null && getExpiredEpoch().before( + new Date())); + } + + public boolean isValidVersion(int majorVersion) { + return getProduct().getVersion() == null + || getProduct().getVersion() >= majorVersion; + + } + + private boolean isValidInfo(String name, String key) { + return getProduct() != null && getProduct().getName() != null + && getLicenseKey() != null + && getProduct().getName().equals(name) + && getLicenseKey().equals(key); + } + } + + /* + * The class with the method for getting json from server side. It is here + * and protected just for replacing it in tests. + */ + public static class CvalServer { + protected String licenseUrl = LICENSE_URL_PROD; + + String askServer(String productName, String productKey, int timeoutMs) + throws IOException { + String url = licenseUrl + productKey; + URLConnection con; + try { + // Send some additional info in the User-Agent string. + String ua = "Cval " + productName + " " + productKey + " " + + getFirstLaunch(); + for (String prop : Arrays.asList("java.vendor.url", + "java.version", "os.name", "os.version", "os.arch")) { + ua += " " + System.getProperty(prop, "-").replace(" ", "_"); + } + con = new URL(url).openConnection(); + con.setRequestProperty("User-Agent", ua); + con.setConnectTimeout(timeoutMs); + con.setReadTimeout(timeoutMs); + String r = IOUtils.toString(con.getInputStream()); + return r; + } catch (MalformedURLException e) { + e.printStackTrace(); + return null; + } + } + + /* + * Get the GWT firstLaunch timestamp. + */ + String getFirstLaunch() { + try { + Class<?> clz = Class + .forName("com.google.gwt.dev.shell.CheckForUpdates"); + return Preferences.userNodeForPackage(clz).get("firstLaunch", + "-"); + } catch (ClassNotFoundException e) { + return "-"; + } + } + } + + /** + * Exception thrown when the user does not have a valid cval license. + */ + public static class InvalidCvalException extends Exception { + private static final long serialVersionUID = 1L; + public final CvalInfo info; + public final String name; + public final String key; + public final String version; + public final String title; + + public InvalidCvalException(String name, String version, String title, + String key, CvalInfo info) { + super(composeMessage(title, version, key, info)); + this.info = info; + this.name = name; + this.key = key; + this.version = version; + this.title = title; + } + + static String composeMessage(String title, String version, String key, + CvalInfo info) { + String msg = ""; + int majorVers = computeMajorVersion(version); + + if (info != null && info.getMessage() != null) { + msg = info.getMessage().replace("\\n", "\n"); + } else if (info != null && info.isLicenseExpired()) { + String type = "evaluation".equals(info.getType()) ? "Evaluation license" + : "License"; + msg = getErrorMessage("expired", title, majorVers, type); + } else if (key == null) { + msg = getErrorMessage("none", title, majorVers); + } else { + msg = getErrorMessage("invalid", title, majorVers); + } + return msg; + } + } + + /** + * Exception thrown when the license server is unreachable + */ + public static class UnreachableCvalServerException extends Exception { + private static final long serialVersionUID = 1L; + public final String name; + + public UnreachableCvalServerException(String name, Exception e) { + super(e); + this.name = name; + } + } + + public static final String LINE = "----------------------------------------------------------------------------------------------------------------------"; + + static final int GRACE_DAYS_MSECS = 2 * 24 * 60 * 60 * 1000; + + private static final String LICENSE_URL_PROD = "https://tools.vaadin.com/vaadin-license-server/licenses/"; + + /* + * used in tests + */ + static void cacheLicenseInfo(CvalInfo info) { + if (info != null) { + Preferences p = Preferences.userNodeForPackage(CvalInfo.class); + if (info.toString().length() > Preferences.MAX_VALUE_LENGTH) { + // This should never happen since MAX_VALUE_LENGTH is big + // enough. + // But server could eventually send a very big message, so we + // discard it in cache and would use hard-coded messages. + info.setMessage(null); + } + p.put(info.getProduct().getName(), info.toString()); + } + } + + /* + * used in tests + */ + static void deleteCache(String productName) { + Preferences p = Preferences.userNodeForPackage(CvalInfo.class); + p.remove(productName); + } + + /** + * Given a product name returns the name of the file with the license key. + * + * Traditionally we have delivered license keys with a name like + * 'vaadin.touchkit.developer.license' but our database product name is + * 'vaadin-touchkit' so we have to replace '-' by '.' to maintain + * compatibility. + */ + static final String computeLicenseName(String productName) { + return productName.replace("-", ".") + ".developer.license"; + } + + static final int computeMajorVersion(String productVersion) { + return productVersion == null || productVersion.isEmpty() ? 0 + : parseInt(productVersion.replaceFirst("[^\\d]+.*$", "")); + } + + /* + * used in tests + */ + static CvalInfo parseJson(String json) { + if (json == null) { + return null; + } + try { + JSONObject o = new JSONObject(json); + return new CvalInfo(o); + } catch (JSONException e) { + return null; + } + } + + private CvalServer provider; + + /** + * The constructor. + */ + public CvalChecker() { + setLicenseProvider(new CvalServer()); + } + + /** + * Validate whether there is a valid license key for a product. + * + * @param productName + * for example vaadin-touchkit + * @param productVersion + * for instance 4.0.1 + * @return CvalInfo Server response or cache response if server is offline + * @throws InvalidCvalException + * when there is no a valid license for the product + * @throws UnreachableCvalServerException + * when we have license key but server is unreachable + */ + public CvalInfo validateProduct(String productName, String productVersion, + String productTitle) throws InvalidCvalException, + UnreachableCvalServerException { + String key = getDeveloperLicenseKey(productName, productVersion, + productTitle); + + CvalInfo info = null; + if (key != null && !key.isEmpty()) { + info = getCachedLicenseInfo(productName); + if (info != null && !info.isValidInfo(productName, key)) { + deleteCache(productName); + info = null; + } + info = askLicenseServer(productName, key, productVersion, info); + if (info != null && info.isValidInfo(productName, key) + && info.isValidVersion(computeMajorVersion(productVersion)) + && !info.isLicenseExpired()) { + return info; + } + } + + throw new InvalidCvalException(productName, productVersion, + productTitle, key, info); + } + + /* + * Change the license provider, only used in tests. + */ + final CvalChecker setLicenseProvider(CvalServer p) { + provider = p; + return this; + } + + private CvalInfo askLicenseServer(String productName, String productKey, + String productVersion, CvalInfo info) + throws UnreachableCvalServerException { + + int majorVersion = computeMajorVersion(productVersion); + + // If we have a valid license info here, it means that we got it from + // cache. + // We add a grace time when so as if the server is unreachable + // we allow the user to use the product. + if (info != null && info.getExpiredEpoch() != null + && !"evaluation".equals(info.getType())) { + long ts = info.getExpiredEpoch().getTime() + GRACE_DAYS_MSECS; + info.setExpiredEpoch(new Date(ts)); + } + + boolean validCache = info != null + && info.isValidInfo(productName, productKey) + && info.isValidVersion(majorVersion) + && !info.isLicenseExpired(); + + // if we have a validCache we set the timeout smaller + int timeout = validCache ? 2000 : 10000; + + try { + CvalInfo srvinfo = parseJson(provider.askServer(productName + "-" + + productVersion, productKey, timeout)); + if (srvinfo != null && srvinfo.isValidInfo(productName, productKey) + && srvinfo.isValidVersion(majorVersion)) { + // We always cache the info if it is valid although it is + // expired + cacheLicenseInfo(srvinfo); + info = srvinfo; + } + } catch (FileNotFoundException e) { + // 404 + return null; + } catch (Exception e) { + if (info == null) { + throw new UnreachableCvalServerException(productName, e); + } + } + return info; + } + + private CvalInfo getCachedLicenseInfo(String productName) { + Preferences p = Preferences.userNodeForPackage(CvalInfo.class); + String json = p.get(productName, ""); + if (!json.isEmpty()) { + CvalInfo info = parseJson(json); + if (info != null) { + return info; + } + } + return null; + } + + private String getDeveloperLicenseKey(String productName, + String productVersion, String productTitle) + throws InvalidCvalException { + String licenseName = computeLicenseName(productName); + + String key = System.getProperty(licenseName); + if (key != null && !key.isEmpty()) { + return key; + } + + try { + String dotLicenseName = "." + licenseName; + String userHome = "file://" + System.getProperty("user.home") + "/"; + for (URL url : new URL[] { new URL(userHome + dotLicenseName), + new URL(userHome + licenseName), + URL.class.getResource("/" + dotLicenseName), + URL.class.getResource("/" + licenseName) }) { + + if (url != null) { + try { + key = IOUtils.toString(url.openStream()); + if (key != null && !(key = key.trim()).isEmpty()) { + return key; + } + } catch (IOException ignored) { + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + throw new InvalidCvalException(productName, productVersion, + productTitle, null, null); + } + + static String getErrorMessage(String key, Object... pars) { + Locale loc = Locale.getDefault(); + ResourceBundle res = ResourceBundle.getBundle( + CvalChecker.class.getName(), loc); + String msg = res.getString(key); + return new MessageFormat(msg, loc).format(pars); + } +} |