diff options
Diffstat (limited to 'client-compiler/src')
4 files changed, 731 insertions, 5 deletions
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java index cc1841ec05..5519dd1aae 100644 --- a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java +++ b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java @@ -1,12 +1,12 @@ /* * 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 @@ -48,6 +48,7 @@ import com.vaadin.client.JsArrayObject; import com.vaadin.client.ServerConnector; import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.metadata.ConnectorBundleLoader; +import com.vaadin.client.metadata.ConnectorBundleLoader.CValUiInfo; import com.vaadin.client.metadata.InvokationHandler; import com.vaadin.client.metadata.OnStateChangeMethod; import com.vaadin.client.metadata.ProxyHandler; @@ -70,6 +71,9 @@ import com.vaadin.shared.communication.ClientRpc; import com.vaadin.shared.communication.ServerRpc; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.tools.CvalAddonsChecker; +import com.vaadin.tools.CvalChecker; +import com.vaadin.tools.CvalChecker.InvalidCvalException; public class ConnectorBundleLoaderFactory extends Generator { /** @@ -211,6 +215,8 @@ public class ConnectorBundleLoaderFactory extends Generator { } + private CvalAddonsChecker cvalChecker = new CvalAddonsChecker(); + @Override public String generate(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException { @@ -231,7 +237,6 @@ public class ConnectorBundleLoaderFactory extends Generator { logger.log(Type.ERROR, getClass() + " failed", e); throw new UnableToCompleteException(); } - } private void generateClass(TreeLogger logger, GeneratorContext context, @@ -243,6 +248,23 @@ public class ConnectorBundleLoaderFactory extends Generator { return; } + List<CValUiInfo> cvalInfos = null; + try { + if (cvalChecker != null) { + cvalInfos = cvalChecker.run(); + // Don't run twice + cvalChecker = null; + } + } catch (InvalidCvalException e) { + System.err.println("\n\n\n\n" + CvalChecker.LINE); + for (String line : e.getMessage().split("\n")) { + System.err.println(line); + } + System.err.println(CvalChecker.LINE + "\n\n\n\n"); + System.exit(1); + throw new UnableToCompleteException(); + } + List<ConnectorBundle> bundles = buildBundles(logger, context.getTypeOracle()); @@ -364,6 +386,18 @@ public class ConnectorBundleLoaderFactory extends Generator { w.println("});"); } + if (cvalInfos != null && !cvalInfos.isEmpty()) { + w.println("{"); + for (CValUiInfo c : cvalInfos) { + if ("evaluation".equals(c.type)) { + w.println("cvals.add(new CValUiInfo(\"" + c.product + + "\", \"" + c.version + "\", \"" + c.widgetset + + "\", null));"); + } + } + w.println("}"); + } + w.outdent(); w.println("}"); @@ -1101,7 +1135,7 @@ public class ConnectorBundleLoaderFactory extends Generator { * {@link ServerConnector} that have a @{@link Connect} annotation. It also * checks that multiple connectors aren't connected to the same server-side * class. - * + * * @param logger * the logger to which information can be logged * @param typeOracle diff --git a/client-compiler/src/com/vaadin/tools/CvalAddonsChecker.java b/client-compiler/src/com/vaadin/tools/CvalAddonsChecker.java new file mode 100644 index 0000000000..6780b1bc75 --- /dev/null +++ b/client-compiler/src/com/vaadin/tools/CvalAddonsChecker.java @@ -0,0 +1,191 @@ +/* + * 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 com.vaadin.tools.CvalChecker.LINE; +import static com.vaadin.tools.CvalChecker.computeMajorVersion; +import static com.vaadin.tools.CvalChecker.getErrorMessage; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import com.vaadin.client.metadata.ConnectorBundleLoader.CValUiInfo; +import com.vaadin.tools.CvalChecker.CvalInfo; +import com.vaadin.tools.CvalChecker.CvalServer; +import com.vaadin.tools.CvalChecker.InvalidCvalException; +import com.vaadin.tools.CvalChecker.UnreachableCvalServerException; + +/** + * This class is able to visit all MANIFEST.MF files present in the classpath, + * filter by name, and check if the user has a valid license. + * + * Manifest files should have a few attributes indicating the license type of + * the addon: + * <ul> + * <li>Implementation-Version: 4.x.x + * <li>AdVaaName: addon_name + * <li>AdVaaLicen: cval, agpl, empty + * <li>AdVaaPkg: package of the widgets in this addon + * </ul> + * + * The class also have a method to check just one product. + */ +public final class CvalAddonsChecker { + + // Manifest attributes + public static final String VAADIN_ADDON_LICENSE = "AdVaaLicen"; + public static final String VAADIN_ADDON_NAME = "AdVaaName"; + public static final String VAADIN_ADDON_WIDGETSET = "Vaadin-Widgetsets"; + public static final String VAADIN_ADDON_VERSION = "Implementation-Version"; + public static final String VAADIN_ADDON_TITLE = "Implementation-Title"; + + // License types + public static final String VAADIN_AGPL = "agpl"; + public static final String VAADIN_CVAL = "cval"; + + private CvalChecker cvalChecker = new CvalChecker(); + private String filterPattern; + + /** + * The constructor. + */ + public CvalAddonsChecker() { + setLicenseProvider(new CvalServer()); + setFilter(".*vaadin.*"); + } + + /** + * Visit all MANIFEST.MF files in the classpath validating licenses. + * + * Return a list of Cval licensed products in order to have enough info to + * generate nag messages in the UI. + */ + public List<CValUiInfo> run() throws InvalidCvalException { + List<CValUiInfo> ret = new ArrayList<CValUiInfo>(); + try { + // Visit all MANIFEST in our classpath + Enumeration<URL> manifests = Thread.currentThread() + .getContextClassLoader() + .getResources(JarFile.MANIFEST_NAME); + while (manifests.hasMoreElements()) { + try { + URL url = manifests.nextElement(); + // Discard manifests whose name does not match the filter + // pattern + if (!url.getPath().matches(filterPattern)) { + continue; + } + InputStream is = url.openStream(); + // Should never happen, but we don't want a NPE here + if (is == null) { + continue; + } + // Read manifest attributes + Manifest manifest = new Manifest(is); + Attributes attribs = manifest.getMainAttributes(); + String license = attribs.getValue(VAADIN_ADDON_LICENSE); + String name = attribs.getValue(VAADIN_ADDON_NAME); + String vers = attribs.getValue(VAADIN_ADDON_VERSION) == null ? "" + : attribs.getValue(VAADIN_ADDON_VERSION); + String title = attribs.getValue(VAADIN_ADDON_TITLE) == null ? name + : attribs.getValue(VAADIN_ADDON_TITLE); + + String widgetsets = attribs + .getValue(VAADIN_ADDON_WIDGETSET) == null ? name + : attribs.getValue(VAADIN_ADDON_WIDGETSET); + + if (name == null || license == null) { + continue; + } + if (VAADIN_AGPL.equals(license)) { + // For agpl version we print an info message + printAgplLicense(title, vers); + } else if (VAADIN_CVAL.equals(license)) { + // We only check cval licensed products + CvalInfo info; + try { + info = cvalChecker.validateProduct(name, vers, + title); + printValidLicense(info, title, vers); + } catch (UnreachableCvalServerException e) { + info = CvalChecker.parseJson("{'product':{'name':'" + + name + "'}}"); + printServerUnreachable(title, vers); + } + for (String w : widgetsets.split("[, ]+")) { + ret.add(new CValUiInfo(title, String + .valueOf(computeMajorVersion(vers)), w, + info.getType())); + } + } + } catch (IOException ignored) { + } + } + } catch (IOException ignored) { + } + return ret; + } + + /** + * Set the filter regexp of .jar names which we have to consider. + * + * default is '.*touchkit.*' + */ + public CvalAddonsChecker setFilter(String regexp) { + filterPattern = regexp; + return this; + } + + /* + * Change the license provider, only used in tests. + */ + protected CvalAddonsChecker setLicenseProvider(CvalServer p) { + cvalChecker.setLicenseProvider(p); + return this; + } + + private void printAgplLicense(String name, String version) { + System.out.println(LINE + "\n" + + getErrorMessage("agpl", name, computeMajorVersion(version)) + + "\n" + LINE); + } + + private void printServerUnreachable(String name, String version) { + System.out.println(LINE + + "\n" + + getErrorMessage("unreachable", name, + computeMajorVersion(version)) + "\n" + LINE); + } + + private void printValidLicense(CvalInfo info, String title, String version) { + String msg = info.getMessage(); + if (msg == null) { + String key = "evaluation".equals(info.getType()) ? "evaluation" + : "valid"; + msg = getErrorMessage(key, title, computeMajorVersion(version), + info.getLicensee()); + } + System.out.println("\n" + LINE + "\n" + msg + "\n" + LINE + "\n"); + } +} 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); + } +} diff --git a/client-compiler/src/com/vaadin/tools/CvalChecker.properties b/client-compiler/src/com/vaadin/tools/CvalChecker.properties new file mode 100644 index 0000000000..3f4fd52cb7 --- /dev/null +++ b/client-compiler/src/com/vaadin/tools/CvalChecker.properties @@ -0,0 +1,13 @@ +expired={2} for {0} {1} has expired. Get a valid license at vaadin.com/pro + +none=License for {0} {1} not found. Go to vaadin.com/pro for more details. + +invalid=License for {0} {1} is not valid. Get a valid license from vaadin.com/pro + +unreachable=License for {0} {1} has not been validated. Check your network connection. + +evaluation= > Using an evaluation license for {0} {1}. + +valid= > Using a valid license for {0} {1}. + +agpl=Using AGPL version of {0} {1}. Commercial licensing options available at vaadin.com/pro
\ No newline at end of file |