/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2006-2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * OpenNMS(R) is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.netmgt.poller.monitors; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.InterruptedIOException; import java.net.ConnectException; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NoRouteToHostException; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.Map; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.opennms.core.utils.Base64; import org.opennms.core.utils.DefaultSocketWrapper; import org.opennms.core.utils.IPLike; import org.opennms.core.utils.InetAddressUtils; import org.opennms.core.utils.ParameterMap; import org.opennms.core.utils.SocketWrapper; import org.opennms.core.utils.TimeoutTracker; import org.opennms.netmgt.model.PollStatus; import org.opennms.netmgt.poller.Distributable; import org.opennms.netmgt.poller.MonitoredService; import org.opennms.netmgt.poller.NetworkInterface; import org.opennms.netmgt.poller.NetworkInterfaceNotSupportedException; /** * This class is designed to be used by the service poller framework to test the availability * of the HTTP service on remote interfaces. The class implements the ServiceMonitor interface * that allows it to be used along with other plug-ins by the service poller framework. * * @author <A HREF="http://www.opennms.org/">OpenNMS </A> * @author <A HREF="mailto:tarus@opennms.org">Tarus Balog </A> * @author <A HREF="mailto:mike@opennms.org">Mike </A> * @author <a href="mailto:david@opennms.org">David Hustace</a> */ @Distributable public class HttpMonitor extends AbstractServiceMonitor { private static final Pattern HEADER_PATTERN = Pattern.compile("header[0-9]+$"); /** * Default HTTP ports. */ private static final int[] DEFAULT_PORTS = { 80, 8080, 8888}; /** * Default retries. */ private static final int DEFAULT_RETRY = 0; /** * Default URL to 'GET' */ private static final String DEFAULT_URL = "/"; /** * Default timeout. Specifies how long (in milliseconds) to block waiting for data from the * monitored interface. */ private static final int DEFAULT_TIMEOUT = 3000; // 3 second timeout on read() public static final String PARAMETER_VERBOSE = "verbose"; public static final String PARAMETER_USER_AGENT = "user-agent"; public static final String PARAMETER_BASIC_AUTHENTICATION = "basic-authentication"; public static final String PARAMETER_USER = "user"; public static final String PARAMETER_PASSWORD = "password"; public static final String PARAMETER_RESOLVE_IP = "resolve-ip"; public static final String PARAMETER_NODE_LABEL_HOST_NAME = "nodelabel-host-name"; public static final String PARAMETER_HOST_NAME = "host-name"; public static final String PARAMETER_RESPONSE_TEXT = "response-text"; public static final String PARAMETER_RESPONSE = "response"; public static final String PARAMETER_URL = "url"; public static final String PARAMETER_PORT = "port"; /** * {@inheritDoc} * * Poll the specified address for HTTP service availability. * * During the poll an attempt is made to connect on the specified port(s) (by default TCP * ports 80, 8080, 8888). If the connection request is successful, an HTTP 'GET' command is * sent to the interface. The response is parsed and a return code extracted and verified. * Provided that the interface's response is valid we set the service status to * SERVICE_AVAILABLE and return. */ public PollStatus poll(final MonitoredService svc, final Map<String, Object> parameters) { final NetworkInterface<InetAddress> iface = svc.getNetInterface(); final String nodeLabel = svc.getNodeLabel(); if (iface.getType() != NetworkInterface.TYPE_INET) { throw new NetworkInterfaceNotSupportedException("Unsupported interface type, only TYPE_INET currently supported"); } // Cycle through the port list // int currentPort = -1; final HttpMonitorClient httpClient = new HttpMonitorClient(nodeLabel, iface, new TreeMap<String, Object>(parameters)); for (int portIndex = 0; portIndex < determinePorts(httpClient.getParameters()).length && httpClient.getPollStatus() != PollStatus.SERVICE_AVAILABLE; portIndex++) { currentPort = determinePorts(httpClient.getParameters())[portIndex]; httpClient.setTimeoutTracker(new TimeoutTracker(parameters, DEFAULT_RETRY, DEFAULT_TIMEOUT)); log().debug("Port = " + currentPort + ", Address = " + (iface.getAddress()) + ", " + httpClient.getTimeoutTracker()); httpClient.setCurrentPort(currentPort); for(httpClient.getTimeoutTracker().reset(); httpClient.getTimeoutTracker().shouldRetry() && httpClient.getPollStatus() != PollStatus.SERVICE_AVAILABLE; httpClient.getTimeoutTracker().nextAttempt()) { try { httpClient.getTimeoutTracker().startAttempt(); httpClient.connect(); log().debug("HttpMonitor: connected to host: " + (iface.getAddress()) + " on port: " + currentPort); httpClient.sendHttpCommand(); if (httpClient.isEndOfStream()) { continue; } httpClient.setResponseTime(httpClient.getTimeoutTracker().elapsedTimeInMillis()); logResponseTimes(httpClient.getResponseTime(), httpClient.getCurrentLine()); if (httpClient.getPollStatus() == PollStatus.SERVICE_AVAILABLE && StringUtils.isNotBlank(httpClient.getResponseText())) { httpClient.setPollStatus(PollStatus.SERVICE_UNAVAILABLE); httpClient.readLinedMatching(); if (httpClient.isEndOfStream()) { continue; } httpClient.read(); if (!httpClient.isResponseTextFound()) { String message = "Matching text: ["+httpClient.getResponseText()+"] not found in body of HTTP response"; log().debug(message); httpClient.setReason("Matching text: ["+httpClient.getResponseText()+"] not found in body of HTTP response"); } } } catch (NoRouteToHostException e) { log().warn("checkStatus: No route to host exception for address " + (iface.getAddress()), e); portIndex = determinePorts(httpClient.getParameters()).length; // Will cause outer for(;;) to terminate httpClient.setReason("No route to host exception"); } catch (SocketTimeoutException e) { log().info("checkStatus: HTTP socket connection timed out with " + httpClient.getTimeoutTracker().toString()); httpClient.setReason("HTTP connection timeout"); } catch (InterruptedIOException e) { log().info(String.format("checkStatus: HTTP connection interrupted after %d bytes transferred with %s", e.bytesTransferred, httpClient.getTimeoutTracker().toString()), e); httpClient.setReason(String.format("HTTP connection interrupted, %d bytes transferred", e.bytesTransferred)); } catch (ConnectException e) { log().warn("Connection exception for " + (iface.getAddress()) + ":" + determinePorts(httpClient.getParameters())[portIndex], e); httpClient.setReason("HTTP connection exception on port: "+determinePorts(httpClient.getParameters())[portIndex]+": "+e.getMessage()); } catch (IOException e) { log().warn("IOException while polling address " + (iface.getAddress()), e); httpClient.setReason("IOException while polling address: "+(iface.getAddress())+": "+e.getMessage()); } catch (Throwable e) { log().warn("Unexpected exception while polling address " + (iface.getAddress()), e); httpClient.setReason("Unexpected exception while polling address: "+(iface.getAddress())+": "+e.getMessage()); } finally { httpClient.closeConnection(); } } // end for (attempts) } // end for (ports) return httpClient.determinePollStatusResponse(); } private void logResponseTimes(Double responseTime, String line) { if (log().isDebugEnabled()) { log().debug("poll: response= " + line); log().debug("poll: responseTime= " + responseTime + "ms"); } } /** * <p>wrapSocket</p> * * @param socket a {@link java.net.Socket} object. * @return a {@link java.net.Socket} object. * @throws java.io.IOException if any. */ protected SocketWrapper getSocketWrapper() { return new DefaultSocketWrapper(); } private static boolean determineVerbosity(final Map<String, Object> parameters) { final String verbose = ParameterMap.getKeyedString(parameters, PARAMETER_VERBOSE, null); return (verbose != null && verbose.equalsIgnoreCase("true")) ? true : false; } private static String determineUserAgent(final Map<String, Object> parameters) { String agent = ParameterMap.getKeyedString(parameters, PARAMETER_USER_AGENT, null); if (isBlank(agent)) { return "OpenNMS HttpMonitor"; } return agent; } static String determineBasicAuthentication(final Map<String, Object> parameters) { String credentials = ParameterMap.getKeyedString(parameters, PARAMETER_BASIC_AUTHENTICATION, null); if (isNotBlank(credentials)) { credentials = new String(Base64.encodeBase64(credentials.getBytes())); } else { String user = ParameterMap.getKeyedString(parameters, PARAMETER_USER, null); if (isBlank(user)) { credentials = null; } else { String passwd = ParameterMap.getKeyedString(parameters, PARAMETER_PASSWORD, ""); credentials = new String(Base64.encodeBase64((user+":"+passwd).getBytes())); } } return credentials; } private static String determineHttpHeader(final Map<String, Object> parameters, String key) { return ParameterMap.getKeyedString(parameters, key, null); } private static String determineResponseText(final Map<String, Object> parameters) { return ParameterMap.getKeyedString(parameters, PARAMETER_RESPONSE_TEXT, null); } private static String determineResponse(final Map<String, Object> parameters) { return ParameterMap.getKeyedString(parameters, PARAMETER_RESPONSE, determineDefaultResponseRange(determineUrl(parameters))); } private static String determineUrl(final Map<String, Object> parameters) { return ParameterMap.getKeyedString(parameters, PARAMETER_URL, DEFAULT_URL); } /** * <p>determinePorts</p> * * @param parameters a {@link java.util.Map} object. * @return an array of int. */ protected int[] determinePorts(final Map<String, Object> parameters) { return ParameterMap.getKeyedIntegerArray(parameters, PARAMETER_PORT, DEFAULT_PORTS); } private static String determineDefaultResponseRange(String url) { if (url == null || url.equals(DEFAULT_URL)) { return "100-499"; } return "100-399"; } private static boolean isNotBlank(String str) { return org.apache.commons.lang.StringUtils.isNotBlank(str); } private static boolean isBlank(String str) { return org.apache.commons.lang.StringUtils.isBlank(str); } final class HttpMonitorClient { private double m_responseTime; final NetworkInterface<InetAddress> m_iface; final Map<String, Object> m_parameters; String m_httpCmd; Socket m_httpSocket; private BufferedReader m_lineRdr; private String m_currentLine; private int m_serviceStatus; private String m_reason; private final StringBuffer m_html = new StringBuffer(); private int m_serverResponseCode; private TimeoutTracker m_timeoutTracker; private int m_currentPort; private String m_responseText; private boolean m_responseTextFound = false; private final String m_nodeLabel; private boolean m_headerFinished = false; HttpMonitorClient(final String nodeLabel, final NetworkInterface<InetAddress> iface, final TreeMap<String, Object>parameters) { m_nodeLabel = nodeLabel; m_iface = iface; m_parameters = parameters; buildCommand(); m_serviceStatus = PollStatus.SERVICE_UNAVAILABLE; m_responseText = determineResponseText(parameters); } public void read() throws IOException { for (int nullCount = 0; nullCount < 2;) { readLinedMatching(); if (isEndOfStream()) { nullCount++; } } } public int getCurrentPort() { return m_currentPort; } public Map<String, Object> getParameters() { return m_parameters; } public boolean isResponseTextFound() { return m_responseTextFound; } public void setResponseTextFound(final boolean found) { m_responseTextFound = found; } private String determineVirtualHost(final NetworkInterface<InetAddress> iface, final Map<String, Object> parameters) { final boolean res = ParameterMap.getKeyedBoolean(parameters, PARAMETER_RESOLVE_IP, false); final boolean useNodeLabel = ParameterMap.getKeyedBoolean(parameters, PARAMETER_NODE_LABEL_HOST_NAME, false); String virtualHost = ParameterMap.getKeyedString(parameters, PARAMETER_HOST_NAME, null); if (isBlank(virtualHost)) { if (res) { return iface.getAddress().getCanonicalHostName(); } else if (useNodeLabel) { return m_nodeLabel; } else { final InetAddress addr = iface.getAddress(); final String host = InetAddressUtils.str(iface.getAddress()); // Wrap IPv6 addresses in square brackets if (addr instanceof Inet6Address) { return "[" + host + "]"; } else { return host; } } } return virtualHost; } public boolean checkCurrentLineMatchesResponseText() { if (!m_headerFinished && StringUtils.isEmpty(m_currentLine)) { m_headerFinished = true; // Set to true when all HTTP headers has been processed. } if (!m_headerFinished) { // Skip perform the regex processing over HTTP headers. return false; } if (m_responseText.charAt(0) == '~' && !m_responseTextFound) { m_responseTextFound = m_currentLine.matches(m_responseText.substring(1)); } else { m_responseTextFound = (m_currentLine.indexOf(m_responseText) != -1 ? true : false); } return m_responseTextFound; } public String getResponseText() { return m_responseText; } public void setResponseText(final String responseText) { m_responseText = responseText; } public void setCurrentPort(final int currentPort) { m_currentPort = currentPort; } public TimeoutTracker getTimeoutTracker() { return m_timeoutTracker; } public void setTimeoutTracker(final TimeoutTracker tracker) { m_timeoutTracker = tracker; } public Double getResponseTime() { return m_responseTime; } public void setResponseTime(final double elapsedTimeInMillis) { m_responseTime = elapsedTimeInMillis; } private void connect() throws IOException, SocketException { m_httpSocket = new Socket(); m_httpSocket.connect(new InetSocketAddress(((InetAddress) m_iface.getAddress()), m_currentPort), m_timeoutTracker.getConnectionTimeout()); m_serviceStatus = PollStatus.SERVICE_UNRESPONSIVE; m_httpSocket.setSoTimeout(m_timeoutTracker.getSoTimeout()); m_httpSocket = getSocketWrapper().wrapSocket(m_httpSocket); } public void closeConnection() { try { if (m_httpSocket != null) { m_httpSocket.close(); m_httpSocket = null; } } catch (final IOException e) { e.fillInStackTrace(); log().warn("Error closing socket connection", e); } } public int getPollStatus() { return m_serviceStatus; } public void setPollStatus(final int serviceStatus) { m_serviceStatus = serviceStatus; } public String getCurrentLine() { return m_currentLine; } public int getServerResponse() { return m_serverResponseCode; } private void determineServerInitialResponse() { int serverResponseValue = -1; if (m_currentLine != null) { if (m_currentLine.startsWith("HTTP/")) { serverResponseValue = parseHttpResponse(); if (IPLike.matchNumericListOrRange(String.valueOf(serverResponseValue), determineResponse(m_parameters))) { if (log().isDebugEnabled()) { log().debug("determineServerResponse: valid server response: "+serverResponseValue+" found."); } m_serviceStatus = PollStatus.SERVICE_AVAILABLE; } else { m_serviceStatus = PollStatus.SERVICE_UNAVAILABLE; final StringBuffer sb = new StringBuffer(); sb.append("HTTP response value: "); sb.append(serverResponseValue); sb.append(". Expecting: "); sb.append(determineResponse(m_parameters)); sb.append("."); m_reason = sb.toString(); } } } m_serverResponseCode = serverResponseValue; } private int parseHttpResponse() { final StringTokenizer t = new StringTokenizer(m_currentLine); if (t.hasMoreTokens()) { t.nextToken(); } int serverResponse = -1; if (t.hasMoreTokens()) { try { serverResponse = Integer.parseInt(t.nextToken()); } catch (final NumberFormatException nfE) { if (log().isInfoEnabled()) { log().info("Error converting response code from host = " + (m_iface.getAddress()) + ", response = " + m_currentLine); } } } return serverResponse; } public boolean isEndOfStream() { if (m_currentLine == null) { return true; } return false; } public String readLine() throws IOException { m_currentLine = m_lineRdr.readLine(); if (determineVerbosity(m_parameters) && log().isDebugEnabled()) { log().debug("\t<<: "+m_currentLine); } m_html.append(m_currentLine); return m_currentLine; } public String readLinedMatching() throws IOException { readLine(); if (m_responseText != null && m_currentLine != null && !m_responseTextFound) { if (checkCurrentLineMatchesResponseText()) { if (log().isDebugEnabled()) { log().debug("response-text: "+m_responseText+": found."); } m_serviceStatus = PollStatus.SERVICE_AVAILABLE; } } return m_currentLine; } public void sendHttpCommand() throws IOException { if (determineVerbosity(m_parameters) && log().isDebugEnabled()) { log().debug("Sending HTTP command: "+m_httpCmd); } m_httpSocket.getOutputStream().write(m_httpCmd.getBytes()); m_lineRdr = new BufferedReader(new InputStreamReader(m_httpSocket.getInputStream())); readLine(); if (determineVerbosity(m_parameters)) { log().debug("Server response: "+m_currentLine); } determineServerInitialResponse(); m_headerFinished = false; // Clean header flag for each HTTP request. } private void buildCommand() { /* * Sorting this map just in case the poller gets changed and the Map * is no longer a TreeMap. */ final StringBuilder sb = new StringBuilder(); sb.append("GET ").append(determineUrl(m_parameters)).append(" HTTP/1.1\r\n"); sb.append("Connection: CLOSE \r\n"); sb.append("Host: ").append(determineVirtualHost(m_iface, m_parameters)).append("\r\n"); sb.append("User-Agent: ").append(determineUserAgent(m_parameters)).append("\r\n"); if (determineBasicAuthentication(m_parameters) != null) { sb.append("Authorization: Basic ").append(determineBasicAuthentication(m_parameters)).append("\r\n"); } for (final String parmKey : m_parameters.keySet()) { if (HEADER_PATTERN.matcher(parmKey).matches()) { sb.append(determineHttpHeader(m_parameters, parmKey)).append("\r\n"); } } sb.append("\r\n"); final String cmd = sb.toString(); if (log().isDebugEnabled()) { log().debug("checkStatus: cmd:\n" + cmd); } m_httpCmd = cmd; } public void setReason(final String reason) { m_reason = reason; } public String getReason() { return m_reason; } public Socket getHttpSocket() { return m_httpSocket; } public void setHttpSocket(final Socket httpSocket) { m_httpSocket = httpSocket; } protected PollStatus determinePollStatusResponse() { /* Add the 'qualifier' parm to the parameter map. This parm will contain the port on which the service was found if AVAILABLE or will contain a comma delimited list of the port(s) which were tried if the service is UNAVAILABLE */ if (getPollStatus() == PollStatus.SERVICE_UNAVAILABLE) { // // Build port string // final StringBuffer testedPorts = new StringBuffer(); for (int i = 0; i < determinePorts(getParameters()).length; i++) { if (i == 0) { testedPorts.append(determinePorts(getParameters())[0]); } else { testedPorts.append(',').append(determinePorts(getParameters())[i]); } } // Add to parameter map getParameters().put("qualifier", testedPorts.toString()); setReason(getReason() + "/Ports: " + testedPorts.toString()); if (log().isDebugEnabled()) { log().debug("checkStatus: Reason: \""+getReason()+"\""); } return PollStatus.unavailable(getReason()); } else if (getPollStatus() == PollStatus.SERVICE_AVAILABLE) { getParameters().put("qualifier", Integer.toString(getCurrentPort())); return PollStatus.available(getResponseTime()); } else { return PollStatus.get(getPollStatus(), getReason()); } } } }