001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.util; 017 018import java.io.IOException; 019import java.net.HttpURLConnection; 020import java.net.MalformedURLException; 021import java.net.SocketTimeoutException; 022import java.net.URL; 023import java.net.UnknownHostException; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.regex.Pattern; 031 032import javax.net.ssl.SSLHandshakeException; 033 034import org.apache.avalon.framework.component.Component; 035import org.apache.commons.lang.StringUtils; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import org.ametys.core.ui.Callable; 040 041/** 042 * Utility class for HTTP urls. 043 */ 044public class HttpUrlUtils implements Component 045{ 046 /** The Avalon role */ 047 public static final String ROLE = HttpUrlUtils.class.getName(); 048 049 /** Regexp for HTTP url */ 050 public static final Pattern HTTP_URL_VALIDATOR = Pattern.compile("^(https?:\\/\\/.+)?$"); 051 052 /** The status of HTTP check */ 053 public static enum HttpCheck 054 { 055 /** All right */ 056 SUCCESS, 057 /** Server error. */ 058 SERVER_ERROR, 059 /** URL not found.*/ 060 NOT_FOUND, 061 /** Unauthorized. */ 062 UNAUTHORIZED, 063 /** Timeout (too long) */ 064 TIMEOUT, 065 /** A redirect occurs */ 066 REDIRECT, 067 /** Security level error */ 068 SECURITY_LEVEL_ERROR, 069 /** No HTTP url */ 070 NOT_HTTP 071 072 } 073 074 private static Logger __logger = LoggerFactory.getLogger(HttpUrlUtils.class); 075 076 /** 077 * Prepare the connection to the remote url 078 * @param url The url 079 * @param userAgent The user agent. Can be null. 080 * @param method The method for teh URL request. Can be null. 081 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 082 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 083 * @param followRedirects Sets to true to follow HTTP redirects 084 * @return The URL connection 085 * @throws MalformedURLException If the given url is a malformed URL 086 * @throws IOException if an I/O exception occurs 087 */ 088 public static HttpURLConnection prepareConnection(String url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects) throws MalformedURLException, IOException 089 { 090 return prepareConnection(url, userAgent, method, timeout, readTimeOut, followRedirects, Collections.EMPTY_MAP); 091 } 092 093 /** 094 * Prepare the connection to the remote url 095 * @param httpUrl The HTTP url 096 * @param userAgent The user agent. Can be null. 097 * @param method The method for teh URL request. Can be null. 098 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 099 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 100 * @param followRedirects Sets to true to follow HTTP redirects 101 * @param requestHeaders The request headers. 102 * @return The URL connection 103 * @throws MalformedURLException If the given url is a malformed URL 104 * @throws IOException if an I/O exception occurs 105 */ 106 public static HttpURLConnection prepareConnection(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) throws MalformedURLException, IOException 107 { 108 return prepareConnection(new URL(httpUrl), userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 109 } 110 111 /** 112 * Prepare the connection to the remote url 113 * @param url The url 114 * @param userAgent The user agent. Can be null. 115 * @param method The method for teh URL request. Can be null. 116 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 117 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 118 * @param followRedirects Sets to true to follow HTTP redirects 119 * @param requestHeaders The request headers. 120 * @return The URL connection 121 * @throws MalformedURLException If the given url is a malformed URL 122 * @throws IOException if an I/O exception occurs 123 */ 124 public static HttpURLConnection prepareConnection(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) throws MalformedURLException, IOException 125 { 126 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(); 127 128 if (userAgent != null) 129 { 130 httpUrlConnection.setRequestProperty("User-Agent", userAgent); 131 } 132 if (method != null) 133 { 134 httpUrlConnection.setRequestMethod(method); 135 } 136 if (timeout != -1) 137 { 138 httpUrlConnection.setConnectTimeout(timeout); 139 } 140 141 if (readTimeOut != -1) 142 { 143 httpUrlConnection.setReadTimeout(readTimeOut); 144 } 145 146 if (followRedirects) 147 { 148 httpUrlConnection.setInstanceFollowRedirects(true); 149 } 150 151 for (Entry<String, String> headers : requestHeaders.entrySet()) 152 { 153 httpUrlConnection.setRequestProperty(headers.getKey(), headers.getValue()); 154 } 155 156 return httpUrlConnection; 157 } 158 159 /** 160 * Check the HTTP url 161 * @param httpUrl The HTTP url to test 162 * @param userAgent The user agent. Can be null. 163 * @param method The method for teh URL request. Can be null. 164 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 165 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 166 * @param followRedirects Sets to true to follow HTTP redirects 167 * @return The URL connection 168 */ 169 public static HttpCheck checkHttpUrl(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects) 170 { 171 return checkHttpUrl(httpUrl, userAgent, method, timeout, readTimeOut, followRedirects, Collections.EMPTY_MAP); 172 } 173 174 /** 175 * Check the HTTP url 176 * @param httpUrl The url to test 177 * @param userAgent The user agent. Can be null. 178 * @param method The method for teh URL request. Can be null. 179 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 180 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 181 * @param followRedirects Sets to true to follow HTTP redirects 182 * @param requestHeaders The request headers. 183 * @return The URL connection 184 */ 185 public static HttpCheck checkHttpUrl(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) 186 { 187 if (!HTTP_URL_VALIDATOR.matcher(StringUtils.defaultIfEmpty(httpUrl, "")).matches()) 188 { 189 __logger.debug("Url '{}' is not a valid HTTP url", httpUrl); 190 return HttpCheck.NOT_HTTP; 191 } 192 193 try 194 { 195 return checkHttpUrl(new URL(httpUrl), userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 196 } 197 catch (MalformedURLException e) 198 { 199 __logger.debug("Unable to parse '{}' as a HTTP url", httpUrl, e); 200 return HttpCheck.NOT_HTTP; 201 } 202 } 203 204 /** 205 * Check the HTTP url 206 * @param url The url to test 207 * @param userAgent The user agent. Can be null. 208 * @param method The method for teh URL request. Can be null. 209 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 210 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 211 * @param followRedirects Sets to true to follow HTTP redirects 212 * @param requestHeaders The request headers. 213 * @return The URL connection 214 */ 215 public static HttpCheck checkHttpUrl(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) 216 { 217 return _checkHttpUrl(url, userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders, new ArrayList<>(), 5); 218 } 219 220 private static HttpCheck _checkHttpUrl(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders, List<String> visitedUrl, int maxRedirects) 221 { 222 try 223 { 224 HttpURLConnection connection = prepareConnection(url, userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 225 226 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) 227 { 228 __logger.debug("Check of URL '{}' successed", url); 229 return HttpCheck.SUCCESS; 230 } 231 else if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP /* 302 */ || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM /* 301 */) 232 { 233 if (followRedirects) 234 { 235 // Redirection between HTTP and HTTPS URLs is not followed (do it manually) 236 if (maxRedirects < 0 || visitedUrl.contains(url.toExternalForm())) 237 { 238 // The maximum number of redirects has been reached or the url was already been visited when following redirects 239 throw new IOException("Exceed max number of redirects"); 240 } 241 242 visitedUrl.add(url.toExternalForm()); 243 244 URL base = connection.getURL(); 245 String location = connection.getHeaderField("Location"); 246 URL redirect = new URL(base, location); // Deal with relative URLs 247 248 return _checkHttpUrl(redirect, userAgent, method, timeout, readTimeOut, true, requestHeaders, visitedUrl, maxRedirects - 1); 249 } 250 else 251 { 252 return HttpCheck.REDIRECT; 253 } 254 } 255 else 256 { 257 __logger.debug("Check of URL '{}' returns the status code {}", url, connection.getResponseCode()); 258 259 int responseCode = connection.getResponseCode(); 260 261 switch (responseCode) 262 { 263 case HttpURLConnection.HTTP_NOT_FOUND: 264 return HttpCheck.NOT_FOUND; 265 case HttpURLConnection.HTTP_FORBIDDEN: 266 case HttpURLConnection.HTTP_UNAUTHORIZED: 267 return HttpCheck.UNAUTHORIZED; 268 case HttpURLConnection.HTTP_INTERNAL_ERROR: 269 default: 270 return HttpCheck.SERVER_ERROR; 271 } 272 } 273 } 274 catch (SSLHandshakeException e) 275 { 276 __logger.debug("Certificate error for URL '{}'", url, e); 277 return HttpCheck.SECURITY_LEVEL_ERROR; 278 } 279 catch (SocketTimeoutException e) 280 { 281 __logger.debug("Aborting test for URL '{}' because too long", url, e); 282 return HttpCheck.TIMEOUT; 283 } 284 catch (UnknownHostException e) 285 { 286 __logger.debug("Unknown host for URL '{}'", url, e); 287 return HttpCheck.NOT_FOUND; 288 } 289 catch (IOException e) 290 { 291 __logger.debug("Cannot test URL '{}'", url, e); 292 return HttpCheck.SERVER_ERROR; 293 } 294 } 295 296 /** 297 * Method to check a HTTP url from client side. 298 * The HTTP redirects will be followed. 299 * @param httpUrl the http url to check 300 * @return the result of the check 301 */ 302 @Callable 303 public Map<String, Object> checkHttpUrl(String httpUrl) 304 { 305 Map<String, Object> result = new HashMap<>(); 306 307 HttpCheck check = checkHttpUrl(httpUrl, null, null, 2000, 2000, true); 308 result.put("success", HttpCheck.SUCCESS.equals(check)); 309 result.put("checkResult", check.name()); 310 311 return result; 312 } 313}