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.ByteArrayOutputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.io.Serializable;
25 import java.net.HttpURLConnection;
26 import java.net.URL;
27 import java.net.URLConnection;
28 import java.nio.charset.StandardCharsets;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.Map;
32 import java.util.zip.GZIPInputStream;
33
34 import javax.servlet.http.HttpServletRequest;
35 import javax.servlet.http.HttpServletResponse;
36
37 import net.bull.javamelody.internal.common.HttpParameter;
38 import net.bull.javamelody.internal.common.HttpPart;
39 import net.bull.javamelody.internal.common.I18N;
40 import net.bull.javamelody.internal.common.InputOutput;
41 import net.bull.javamelody.internal.common.LOG;
42 import net.bull.javamelody.internal.common.Parameters;
43
44 /**
45  * Classe permettant d'ouvrir une connexion http et de récupérer les objets java sérialisés dans la réponse.
46  * Utilisée dans le serveur de collecte.
47  * @author Emeric Vernat
48  */

49 public class LabradorRetriever {
50     /** Timeout des connections serveur en millisecondes (0 : pas de timeout). */
51     private static final int CONNECTION_TIMEOUT = 20000;
52
53     /** Timeout de lecture des connections serveur en millisecondes (0 : pas de timeout). */
54     private static final int READ_TIMEOUT = 60000;
55
56     private final URL url;
57     private final Map<String, String> headers;
58
59     // Rq: les configurations suivantes sont celles par défaut, on ne les change pas
60     //        static { HttpURLConnection.setFollowRedirects(true);
61     //        URLConnection.setDefaultAllowUserInteraction(true); }
62
63     private static class CounterInputStream extends InputStream {
64         private final InputStream inputStream;
65         private int dataLength;
66
67         CounterInputStream(InputStream inputStream) {
68             super();
69             this.inputStream = inputStream;
70         }
71
72         int getDataLength() {
73             return dataLength;
74         }
75
76         @Override
77         public int read() throws IOException {
78             final int result = inputStream.read();
79             if (result != -1) {
80                 dataLength += 1;
81             }
82             return result;
83         }
84
85         @Override
86         public int read(byte[] bytes) throws IOException {
87             final int result = inputStream.read(bytes);
88             if (result != -1) {
89                 dataLength += result;
90             }
91             return result;
92         }
93
94         @Override
95         public int read(byte[] bytes, int off, int len) throws IOException {
96             final int result = inputStream.read(bytes, off, len);
97             if (result != -1) {
98                 dataLength += result;
99             }
100             return result;
101         }
102
103         @Override
104         public long skip(long n) throws IOException {
105             return inputStream.skip(n);
106         }
107
108         @Override
109         public int available() throws IOException {
110             return inputStream.available();
111         }
112
113         @Override
114         public void close() throws IOException {
115             inputStream.close();
116         }
117
118         @Override
119         public boolean markSupported() {
120             return false// Assume that mark is NO good for a counterInputStream
121         }
122     }
123
124     public LabradorRetriever(URL url) {
125         this(url, null);
126     }
127
128     public LabradorRetriever(URL url, Map<String, String> headers) {
129         super();
130         assert url != null;
131         this.url = url;
132         this.headers = headers;
133     }
134
135     <T> T call() throws IOException {
136         if (shouldMock()) {
137             // ce générique doit être conservé pour la compilation javac en intégration continue
138             return this.<T> createMockResultOfCall();
139         }
140         final long start = System.currentTimeMillis();
141         int dataLength = -1;
142         try {
143             final URLConnection connection = openConnection();
144             // pour traductions (si on vient de CollectorServlet.forwardActionAndUpdateData,
145             // cela permet d'avoir les messages dans la bonne langue)
146             connection.setRequestProperty("Accept-Language", I18N.getCurrentLocale().getLanguage());
147
148             // Rq: on ne gère pas ici les éventuels cookie de session http,
149             // puisque le filtre de monitoring n'est pas censé créer des sessions
150             //        if (cookie != null) { connection.setRequestProperty("Cookie", cookie); }
151
152             connection.connect();
153
154             //        final String setCookie = connection.getHeaderField("Set-Cookie");
155             //        if (setCookie != null) { cookie = setCookie; }
156
157             final CounterInputStream counterInputStream = new CounterInputStream(
158                     connection.getInputStream());
159
160             final T result;
161             try {
162                 @SuppressWarnings("unchecked")
163                 final T tmp = (T) read(connection, counterInputStream);
164                 result = tmp;
165             } finally {
166                 counterInputStream.close();
167                 dataLength = counterInputStream.getDataLength();
168             }
169             LOG.debug("read on " + url + " : " + result);
170
171             if (result instanceof RuntimeException) {
172                 throw (RuntimeException) result;
173             } else if (result instanceof Error) {
174                 throw (Error) result;
175             } else if (result instanceof IOException) {
176                 throw (IOException) result;
177             } else if (result instanceof Exception) {
178                 throw createIOException((Exception) result);
179             }
180             return result;
181         } catch (final ClassNotFoundException e) {
182             throw createIOException(e);
183         } finally {
184             LOG.info("http call done in " + (System.currentTimeMillis() - start) + " ms with "
185                     + dataLength / 1024 + " KB read for " + url);
186         }
187     }
188
189     private static IOException createIOException(Exception e) {
190         // Rq: le constructeur de IOException avec message et cause n'existe qu'en jdk 1.6
191         return new IOException(e.getMessage(), e);
192     }
193
194     public void copyTo(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
195             throws IOException {
196         if (shouldMock()) {
197             return;
198         }
199         assert httpRequest != null;
200         assert httpResponse != null;
201         final long start = System.currentTimeMillis();
202         int dataLength = -1;
203         try {
204             final URLConnection connection = openConnection();
205             // pour traductions
206             connection.setRequestProperty("Accept-Language",
207                     httpRequest.getHeader("Accept-Language"));
208             connection.connect();
209             httpResponse.setContentType(connection.getContentType());
210             // Content-Disposition pour téléchargement hs_err_pid par exemple
211             final String contentDisposition = connection.getHeaderField("Content-Disposition");
212             if (contentDisposition != null) {
213                 httpResponse.setHeader("Content-Disposition", contentDisposition);
214             }
215             final OutputStream output = httpResponse.getOutputStream();
216             dataLength = pump(output, connection);
217         } finally {
218             LOG.info("http call done in " + (System.currentTimeMillis() - start) + " ms with "
219                     + dataLength / 1024 + " KB read for " + url);
220         }
221     }
222
223     void downloadTo(OutputStream output) throws IOException {
224         if (shouldMock()) {
225             return;
226         }
227         assert output != null;
228         final long start = System.currentTimeMillis();
229         int dataLength = -1;
230         try {
231             final URLConnection connection = openConnection();
232             connection.connect();
233
234             dataLength = pump(output, connection);
235         } finally {
236             LOG.info("http call done in " + (System.currentTimeMillis() - start) + " ms with "
237                     + dataLength / 1024 + " KB read for " + url);
238         }
239     }
240
241     private int pump(OutputStream output, URLConnection connection) throws IOException {
242         final int dataLength;
243         final CounterInputStream counterInputStream = new CounterInputStream(
244                 connection.getInputStream());
245         InputStream input = counterInputStream;
246         try {
247             if ("gzip".equals(connection.getContentEncoding())) {
248                 input = new GZIPInputStream(input);
249             }
250             InputOutput.pump(input, output);
251         } finally {
252             try {
253                 input.close();
254             } finally {
255                 close(connection);
256                 dataLength = counterInputStream.getDataLength();
257             }
258         }
259         return dataLength;
260     }
261
262     public void post(ByteArrayOutputStream payload) throws IOException {
263         final HttpURLConnection connection = (HttpURLConnection) openConnection();
264         connection.setRequestMethod("POST");
265         connection.setDoOutput(true);
266
267         if (payload != null) {
268             final OutputStream outputStream = connection.getOutputStream();
269             payload.writeTo(outputStream);
270             outputStream.flush();
271         }
272
273         final int status = connection.getResponseCode();
274         if (status >= HttpURLConnection.HTTP_BAD_REQUEST) {
275             final String error = InputOutput.pumpToString(connection.getErrorStream(),
276                     StandardCharsets.UTF_8);
277             final String msg = "Error connecting to " + url + '(' + status + "): " + error;
278             throw new IOException(msg);
279         }
280         connection.disconnect();
281     }
282
283     /**
284      * Ouvre la connection http.
285      * @return Object
286      * @throws IOException   Exception de communication
287      */

288     private URLConnection openConnection() throws IOException {
289         final URLConnection connection = url.openConnection();
290         connection.setUseCaches(false);
291         if (CONNECTION_TIMEOUT > 0) {
292             connection.setConnectTimeout(CONNECTION_TIMEOUT);
293         }
294         if (READ_TIMEOUT > 0) {
295             connection.setReadTimeout(READ_TIMEOUT);
296         }
297         // grâce à cette propriété, l'application retournera un flux compressé si la taille
298         // dépasse x Ko
299         connection.setRequestProperty("Accept-Encoding""gzip");
300         if (headers != null) {
301             for (final Map.Entry<String, String> entry : headers.entrySet()) {
302                 connection.setRequestProperty(entry.getKey(), entry.getValue());
303             }
304         }
305         if (url.getUserInfo() != null) {
306             final String authorization = Base64Coder.encodeString(url.getUserInfo());
307             connection.setRequestProperty("Authorization""Basic " + authorization);
308         }
309         return connection;
310     }
311
312     /**
313      * Lit l'objet renvoyé dans le flux de réponse.
314      * @return Object
315      * @param connection URLConnection
316      * @param inputStream InputStream à utiliser à la place de connection.getInputStream()
317      * @throws IOException   Exception de communication
318      * @throws ClassNotFoundException   Une classe transmise par le serveur n'a pas été trouvée
319      */

320     private static Serializable read(URLConnection connection, InputStream inputStream)
321             throws IOException, ClassNotFoundException {
322         InputStream input = inputStream;
323         try {
324             if ("gzip".equals(connection.getContentEncoding())) {
325                 // si la taille du flux dépasse x Ko et que l'application a retourné un flux compressé
326                 // alors on le décompresse
327                 input = new GZIPInputStream(input);
328             }
329             final String contentType = connection.getContentType();
330             final TransportFormat transportFormat;
331             if (contentType != null) {
332                 if (contentType.startsWith("text/xml")) {
333                     transportFormat = TransportFormat.XML;
334                 } else if (contentType.startsWith("text/html")) {
335                     throw new IllegalStateException(
336                             "Unexpected html content type, maybe not authentified");
337                 } else {
338                     transportFormat = TransportFormat.SERIALIZED;
339                 }
340             } else {
341                 transportFormat = TransportFormat.SERIALIZED;
342             }
343             return transportFormat.readSerializableFrom(input);
344         } finally {
345             try {
346                 input.close();
347             } finally {
348                 close(connection);
349             }
350         }
351     }
352
353     private static void close(URLConnection connection) throws IOException {
354         // ce close doit être fait en finally
355         // (http://java.sun.com/j2se/1.5.0/docs/guide/net/http-keepalive.html)
356         connection.getInputStream().close();
357
358         if (connection instanceof HttpURLConnection) {
359             final InputStream error = ((HttpURLConnection) connection).getErrorStream();
360             if (error != null) {
361                 error.close();
362             }
363         }
364     }
365
366     private static boolean shouldMock() {
367         return Boolean.parseBoolean(
368                 System.getProperty(Parameters.PARAMETER_SYSTEM_PREFIX + "mockLabradorRetriever"));
369     }
370
371     // bouchon pour tests unitaires
372     @SuppressWarnings("unchecked")
373     private <T> T createMockResultOfCall() throws IOException {
374         final Object result;
375         final String request = url.toString();
376         if (!request.contains(HttpParameter.PART.getName() + '=')
377                 && !request.contains(HttpParameter.JMX_VALUE.getName())
378                 || request.contains(HttpPart.DEFAULT_WITH_CURRENT_REQUESTS.getName())) {
379             final String message = request.contains("/test2") ? null
380                     : "ceci est message pour le rapport";
381             result = Arrays.asList(new Counter(Counter.HTTP_COUNTER_NAME, null),
382                     new Counter("services"null), new Counter(Counter.ERROR_COUNTER_NAME, null),
383                     new JavaInformations(nulltrue), message);
384         } else {
385             result = LabradorMock.createMockResultOfPartCall(request);
386         }
387         return (T) result;
388     }
389
390     private static class LabradorMock { // NOPMD
391         // CHECKSTYLE:OFF
392         static Object createMockResultOfPartCall(String request) throws IOException {
393             // CHECKSTYLE:ON
394             final Object result;
395             if (request.contains(HttpPart.SESSIONS.getName())
396                     && request.contains(HttpParameter.SESSION_ID.getName())) {
397                 result = null;
398             } else if (request.contains(HttpPart.SESSIONS.getName())
399                     || request.contains(HttpPart.PROCESSES.getName())
400                     || request.contains(HttpPart.JNDI.getName())
401                     || request.contains(HttpPart.CONNECTIONS.getName())
402                     || request.contains(HttpPart.MBEANS.getName())
403                     || request.contains(HttpPart.HOTSPOTS.getName())) {
404                 result = Collections.emptyList();
405             } else if (request.contains(HttpPart.CURRENT_REQUESTS.getName())
406                     || request.contains(HttpPart.WEBAPP_VERSIONS.getName())
407                     || request.contains(HttpPart.DEPENDENCIES.getName())) {
408                 result = Collections.emptyMap();
409             } else if (request.contains(HttpPart.DATABASE.getName())) {
410                 try {
411                     result = new DatabaseInformations(0);
412                 } catch (final Exception e) {
413                     throw new IllegalStateException(e);
414                 }
415             } else if (request.contains(HttpPart.HEAP_HISTO.getName())) {
416                 try (InputStream input = LabradorMock.class.getResourceAsStream("/heaphisto.txt")) {
417                     result = new HeapHistogram(input, false);
418                 }
419             } else if (request.contains(HttpPart.LAST_VALUE.getName())) {
420                 result = -1d;
421             } else if (request.contains(HttpParameter.JMX_VALUE.getName())) {
422                 result = "-1";
423             } else if (request.contains(HttpPart.JVM.getName())) {
424                 result = Collections
425                         .singletonList(new JavaInformations(Parameters.getServletContext(), false));
426             } else {
427                 result = null;
428             }
429             return result;
430         }
431
432     }
433 }
434