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}