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.Optional; 031import java.util.regex.Pattern; 032 033import javax.net.ssl.SSLHandshakeException; 034 035import org.apache.avalon.framework.component.Component; 036import org.apache.commons.lang3.StringUtils; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040import org.ametys.core.ui.Callable; 041 042/** 043 * Utility class for HTTP urls. 044 */ 045public class HttpUrlUtils implements Component 046{ 047 /** The Avalon role */ 048 public static final String ROLE = HttpUrlUtils.class.getName(); 049 050 /** Regexp for HTTP url */ 051 public static final Pattern HTTP_URL_VALIDATOR = Pattern.compile("^(https?:\\/\\/.+)?$"); 052 053 /** The status of HTTP check */ 054 public static enum HttpCheck 055 { 056 /** All right */ 057 SUCCESS, 058 /** Server error. */ 059 SERVER_ERROR, 060 /** URL not found.*/ 061 NOT_FOUND, 062 /** Unauthorized. */ 063 UNAUTHORIZED, 064 /** Timeout (too long) */ 065 TIMEOUT, 066 /** A redirect occurs */ 067 REDIRECT, 068 /** Security level error */ 069 SECURITY_LEVEL_ERROR, 070 /** No HTTP url */ 071 NOT_HTTP 072 073 } 074 075 /** 076 * HTTP Status-Code 307: Temporay Redirect 077 */ 078 public static final int HTTP_REDIRECT_TEMP = 307; 079 080 /** 081 * HTTP Status-Code 308: Temporay Redirect 082 */ 083 public static final int HTTP_REDIRECT_PERM = 308; 084 085 086 private static Logger __logger = LoggerFactory.getLogger(HttpUrlUtils.class); 087 088 /** 089 * Prepare the connection to the remote url 090 * @param url The url 091 * @param userAgent The user agent. Can be null. 092 * @param method The method for teh URL request. Can be null. 093 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 094 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 095 * @param followRedirects Sets to true to follow HTTP redirects 096 * @return The URL connection 097 * @throws MalformedURLException If the given url is a malformed URL 098 * @throws IOException if an I/O exception occurs 099 */ 100 public static HttpURLConnection prepareConnection(String url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects) throws MalformedURLException, IOException 101 { 102 return prepareConnection(url, userAgent, method, timeout, readTimeOut, followRedirects, Collections.EMPTY_MAP); 103 } 104 105 /** 106 * Prepare the connection to the remote url 107 * @param httpUrl The HTTP url 108 * @param userAgent The user agent. Can be null. 109 * @param method The method for teh URL request. Can be null. 110 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 111 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 112 * @param followRedirects Sets to true to follow HTTP redirects 113 * @param requestHeaders The request headers. 114 * @return The URL connection 115 * @throws MalformedURLException If the given url is a malformed URL 116 * @throws IOException if an I/O exception occurs 117 */ 118 public static HttpURLConnection prepareConnection(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) throws MalformedURLException, IOException 119 { 120 return prepareConnection(new URL(httpUrl), userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 121 } 122 123 /** 124 * Prepare the connection to the remote url 125 * @param url The url 126 * @param userAgent The user agent. Can be null. 127 * @param method The method for teh URL request. Can be null. 128 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 129 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 130 * @param followRedirects Sets to true to follow HTTP redirects 131 * @param requestHeaders The request headers. 132 * @return The URL connection 133 * @throws MalformedURLException If the given url is a malformed URL 134 * @throws IOException if an I/O exception occurs 135 */ 136 public static HttpURLConnection prepareConnection(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) throws MalformedURLException, IOException 137 { 138 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(); 139 140 if (userAgent != null) 141 { 142 httpUrlConnection.setRequestProperty("User-Agent", userAgent); 143 } 144 if (method != null) 145 { 146 httpUrlConnection.setRequestMethod(method); 147 } 148 if (timeout != -1) 149 { 150 httpUrlConnection.setConnectTimeout(timeout); 151 } 152 153 if (readTimeOut != -1) 154 { 155 httpUrlConnection.setReadTimeout(readTimeOut); 156 } 157 158 if (followRedirects) 159 { 160 httpUrlConnection.setInstanceFollowRedirects(true); 161 } 162 163 for (Entry<String, String> headers : requestHeaders.entrySet()) 164 { 165 httpUrlConnection.setRequestProperty(headers.getKey(), headers.getValue()); 166 } 167 168 return httpUrlConnection; 169 } 170 171 /** 172 * Check the HTTP url 173 * @param httpUrl The HTTP url to test 174 * @param userAgent The user agent. Can be null. 175 * @param method The method for teh URL request. Can be null. 176 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 177 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 178 * @param followRedirects Sets to true to follow HTTP redirects 179 * @return The URL connection 180 */ 181 public static HttpCheckReport checkHttpUrl(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects) 182 { 183 return checkHttpUrl(httpUrl, userAgent, method, timeout, readTimeOut, followRedirects, Collections.EMPTY_MAP); 184 } 185 186 /** 187 * Check the HTTP url 188 * @param httpUrl The url to test 189 * @param userAgent The user agent. Can be null. 190 * @param method The method for teh URL request. Can be null. 191 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 192 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 193 * @param followRedirects Sets to true to follow HTTP redirects 194 * @param requestHeaders The request headers. 195 * @return The URL connection 196 */ 197 public static HttpCheckReport checkHttpUrl(String httpUrl, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) 198 { 199 if (!HTTP_URL_VALIDATOR.matcher(StringUtils.defaultIfEmpty(httpUrl, "")).matches()) 200 { 201 __logger.debug("Url '{}' is not a valid HTTP url", httpUrl); 202 return new HttpCheckReport(HttpCheck.NOT_HTTP); 203 } 204 205 try 206 { 207 return checkHttpUrl(new URL(httpUrl), userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 208 } 209 catch (MalformedURLException e) 210 { 211 __logger.debug("Unable to parse '{}' as a HTTP url", httpUrl, e); 212 return new HttpCheckReport(HttpCheck.NOT_HTTP); 213 } 214 } 215 216 /** 217 * Check the HTTP url 218 * @param url The url to test 219 * @param userAgent The user agent. Can be null. 220 * @param method The method for teh URL request. Can be null. 221 * @param timeout The connection timeout in milliseconds. Set to -1 to not set a timeout. 222 * @param readTimeOut The read timeout in milliseconds. Set to -1 to not set a timeout. 223 * @param followRedirects Sets to true to follow HTTP redirects 224 * @param requestHeaders The request headers. 225 * @return The URL connection 226 */ 227 public static HttpCheckReport checkHttpUrl(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders) 228 { 229 return _checkHttpUrl(url, userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders, new ArrayList<>(), 5); 230 } 231 232 private static HttpCheckReport _checkHttpUrl(URL url, String userAgent, String method, int timeout, int readTimeOut, boolean followRedirects, Map<String, String> requestHeaders, List<String> visitedUrl, int maxRedirects) 233 { 234 try 235 { 236 HttpURLConnection connection = prepareConnection(url, userAgent, method, timeout, readTimeOut, followRedirects, requestHeaders); 237 238 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) 239 { 240 __logger.debug("Check of URL '{}' successed", url); 241 return new HttpCheckReport(HttpCheck.SUCCESS); 242 } 243 else if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP /* 302 */ 244 || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM /* 301 */ 245 || connection.getResponseCode() == HttpURLConnection.HTTP_SEE_OTHER /* 303 */ 246 || connection.getResponseCode() == HTTP_REDIRECT_TEMP /* 307 */ 247 || connection.getResponseCode() == HTTP_REDIRECT_PERM /* 308 */) 248 { 249 if (followRedirects) 250 { 251 // Redirection between HTTP and HTTPS URLs is not followed (do it manually) 252 if (maxRedirects < 0 || visitedUrl.contains(url.toExternalForm())) 253 { 254 // The maximum number of redirects has been reached or the url was already been visited when following redirects 255 throw new IOException("Exceed max number of redirects"); 256 } 257 258 visitedUrl.add(url.toExternalForm()); 259 260 URL base = connection.getURL(); 261 String location = connection.getHeaderField("Location"); 262 URL redirect = new URL(base, location); // Deal with relative URLs 263 264 return _checkHttpUrl(redirect, userAgent, method, timeout, readTimeOut, true, requestHeaders, visitedUrl, maxRedirects - 1); 265 } 266 else 267 { 268 return new HttpCheckReport(HttpCheck.REDIRECT); 269 } 270 } 271 else 272 { 273 __logger.debug("Check of URL '{}' returns the status code {}", url, connection.getResponseCode()); 274 275 int responseCode = connection.getResponseCode(); 276 277 switch (responseCode) 278 { 279 case HttpURLConnection.HTTP_NOT_FOUND: 280 return new HttpCheckReport(HttpCheck.NOT_FOUND); 281 case HttpURLConnection.HTTP_FORBIDDEN: 282 case HttpURLConnection.HTTP_UNAUTHORIZED: 283 return new HttpCheckReport(HttpCheck.UNAUTHORIZED); 284 case HttpURLConnection.HTTP_INTERNAL_ERROR: 285 default: 286 return new HttpCheckReport(HttpCheck.SERVER_ERROR, Optional.ofNullable(connection.getResponseMessage())); 287 } 288 } 289 } 290 catch (SSLHandshakeException e) 291 { 292 __logger.debug("Certificate error for URL '{}'", url, e); 293 return new HttpCheckReport(HttpCheck.SECURITY_LEVEL_ERROR, Optional.ofNullable(e.getMessage())); 294 } 295 catch (SocketTimeoutException e) 296 { 297 __logger.debug("Aborting test for URL '{}' because too long", url, e); 298 return new HttpCheckReport(HttpCheck.TIMEOUT); 299 } 300 catch (UnknownHostException e) 301 { 302 __logger.debug("Unknown host for URL '{}'", url, e); 303 return new HttpCheckReport(HttpCheck.NOT_FOUND); 304 } 305 catch (IOException e) 306 { 307 __logger.debug("Cannot test URL '{}'", url, e); 308 return new HttpCheckReport(HttpCheck.SERVER_ERROR, Optional.ofNullable(e.getMessage())); 309 } 310 } 311 312 /** 313 * Method to check a HTTP url from client side. 314 * The HTTP redirects will be followed. 315 * @param httpUrl the http url to check 316 * @return the result of the check 317 */ 318 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 319 public Map<String, Object> checkHttpUrl(String httpUrl) 320 { 321 Map<String, Object> result = new HashMap<>(); 322 323 HttpCheckReport report = checkHttpUrl(httpUrl, null, null, 2000, 2000, true); 324 result.put("success", HttpCheck.SUCCESS.equals(report.status())); 325 if (report.message().isPresent()) 326 { 327 result.put("message", report.message().get()); 328 } 329 result.put("checkResult", report.status().name()); 330 331 return result; 332 } 333 334 /** 335 * Represent the result of a check and a potential message 336 * @param status the result status 337 * @param message the message if needed 338 */ 339 public record HttpCheckReport(HttpCheck status, Optional<String> message) 340 { 341 /** 342 * Create a report with the provided status and no message. 343 * @param result the result status 344 */ 345 public HttpCheckReport(HttpCheck result) 346 { 347 this(result, Optional.empty()); 348 } 349 } 350}