1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.net.HttpURLConnection;
23 import java.net.URL;
24 import java.security.MessageDigest;
25 import java.security.NoSuchAlgorithmException;
26 import java.sql.Connection;
27 import java.sql.DatabaseMetaData;
28 import java.sql.DriverManager;
29 import java.sql.SQLException;
30 import java.util.ArrayList;
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.Properties;
37 import java.util.Timer;
38 import java.util.TimerTask;
39
40 import javax.sql.DataSource;
41
42 import net.bull.javamelody.JdbcWrapper;
43 import net.bull.javamelody.Parameter;
44 import net.bull.javamelody.internal.common.Parameters;
45
46 /**
47  * Checks for update of javamelody version.
48  * @author Emeric Vernat
49  */

50 public final class UpdateChecker {
51     static final String COLLECTOR_SERVER_APPLICATION_TYPE = "Collector server";
52
53     private static final char SEPARATOR = '|';
54
55     private static final String SERVER_URL = "http://javamelody.org/usage/stats";
56
57     private static String newJavamelodyVersion;
58
59     private final Collector collector;
60
61     private final String applicationType;
62
63     private final String serverUrl;
64
65     private UpdateChecker(final Collector collector, final String applicationType,
66             final String serverUrl) {
67         super();
68         assert applicationType != null;
69         assert collector != null || COLLECTOR_SERVER_APPLICATION_TYPE.equals(applicationType);
70         this.collector = collector;
71         this.applicationType = applicationType;
72         this.serverUrl = serverUrl;
73     }
74
75     public static void init(Timer timer, Collector collector, String applicationType) {
76         if (!Parameter.UPDATE_CHECK_DISABLED.getValueAsBoolean()) {
77             final UpdateChecker updateChecker = new UpdateChecker(collector, applicationType,
78                     SERVER_URL);
79             final TimerTask updateCheckerTimerTask = new TimerTask() {
80                 @Override
81                 public void run() {
82                     try {
83                         updateChecker.checkForUpdate();
84                     } catch (final Throwable t) { // NOPMD
85                         // probablement pas connecté à Internet, tant pis
86                     }
87                 }
88             };
89             // on laisse 10 minutes pour que la webapp démarre tranquillement, puis toutes les 24h
90             timer.scheduleAtFixedRate(updateCheckerTimerTask, 10L * 60 * 1000,
91                     24L * 60 * 60 * 1000);
92         }
93     }
94
95     static UpdateChecker createForTest(final Collector collector, final String applicationType,
96             final String serverUrl) {
97         return new UpdateChecker(collector, applicationType, serverUrl);
98     }
99
100     public static String getNewJavamelodyVersion() {
101         return newJavamelodyVersion;
102     }
103
104     private static void setNewJavamelodyVersion(final String javamelodyVersion) {
105         newJavamelodyVersion = javamelodyVersion;
106     }
107
108     void checkForUpdate() throws IOException {
109         final String anonymousData = getAnonymousData();
110         final HttpURLConnection connection = (HttpURLConnection) new URL(serverUrl)
111                 .openConnection();
112         connection.setUseCaches(false);
113         connection.setDoOutput(true);
114         connection.setRequestMethod("POST");
115         connection.setConnectTimeout(60000);
116         connection.setReadTimeout(60000);
117         connection.setRequestProperty("data", anonymousData);
118         connection.connect();
119
120         final Properties properties = new Properties();
121         try (InputStream input = connection.getInputStream()) {
122             properties.load(input);
123         }
124         final String javamelodyVersion = properties.getProperty("version");
125         if (javamelodyVersion != null && Parameters.JAVAMELODY_VERSION != null
126                 && javamelodyVersion.compareTo(Parameters.JAVAMELODY_VERSION) > 0) {
127             setNewJavamelodyVersion(javamelodyVersion);
128         }
129     }
130
131     private String getAnonymousData() throws IOException {
132         final JavaInformations javaInformations = new JavaInformations(
133                 Parameters.getServletContext(), true);
134         // compute a hash number as unique id
135         final String uniqueId = hash(
136                 Parameters.getHostAddress() + '_' + javaInformations.getContextPath());
137         final String javamelodyVersion = Parameters.JAVAMELODY_VERSION;
138         final String serverInfo = javaInformations.getServerInfo();
139         final String javaVersion = javaInformations.getJavaVersion();
140         final String jvmVersion = javaInformations.getJvmVersion();
141         final String maxMemory = String
142                 .valueOf(javaInformations.getMemoryInformations().getMaxMemory() / 1024 / 1024);
143         final String availableProcessors = String
144                 .valueOf(javaInformations.getAvailableProcessors());
145         final String os = javaInformations.getOS();
146         final String databases = getDatabasesUsed();
147         final String countersUsed = getCountersUsed();
148         final String parametersUsed = getParametersUsed();
149         final String featuresUsed = getFeaturesUsed(javaInformations);
150         final String locale = Locale.getDefault().toString();
151         final long usersMean = getUsersMean();
152         final int collectorApplications;
153         if (COLLECTOR_SERVER_APPLICATION_TYPE.equals(applicationType)) {
154             collectorApplications = Parameters.getCollectorUrlsByApplications().size();
155         } else {
156             collectorApplications = -1;
157         }
158
159         return "{uniqueId=" + encode(uniqueId) + ", serverInfo=" + encode(serverInfo)
160                 + ", javamelodyVersion=" + encode(javamelodyVersion) + ", applicationType="
161                 + encode(applicationType) + ", javaVersion=" + encode(javaVersion) + ", jvmVersion="
162                 + encode(jvmVersion) + ", maxMemory=" + encode(maxMemory) + ", availableProcessors="
163                 + encode(availableProcessors) + ", os=" + encode(os) + ", databases="
164                 + encode(databases) + ", countersUsed=" + encode(countersUsed) + ", parametersUsed="
165                 + encode(parametersUsed) + ", featuresUsed=" + encode(featuresUsed) + ", locale="
166                 + encode(locale) + ", usersMean=" + usersMean + ", collectorApplications="
167                 + collectorApplications + '}';
168     }
169
170     private long getUsersMean() throws IOException {
171         if (collector != null) {
172             final JRobin httpSessionsJRobin = collector.getJRobin("httpSessions");
173             if (httpSessionsJRobin != null) {
174                 final double usersMean = httpSessionsJRobin.getMeanValue(getYesterdayRange());
175                 // round to closest integer
176                 return Math.round(usersMean);
177             }
178         }
179         return 0;
180     }
181
182     private Range getYesterdayRange() {
183         final Calendar calendar = Calendar.getInstance();
184         calendar.set(Calendar.HOUR_OF_DAY, 0);
185         calendar.set(Calendar.MINUTE, 0);
186         calendar.set(Calendar.SECOND, 0);
187         calendar.set(Calendar.MILLISECOND, 0);
188         final Date endDate = calendar.getTime();
189         calendar.add(Calendar.DAY_OF_YEAR, -1);
190         final Date startDate = calendar.getTime();
191         return Range.createCustomRange(startDate, endDate);
192     }
193
194     private String getParametersUsed() {
195         final StringBuilder sb = new StringBuilder();
196         for (final Parameter parameter : Parameter.values()) {
197             final String value = parameter.getValue();
198             if (value != null) {
199                 if (sb.length() != 0) {
200                     sb.append(SEPARATOR);
201                 }
202                 sb.append(parameter.getCode());
203             }
204         }
205         return sb.toString();
206     }
207
208     private String getCountersUsed() {
209         if (collector == null) {
210             return "";
211         }
212         try {
213             final List<Counter> counters = collector
214                     .getRangeCountersToBeDisplayed(Period.TOUT.getRange());
215             final StringBuilder sb = new StringBuilder();
216             for (final Counter counter : counters) {
217                 if (sb.length() != 0) {
218                     sb.append(SEPARATOR);
219                 }
220                 sb.append(counter.getName());
221             }
222             return sb.toString();
223         } catch (final IOException e) {
224             return e.getClass().getSimpleName();
225         }
226     }
227
228     private String getFeaturesUsed(JavaInformations javaInformations) {
229         final List<String> features = new ArrayList<>();
230         if (Parameters.isPdfEnabled()) {
231             features.add("pdf");
232         }
233         if (javaInformations.isCacheEnabled()) {
234             features.add("caches");
235         }
236         if (javaInformations.isJobEnabled()) {
237             features.add("jobs");
238         }
239         if (features.isEmpty()) {
240             return "";
241         }
242         final StringBuilder sb = new StringBuilder();
243         for (final String s : features) {
244             if (sb.length() != 0) {
245                 sb.append(SEPARATOR);
246             }
247             sb.append(s);
248         }
249         return sb.toString();
250     }
251
252     private String getDatabasesUsed() {
253         if (Parameters.isNoDatabase()) {
254             return "";
255         }
256         final StringBuilder result = new StringBuilder();
257         try {
258             if (Parameters.getLastConnectUrl() != null) {
259                 final Connection connection = DriverManager.getConnection(
260                         Parameters.getLastConnectUrl(), Parameters.getLastConnectInfo());
261                 connection.setAutoCommit(false);
262                 return getDatabaseInfo(connection);
263             }
264
265             final Map<String, DataSource> dataSources = JdbcWrapper.getJndiAndSpringDataSources();
266             for (final DataSource dataSource : dataSources.values()) {
267                 final Connection connection = dataSource.getConnection();
268                 if (result.length() > 0) {
269                     result.append(SEPARATOR);
270                 }
271                 result.append(getDatabaseInfo(connection));
272             }
273         } catch (final Exception e) {
274             result.append(e);
275         }
276         return result.toString();
277     }
278
279     private String getDatabaseInfo(final Connection connection) throws SQLException {
280         try {
281             final DatabaseMetaData metaData = connection.getMetaData();
282             return metaData.getDatabaseProductName() + ' ' + metaData.getDatabaseProductVersion();
283         } finally {
284             connection.close();
285         }
286     }
287
288     private static String encode(final String s) {
289         if (s != null) {
290             return "\"" + s.replace("\\", "\\\\").replace("\"""\\\"").replace('\n', ' ')
291                     .replace('\r', ' ') + "\"";
292         }
293         return null;
294     }
295
296     private static MessageDigest getMessageDigestInstance() {
297         // SHA1 est un algorithme de hashage qui évite les conflits à 2^80 près entre
298         // les identifiants supposés uniques (SHA1 est mieux que MD5 qui est mieux que CRC32).
299         try {
300             return MessageDigest.getInstance("SHA-1");
301         } catch (final NoSuchAlgorithmException e) {
302             // ne peut arriver car SHA1 est un algorithme disponible par défaut dans le JDK Sun
303             throw new IllegalStateException(e);
304         }
305     }
306
307     private static String hash(String value) {
308         final MessageDigest messageDigest = getMessageDigestInstance();
309         messageDigest.update(value.getBytes());
310         final byte[] digest = messageDigest.digest();
311
312         final StringBuilder sb = new StringBuilder(digest.length * 2);
313         // encodage en chaîne hexadécimale,
314         // puisque les caractères bizarres ne peuvent être utilisés sur un système de fichiers
315         int j;
316         for (final byte element : digest) {
317             j = element < 0 ? 256 + element : element;
318             if (j < 16) {
319                 sb.append('0');
320             }
321             sb.append(Integer.toHexString(j));
322         }
323
324         return sb.toString();
325     }
326 }
327