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}