/******************************************************************************* * Copyright (c) 2010-2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.services.extension.validators; import static org.apache.http.HttpStatus.*; import java.io.IOException; import java.net.ConnectException; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.Collection; import java.util.SortedSet; import java.util.TreeSet; import java.util.UUID; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.params.HttpClientParams; import org.apache.http.params.HttpParams; import org.eclipse.skalli.commons.CollectionUtils; import org.eclipse.skalli.commons.HtmlUtils; import org.eclipse.skalli.commons.HttpUtils; import org.eclipse.skalli.commons.Link; import org.eclipse.skalli.commons.URLUtils; import org.eclipse.skalli.model.ExtensionEntityBase; import org.eclipse.skalli.model.Issue; import org.eclipse.skalli.model.Issuer; import org.eclipse.skalli.model.PropertyName; import org.eclipse.skalli.model.Severity; import org.eclipse.skalli.services.destination.Destinations; import org.eclipse.skalli.services.extension.PropertyValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Property validator to check that a given host/URL is reachable by trying to establish a connection * (using {@link Destinations#getClient(URL)} to create a suitable HTTP client). * <p> * Note, you may need to configure a proxy and required credentials for this validator to work properly * for certain destinations. This validator never produces {@link Severity#FATAL} issues. * <p> * <p>The following issue severities are covered: * <ul> * <li><strong>ERROR</strong> permanent and serious problems with a connection, such as malformed URL, * unknown host, unknown resource (404 NOT FOUND) or lack of permission (403 FORBIDDEN).</li> * <li><strong>WARNING</strong> likely temporary, but probably serious problems with a connection, * such as permanent redirects (301 MOVED PERMANENTLY) and server downtimes (503 SERVICE UNAVAILABLE).</li> * <li><strong>INFO</strong> temporary problems with a connection, such as temorary redirects * (307 TEMPORARY REDIRECT).</li> * </ul> * </p> * <p> * This validator can be applied to single-valued properties and to {@link java.util.Collection collections}. * </p> */ public class HostReachableValidator implements Issuer, PropertyValidator { private static final Logger LOG = LoggerFactory.getLogger(HostReachableValidator.class); // TODO: I18N private static final String TXT_RESOURCE_FOUND_REDIRECT = "''{0}'' found, but a redirect is necessary ({1} {2})."; private static final String TXT_VALIDATOR_NEEDS_UPDATE = "Could not valdiate ''{0}''. Validator might need an update ({1} {2})."; private static final String TXT_MISSING_PROXY = "''{0}'' not found due to missing proxy ({1} {2})."; private static final String TXT_AUTH_REQUIRED = "''{0}'' found, but authentication required ({1} {2})."; private static final String TXT_RESOURCE_MOVED = "''{0}'' moved permanently ({1} {2})."; private static final String TXT_RESOURCE_LOCKED = "''{0}'' found, but locked ({1} {2})."; private static final String TXT_TEMP_SERVER_PROBLEM = "''{0}'' not found due to temporary problem on target server ({1} {2})."; private static final String TXT_PERMANENT_SERVER_PROBLEM = "''{0}'' not found due to a permanent problem on target server ({1} {2})."; private static final String TXT_PERMANENT_REQUEST_PROBLEM = "''{0}'' not found due to a permanent problem with the request ({1} {2})."; private static final String TXT_HOST_NOT_REACHABLE = "Host ''{0}'' is not reachable."; private static final String TXT_HOST_UNKNOWN = "Host ''{0}'' is unknown or cannot be resolved."; private static final String TXT_CONNECT_FAILED = "Could not connect to host ''{0}''."; private static final String TXT_MALFORMED_URL = "URL ''{0}'' is malformed."; // general timeout for connection requests private static final int TIMEOUT = 10000; private final Class<? extends ExtensionEntityBase> extension; private final String property; /** * Creates a validator for checking of HTTP connections. * * @param extension the class of the model extension the property belongs to, or <code>null</code>. * @param property the name of a property (see {@link PropertyName}). */ public HostReachableValidator(Class<? extends ExtensionEntityBase> extension, String property) { if (extension == null) { throw new IllegalArgumentException("argument 'extension' must not be null"); } if (StringUtils.isBlank(property)) { throw new IllegalArgumentException("argument 'property' must not be null or an empty string"); } this.extension = extension; this.property = property; } @Override public SortedSet<Issue> validate(UUID entityId, Object value, Severity minSeverity) { final SortedSet<Issue> issues = new TreeSet<Issue>(); // Do not participate in checks with Severity.FATAL & ignore null if (minSeverity.equals(Severity.FATAL) || value == null) { return issues; } if (value instanceof Collection) { int item = 0; for (Object collectionEntry : (Collection<?>) value) { validate(issues, entityId, collectionEntry, minSeverity, item); ++item; } } else { validate(issues, entityId, value, minSeverity, 0); } return issues; } protected void validate(SortedSet<Issue> issues, UUID entityId, Object value, final Severity minSeverity, int item) { if (value == null) { return; } URL url = null; String label = null; if (value instanceof URL) { url = (URL)value; label = url.toExternalForm(); } else if (value instanceof Link) { Link link = (Link) value; try { url = URLUtils.asURL(link.getUrl()); label = link.getLabel(); } catch (MalformedURLException e) { CollectionUtils.addSafe(issues, getIssueByReachableHost(minSeverity, entityId, item, link.getUrl())); } } else { try { url = URLUtils.asURL(value.toString()); label = url != null ? url.toExternalForm() : value.toString(); } catch (MalformedURLException e) { CollectionUtils.addSafe(issues, getIssueByReachableHost(minSeverity, entityId, item, value.toString())); } } if (url == null) { return; } HttpClient client = Destinations.getClient(url); if (client != null) { HttpResponse response = null; try { HttpParams params = client.getParams(); HttpClientParams.setRedirecting(params, false); // we want to find 301 MOVED PERMANTENTLY HttpGet method = new HttpGet(url.toExternalForm()); LOG.info("GET " + url); //$NON-NLS-1$ response = client.execute(method); int status = response.getStatusLine().getStatusCode(); LOG.info(status + " " + response.getStatusLine().getReasonPhrase()); //$NON-NLS-1$ CollectionUtils.addSafe(issues, getIssueByResponseCode(minSeverity, entityId, item, response.getStatusLine(), label)); } catch (UnknownHostException e) { issues.add(newIssue(Severity.ERROR, entityId, item, TXT_HOST_UNKNOWN, url.getHost())); } catch (ConnectException e) { issues.add(newIssue(Severity.ERROR, entityId, item, TXT_CONNECT_FAILED, url.getHost())); } catch (MalformedURLException e) { issues.add(newIssue(Severity.ERROR, entityId, item, TXT_MALFORMED_URL, url)); } catch (IOException e) { LOG.warn(MessageFormat.format("I/O Exception on validation: {0}", e.getMessage()), e); //$NON-NLS-1$ } catch (RuntimeException e) { LOG.error(MessageFormat.format("RuntimeException on validation: {0}", e.getMessage()), e); //$NON-NLS-1$ } finally { HttpUtils.consumeQuietly(response); } } else { CollectionUtils.addSafe(issues, getIssueByReachableHost(minSeverity, entityId, item, url.getHost())); } } /** * Returning an issue (Severity.ERROR) if host was not reachable, might be null */ private Issue getIssueByReachableHost(Severity minSeverity, UUID entityId, int item, String host) { if (Severity.ERROR.compareTo(minSeverity) <= 0) { try { if (!InetAddress.getByName(host).isReachable(TIMEOUT)) { return newIssue(Severity.ERROR, entityId, item, TXT_HOST_NOT_REACHABLE, host); } } catch (UnknownHostException e) { return newIssue(Severity.ERROR, entityId, item, TXT_HOST_UNKNOWN, host); } catch (IOException e) { LOG.warn(MessageFormat.format("I/O Exception on validation: {0}", e.getMessage()), e); //$NON-NLS-1$ return null; } } return null; } /** * Returning an issue depending on the HTTP response code, might be null */ private Issue getIssueByResponseCode(Severity minSeverity, UUID entityId, int item, StatusLine statusLine, String label) { // everything below HTTP 300 is OK. Do not generate issues... if (statusLine.getStatusCode() < 300) { return null; } switch (minSeverity) { case INFO: switch (statusLine.getStatusCode()) { case SC_MULTIPLE_CHOICES: // Confluence Wiki generates a 302 for anonymous requests (for ANY page). This would mess up the entries using SAPs wiki. // case SC_MOVED_TEMPORARILY: case SC_SEE_OTHER: case SC_TEMPORARY_REDIRECT: return newIssue(Severity.INFO, entityId, item, TXT_RESOURCE_FOUND_REDIRECT, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); case SC_REQUEST_TIMEOUT: return newIssue(Severity.INFO, entityId, item, TXT_VALIDATOR_NEEDS_UPDATE, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); } case WARNING: switch (statusLine.getStatusCode()) { case SC_MOVED_PERMANENTLY: return newIssue(Severity.ERROR, entityId, item, TXT_RESOURCE_MOVED, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); case SC_USE_PROXY: case SC_PROXY_AUTHENTICATION_REQUIRED: return newIssue(Severity.WARNING, entityId, item, TXT_MISSING_PROXY, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); case SC_UNAUTHORIZED: // do not create an issue, as the link might be checked with an anonymous user; // project members might have the rights, you can't know. return null; case SC_LOCKED: return newIssue(Severity.WARNING, entityId, item, TXT_RESOURCE_LOCKED, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); case SC_INTERNAL_SERVER_ERROR: case SC_SERVICE_UNAVAILABLE: case SC_GATEWAY_TIMEOUT: case SC_INSUFFICIENT_STORAGE: return newIssue(Severity.WARNING, entityId, item, TXT_TEMP_SERVER_PROBLEM, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); } case ERROR: switch (statusLine.getStatusCode()) { case SC_BAD_REQUEST: case SC_FORBIDDEN: case SC_NOT_FOUND: case SC_METHOD_NOT_ALLOWED: case SC_NOT_ACCEPTABLE: case SC_CONFLICT: case SC_GONE: case SC_LENGTH_REQUIRED: case SC_PRECONDITION_FAILED: case SC_REQUEST_TOO_LONG: case SC_REQUEST_URI_TOO_LONG: case SC_UNSUPPORTED_MEDIA_TYPE: case SC_REQUESTED_RANGE_NOT_SATISFIABLE: case SC_EXPECTATION_FAILED: case SC_UNPROCESSABLE_ENTITY: case SC_FAILED_DEPENDENCY: return newIssue(Severity.ERROR, entityId, item, TXT_PERMANENT_REQUEST_PROBLEM, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); case SC_NOT_IMPLEMENTED: case SC_BAD_GATEWAY: return newIssue(Severity.ERROR, entityId, item, TXT_PERMANENT_SERVER_PROBLEM, label, statusLine.getStatusCode(), statusLine.getReasonPhrase()); } case FATAL: break; default: break; } return null; } protected Issue newIssue(Severity severity, UUID entityId, int item, String message, Object... messageArguments) { return newIssue(severity, entityId, item, HtmlUtils.formatEscaped(message, messageArguments)); } protected Issue newIssue(Severity severity, UUID entityId, int item, String message) { return new Issue(severity, getClass(), entityId, extension, property, item, message); } }