summaryrefslogtreecommitdiffstats
path: root/client-compiler/src/com/vaadin/tools/CvalChecker.java
blob: 53cbce798e2da9b6b25d9f980f3b8ff6b062d453 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
/*
 * 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.File;
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.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.prefs.Preferences;

import elemental.json.JsonException;
import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;
import org.apache.commons.io.IOUtils;

/**
 * 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.
 * 
 * @since 7.3
 */
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.getObject(k);
                } else if (clz == Integer.class) {
                    ret = Integer.valueOf((int) o.getNumber(k));
                } else if (clz == Date.class) {
                    ret = new Date((long) o.getNumber(k));
                } else if (clz == Boolean.class) {
                    ret = o.getBoolean(k);
                }
            } catch (JsonException e) {
            }
            return (T) ret;
        }

        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) {
            o.put("expiredEpoch", expiredEpoch.getTime());
        }

        public void setMessage(String msg) {
            o.put("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 = JsonUtil.parse(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 = System.getProperty("user.home");
            for (URL url : new URL[] {
                    new File(userHome, dotLicenseName).toURI().toURL(),
                    new File(userHome, licenseName).toURI().toURL(),
                    URL.class.getResource("/" + dotLicenseName),
                    URL.class.getResource("/" + licenseName) }) {
                if (url != null) {
                    try {
                        key = readKeyFromFile(url,
                                computeMajorVersion(productVersion));
                        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);
    }

    String readKeyFromFile(URL url, int majorVersion) throws IOException {
        String majorVersionStr = String.valueOf(majorVersion);
        List<String> lines = IOUtils.readLines(url.openStream());
        String defaultKey = null;
        for (String line : lines) {
            String[] parts = line.split("\\s*=\\s*");
            if (parts.length < 2) {
                defaultKey = parts[0].trim();
            }
            if (parts[0].equals(majorVersionStr)) {
                return parts[1].trim();
            }
        }
        return defaultKey;
    }

    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);
    }
}