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.web; // NOPMD
19
20 import java.io.BufferedWriter;
21 import java.io.File;
22 import java.io.FileWriter;
23 import java.io.IOException;
24 import java.io.OutputStreamWriter;
25 import java.nio.charset.StandardCharsets;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.Map;
29
30 import javax.servlet.http.HttpServletRequest;
31 import javax.servlet.http.HttpServletResponse;
32
33 import net.bull.javamelody.JdbcWrapper;
34 import net.bull.javamelody.Parameter;
35 import net.bull.javamelody.SessionListener;
36 import net.bull.javamelody.internal.common.HttpParameter;
37 import net.bull.javamelody.internal.common.HttpPart;
38 import net.bull.javamelody.internal.common.I18N;
39 import net.bull.javamelody.internal.common.LOG;
40 import net.bull.javamelody.internal.common.Parameters;
41 import net.bull.javamelody.internal.model.Action;
42 import net.bull.javamelody.internal.model.CacheInformations;
43 import net.bull.javamelody.internal.model.Collector;
44 import net.bull.javamelody.internal.model.CollectorServer;
45 import net.bull.javamelody.internal.model.DatabaseInformations;
46 import net.bull.javamelody.internal.model.HeapHistogram;
47 import net.bull.javamelody.internal.model.JCacheInformations;
48 import net.bull.javamelody.internal.model.JavaInformations;
49 import net.bull.javamelody.internal.model.JndiBinding;
50 import net.bull.javamelody.internal.model.MBeanNode;
51 import net.bull.javamelody.internal.model.MBeans;
52 import net.bull.javamelody.internal.model.Period;
53 import net.bull.javamelody.internal.model.ProcessInformations;
54 import net.bull.javamelody.internal.model.Range;
55 import net.bull.javamelody.internal.model.SamplingProfiler.SampledMethod;
56 import net.bull.javamelody.internal.model.SessionInformations;
57 import net.bull.javamelody.internal.model.VirtualMachine;
58 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestParameter;
59 import net.bull.javamelody.internal.web.RequestToMethodMapper.RequestPart;
60 import net.bull.javamelody.internal.web.html.HtmlReport;
61
62 /**
63  * Contrôleur au sens MVC de l'ihm de monitoring pour la partie html.
64  * @author Emeric Vernat
65  */

66 public class HtmlController {
67     static final String HTML_BODY_FORMAT = "htmlbody";
68     private static final boolean CONTENT_SECURITY_POLICY_ENABLED = Parameter.CONTENT_SECURITY_POLICY_ENABLED
69             .getValue() == null || Parameter.CONTENT_SECURITY_POLICY_ENABLED.getValueAsBoolean();
70     private static final String X_FRAME_OPTIONS = Parameter.X_FRAME_OPTIONS.getValue();
71     private static final RequestToMethodMapper<HtmlController> REQUEST_TO_METHOD_MAPPER = new RequestToMethodMapper<>(
72             HtmlController.class);
73     private final HttpCookieManager httpCookieManager = new HttpCookieManager();
74     private final Collector collector;
75     private final CollectorServer collectorServer;
76     private final String messageForReport;
77     private final String anchorNameForRedirect;
78     private HtmlReport htmlReport;
79
80     HtmlController(Collector collector, CollectorServer collectorServer, String messageForReport,
81             String anchorNameForRedirect) {
82         super();
83         assert collector != null;
84         this.collector = collector;
85         this.collectorServer = collectorServer;
86         this.messageForReport = messageForReport;
87         this.anchorNameForRedirect = anchorNameForRedirect;
88     }
89
90     void doHtml(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
91             List<JavaInformations> javaInformationsList) throws IOException {
92         final String part = HttpParameter.PART.getParameterFrom(httpRequest);
93         if (!isFromCollectorServer() && isLocalCollectNeeded(part) && !collector.isStopped()) {
94             // avant de faire l'affichage on fait une collecte, pour que les courbes
95             // et les compteurs par jour soit à jour avec les dernières requêtes,
96             // sauf si c'est un serveur de collecte
97             // ou si la page de monitoring d'une webapp monitorée par un serveur de collecte est appelée par erreur
98             collector.collectLocalContextWithoutErrors();
99         }
100
101         // simple appel de monitoring sans format
102         try (BufferedWriter writer = getWriter(httpResponse)) {
103             final Range range = httpCookieManager.getRange(httpRequest, httpResponse);
104             this.htmlReport = new HtmlReport(collector, collectorServer, javaInformationsList,
105                     range, writer);
106             if (part == null) {
107                 htmlReport.toHtml(messageForReport, anchorNameForRedirect);
108             } else if (HttpPart.THREADS_DUMP.isPart(httpRequest)) {
109                 httpResponse.setContentType("text/plain; charset=UTF-8");
110                 htmlReport.writeThreadsDump();
111             } else {
112                 REQUEST_TO_METHOD_MAPPER.invoke(httpRequest, this);
113             }
114         }
115     }
116
117     static boolean isLocalCollectNeeded(String part) {
118         return part == null || HttpPart.CURRENT_REQUESTS.getName().equals(part)
119                 || HttpPart.GRAPH.getName().equals(part)
120                 || HttpPart.COUNTER_SUMMARY_PER_CLASS.getName().equals(part);
121     }
122
123     public static BufferedWriter getWriter(HttpServletResponse httpResponse) throws IOException {
124         httpResponse.setContentType("text/html; charset=UTF-8");
125         if (CONTENT_SECURITY_POLICY_ENABLED) {
126             final String analyticsId = Parameter.ANALYTICS_ID.getValue();
127             final boolean analyticsEnabled = analyticsId != null && !"disabled".equals(analyticsId);
128             httpResponse.setHeader("Content-Security-Policy",
129                     "default-src 'self'"
130                             + (analyticsEnabled ? " https://ssl.google-analytics.com" : "")
131                             + "; object-src 'none';");
132         }
133         if (X_FRAME_OPTIONS == null) {
134             // default value of X-Frame-Options is SAMEORIGIN
135             httpResponse.setHeader("X-Frame-Options""SAMEORIGIN");
136         } else if (!"ALLOWALL".equals(X_FRAME_OPTIONS)) {
137             httpResponse.setHeader("X-Frame-Options", X_FRAME_OPTIONS);
138         }
139         try {
140             return new BufferedWriter(
141                     new OutputStreamWriter(httpResponse.getOutputStream(), StandardCharsets.UTF_8));
142         } catch (final IllegalStateException e) {
143             // just in caseif httpResponse.getWriter() was already called (for an exception in PrometheusController for example)
144             return new BufferedWriter(httpResponse.getWriter());
145         }
146     }
147
148     @RequestPart(HttpPart.GRAPH)
149     void doRequestGraphAndDetail(@RequestParameter(HttpParameter.GRAPH) String graphName)
150             throws IOException {
151         htmlReport.writeRequestAndGraphDetail(graphName);
152     }
153
154     @RequestPart(HttpPart.USAGES)
155     void doRequestUsages(@RequestParameter(HttpParameter.GRAPH) String graphName)
156             throws IOException {
157         htmlReport.writeRequestUsages(graphName);
158     }
159
160     @RequestPart(HttpPart.CURRENT_REQUESTS)
161     void doAllCurrentRequestsAsPart() throws IOException {
162         htmlReport.writeAllCurrentRequestsAsPart();
163     }
164
165     @RequestPart(HttpPart.THREADS)
166     void doAllThreadsAsPart() throws IOException {
167         htmlReport.writeAllThreadsAsPart();
168     }
169
170     @RequestPart(HttpPart.COUNTER_SUMMARY_PER_CLASS)
171     void doCounterSummaryPerClass(@RequestParameter(HttpParameter.COUNTER) String counterName,
172             @RequestParameter(HttpParameter.GRAPH) String requestId) throws IOException {
173         htmlReport.writeCounterSummaryPerClass(counterName, requestId);
174     }
175
176     @RequestPart(HttpPart.SOURCE)
177     void doSource(@RequestParameter(HttpParameter.CLASS) String className) throws IOException {
178         htmlReport.writeSource(className);
179     }
180
181     @RequestPart(HttpPart.DEPENDENCIES)
182     void doDependencies() throws IOException {
183         htmlReport.writeDependencies();
184     }
185
186     @RequestPart(HttpPart.SESSIONS)
187     void doSessions(@RequestParameter(HttpParameter.SESSION_ID) String sessionId)
188             throws IOException {
189         // par sécurité
190         Action.checkSystemActionsEnabled();
191         final List<SessionInformations> sessionsInformations;
192         if (!isFromCollectorServer()) {
193             if (sessionId == null) {
194                 sessionsInformations = SessionListener.getAllSessionsInformations();
195             } else {
196                 sessionsInformations = Collections.singletonList(
197                         SessionListener.getSessionInformationsBySessionId(sessionId));
198             }
199         } else {
200             sessionsInformations = collectorServer.collectSessionInformations(getApplication(),
201                     sessionId);
202         }
203         if (sessionId == null || sessionsInformations.isEmpty()) {
204             htmlReport.writeSessions(sessionsInformations, messageForReport,
205                     HttpPart.SESSIONS.getName());
206         } else {
207             final SessionInformations sessionInformation = sessionsInformations.get(0);
208             htmlReport.writeSessionDetail(sessionId, sessionInformation);
209         }
210     }
211
212     @RequestPart(HttpPart.HOTSPOTS)
213     void doHotspots() throws IOException {
214         // par sécurité
215         Action.checkSystemActionsEnabled();
216         if (!isFromCollectorServer()) {
217             final List<SampledMethod> hotspots = collector.getHotspots();
218             htmlReport.writeHotspots(hotspots);
219         } else {
220             final List<SampledMethod> hotspots = collectorServer.collectHotspots(getApplication());
221             htmlReport.writeHotspots(hotspots);
222         }
223     }
224
225     @RequestPart(HttpPart.HEAP_HISTO)
226     void doHeapHisto() throws IOException {
227         // par sécurité
228         Action.checkSystemActionsEnabled();
229         final HeapHistogram heapHistogram;
230         try {
231             if (!isFromCollectorServer()) {
232                 heapHistogram = VirtualMachine.createHeapHistogram();
233             } else {
234                 heapHistogram = collectorServer.collectHeapHistogram(getApplication());
235             }
236         } catch (final Exception e) {
237             LOG.warn("heaphisto report failed", e);
238             htmlReport.writeMessageIfNotNull(String.valueOf(e.getMessage()), null);
239             return;
240         }
241         htmlReport.writeHeapHistogram(heapHistogram, messageForReport,
242                 HttpPart.HEAP_HISTO.getName());
243     }
244
245     @RequestPart(HttpPart.PROCESSES)
246     void doProcesses() throws IOException {
247         // par sécurité
248         Action.checkSystemActionsEnabled();
249         try {
250             if (!isFromCollectorServer()) {
251                 final List<ProcessInformations> processInformationsList = ProcessInformations
252                         .buildProcessInformations();
253                 htmlReport.writeProcesses(processInformationsList);
254             } else {
255                 final Map<String, List<ProcessInformations>> processInformationsByTitle = collectorServer
256                         .collectProcessInformations(getApplication());
257                 htmlReport.writeProcesses(processInformationsByTitle);
258             }
259         } catch (final Exception e) {
260             LOG.warn("processes report failed", e);
261             htmlReport.writeMessageIfNotNull(String.valueOf(e.getMessage()), null);
262         }
263     }
264
265     @RequestPart(HttpPart.DATABASE)
266     void doDatabase(@RequestParameter(HttpParameter.REQUEST) String requestIndex)
267             throws IOException {
268         // par sécurité
269         Action.checkSystemActionsEnabled();
270         try {
271             final int index = DatabaseInformations.parseRequestIndex(requestIndex);
272             final DatabaseInformations databaseInformations;
273             if (!isFromCollectorServer()) {
274                 databaseInformations = new DatabaseInformations(index);
275             } else {
276                 databaseInformations = collectorServer.collectDatabaseInformations(getApplication(),
277                         index);
278             }
279             htmlReport.writeDatabase(databaseInformations);
280         } catch (final Exception e) {
281             LOG.warn("database report failed", e);
282             htmlReport.writeMessageIfNotNull(String.valueOf(e.getMessage()), null);
283         }
284     }
285
286     @RequestPart(HttpPart.CONNECTIONS)
287     void doConnections(@RequestParameter(HttpParameter.FORMAT) String format) throws IOException {
288         assert !isFromCollectorServer();
289         // par sécurité
290         Action.checkSystemActionsEnabled();
291         final boolean withoutHeaders = HTML_BODY_FORMAT.equalsIgnoreCase(format);
292         htmlReport.writeConnections(JdbcWrapper.getConnectionInformationsList(), withoutHeaders);
293     }
294
295     @RequestPart(HttpPart.JNDI)
296     void doJndi(@RequestParameter(HttpParameter.PATH) String path) throws IOException {
297         // par sécurité
298         Action.checkSystemActionsEnabled();
299         try {
300             final List<JndiBinding> jndiBindings;
301             if (!isFromCollectorServer()) {
302                 jndiBindings = JndiBinding.listBindings(path);
303             } else {
304                 jndiBindings = collectorServer.collectJndiBindings(getApplication(), path);
305             }
306             htmlReport.writeJndi(jndiBindings, path);
307         } catch (final Exception e) {
308             LOG.warn("jndi report failed", e);
309             htmlReport.writeMessageIfNotNull(String.valueOf(e.getMessage()), null);
310         }
311     }
312
313     @RequestPart(HttpPart.MBEANS)
314     void doMBeans() throws IOException {
315         // par sécurité
316         Action.checkSystemActionsEnabled();
317         try {
318             if (!isFromCollectorServer()) {
319                 final List<MBeanNode> nodes = MBeans.getAllMBeanNodes();
320                 htmlReport.writeMBeans(nodes);
321             } else {
322                 final Map<String, List<MBeanNode>> allMBeans = collectorServer
323                         .collectMBeans(getApplication());
324                 htmlReport.writeMBeans(allMBeans);
325             }
326         } catch (final Exception e) {
327             LOG.warn("mbeans report failed", e);
328             htmlReport.writeMessageIfNotNull(String.valueOf(e.getMessage()), null);
329         }
330     }
331
332     @RequestPart(HttpPart.CRASHES)
333     void doCrashes() throws IOException {
334         Action.checkSystemActionsEnabled();
335         htmlReport.writeCrashes();
336     }
337
338     @RequestPart(HttpPart.SPRING_BEANS)
339     void doSpringBeans() throws IOException {
340         htmlReport.writeSpringContext();
341     }
342
343     @RequestPart(HttpPart.CACHE_KEYS)
344     void doCacheKeys(@RequestParameter(HttpParameter.CACHE_ID) String cacheId,
345             @RequestParameter(HttpParameter.FORMAT) String format) throws IOException {
346         assert !isFromCollectorServer();
347         final CacheInformations cacheInfo = CacheInformations
348                 .buildCacheInformationsWithKeys(cacheId);
349         final boolean withoutHeaders = HTML_BODY_FORMAT.equalsIgnoreCase(format);
350         final String cacheKeysPart = HttpPart.CACHE_KEYS.toString() + '&' + HttpParameter.CACHE_ID
351                 + '=' + I18N.urlEncode(cacheId);
352         htmlReport.writeCacheWithKeys(cacheId, cacheInfo, messageForReport, cacheKeysPart,
353                 withoutHeaders);
354     }
355
356     @RequestPart(HttpPart.JCACHE_KEYS)
357     void doJCacheKeys(@RequestParameter(HttpParameter.CACHE_ID) String cacheId,
358             @RequestParameter(HttpParameter.FORMAT) String format) throws IOException {
359         assert !isFromCollectorServer();
360         final JCacheInformations cacheInfo = JCacheInformations
361                 .buildJCacheInformationsWithKeys(cacheId);
362         final boolean withoutHeaders = HTML_BODY_FORMAT.equalsIgnoreCase(format);
363         final String jcacheKeysPart = HttpPart.JCACHE_KEYS.toString() + '&' + HttpParameter.CACHE_ID
364                 + '=' + I18N.urlEncode(cacheId);
365         htmlReport.writeJCacheWithKeys(cacheId, cacheInfo, messageForReport, jcacheKeysPart,
366                 withoutHeaders);
367     }
368
369     @RequestPart(HttpPart.HASH_PASSWORD)
370     void doHashPassword(@RequestParameter(HttpParameter.ALGORITHM) String algorithm,
371             @RequestParameter(HttpParameter.REQUEST) String password) throws IOException {
372         htmlReport.writeHashPassword(algorithm, password);
373     }
374
375     void writeHtmlToLastShutdownFile() {
376         try {
377             final File dir = Parameters.getStorageDirectory(getApplication());
378             if (!dir.mkdirs() && !dir.exists()) {
379                 throw new IOException("JavaMelody directory can't be created: " + dir.getPath());
380             }
381             final File lastShutdownFile = new File(dir, "last_shutdown.html");
382             try (BufferedWriter writer = new BufferedWriter(new FileWriter(lastShutdownFile))) {
383                 final JavaInformations javaInformations = new JavaInformations(
384                         Parameters.getServletContext(), true);
385                 // on pourrait faire I18N.bindLocale(Locale.getDefault()), mais cela se fera tout seul
386                 final HtmlReport myHtmlReport = new HtmlReport(collector, collectorServer,
387                         Collections.singletonList(javaInformations), Period.JOUR, writer);
388                 myHtmlReport.writeLastShutdown();
389             }
390         } catch (final IOException e) {
391             LOG.warn("exception while writing the last shutdown report", e);
392         }
393     }
394
395     private String getApplication() {
396         return collector.getApplication();
397     }
398
399     private boolean isFromCollectorServer() {
400         return collectorServer != null;
401     }
402 }
403