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 }