1 package com.atlassian.seraph.util;
2
3 import com.atlassian.seraph.config.SecurityConfig;
4 import com.atlassian.seraph.config.SecurityConfigFactory;
5 import com.atlassian.seraph.filter.SecurityFilter;
6 import com.atlassian.seraph.RequestParameterConstants;
7
8 import javax.servlet.http.HttpServletRequest;
9
10 import java.io.UnsupportedEncodingException;
11 import java.net.URLEncoder;
12
13 /**
14 * Utilities for login link redirection.
15 */
16 public class RedirectUtils
17 {
18 private static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
19
20 /**
21 * Returns a login URL that would log the user in to access resource indicated by <code>request</code>.
22 * <p>
23 * For instance, if <code>request</code> is for protected path "/browse/JRA-123" and the user must login before accessing this resource, this
24 * method might return "/login.jsp?os_destination=%2Fbrowse%2FJRA-123". Presumably the login.jsp page will redirect back to 'os_destination' once
25 * logged in.
26 * <p>
27 * The returned path is derived from the <code>login.url</code> parameter in seraph-config.xml, which in the example above would be
28 * "/login.jsp?os_destination={originalurl}". The '${originalurl}' token is replaced at runtime with a relative or absolute path to the original
29 * resource requested by <code>request</code> ('/browse/JRA-123').
30 * <p>
31 * Both the returned URL and the ${originalurl} replacement URL may be absolute or root-relative, depending on whether the seraph-config.xml
32 * <code>login.url</code> parameter is. This allows for redirection to external <acronym title="Single Sign-on">SSO</acronym> apps, which are
33 * passed an absolute path to the originally requested resource.
34 * <p>
35 * 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
36 * done this before calling this method.
37 *
38 * @param request The original request made by the user for a resource.
39 * @return A root-relative or absolute URL of a login link that would log the user in to access the resource.
40 */
41 public static String getLoginUrl(final HttpServletRequest request)
42 {
43 final SecurityConfig securityConfig = SecurityConfigFactory.getInstance();
44 final String loginURL = securityConfig.getLoginURL();
45 return getLoginURL(loginURL, request);
46 }
47
48 /**
49 * Returns a login URL that would log the user in to access resource indicated by <code>request</code>. Identical to
50 * {@link #getLoginUrl(javax.servlet.http.HttpServletRequest)}, except uses the 'link.login.url' parameter in seraph-config.xml instead of
51 * 'login.url', which allows for different login pages depending on whether invoked from a link ("link.login.url") or from a servlet filter that
52 * intercepted a request ("login.url").
53 *
54 * @see #getLoginUrl(javax.servlet.http.HttpServletRequest) for parameters, etc
55 */
56 public static String getLinkLoginURL(final HttpServletRequest request)
57 {
58 final SecurityConfig securityConfig = SecurityConfigFactory.getInstance();
59 final String loginURL = securityConfig.getLinkLoginURL();
60 return getLoginURL(loginURL, request);
61 }
62
63 private static String getLoginURL(String loginURL, final HttpServletRequest request)
64 {
65 final boolean externalLoginLink = isExternalLoginLink(loginURL);
66 loginURL = replaceOriginalURL(loginURL, request, externalLoginLink);
67 if (externalLoginLink)
68 {
69 return loginURL;
70 }
71 return request.getContextPath() + loginURL;
72 }
73
74 private static boolean isExternalLoginLink(final String loginURL)
75 {
76 return (loginURL.indexOf("://") != -1);
77 }
78
79 /**
80 * Replace ${originalurl} token in a string with a URL for a Request.
81 */
82 private static String replaceOriginalURL(final String loginURL, final HttpServletRequest request, final boolean external)
83 {
84 final int i = loginURL.indexOf("${originalurl}");
85 if (i != -1)
86 {
87 final String originalURL = getOriginalURL(request, external);
88 final String osDest = request.getParameter(RequestParameterConstants.OS_DESTINATION);
89 return loginURL.substring(0, i) + ((osDest != null) ? encodeUrl(osDest) : encodeUrl(originalURL)) + loginURL.substring(i + "${originalurl}".length());
90 }
91 return loginURL;
92 }
93
94 private static String encodeUrl(final String url)
95 {
96 try
97 {
98 return URLEncoder.encode(url, "UTF-8");
99 }
100 catch (final UnsupportedEncodingException e)
101 {
102 throw new AssertionError(e);
103 }
104 }
105
106 /**
107 * Recreate a URL from a Request.
108 */
109 private static String getOriginalURL(final HttpServletRequest request, final boolean external)
110 {
111 final String originalURL = (String) request.getAttribute(SecurityFilter.ORIGINAL_URL);
112 if (originalURL != null)
113 {
114 if (external)
115 {
116 return getServerNameAndPath(request) + originalURL;
117 }
118 return originalURL;
119 }
120
121 if (external)
122 {
123 return request.getRequestURL() + (request.getQueryString() == null ? "" : "?" + request.getQueryString());
124 }
125 return request.getServletPath() + (request.getPathInfo() == null ? "" : request.getPathInfo()) + (request.getQueryString() == null ? "" : "?" + request.getQueryString());
126 }
127
128 /**
129 * Reconstruct the request URL from a HttpServletRequest.
130 */
131 public static String getServerNameAndPath(final HttpServletRequest request)
132 {
133 final StringBuffer buf = new StringBuffer();
134 buf.append(request.getScheme()).append("://").append(request.getServerName());
135 if (!(("http".equals(request.getScheme()) && (request.getServerPort() == 80)) || ("https".equals(request.getScheme()) && (request.getServerPort() == 443))))
136 {
137 buf.append(":").append(request.getServerPort());
138 }
139 buf.append(request.getContextPath());
140 return buf.toString();
141 }
142
143 /**
144 * Check whether the request authentication strategy is using
145 * <a href="http://www.ietf.org/rfc/rfc2617.txt">HTTP Basic Authentication</a>
146 *
147 * @param request the current {@link HttpServletRequest}
148 * @param basicAuthParameterName the name of the request parameter that sets the type of authorisation to apply
149 * for the current <code>request</code>
150 */
151 public static boolean isBasicAuthentication(final HttpServletRequest request, final String basicAuthParameterName)
152 {
153 return hasHttpBasicAuthenticationRequestParameter(request, basicAuthParameterName) || hasHttpBasicAuthenticationRequestHeader(request);
154 }
155
156 /**
157 * Check whether the * <a href="http://www.ietf.org/rfc/rfc2617.txt">HTTP Basic Authentication</a> header is set on
158 * the request header
159 *
160 * @param request the current {@link HttpServletRequest}
161 * @return <code>true</code> if the <code>Authorisation</code> header was set to <code>Basic</code>,
162 * <code>false</code> otherwise.
163 */
164 static boolean hasHttpBasicAuthenticationRequestHeader(HttpServletRequest request)
165 {
166 return containsIgnoreCase(request.getHeader(HTTP_BASIC_AUTH_HEADER), HttpServletRequest.BASIC_AUTH);
167 }
168
169 /**
170 * Check whether the * <a href="http://www.ietf.org/rfc/rfc2617.txt">HTTP Basic Authentication</a> is set on
171 * the request parameter. The value of the request parameter is case insensitive.
172 *
173 * @param request the current {@link HttpServletRequest}
174 * @param basicAuthParameterName the name of the request parameter specifying the authentication type
175 * @return <code>true</code> if the request contains the give request parameter sepcifying <code>basic</code> as the
176 * authentication type, <code>false</code> otherwise.
177 *
178 */
179 static boolean hasHttpBasicAuthenticationRequestParameter(HttpServletRequest request, String basicAuthParameterName)
180 {
181 // here we work on the query string to avoid request parsing in the filter
182 String queryString = request.getQueryString() == null ? "" : request.getQueryString();
183
184 queryString = "&" + queryString + "&"; // for easy pattern matching
185 return queryString.indexOf("&" + basicAuthParameterName + "=" + HttpServletRequest.BASIC_AUTH.toLowerCase() + "&") != -1;
186 }
187
188 /**
189 * Appends the path to the context, dealing with any missing slashes properly. Does NOT resolve the path using URL resolution rules. Does NOT
190 * attempt to normalise the resulting URL.
191 *
192 * @param context
193 * a context path as returned by HttpServletRequest.getContextPath, e.g. "/confluence". If null, it will be treated as the default
194 * context ("").
195 * @param path
196 * 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,
197 * otherwise the empty string will be returned.
198 * @return a URL of the given path within the context, e.g. "/confluence/homepage.action".
199 */
200 public static String appendPathToContext(String context, final String path)
201 {
202 if (context == null)
203 {
204 context = "";
205 }
206 if (path == null)
207 {
208 return context;
209 }
210
211 final StringBuffer result = new StringBuffer(context);
212 if (!context.endsWith("/"))
213 {
214 result.append("/");
215 }
216
217 String pathToAppend = path;
218 if (pathToAppend.startsWith("/"))
219 {
220 pathToAppend = pathToAppend.substring(1);
221 }
222
223 result.append(pathToAppend);
224 return result.toString();
225 }
226
227 /*
228 * copied from commons-lang 2.4
229 */
230 static boolean containsIgnoreCase(String str, String searchStr)
231 {
232 if (str == null || searchStr == null)
233 {
234 return false;
235 }
236 return contains(str.toUpperCase(), searchStr.toUpperCase());
237 }
238
239 /*
240 * copied from commons-lang 2.4
241 */
242 static boolean contains(String str, String searchStr)
243 {
244 if (str == null || searchStr == null)
245 {
246 return false;
247 }
248 return str.indexOf(searchStr) >= 0;
249 }
250 }