View Javadoc

1   /*******************************************************************************
2    * Copyright (c) 2007, 2014 Massimiliano Ziccardi
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *******************************************************************************/
16  package it.jnrpe.plugin;
17  
18  import it.jnrpe.ICommandLine;
19  import it.jnrpe.Status;
20  import it.jnrpe.plugin.utils.HttpUtils;
21  import it.jnrpe.plugin.utils.Utils;
22  import it.jnrpe.plugins.Metric;
23  import it.jnrpe.plugins.MetricGatheringException;
24  import it.jnrpe.plugins.PluginBase;
25  import it.jnrpe.utils.BadThresholdException;
26  import it.jnrpe.utils.thresholds.ThresholdsEvaluatorBuilder;
27  
28  import java.io.UnsupportedEncodingException;
29  import java.math.BigDecimal;
30  import java.net.HttpURLConnection;
31  import java.net.MalformedURLException;
32  import java.net.URL;
33  import java.net.URLEncoder;
34  import java.security.SecureRandom;
35  import java.security.cert.Certificate;
36  import java.security.cert.CertificateException;
37  import java.security.cert.X509Certificate;
38  import java.util.ArrayList;
39  import java.util.Collection;
40  import java.util.Date;
41  import java.util.List;
42  import java.util.Properties;
43  import java.util.regex.Pattern;
44  
45  import javax.net.ssl.HostnameVerifier;
46  import javax.net.ssl.HttpsURLConnection;
47  import javax.net.ssl.KeyManager;
48  import javax.net.ssl.SSLContext;
49  import javax.net.ssl.SSLSession;
50  import javax.net.ssl.TrustManager;
51  import javax.net.ssl.X509TrustManager;
52  
53  import org.apache.commons.codec.binary.Base64;
54  
55  /**
56   * This plugin tests the HTTP service on the specified host. It can test normal
57   * (http) and secure (https) servers, follow redirects, search for strings and
58   * regular expressions on page results and check connection times.
59   * 
60   * @author Frederico Campos
61   * 
62   */
63  
64  public class CheckHttp extends PluginBase {
65  
66  	/**
67  	 * Default HTTP port.
68  	 */
69  	private static final String DEFAULT_PORT = "80";
70  
71  	/**
72  	 * Default HTTPS port.
73  	 */
74  	private static final String DEFAULT_SSL_PORT = "443";
75  
76  	/**
77  	 * Default timeout.
78  	 */
79  	private static final int DEFAULT_TIMEOUT = 30;
80  
81  	/**
82  	 * Default http path.
83  	 */
84  	private static final String DEFAULT_PATH = "/";
85  
86  	/**
87  	 * Default HTTP method.
88  	 */
89  	private static final String DEFAULT_METHOD = "GET";
90  
91  	/**
92  	 * Default user agent.
93  	 */
94  	private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 "
95  			+ "(KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36";
96  
97  	private static final String CHARSET = "UTF-8";
98  
99  	/**
100 	 * Configures the threshold evaluator. This plugin supports both the legacy
101 	 * threshold format and the new format specification.
102 	 * 
103 	 * @param thrb
104 	 *            - the evaluator to be configured
105 	 * @param cl
106 	 *            - the received command line
107 	 * @throws BadThresholdException
108 	 *             - if the threshold can't be parsed
109 	 */
110 	@Override
111 	public final void configureThresholdEvaluatorBuilder(
112 			final ThresholdsEvaluatorBuilder thrb, final ICommandLine cl)
113 			throws BadThresholdException {
114 		thrb.withLegacyThreshold("time", null, cl.getOptionValue("warning"),
115 				cl.getOptionValue("critical"));
116 		if (cl.hasOption("regex")) {
117 			if (cl.hasOption("invert-regex")) {
118 				// invert-regex: CRITICAL value if regex is found (true = 1)
119 				thrb.withLegacyThreshold("invert-regex", null, null, "1");
120 			} else {
121 				// WARNING if regex not found (false = 0)
122 				thrb.withLegacyThreshold("regex", null, null, "0");
123 			}
124 		}
125 		if (cl.hasOption("expect")) {
126 			// WARNING if expected string not found (false = 0)
127 			thrb.withLegacyThreshold("expect", null, "0", null);
128 		}
129 		if (cl.hasOption("string")) {
130 			// WARNING if expected string not found (false = 0)
131 			thrb.withLegacyThreshold("string", null, "0", null);
132 		}
133 		if (cl.hasOption("onredirect")) {
134 			String redirect = cl.getOptionValue("onredirect").toUpperCase();
135 			if ("OK".equals(redirect)) {
136 				thrb.withLegacyThreshold("onredirect", "1:", null, null);
137 			} else if ("CRITICAL".equals(redirect)) {
138 				thrb.withLegacyThreshold("onredirect", null, null, "1:");
139 			} else if ("WARNING".equals(redirect)) {
140 				thrb.withLegacyThreshold("onredirect", null, "1:", null);
141 			}
142 		}
143 		if (cl.hasOption("certificate")) {
144 			String ok = cl.getOptionValue("certificate");
145 			thrb.withLegacyThreshold("certificate", ok, null, null);
146 		}
147 	}
148 
149 	/**
150 	 * Execute and gather metrics.
151 	 * 
152 	 * @param cl
153 	 *            - The command line parameters
154 	 * @throws MetricGatheringException
155 	 *             - If any error occurs during metric gathering process
156 	 * @return the gathered metrics
157 	 */
158 	@Override
159 	public final Collection<Metric> gatherMetrics(final ICommandLine cl)
160 			throws MetricGatheringException {
161 		List<Metric> metrics = new ArrayList<Metric>();
162 		String hostname = cl.getOptionValue("hostname");
163 		if (hostname == null) {
164 			throw new MetricGatheringException("No hostname specified.",
165 					Status.WARNING, null);
166 		}
167 
168 		String port = cl.getOptionValue("port", DEFAULT_PORT);
169 		String path = cl.getOptionValue("url", DEFAULT_PATH);
170 		String method = cl.getOptionValue("method", DEFAULT_METHOD);
171 
172 		int timeout = DEFAULT_TIMEOUT;
173 		if (cl.hasOption("post")) {
174 			method = "POST";
175 		}
176 		boolean ssl = false;
177 		if (cl.hasOption("ssl") || cl.getOptionValue("certificate") != null) {
178 			port = cl.getOptionValue("ssl", DEFAULT_SSL_PORT);
179 			ssl = true;
180 		}
181 
182 		if (cl.hasOption("timeout")) {
183 			try {
184 				timeout = Integer.parseInt(cl.getOptionValue("timeout"));
185 			} catch (NumberFormatException e) {
186 				throw new MetricGatheringException(
187 						"Invalid numeric value for timeout.", Status.CRITICAL,
188 						e);
189 			}
190 		}
191 		if (!path.startsWith("/")) {
192 			path = "/" + path;
193 		}
194 		if (hostname.endsWith("/")) {
195 			hostname = hostname.substring(0, hostname.length() - 1);
196 		}
197 
198 		long then = System.currentTimeMillis();
199 
200 		String response = getHttpResponse(cl, hostname, port, method, path,
201 				timeout, ssl, metrics);
202 		int elapsed = (int) Utils.milliToSec(System.currentTimeMillis() - then);
203 		if (response != null) {
204 			metrics.addAll(analyzeResponse(cl, response, elapsed));
205 		}
206 		return metrics;
207 	}
208 
209 	/**
210 	 * Do the actual http request and return the response string.
211 	 * 
212 	 * @param cl
213 	 *            - The received command line
214 	 * @param hostname
215 	 *            - The server hostname
216 	 * @param port
217 	 *            - The server port
218 	 * @param method
219 	 *            - The HTTP method
220 	 * @param path
221 	 *            - The connection path
222 	 * @param timeout
223 	 *            - The timeout
224 	 * @param ssl
225 	 *            - if SSL must be used
226 	 * @param metrics
227 	 *            - This list will be filled with the gathered metrics
228 	 * @return - the response
229 	 * @throws MetricGatheringException
230 	 *             - if an error occurs during the execution
231 	 */
232 	private String getHttpResponse(final ICommandLine cl,
233 			final String hostname, final String port, final String method,
234 			final String path, final int timeout, final boolean ssl,
235 			final List<Metric> metrics) throws MetricGatheringException {
236 		Properties props = null;
237 		try {
238 			props = getRequestProperties(cl, method);
239 		} catch (UnsupportedEncodingException e) {
240 			throw new MetricGatheringException("Error occurred: "
241 					+ e.getMessage(), Status.CRITICAL, e);
242 		}
243 		String response = null;
244 		String redirect = cl.getOptionValue("onredirect");
245 		boolean ignoreBody = false;
246 		try {
247 			String data = null;
248 			if ("POST".equals(method)) {
249 				data = getPostData(cl);
250 			}
251 
252 			if (cl.hasOption("no-body")) {
253 				ignoreBody = true;
254 			}
255 			String urlString = hostname + ":" + port + path;
256 			if (cl.hasOption("authorization")) {
257 				urlString = cl.getOptionValue("authorization") + "@"
258 						+ urlString;
259 			} else if (cl.hasOption("proxy-authorization")) {
260 				urlString = cl.getOptionValue("proxy-authorization") + "@"
261 						+ urlString;
262 			}
263 			if (ssl) {
264 				urlString = "https://" + urlString;
265 			} else {
266 				urlString = "http://" + urlString;
267 			}
268 			URL url = new URL(urlString);
269 			if (cl.getOptionValue("certificate") != null) {
270 				checkCertificateExpiryDate(url, metrics);
271 			} else if (redirect != null) {
272 				response = checkRedirectResponse(url, method, timeout, props,
273 						data, redirect, ignoreBody, metrics);
274 			} else {
275 				try {
276 					if ("GET".equals(method)) {
277 						response = HttpUtils.doGET(url, props, timeout, true,
278 								ignoreBody);
279 					} else if ("POST".equals(method)) {
280 						response = HttpUtils.doPOST(url, props, null, data,
281 								true, ignoreBody);
282 					} else if ("HEAD".equals(method)) {
283 						response = HttpUtils.doHEAD(url, props, timeout, true,
284 								ignoreBody);
285 					}
286 					// @TODO complete for other http methods
287 
288 				} catch (MalformedURLException e) {
289 					log.error("Bad url", e);
290 					throw new MetricGatheringException("Bad url string : "
291 							+ urlString, Status.CRITICAL, e);
292 				}
293 			}
294 
295 		} catch (Exception e) {
296 			log.error("Exception: " + e.getMessage(), e);
297 			throw new MetricGatheringException(e.getClass().getName() + ": "
298 					+ e.getMessage(), Status.CRITICAL, e);
299 		}
300 		return response;
301 	}
302 
303 	/**
304 	 * Apply the logic to check for url redirects.
305 	 * 
306 	 * @param url
307 	 *            - The server URL
308 	 * @param method
309 	 *            - The HTTP method
310 	 * @param timeout
311 	 *            - The timeout
312 	 * @param props
313 	 *            -
314 	 * @param postData
315 	 *            -
316 	 * @param redirect
317 	 *            -
318 	 * @param ignoreBody
319 	 *            -
320 	 * @param metrics
321 	 *            - This list will be filled with the gathered metrics
322 	 * @return String
323 	 * @throws Exception
324 	 *             -
325 	 */
326 	private String checkRedirectResponse(final URL url, final String method,
327 			final Integer timeout, final Properties props,
328 			final String postData, final String redirect,
329 			final boolean ignoreBody, final List<Metric> metrics)
330 			throws Exception {
331 		// @todo handle sticky/port and follow param options
332 
333 		String response = null;
334 		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
335 		conn.setRequestMethod(method);
336 		HttpUtils.setRequestProperties(props, conn, timeout);
337 		String initialUrl = conn.getURL() + "";
338 		String redirectedUrl = null;
339 		if (method.equals("POST")) {
340 			HttpUtils.sendPostData(conn, postData);
341 		}
342 		response = HttpUtils.parseHttpResponse(conn, false, ignoreBody);
343 		redirectedUrl = conn.getURL() + "";
344 
345 		if (!redirectedUrl.equals(initialUrl)) {
346 			Metric metric = new Metric("onredirect", "", new BigDecimal(1),
347 					null, null);
348 			metrics.add(metric);
349 		}
350 		return response;
351 	}
352 
353 	/**
354 	 * Apply logic to the http response and build metrics.
355 	 * 
356 	 * @param opt
357 	 *            -
358 	 * @param response
359 	 *            -
360 	 * @param elapsed
361 	 *            -
362 	 * @return - The metrics
363 	 * @throws MetricGatheringException
364 	 *             List<Metric>
365 	 */
366 	private List<Metric> analyzeResponse(final ICommandLine opt,
367 			final String response, final int elapsed)
368 			throws MetricGatheringException {
369 		List<Metric> metrics = new ArrayList<Metric>();
370 		metrics.add(new Metric("time", "", new BigDecimal(elapsed), null, null));
371 
372 		if (opt.getOptionValue("certificate") == null
373 				&& !opt.hasOption("certificate")) {
374 			if (opt.getOptionValue("string") != null
375 					&& !opt.getOptionValue("string").equals("")) {
376 				boolean found = false;
377 				String string = opt.getOptionValue("string");
378 				found = response.contains(string);
379 				metrics.add(new Metric("string", "", new BigDecimal(Utils
380 						.getIntValue(found)), null, null));
381 			}
382 			if (opt.getOptionValue("expect") != null) {
383 				int count = 0;
384 				String[] values = opt.getOptionValue("expect").split(",");
385 				for (String value : values) {
386 					if (response.contains(value)) {
387 						count++;
388 					}
389 				}
390 				metrics.add(new Metric("expect", "" + count + " times. ",
391 						new BigDecimal(count), null, null));
392 			}
393 			if (opt.getOptionValue("regex") != null) {
394 				String regex = opt.getOptionValue("regex");
395 				Pattern p = null;
396 				int flags = 0;
397 				if (opt.hasOption("eregi")) {
398 					flags = Pattern.CASE_INSENSITIVE;
399 				}
400 				if (opt.hasOption("linespan")) {
401 					flags = flags | Pattern.MULTILINE;
402 				}
403 				p = Pattern.compile(regex, flags);
404 				boolean found = p.matcher(response).find();
405 				if (opt.hasOption("invert-regex")) {
406 					metrics.add(new Metric("invert-regex", "" + found,
407 							new BigDecimal(Utils.getIntValue(found)), null,
408 							null));
409 				} else {
410 					metrics.add(new Metric("regex", "" + found, new BigDecimal(
411 							Utils.getIntValue(found)), null, null));
412 				}
413 			}
414 		}
415 		return metrics;
416 	}
417 
418 	/**
419 	 * Set the http request properties and headers.
420 	 * 
421 	 * @param cl
422 	 *            The received command line
423 	 * @param method
424 	 *            The HTTP method
425 	 * @return Properties
426 	 * @throws UnsupportedEncodingException
427 	 */
428 	private Properties getRequestProperties(final ICommandLine cl,
429 			final String method) throws UnsupportedEncodingException {
430 		Properties props = new Properties();
431 		if (cl.getOptionValue("useragent") != null) {
432 			props.put("User-Agent", cl.getOptionValue("useragent"));
433 		} else {
434 			props.put("User-Agent", DEFAULT_USER_AGENT);
435 		}
436 		if (cl.getOptionValue("content-type") != null
437 				&& method.toUpperCase().equals("POST")) {
438 			props.put("Content-Type", cl.getOptionValue("content-type"));
439 		}
440 		if (cl.getOptionValues("header") != null) {
441 			List headers = cl.getOptionValues("header");
442 			for (Object obj : headers) {
443 				String header = (String) obj;
444 				String key = header.split(":")[0].trim();
445 				String value = header.split(":")[1].trim();
446 				props.put(key, value);
447 			}
448 		}
449 		String auth = null;
450 		String encoded = null;
451 		if (cl.hasOption("authorization")) {
452 			encoded = Base64.encodeBase64String(cl.getOptionValue(
453 					"authorization").getBytes(CHARSET));
454 			auth = "Authorization";
455 		} else if (cl.hasOption("proxy-authorization")) {
456 			encoded = Base64.encodeBase64String(cl.getOptionValue(
457 					"proxy-authorization").getBytes(CHARSET));
458 			auth = "Proxy-Authorization";
459 		}
460 		if (auth != null && encoded != null) {
461 			props.put(auth, "Basic " + encoded);
462 		}
463 		return props;
464 	}
465 
466 	/**
467 	 * Returns encoded post data.
468 	 * 
469 	 * @param cl
470 	 *            - The received command line
471 	 * @return - The encoded post data
472 	 * @throws Exception
473 	 *             -
474 	 */
475 	private String getPostData(final ICommandLine cl) throws Exception {
476 		// String encoded = "";
477 		StringBuilder encoded = new StringBuilder();
478 		String data = cl.getOptionValue("post");
479 		if (data == null) {
480 			return null;
481 		}
482 		String[] values = data.split("&");
483 		for (String value : values) {
484 			String[] splitted = value.split("=");
485 			String key = splitted[0];
486 			String val = "";
487 			if (splitted.length > 1) {
488 				val = splitted[1];
489 			}
490 			if (encoded.length() != 0) {
491 				encoded.append('&');
492 			}
493 			encoded.append(key).append('=')
494 					.append(URLEncoder.encode(val, "UTF-8"));
495 			// encoded += key + "=" + URLEncoder.encode(val, "UTF-8") + "&";
496 		}
497 		// if (encoded.endsWith("&")) {
498 		// StringUtils.removeEnd(encoded, "&");
499 		// }
500 		return encoded.toString();
501 	}
502 
503 	/*
504 	 * (non-Javadoc)
505 	 * 
506 	 * @see it.jnrpe.plugins.PluginBase#getPluginName()
507 	 */
508 	@Override
509 	protected String getPluginName() {
510 		return "CHECK_HTTP";
511 	}
512 
513 	// stuff for checking certificate
514 	private void checkCertificateExpiryDate(URL url, List<Metric> metrics)
515 			throws Exception {
516 		SSLContext ctx = SSLContext.getInstance("TLS");
517 		ctx.init(new KeyManager[0],
518 				new TrustManager[] { new DefaultTrustManager() },
519 				new SecureRandom());
520 		SSLContext.setDefault(ctx);
521 		HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
522 		conn.setHostnameVerifier(new HostnameVerifier() {
523 			public boolean verify(String arg0, SSLSession arg1) {
524 				return true;
525 			}
526 		});
527 		List<Date> expiryDates = new ArrayList<Date>();
528 		conn.getResponseCode();
529 		Certificate[] certs = conn.getServerCertificates();
530 		for (Certificate cert : certs) {
531 			X509Certificate x509 = (X509Certificate) cert;
532 			Date expiry = x509.getNotAfter();
533 			expiryDates.add(expiry);
534 		}
535 
536 		conn.disconnect();
537 		Date today = new Date();
538 		for (Date date : expiryDates) {
539 			int diffInDays = (int) ((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
540 			metrics.add(new Metric("certificate", "",
541 					new BigDecimal(diffInDays), null, null));
542 		}
543 	}
544 
545 	/**
546 	 * The trustall trust manager .
547 	 */
548 	private static class DefaultTrustManager implements X509TrustManager {
549 
550 		/*
551 		 * (non-Javadoc)
552 		 * 
553 		 * @see
554 		 * javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert
555 		 * .X509Certificate[], java.lang.String)
556 		 */
557 		public void checkClientTrusted(X509Certificate[] arg0, String arg1)
558 				throws CertificateException {
559 
560 		}
561 
562 		/*
563 		 * (non-Javadoc)
564 		 * 
565 		 * @see
566 		 * javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert
567 		 * .X509Certificate[], java.lang.String)
568 		 */
569 		public void checkServerTrusted(X509Certificate[] arg0, String arg1)
570 				throws CertificateException {
571 
572 		}
573 
574 		/*
575 		 * (non-Javadoc)
576 		 * 
577 		 * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
578 		 */
579 		public X509Certificate[] getAcceptedIssuers() {
580 			return null;
581 		}
582 	}
583 
584 }