View Javadoc

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   * &lt;action path="/someSecurePath" type="some.important.Action"&gt;
50   *     &lt;set-property property="secure" value="true"/&gt;
51   *     &lt;forward name="success" path="/somePage.vm" /&gt;
52   * &lt;/action&gt;
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   * &lt;!-- Use just like a regular StrutsLinkTool --&gt;
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   * &lt;tools&gt;
95   *   &lt;toolbox scope="request"&gt;
96   *     &lt;tool class="org.apache.velocity.tools.struts.SecureLinkTool"/&gt;
97   *   &lt;/toolbox&gt;
98   * &lt;/tools&gt;
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 }