1 package org.apache.velocity.tools.struts; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import java.util.Iterator; 23 import javax.servlet.ServletContext; 24 import javax.servlet.http.HttpServletRequest; 25 import org.apache.struts.action.SecurePlugInInterface; 26 import org.apache.struts.config.ModuleConfig; 27 import org.apache.struts.config.SecureActionConfig; 28 import org.apache.velocity.tools.generic.ValueParser; 29 import org.apache.velocity.tools.view.LinkTool; 30 import org.apache.velocity.tools.view.ViewContext; 31 32 /** 33 * Tool to be able to use Struts SSL Extensions with Velocity. 34 * <p>It has the same interface as StrutsLinkTool and can function as a 35 * substitute if Struts 1.x and SSL Ext are installed. </p> 36 * 37 * <p>The SecureLinkTool extends the standard 38 * {@link LinkTool} and has the exact same interface as 39 * {@link StrutsLinkTool} and the same function. It should 40 * substitute the {@link StrutsLinkTool} in the toolbox if 41 * <a href="http://sslext.sourceforge.net">SSL Ext</a> is installed. 42 * It's functionality is a subset of the functionality provided by the 43 * sslext tag library for JSP.</p> 44 * 45 * <p>The SSL Ext. Struts extension package makes it possible to declare Struts actions 46 * secure, non-secure, or neutral in the struts config like so:</p> 47 * 48 * <pre> 49 * <action path="/someSecurePath" type="some.important.Action"> 50 * <set-property property="secure" value="true"/> 51 * <forward name="success" path="/somePage.vm" /> 52 * </action> 53 * </pre> 54 * 55 * <p>If an action is declared secure the SecureLinkTool will render the relevant link 56 * as https (if not already in ssl-mode). In the same way, if an action is declared 57 * non-secure the SecureLinkTool will render the relevant link as http (if in ssl-mode). 58 * If the action is declared as neutral (with a "secure" property of "any") then the 59 * SecureLinkTool won't force a protocol change either way.<br/> If the custom 60 * request processor is also used then a request will be redirected to the correct 61 * protocol if an action URL is manually entered into the browser with the wrong protocol</p> 62 * 63 * <p>These are the steps needed to enable SSL Ext:</p> 64 * <ul> 65 * <li>SSL connections need to be enabled on the webserver.</li> 66 * <li>The Java Secure Socket Extension (JSSE) package needs to be in place (it's 67 * integrated into the Java 2 SDK Standard Edition, v. 1.4 but optional for earlier 68 * versions)</li> 69 * <li>In your tools.xml, add the SecureLinkTool to replace (same key) or complement 70 * (alternate key) the {@link StrutsLinkTool}</li> 71 * <li>In struts-conf.xml the custom action-mapping class needs to be specified</li> 72 * <li>In struts-conf.xml the custom controller class can optionally be specified 73 * (if the redirect feature is wanted)</li> 74 * <li>In struts-conf.xml the SecurePlugIn needs to be added</li> 75 * <li>In struts-conf.xml, when using Tiles, the SecureTilesPlugin substitues both the 76 * TilesPlugin and the SecurePlugIn and it also takes care of setting the correct 77 * controller so there is no need to specify the custom controller.</li> 78 * </ul> 79 * 80 * See <a href="http://sslext.sourceforge.net">SSL Ext.project home</a> for more info. 81 * 82 * <p>Usage: 83 * <pre> 84 * Template example: 85 * <!-- Use just like a regular StrutsLinkTool --> 86 * $link.action.nameOfAction 87 * $link.action.nameOfForward 88 * 89 * If the action or forward is marked as secure, or not, 90 * in your struts-config then the link will be rendered 91 * with https or http accordingly. 92 * 93 * Toolbox configuration: 94 * <tools> 95 * <toolbox scope="request"> 96 * <tool class="org.apache.velocity.tools.struts.SecureLinkTool"/> 97 * </toolbox> 98 * </tools> 99 * </pre> 100 * </p> 101 * @since VelocityTools 1.1 102 * @author <a href="mailto:marinoj@centrum.is">Marino A. Jonsson</a> 103 * @version $Revision: 707788 $ $Date: 2008-10-24 16:28:06 -0700 (Fri, 24 Oct 2008) $ 104 */ 105 public class SecureLinkTool extends LinkTool 106 { 107 protected ServletContext application; 108 109 private static final String HTTP = "http"; 110 private static final String HTTPS = "https"; 111 private static final String STD_HTTP_PORT = "80"; 112 private static final String STD_HTTPS_PORT = "443"; 113 114 @Override 115 protected void configure(ValueParser props) 116 { 117 super.configure(props); 118 119 this.application = (ServletContext)props.getValue(ViewContext.SERVLET_CONTEXT_KEY); 120 } 121 122 /** 123 * <p>Returns a copy of the link with the given action name 124 * converted into a server-relative URI reference. This method 125 * does not check if the specified action really is defined. 126 * This method will overwrite any previous URI reference settings 127 * but will copy the query string.</p> 128 * 129 * @param action an action path as defined in struts-config.xml 130 * 131 * @return a new instance of StrutsLinkTool 132 */ 133 public SecureLinkTool setAction(String action) 134 { 135 String link = StrutsUtils.getActionMappingURL(application, request, action); 136 return (SecureLinkTool)absolute(computeURL(request, application, link)); 137 } 138 139 /** 140 * <p>Returns a copy of the link with the given global forward name 141 * converted into a server-relative URI reference. If the parameter 142 * does not map to an existing global forward name, <code>null</code> 143 * is returned. This method will overwrite any previous URI reference 144 * settings but will copy the query string.</p> 145 * 146 * @param forward a global forward name as defined in struts-config.xml 147 * 148 * @return a new instance of StrutsLinkTool 149 */ 150 public SecureLinkTool setForward(String forward) 151 { 152 String url = StrutsUtils.getForwardURL(request, application, forward); 153 if (url == null) 154 { 155 return null; 156 } 157 return (SecureLinkTool)absolute(url); 158 } 159 160 /** 161 * Compute a hyperlink URL based on the specified action link. 162 * The returned URL will have already been passed to 163 * <code>response.encodeURL()</code> for adding a session identifier. 164 * 165 * @param request the current request. 166 * @param app the current ServletContext. 167 * @param link the action that is to be converted to a hyperlink URL 168 * @return the computed hyperlink URL 169 */ 170 public String computeURL(HttpServletRequest request, 171 ServletContext app, String link) 172 { 173 StringBuilder url = new StringBuilder(link); 174 175 String contextPath = request.getContextPath(); 176 177 SecurePlugInInterface securePlugin = (SecurePlugInInterface)app.getAttribute(SecurePlugInInterface.SECURE_PLUGIN); 178 179 if (securePlugin.getSslExtEnable() && 180 url.toString().startsWith(contextPath)) 181 { 182 // Initialize the scheme and ports we are using 183 String usingScheme = request.getScheme(); 184 String usingPort = String.valueOf(request.getServerPort()); 185 186 // Get the servlet context relative link URL 187 String linkString = url.toString().substring(contextPath.length()); 188 189 // See if link references an action somewhere in our app 190 SecureActionConfig secureConfig = getActionConfig(app, linkString); 191 192 // If link is an action, find the desired port and scheme 193 if (secureConfig != null && 194 !SecureActionConfig.ANY.equalsIgnoreCase(secureConfig.getSecure())) 195 { 196 String desiredScheme = Boolean.valueOf(secureConfig.getSecure()).booleanValue() ? 197 HTTPS : HTTP; 198 String desiredPort = Boolean.valueOf(secureConfig.getSecure()).booleanValue() ? 199 securePlugin.getHttpsPort() : securePlugin.getHttpPort(); 200 201 // If scheme and port we are using do not match the ones we want 202 if (!desiredScheme.equals(usingScheme) || 203 !desiredPort.equals(usingPort)) 204 { 205 url.insert(0, startNewUrlString(request, desiredScheme, desiredPort)); 206 207 // This is a hack to help us overcome the problem that some 208 // older browsers do not share sessions between http & https 209 // If this feature is diabled, session ID could still be added 210 // the previous call to the RequestUtils.computeURL() method, 211 // but only if needed due to cookies disabled, etc. 212 if (securePlugin.getSslExtAddSession() && url.toString().indexOf(";jsessionid=") < 0) 213 { 214 // Add the session identifier 215 url = new StringBuilder(toEncoded(url.toString(), 216 request.getSession().getId())); 217 } 218 } 219 } 220 } 221 return url.toString(); 222 } 223 224 /** 225 * Finds the configuration definition for the specified action link 226 * 227 * @param app the current ServletContext. 228 * @param linkString The action we are searching for, specified as a 229 * link. (i.e. may include "..") 230 * @return The SecureActionConfig object entry for this action, 231 * or null if not found 232 */ 233 private static SecureActionConfig getActionConfig(ServletContext app, 234 String linkString) 235 { 236 ModuleConfig moduleConfig = StrutsUtils.selectModule(linkString, app); 237 238 // Strip off the module path, if any 239 linkString = linkString.substring(moduleConfig.getPrefix().length()); 240 241 // Use our servlet mapping, if one is specified 242 //String servletMapping = (String)app.getAttribute(Globals.SERVLET_KEY); 243 244 SecurePlugInInterface spi = (SecurePlugInInterface)app.getAttribute( 245 SecurePlugInInterface.SECURE_PLUGIN); 246 Iterator mappingItr = spi.getServletMappings().iterator(); 247 while (mappingItr.hasNext()) 248 { 249 String servletMapping = (String)mappingItr.next(); 250 251 int starIndex = servletMapping != null ? servletMapping.indexOf('*') 252 : -1; 253 if (starIndex == -1) 254 { 255 continue; 256 } // No servlet mapping or no usable pattern defined, short circuit 257 258 String prefix = servletMapping.substring(0, starIndex); 259 String suffix = servletMapping.substring(starIndex + 1); 260 261 // Strip off the jsessionid, if any 262 int jsession = linkString.indexOf(";jsessionid="); 263 if (jsession >= 0) 264 { 265 linkString = linkString.substring(0, jsession); 266 } 267 268 // Strip off the query string, if any 269 // (differs from the SSL Ext. version - query string before anchor) 270 int question = linkString.indexOf('?'); 271 if (question >= 0) 272 { 273 linkString = linkString.substring(0, question); 274 } 275 276 // Strip off the anchor, if any 277 int anchor = linkString.indexOf('#'); 278 if (anchor >= 0) 279 { 280 linkString = linkString.substring(0, anchor); 281 } 282 283 284 // Unable to establish this link as an action, short circuit 285 if (!(linkString.startsWith(prefix) && linkString.endsWith(suffix))) 286 { 287 continue; 288 } 289 290 // Chop off prefix and suffix 291 linkString = linkString.substring(prefix.length()); 292 linkString = linkString.substring(0, 293 linkString.length() 294 - suffix.length()); 295 if (!linkString.startsWith("/")) 296 { 297 linkString = "/" + linkString; 298 } 299 300 SecureActionConfig secureConfig = (SecureActionConfig)moduleConfig. 301 findActionConfig(linkString); 302 303 return secureConfig; 304 } 305 return null; 306 307 } 308 309 /** 310 * Builds the protocol, server name, and port portion of the new URL 311 * @param request The current request 312 * @param desiredScheme The scheme (http or https) to be used in the new URL 313 * @param desiredPort The port number to be used in th enew URL 314 * @return The new URL as a StringBuilder 315 */ 316 private static StringBuilder startNewUrlString(HttpServletRequest request, 317 String desiredScheme, 318 String desiredPort) 319 { 320 StringBuilder url = new StringBuilder(); 321 String serverName = request.getServerName(); 322 url.append(desiredScheme).append("://").append(serverName); 323 324 if ((HTTP.equals(desiredScheme) && !STD_HTTP_PORT.equals(desiredPort)) || 325 (HTTPS.equals(desiredScheme) && !STD_HTTPS_PORT.equals(desiredPort))) 326 { 327 url.append(":").append(desiredPort); 328 } 329 return url; 330 } 331 332 /** 333 * Return the specified URL with the specified session identifier 334 * suitably encoded. 335 * 336 * @param url URL to be encoded with the session id 337 * @param sessionId Session id to be included in the encoded URL 338 * @return the specified URL with the specified session identifier suitably encoded 339 */ 340 public String toEncoded(String url, String sessionId) 341 { 342 if (url == null || sessionId == null) 343 { 344 return (url); 345 } 346 347 String path = url; 348 String query = ""; 349 String anchor = ""; 350 351 // (differs from the SSL Ext. version - anchor before query string) 352 int pound = url.indexOf('#'); 353 if (pound >= 0) 354 { 355 path = url.substring(0, pound); 356 anchor = url.substring(pound); 357 } 358 int question = path.indexOf('?'); 359 if (question >= 0) 360 { 361 query = path.substring(question); 362 path = path.substring(0, question); 363 } 364 StringBuilder sb = new StringBuilder(path); 365 // jsessionid can't be first. 366 if (sb.length() > 0) 367 { 368 sb.append(";jsessionid="); 369 sb.append(sessionId); 370 } 371 sb.append(query); 372 sb.append(anchor); 373 return sb.toString(); 374 } 375 376 }