1   package com.atlassian.seraph.util;
2   
3   import com.atlassian.seraph.RequestParameterConstants;
4   import com.atlassian.seraph.config.SecurityConfig;
5   import com.atlassian.seraph.config.SecurityConfigFactory;
6   import com.atlassian.seraph.filter.SecurityFilter;
7   
8   import java.io.UnsupportedEncodingException;
9   import java.net.URLEncoder;
10  import javax.servlet.http.HttpServletRequest;
11  
12  /**
13   * Utilities for login link redirection.
14   */
15  public class RedirectUtils
16  {
17      private static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
18  
19      /**
20       * Returns a login URL that would log the user in to access resource indicated by <code>request</code>.
21       * <p>
22       * For instance, if <code>request</code> is for protected path "/browse/JRA-123" and the user must login before accessing this resource, this
23       * method might return "/login.jsp?os_destination=%2Fbrowse%2FJRA-123". Presumably the login.jsp page will redirect back to 'os_destination' once
24       * logged in.
25       * <p>
26       * The returned path is derived from the <code>login.url</code> parameter in seraph-config.xml, which in the example above would be
27       * "/login.jsp?os_destination={originalurl}". The '${originalurl}' token is replaced at runtime with a relative or absolute path to the original
28       * resource requested by <code>request</code> ('/browse/JRA-123').
29       * <p>
30       * Both the returned URL and the ${originalurl} replacement URL may be absolute or root-relative, depending on whether the seraph-config.xml
31       * <code>login.url</code> parameter is. This allows for redirection to external <acronym title="Single Sign-on">SSO</acronym> apps, which are
32       * passed an absolute path to the originally requested resource.
33       * <p>
34       * No actual permission checks are performed to determine whether the user needs to log in to access the resource. The caller is assumed to have
35       * done this before calling this method.
36       *
37       * @param request The original request made by the user for a resource.
38       * @return A root-relative or absolute URL of a login link that would log the user in to access the resource.
39       */
40      public static String getLoginUrl(final HttpServletRequest request)
41      {
42          final SecurityConfig securityConfig = SecurityConfigFactory.getInstance();
43          final String loginURL = securityConfig.getLoginURL();
44          return getLoginURL(loginURL, request);
45      }
46  
47      /**
48       * Returns a login URL that would log the user in to access resource indicated by <code>request</code>. Identical to
49       * {@link #getLoginUrl(javax.servlet.http.HttpServletRequest)}, except uses the 'link.login.url' parameter in seraph-config.xml instead of
50       * 'login.url', which allows for different login pages depending on whether invoked from a link ("link.login.url") or from a servlet filter that
51       * intercepted a request ("login.url").
52       *
53       * @see #getLoginUrl(javax.servlet.http.HttpServletRequest) for parameters, etc
54       */
55      public static String getLinkLoginURL(final HttpServletRequest request)
56      {
57          final SecurityConfig securityConfig = SecurityConfigFactory.getInstance();
58          final String loginURL = securityConfig.getLinkLoginURL();
59          return getLoginURL(loginURL, request);
60      }
61  
62      private static String getLoginURL(String loginURL, final HttpServletRequest request)
63      {
64          final boolean externalLoginLink = isExternalLoginLink(loginURL);
65          loginURL = replaceOriginalURL(loginURL, request, externalLoginLink);
66          if (externalLoginLink)
67          {
68              return loginURL;
69          }
70          return request.getContextPath() + loginURL;
71      }
72  
73      private static boolean isExternalLoginLink(final String loginURL)
74      {
75          return (loginURL.indexOf("://") != -1);
76      }
77  
78      /**
79       * Replace ${originalurl} token in a string with a URL for a Request.
80       */
81      private static String replaceOriginalURL(final String loginURL, final HttpServletRequest request, final boolean external)
82      {
83          final int i = loginURL.indexOf("${originalurl}");
84          if (i != -1)
85          {
86              final String originalURL = getOriginalURL(request, external);
87              final String osDest = request.getParameter(RequestParameterConstants.OS_DESTINATION);
88              return loginURL.substring(0, i) + ((osDest != null) ? encodeUrl(osDest) : encodeUrl(originalURL)) + loginURL.substring(i + "${originalurl}".length());
89          }
90          return loginURL;
91      }
92  
93      private static String encodeUrl(final String url)
94      {
95          try
96          {
97              return URLEncoder.encode(url, "UTF-8");
98          }
99          catch (final UnsupportedEncodingException e)
100         {
101             throw new AssertionError(e);
102         }
103     }
104 
105     /**
106      * Recreate a URL from a Request.
107      */
108     private static String getOriginalURL(final HttpServletRequest request, final boolean external)
109     {
110         final String originalURL = (String) request.getAttribute(SecurityFilter.ORIGINAL_URL);
111         if (originalURL != null)
112         {
113             if (external)
114             {
115                 return getServerNameAndPath(request) + originalURL;
116             }
117             return originalURL;
118         }
119 
120         if (external)
121         {
122             return request.getRequestURL() + (request.getQueryString() == null ? "" : "?" + request.getQueryString());
123         }
124         return request.getServletPath() + (request.getPathInfo() == null ? "" : request.getPathInfo()) + (request.getQueryString() == null ? "" : "?" + request.getQueryString());
125     }
126 
127     /**
128      * Reconstruct the context part of a request URL from a HttpServletRequest.
129      * @param request the HttpServletRequest.
130      * @return the context part of a request URL from the given HttpServletRequest.
131      */
132     public static String getServerNameAndPath(final HttpServletRequest request)
133     {
134         return getServerNameAndPath(request, false);
135     }
136 
137     /**
138      * Reconstruct the context part of a request URL from a HttpServletRequest.
139      * @param request the HttpServletRequest.
140      * @param showDefaultPortNumber If true, then we explicitly include the port number even when it is the default port.
141      * @return the context part of a request URL from the given HttpServletRequest.
142      */
143     private static String getServerNameAndPath(HttpServletRequest request, boolean showDefaultPortNumber)
144     {
145         StringBuffer buf = new StringBuffer();
146         buf.append(request.getScheme()).
147                 append("://").
148                 append(request.getServerName());
149         if (showDefaultPortNumber ||
150             ("http".equals(request.getScheme()) && request.getServerPort() != 80) || 
151             ("https".equals(request.getScheme()) && request.getServerPort() != 443)
152             )
153         {
154             buf.append(":").append(request.getServerPort());
155         }
156         buf.append(request.getContextPath());
157         return buf.toString();
158     }
159 
160     /**
161      * Check whether the request authentication strategy is using
162      * <a href="http://www.ietf.org/rfc/rfc2617.txt">HTTP Basic Authentication</a>
163      *
164      * @param request the current {@link HttpServletRequest}
165      * @param basicAuthParameterName the name of the request parameter that sets the type of authorisation to apply
166      * for the current <code>request</code>
167      */
168     public static boolean isBasicAuthentication(final HttpServletRequest request, final String basicAuthParameterName)
169     {
170         return hasHttpBasicAuthenticationRequestParameter(request, basicAuthParameterName) || hasHttpBasicAuthenticationRequestHeader(request);
171     }
172 
173     /**
174      * Check whether the * <a href="http://www.ietf.org/rfc/rfc2617.txt">HTTP Basic Authentication</a> header is set on
175      * the request header
176      *
177      * @param request the current {@link HttpServletRequest}
178      * @return <code>true</code> if the <code>Authorisation</code> header was set to <code>Basic</code>,
179      *         <code>false</code> otherwise.
180      */
181     static boolean hasHttpBasicAuthenticationRequestHeader(HttpServletRequest request)
182     {
183         return containsIgnoreCase(request.getHeader(HTTP_BASIC_AUTH_HEADER), HttpServletRequest.BASIC_AUTH);
184     }
185 
186     /**
187      * Check whether the * <a href="http://www.ietf.org/rfc/rfc2617.txt">HTTP Basic Authentication</a> is set on
188      * the request parameter. The value of the request parameter is case insensitive.
189      *
190      * @param request the current {@link HttpServletRequest}
191      * @param basicAuthParameterName the name of the request parameter specifying the authentication type
192      * @return <code>true</code> if the request contains the give request parameter sepcifying <code>basic</code> as the
193      * authentication type, <code>false</code> otherwise.
194      *
195      */
196     static boolean hasHttpBasicAuthenticationRequestParameter(HttpServletRequest request, String basicAuthParameterName)
197     {
198         // here we work on the query string to avoid request parsing in the filter
199         String queryString = request.getQueryString();
200 
201         queryString = (queryString == null ? "&&" : "&" + queryString + "&"); // for easy pattern matching
202         return queryString.indexOf("&" + basicAuthParameterName + "=" + HttpServletRequest.BASIC_AUTH.toLowerCase() + "&") != -1;
203         /**
204          * This actually seems WRONG in that it states that Basic Auth in case insensitive and SVN history tells me that we used to do
205          * this case insensitive but this is NOT case insensitive.  I would change it but Sam actually wrote test for case senstivity
206          * in 20008.  So I am leaving as is.
207          *
208          * Damn you backward compatibility...damn you to hell! 
209          */
210     }
211 
212     /**
213      * Appends the path to the context, dealing with any missing slashes properly. Does NOT resolve the path using URL resolution rules. Does NOT
214      * attempt to normalise the resulting URL.
215      *
216      * @param context
217      *            a context path as returned by HttpServletRequest.getContextPath, e.g. "/confluence". If null, it will be treated as the default
218      *            context ("").
219      * @param path
220      *            a path to be appended to the context, e.g. "/homepage.action". If this is null, the context will be returned if it is non-null,
221      *            otherwise the empty string will be returned.
222      * @return a URL of the given path within the context, e.g. "/confluence/homepage.action".
223      */
224     public static String appendPathToContext(String context, final String path)
225     {
226         if (context == null)
227         {
228             context = "";
229         }
230         if (path == null)
231         {
232             return context;
233         }
234 
235         final StringBuffer result = new StringBuffer(context);
236         if (!context.endsWith("/"))
237         {
238             result.append("/");
239         }
240 
241         String pathToAppend = path;
242         if (pathToAppend.startsWith("/"))
243         {
244             pathToAppend = pathToAppend.substring(1);
245         }
246 
247         result.append(pathToAppend);
248         return result.toString();
249     }
250 
251     /*
252      * copied from commons-lang 2.4
253      */
254     static boolean containsIgnoreCase(String str, String searchStr)
255     {
256         if (str == null || searchStr == null)
257         {
258             return false;
259         }
260         return contains(str.toUpperCase(), searchStr.toUpperCase());
261     }
262 
263     /*
264      * copied from commons-lang 2.4
265      */
266     static boolean contains(String str, String searchStr)
267     {
268         if (str == null || searchStr == null)
269         {
270             return false;
271         }
272         return str.indexOf(searchStr) >= 0;
273     }
274 
275     /**
276      * Tests if a given (absolute) URL is in the same context as the incoming HttpServletRequest.
277      * This is useful for checking if we will allow a redirect to the given URL.
278      *
279      * @param url The URL.
280      * @param request the incoming HttpServletRequest.
281      * @return <code>true</code> if the given URL is in the same context as the incoming HttpServletRequest.
282      */
283     public static boolean sameContext(final String url, final HttpServletRequest request)
284     {
285         // build up the context from the request
286         String context = getServerNameAndPath(request, false);
287         if (sameContext(url, context))
288         {
289             return true;
290         }
291         // Its possible that the requested URL contains an explicit port number even though it is not required. Check for this.
292         context = getServerNameAndPath(request, true);
293         return sameContext(url, context);
294     }
295     private static boolean sameContext(final String url, String requestContext)
296     {
297         // Now, if the incoming context is "/jira", we want "/jira" and "/jira/whatever" to be considered the same context
298         // but not "/jiranot"
299         if (url.equals(requestContext))
300         {
301             return true;
302         }
303         // http://java.sun.com/javaee/5/docs/api/javax/servlet/ServletContext.html#getContextPath()
304         // Note that Context path should not include a trailing '/', but we will be careful anyway
305         if (!requestContext.endsWith("/"))
306         {
307             requestContext = requestContext + '/';
308         }
309         return url.startsWith(requestContext);
310     }
311 
312 }