Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
ShiroHttpServletResponse |
|
| 4.8;4.8 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one | |
3 | * or more contributor license agreements. See the NOTICE file | |
4 | * distributed with this work for additional information | |
5 | * regarding copyright ownership. The ASF licenses this file | |
6 | * to you under the Apache License, Version 2.0 (the | |
7 | * "License"); you may not use this file except in compliance | |
8 | * with the License. You may obtain a copy of the License at | |
9 | * | |
10 | * http://www.apache.org/licenses/LICENSE-2.0 | |
11 | * | |
12 | * Unless required by applicable law or agreed to in writing, | |
13 | * software distributed under the License is distributed on an | |
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
15 | * KIND, either express or implied. See the License for the | |
16 | * specific language governing permissions and limitations | |
17 | * under the License. | |
18 | */ | |
19 | package org.apache.shiro.web.servlet; | |
20 | ||
21 | import javax.servlet.ServletContext; | |
22 | import javax.servlet.http.HttpServletRequest; | |
23 | import javax.servlet.http.HttpServletResponse; | |
24 | import javax.servlet.http.HttpServletResponseWrapper; | |
25 | import javax.servlet.http.HttpSession; | |
26 | import java.io.IOException; | |
27 | import java.net.MalformedURLException; | |
28 | import java.net.URL; | |
29 | import java.net.URLEncoder; | |
30 | ||
31 | /** | |
32 | * HttpServletResponse implementation to support URL Encoding of Shiro Session IDs. | |
33 | * <p/> | |
34 | * It is only used when using Shiro's native Session Management configuration (and not when using the Servlet | |
35 | * Container session configuration, which is Shiro's default in a web environment). Because the servlet container | |
36 | * already performs url encoding of its own session ids, instances of this class are only needed when using Shiro | |
37 | * native sessions. | |
38 | * <p/> | |
39 | * Note that this implementation relies in part on source code from the Tomcat 6.x distribution for | |
40 | * encoding URLs for session ID URL Rewriting (we didn't want to re-invent the wheel). Since Shiro is also | |
41 | * Apache 2.0 license, all regular licenses and conditions have remained in tact. | |
42 | * | |
43 | * @since 0.2 | |
44 | */ | |
45 | public class ShiroHttpServletResponse extends HttpServletResponseWrapper { | |
46 | ||
47 | //TODO - complete JavaDoc | |
48 | ||
49 | private static final String DEFAULT_SESSION_ID_PARAMETER_NAME = ShiroHttpSession.DEFAULT_SESSION_ID_NAME; | |
50 | ||
51 | 0 | private ServletContext context = null; |
52 | //the associated request | |
53 | 0 | private ShiroHttpServletRequest request = null; |
54 | ||
55 | public ShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) { | |
56 | 0 | super(wrapped); |
57 | 0 | this.context = context; |
58 | 0 | this.request = request; |
59 | 0 | } |
60 | ||
61 | @SuppressWarnings({"UnusedDeclaration"}) | |
62 | public ServletContext getContext() { | |
63 | 0 | return context; |
64 | } | |
65 | ||
66 | @SuppressWarnings({"UnusedDeclaration"}) | |
67 | public void setContext(ServletContext context) { | |
68 | 0 | this.context = context; |
69 | 0 | } |
70 | ||
71 | public ShiroHttpServletRequest getRequest() { | |
72 | 0 | return request; |
73 | } | |
74 | ||
75 | @SuppressWarnings({"UnusedDeclaration"}) | |
76 | public void setRequest(ShiroHttpServletRequest request) { | |
77 | 0 | this.request = request; |
78 | 0 | } |
79 | ||
80 | /** | |
81 | * Encode the session identifier associated with this response | |
82 | * into the specified redirect URL, if necessary. | |
83 | * | |
84 | * @param url URL to be encoded | |
85 | */ | |
86 | public String encodeRedirectURL(String url) { | |
87 | 0 | if (isEncodeable(toAbsolute(url))) { |
88 | 0 | return toEncoded(url, request.getSession().getId()); |
89 | } else { | |
90 | 0 | return url; |
91 | } | |
92 | } | |
93 | ||
94 | ||
95 | public String encodeRedirectUrl(String s) { | |
96 | 0 | return encodeRedirectURL(s); |
97 | } | |
98 | ||
99 | ||
100 | /** | |
101 | * Encode the session identifier associated with this response | |
102 | * into the specified URL, if necessary. | |
103 | * | |
104 | * @param url URL to be encoded | |
105 | */ | |
106 | public String encodeURL(String url) { | |
107 | 0 | String absolute = toAbsolute(url); |
108 | 0 | if (isEncodeable(absolute)) { |
109 | // W3c spec clearly said | |
110 | 0 | if (url.equalsIgnoreCase("")) { |
111 | 0 | url = absolute; |
112 | } | |
113 | 0 | return toEncoded(url, request.getSession().getId()); |
114 | } else { | |
115 | 0 | return url; |
116 | } | |
117 | } | |
118 | ||
119 | public String encodeUrl(String s) { | |
120 | 0 | return encodeURL(s); |
121 | } | |
122 | ||
123 | /** | |
124 | * Return <code>true</code> if the specified URL should be encoded with | |
125 | * a session identifier. This will be true if all of the following | |
126 | * conditions are met: | |
127 | * <ul> | |
128 | * <li>The request we are responding to asked for a valid session | |
129 | * <li>The requested session ID was not received via a cookie | |
130 | * <li>The specified URL points back to somewhere within the web | |
131 | * application that is responding to this request | |
132 | * </ul> | |
133 | * | |
134 | * @param location Absolute URL to be validated | |
135 | * @return {@code true} if the specified URL should be encoded with a session identifier, {@code false} otherwise. | |
136 | */ | |
137 | protected boolean isEncodeable(final String location) { | |
138 | ||
139 | 0 | if (location == null) |
140 | 0 | return (false); |
141 | ||
142 | // Is this an intra-document reference? | |
143 | 0 | if (location.startsWith("#")) |
144 | 0 | return (false); |
145 | ||
146 | // Are we in a valid session that is not using cookies? | |
147 | 0 | final HttpServletRequest hreq = request; |
148 | 0 | final HttpSession session = hreq.getSession(false); |
149 | 0 | if (session == null) |
150 | 0 | return (false); |
151 | 0 | if (hreq.isRequestedSessionIdFromCookie()) |
152 | 0 | return (false); |
153 | ||
154 | 0 | return doIsEncodeable(hreq, session, location); |
155 | } | |
156 | ||
157 | private boolean doIsEncodeable(HttpServletRequest hreq, HttpSession session, String location) { | |
158 | // Is this a valid absolute URL? | |
159 | URL url; | |
160 | try { | |
161 | 0 | url = new URL(location); |
162 | 0 | } catch (MalformedURLException e) { |
163 | 0 | return (false); |
164 | 0 | } |
165 | ||
166 | // Does this URL match down to (and including) the context path? | |
167 | 0 | if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol())) |
168 | 0 | return (false); |
169 | 0 | if (!hreq.getServerName().equalsIgnoreCase(url.getHost())) |
170 | 0 | return (false); |
171 | 0 | int serverPort = hreq.getServerPort(); |
172 | 0 | if (serverPort == -1) { |
173 | 0 | if ("https".equals(hreq.getScheme())) |
174 | 0 | serverPort = 443; |
175 | else | |
176 | 0 | serverPort = 80; |
177 | } | |
178 | 0 | int urlPort = url.getPort(); |
179 | 0 | if (urlPort == -1) { |
180 | 0 | if ("https".equals(url.getProtocol())) |
181 | 0 | urlPort = 443; |
182 | else | |
183 | 0 | urlPort = 80; |
184 | } | |
185 | 0 | if (serverPort != urlPort) |
186 | 0 | return (false); |
187 | ||
188 | 0 | String contextPath = getRequest().getContextPath(); |
189 | 0 | if (contextPath != null) { |
190 | 0 | String file = url.getFile(); |
191 | 0 | if ((file == null) || !file.startsWith(contextPath)) |
192 | 0 | return (false); |
193 | 0 | String tok = ";" + DEFAULT_SESSION_ID_PARAMETER_NAME + "=" + session.getId(); |
194 | 0 | if (file.indexOf(tok, contextPath.length()) >= 0) |
195 | 0 | return (false); |
196 | } | |
197 | ||
198 | // This URL belongs to our web application, so it is encodeable | |
199 | 0 | return (true); |
200 | ||
201 | } | |
202 | ||
203 | ||
204 | /** | |
205 | * Convert (if necessary) and return the absolute URL that represents the | |
206 | * resource referenced by this possibly relative URL. If this URL is | |
207 | * already absolute, return it unchanged. | |
208 | * | |
209 | * @param location URL to be (possibly) converted and then returned | |
210 | * @return resource location as an absolute url | |
211 | * @throws IllegalArgumentException if a MalformedURLException is | |
212 | * thrown when converting the relative URL to an absolute one | |
213 | */ | |
214 | private String toAbsolute(String location) { | |
215 | ||
216 | 0 | if (location == null) |
217 | 0 | return (location); |
218 | ||
219 | 0 | boolean leadingSlash = location.startsWith("/"); |
220 | ||
221 | 0 | if (leadingSlash || !hasScheme(location)) { |
222 | ||
223 | 0 | StringBuilder buf = new StringBuilder(); |
224 | ||
225 | 0 | String scheme = request.getScheme(); |
226 | 0 | String name = request.getServerName(); |
227 | 0 | int port = request.getServerPort(); |
228 | ||
229 | try { | |
230 | 0 | buf.append(scheme).append("://").append(name); |
231 | 0 | if ((scheme.equals("http") && port != 80) |
232 | || (scheme.equals("https") && port != 443)) { | |
233 | 0 | buf.append(':').append(port); |
234 | } | |
235 | 0 | if (!leadingSlash) { |
236 | 0 | String relativePath = request.getRequestURI(); |
237 | 0 | int pos = relativePath.lastIndexOf('/'); |
238 | 0 | relativePath = relativePath.substring(0, pos); |
239 | ||
240 | 0 | String encodedURI = URLEncoder.encode(relativePath, getCharacterEncoding()); |
241 | 0 | buf.append(encodedURI).append('/'); |
242 | } | |
243 | 0 | buf.append(location); |
244 | 0 | } catch (IOException e) { |
245 | 0 | IllegalArgumentException iae = new IllegalArgumentException(location); |
246 | 0 | iae.initCause(e); |
247 | 0 | throw iae; |
248 | 0 | } |
249 | ||
250 | 0 | return buf.toString(); |
251 | ||
252 | } else { | |
253 | 0 | return location; |
254 | } | |
255 | } | |
256 | ||
257 | /** | |
258 | * Determine if the character is allowed in the scheme of a URI. | |
259 | * See RFC 2396, Section 3.1 | |
260 | * | |
261 | * @param c the character to check | |
262 | * @return {@code true} if the character is allowed in a URI scheme, {@code false} otherwise. | |
263 | */ | |
264 | public static boolean isSchemeChar(char c) { | |
265 | 0 | return Character.isLetterOrDigit(c) || |
266 | c == '+' || c == '-' || c == '.'; | |
267 | } | |
268 | ||
269 | ||
270 | /** | |
271 | * Returns {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise. | |
272 | * | |
273 | * @param uri the URI string to check for a scheme component | |
274 | * @return {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise. | |
275 | */ | |
276 | private boolean hasScheme(String uri) { | |
277 | 0 | int len = uri.length(); |
278 | 0 | for (int i = 0; i < len; i++) { |
279 | 0 | char c = uri.charAt(i); |
280 | 0 | if (c == ':') { |
281 | 0 | return i > 0; |
282 | 0 | } else if (!isSchemeChar(c)) { |
283 | 0 | return false; |
284 | } | |
285 | } | |
286 | 0 | return false; |
287 | } | |
288 | ||
289 | /** | |
290 | * Return the specified URL with the specified session identifier suitably encoded. | |
291 | * | |
292 | * @param url URL to be encoded with the session id | |
293 | * @param sessionId Session id to be included in the encoded URL | |
294 | * @return the url with the session identifer properly encoded. | |
295 | */ | |
296 | protected String toEncoded(String url, String sessionId) { | |
297 | ||
298 | 0 | if ((url == null) || (sessionId == null)) |
299 | 0 | return (url); |
300 | ||
301 | 0 | String path = url; |
302 | 0 | String query = ""; |
303 | 0 | String anchor = ""; |
304 | 0 | int question = url.indexOf('?'); |
305 | 0 | if (question >= 0) { |
306 | 0 | path = url.substring(0, question); |
307 | 0 | query = url.substring(question); |
308 | } | |
309 | 0 | int pound = path.indexOf('#'); |
310 | 0 | if (pound >= 0) { |
311 | 0 | anchor = path.substring(pound); |
312 | 0 | path = path.substring(0, pound); |
313 | } | |
314 | 0 | StringBuilder sb = new StringBuilder(path); |
315 | 0 | if (sb.length() > 0) { // session id param can't be first. |
316 | 0 | sb.append(";"); |
317 | 0 | sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME); |
318 | 0 | sb.append("="); |
319 | 0 | sb.append(sessionId); |
320 | } | |
321 | 0 | sb.append(anchor); |
322 | 0 | sb.append(query); |
323 | 0 | return (sb.toString()); |
324 | ||
325 | } | |
326 | } |