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.lang3.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 /** 075 * HTTP Status-Code 307: Temporay Redirect 076 */ 077 public static final int HTTP_REDIRECT_TEMP = 307; 078 079 /** 080 * HTTP Status-Code 308: Temporay Redirect 081 */ 082 public static final int HTTP_REDIRECT_PERM = 308; 083 084 085 private static Logger __logger = LoggerFactory.getLogger(HttpUrlUtils.class); 086 087 /** 088 * Prepare the connection to the remote url 089 * @param url The url 090 * @param userAgent The user agent. Can be null. 091 * @param method The method for teh URL request. Can be null. 092 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 093 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 094 * @param followRedirects Sets to true to follow HTTP redirects 095 * @return The URL connection 096 * @throws MalformedURLException If the given url is a malformed URL 097 * @throws IOException if an I/O exception occurs 098 */ 099 public static HttpURLConnection prepareConnection(String url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects) throws MalformedURLException, IOException 100 { 101 return prepareConnection(url, userAgent, method, timeout, readTimeOut, followRedirects, Collections.EMPTY_MAP); 102 } 103 104 /** 105 * Prepare the connection to the remote url 106 * @param httpUrl The HTTP url 107 * @param userAgent The user agent. Can be null. 108 * @param method The method for teh URL request. Can be null. 109 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 110 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 111 * @param followRedirects Sets to true to follow HTTP redirects 112 * @param requestHeaders The request headers. 113 * @return The URL connection 114 * @throws MalformedURLException If the given url is a malformed URL 115 * @throws IOException if an I/O exception occurs 116 */ 117 public static HttpURLConnection prepareConnection(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) throws MalformedURLException, IOException 118 { 119 return prepareConnection(new URL(httpUrl), userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 120 } 121 122 /** 123 * Prepare the connection to the remote url 124 * @param url The url 125 * @param userAgent The user agent. Can be null. 126 * @param method The method for teh URL request. Can be null. 127 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 128 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 129 * @param followRedirects Sets to true to follow HTTP redirects 130 * @param requestHeaders The request headers. 131 * @return The URL connection 132 * @throws MalformedURLException If the given url is a malformed URL 133 * @throws IOException if an I/O exception occurs 134 */ 135 public static HttpURLConnection prepareConnection(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) throws MalformedURLException, IOException 136 { 137 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(); 138 139 if (userAgent != null) 140 { 141 httpUrlConnection.setRequestProperty("User-Agent", userAgent); 142 } 143 if (method != null) 144 { 145 httpUrlConnection.setRequestMethod(method); 146 } 147 if (timeout != -1) 148 { 149 httpUrlConnection.setConnectTimeout(timeout); 150 } 151 152 if (readTimeOut != -1) 153 { 154 httpUrlConnection.setReadTimeout(readTimeOut); 155 } 156 157 if (followRedirects) 158 { 159 httpUrlConnection.setInstanceFollowRedirects(true); 160 } 161 162 for (Entry<String, String> headers : requestHeaders.entrySet()) 163 { 164 httpUrlConnection.setRequestProperty(headers.getKey(), headers.getValue()); 165 } 166 167 return httpUrlConnection; 168 } 169 170 /** 171 * Check the HTTP url 172 * @param httpUrl The HTTP url to test 173 * @param userAgent The user agent. Can be null. 174 * @param method The method for teh URL request. Can be null. 175 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 176 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 177 * @param followRedirects Sets to true to follow HTTP redirects 178 * @return The URL connection 179 */ 180 public static HttpCheck checkHttpUrl(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects) 181 { 182 return checkHttpUrl(httpUrl, userAgent, method, timeout, readTimeOut, followRedirects, Collections.EMPTY_MAP); 183 } 184 185 /** 186 * Check the HTTP url 187 * @param httpUrl The url to test 188 * @param userAgent The user agent. Can be null. 189 * @param method The method for teh URL request. Can be null. 190 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 191 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 192 * @param followRedirects Sets to true to follow HTTP redirects 193 * @param requestHeaders The request headers. 194 * @return The URL connection 195 */ 196 public static HttpCheck checkHttpUrl(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) 197 { 198 if (!HTTP_URL_VALIDATOR.matcher(StringUtils.defaultIfEmpty(httpUrl, "")).matches()) 199 { 200 __logger.debug("Url '{}' is not a valid HTTP url", httpUrl); 201 return HttpCheck.NOT_HTTP; 202 } 203 204 try 205 { 206 return checkHttpUrl(new URL(httpUrl), userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 207 } 208 catch (MalformedURLException e) 209 { 210 __logger.debug("Unable to parse '{}' as a HTTP url", httpUrl, e); 211 return HttpCheck.NOT_HTTP; 212 } 213 } 214 215 /** 216 * Check the HTTP url 217 * @param url The url to test 218 * @param userAgent The user agent. Can be null. 219 * @param method The method for teh URL request. Can be null. 220 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 221 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 222 * @param followRedirects Sets to true to follow HTTP redirects 223 * @param requestHeaders The request headers. 224 * @return The URL connection 225 */ 226 public static HttpCheck checkHttpUrl(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) 227 { 228 return _checkHttpUrl(url, userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders, new ArrayList<>(), 5); 229 } 230 231 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) 232 { 233 try 234 { 235 HttpURLConnection connection = prepareConnection(url, userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 236 237 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) 238 { 239 __logger.debug("Check of URL '{}' successed", url); 240 return HttpCheck.SUCCESS; 241 } 242 else if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP /* 302 */ 243 || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM /* 301 */ 244 || connection.getResponseCode() == HttpURLConnection.HTTP_SEE_OTHER /* 303 */ 245 || connection.getResponseCode() == HTTP_REDIRECT_TEMP /* 307 */ 246 || connection.getResponseCode() == HTTP_REDIRECT_PERM /* 308 */) 247 { 248 if (followRedirects) 249 { 250 // Redirection between HTTP and HTTPS URLs is not followed (do it manually) 251 if (maxRedirects < 0 || visitedUrl.contains(url.toExternalForm())) 252 { 253 // The maximum number of redirects has been reached or the url was already been visited when following redirects 254 throw new IOException("Exceed max number of redirects"); 255 } 256 257 visitedUrl.add(url.toExternalForm()); 258 259 URL base = connection.getURL(); 260 String location = connection.getHeaderField("Location"); 261 URL redirect = new URL(base, location); // Deal with relative URLs 262 263 return _checkHttpUrl(redirect, userAgent, method, timeout, readTimeOut, true, requestHeaders, visitedUrl, maxRedirects - 1); 264 } 265 else 266 { 267 return HttpCheck.REDIRECT; 268 } 269 } 270 else 271 { 272 __logger.debug("Check of URL '{}' returns the status code {}", url, connection.getResponseCode()); 273 274 int responseCode = connection.getResponseCode(); 275 276 switch (responseCode) 277 { 278 case HttpURLConnection.HTTP_NOT_FOUND: 279 return HttpCheck.NOT_FOUND; 280 case HttpURLConnection.HTTP_FORBIDDEN: 281 case HttpURLConnection.HTTP_UNAUTHORIZED: 282 return HttpCheck.UNAUTHORIZED; 283 case HttpURLConnection.HTTP_INTERNAL_ERROR: 284 default: 285 return HttpCheck.SERVER_ERROR; 286 } 287 } 288 } 289 catch (SSLHandshakeException e) 290 { 291 __logger.debug("Certificate error for URL '{}'", url, e); 292 return HttpCheck.SECURITY_LEVEL_ERROR; 293 } 294 catch (SocketTimeoutException e) 295 { 296 __logger.debug("Aborting test for URL '{}' because too long", url, e); 297 return HttpCheck.TIMEOUT; 298 } 299 catch (UnknownHostException e) 300 { 301 __logger.debug("Unknown host for URL '{}'", url, e); 302 return HttpCheck.NOT_FOUND; 303 } 304 catch (IOException e) 305 { 306 __logger.debug("Cannot test URL '{}'", url, e); 307 return HttpCheck.SERVER_ERROR; 308 } 309 } 310 311 /** 312 * Method to check a HTTP url from client side. 313 * The HTTP redirects will be followed. 314 * @param httpUrl the http url to check 315 * @return the result of the check 316 */ 317 @Callable 318 public Map<String, Object> checkHttpUrl(String httpUrl) 319 { 320 Map<String, Object> result = new HashMap<>(); 321 322 HttpCheck check = checkHttpUrl(httpUrl, null, null, 2000, 2000, true); 323 result.put("success", HttpCheck.SUCCESS.equals(check)); 324 result.put("checkResult", check.name()); 325 326 return result; 327 } 328}