001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017/*
018 * ====================================================================
019 *
020 * The Apache Software License, Version 1.1
021 *
022 * Copyright (c) 1999-2003 The Apache Software Foundation.  All rights
023 * reserved.
024 *
025 * Redistribution and use in source and binary forms, with or without
026 * modification, are permitted provided that the following conditions
027 * are met:
028 *
029 * 1. Redistributions of source code must retain the above copyright
030 *    notice, this list of conditions and the following disclaimer.
031 *
032 * 2. Redistributions in binary form must reproduce the above copyright
033 *    notice, this list of conditions and the following disclaimer in
034 *    the documentation and/or other materials provided with the
035 *    distribution.
036 *
037 * 3. The end-user documentation included with the redistribution, if
038 *    any, must include the following acknowlegement:
039 *       "This product includes software developed by the
040 *        Apache Software Foundation (http://www.apache.org/)."
041 *    Alternately, this acknowlegement may appear in the software itself,
042 *    if and wherever such third-party acknowlegements normally appear.
043 *
044 * 4. The names "The Jakarta Project", "Struts", and "Apache Software
045 *    Foundation" must not be used to endorse or promote products derived
046 *    from this software without prior written permission. For written
047 *    permission, please contact apache@apache.org.
048 *
049 * 5. Products derived from this software may not be called "Apache"
050 *    nor may "Apache" appear in their names without prior written
051 *    permission of the Apache Group.
052 *
053 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
054 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
055 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
056 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
057 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
058 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
059 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
060 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
061 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
062 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
063 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
064 * SUCH DAMAGE.
065 * ====================================================================
066 *
067 * This software consists of voluntary contributions made by many
068 * individuals on behalf of the Apache Software Foundation.  For more
069 * information on the Apache Software Foundation, please see
070 * <http://www.apache.org/>.
071 *
072 */
073package com.echothree.view.client.web.struts.sslext.util;
074
075import com.echothree.view.client.web.struts.sslext.action.SecurePlugInInterface;
076import com.echothree.view.client.web.struts.sslext.config.SecureActionMapping;
077import java.net.MalformedURLException;
078import java.util.Enumeration;
079import java.util.HashMap;
080import java.util.Iterator;
081import java.util.List;
082import java.util.Map;
083import java.util.Set;
084import javax.servlet.ServletContext;
085import javax.servlet.http.HttpServletRequest;
086import javax.servlet.http.HttpServletResponse;
087import javax.servlet.jsp.PageContext;
088import org.apache.commons.logging.Log;
089import org.apache.commons.logging.LogFactory;
090import org.apache.struts.Globals;
091import org.apache.struts.config.ModuleConfig;
092import org.apache.struts.taglib.TagUtils;
093import org.apache.struts.util.MessageResources;
094import org.apache.struts.util.ModuleUtils;
095
096/**
097 * Define some additional utility methods utilized by sslext.
098 */
099public class SecureRequestUtils {
100    
101    /**
102     * The message resources.
103     */
104    protected static MessageResources messages = MessageResources.getMessageResources("org.apache.struts.taglib.html.LocalStrings");
105    
106    private static Log sLog = LogFactory.getLog(SecureRequestUtils.class);
107    
108    private static final String HTTP = "http";
109    private static final String HTTPS = "https";
110    private static final String STD_HTTP_PORT = "80";
111    private static final String STD_HTTPS_PORT = "443";
112    
113    private static final String STOWED_REQUEST_ATTRIBS = "ssl.redirect.attrib.stowed";
114    
115    /**
116     * Compute a hyperlink URL based on the <code>forward</code>,
117     * <code>href</code>, or <code>page</code> parameter that is not null.
118     * The returned URL will have already been passed to
119     * <code>response.encodeURL()</code> for adding a session identifier.
120     *
121     * @param pageContext PageContext for the tag making this call
122     *
123     * @param forward Logical forward name for which to look up
124     *  the context-relative URI (if specified)
125     * @param href URL to be utilized unmodified (if specified)
126     * @param page Context-relative page for which a URL should
127     *  be created (if specified)
128     * @param action a Struts action name
129     *
130     * @param params Map of parameters to be dynamically included (if any)
131     * @param anchor Anchor to be dynamically included (if any)
132     *
133     * @param redirect Is this URL for a <code>response.sendRedirect()</code>?
134     *
135     * @exception MalformedURLException if a URL cannot be created
136     *  for the specified parameters
137     */
138    public static String computeURL(PageContext pageContext, String forward, String href, String page, String action, Map params, String anchor, boolean redirect)
139    throws MalformedURLException {
140        StringBuilder url = new StringBuilder(TagUtils.getInstance().computeURL(pageContext, forward, href, page, action, null, params, anchor, redirect));
141        HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
142        
143        // Get the action servlet's context, we'll need it later
144        ServletContext servletContext = pageContext.getServletContext();
145        String contextPath = request.getContextPath();
146        SecurePlugInInterface securePlugin = (SecurePlugInInterface)servletContext.getAttribute(SecurePlugInInterface.SECURE_PLUGIN);
147        if(securePlugin.getSslExtEnable() && url.toString().startsWith(contextPath)) {
148            
149            // Initialize the scheme and ports we are using
150            String usingScheme = request.getScheme();
151            String usingPort = String.valueOf(request.getServerPort());
152            
153            // Get the servlet context relative link URL
154            String linkString = url.toString().substring(contextPath.length());
155            
156            // See if link references an action somewhere in our app
157            SecureActionMapping secureConfig = getActionConfig(pageContext, linkString);
158            
159            // If link is an action, find the desired port and scheme
160            if(secureConfig != null && !SecureActionMapping.ANY.equalsIgnoreCase(secureConfig.getSecure())) {
161                
162                String desiredScheme = Boolean.valueOf(secureConfig.getSecure()) ? HTTPS : HTTP;
163                String desiredPort = Boolean.valueOf(secureConfig.getSecure()) ? securePlugin.getHttpsPort() : securePlugin.getHttpPort();
164                
165                // If scheme and port we are using do not match the ones we want
166                if((!desiredScheme.equals(usingScheme) || !desiredPort.equals(usingPort))) {
167                    url.insert(0, startNewUrlString(request, desiredScheme, desiredPort));
168                    
169                    // This is a hack to help us overcome the problem that some
170                    // older browsers do not share sessions between http & https
171                    // If this feature is diabled, session ID could still be added
172                    // the previous call to the RequestUtils.computeURL() method,
173                    // but only if needed due to cookies disabled, etc.
174                    if(securePlugin.getSslExtAddSession() && url.toString().indexOf(";jsessionid=") < 0) {
175                        // Add the session identifier
176                        url = new StringBuilder(toEncoded(url.toString(),
177                        request.getSession().getId()));
178                    }
179                }
180            }
181        }
182        
183        return url.toString();
184    }
185    
186    /**
187     * Finds the configuration definition for the specified action link
188     * @param pageContext the current page context.
189     * @param linkString The action we are searching for, specified as a link. (i.e. may include "..")
190     * @return The SecureActionMapping object entry for this action, or null if not found
191     */
192    private static SecureActionMapping getActionConfig(PageContext pageContext, String linkString) {
193        ModuleConfig moduleConfig = SecureRequestUtils.selectModule(linkString, pageContext);
194        
195        // Strip off the subapp path, if any
196        linkString = linkString.substring(moduleConfig.getPrefix().length());
197        
198        // Get all the servlet mappings for the ActionServlet, loop thru to find
199        // the correct action being specified
200        ServletContext servletContext = pageContext.getServletContext();
201        SecurePlugInInterface spi = (SecurePlugInInterface)servletContext.getAttribute(SecurePlugInInterface.SECURE_PLUGIN);
202        Iterator mappingItr = spi.getServletMappings().iterator();
203        
204        while(mappingItr.hasNext()) {
205            String servletMapping = (String) mappingItr.next();
206            
207            int starIndex = servletMapping != null ? servletMapping.indexOf('*') : -1;
208            if(starIndex == -1) {
209                continue;
210            }// No servlet mapping or no usable pattern defined, short circuit
211            
212            String prefix = servletMapping.substring(0, starIndex);
213            String suffix = servletMapping.substring(starIndex + 1);
214            
215            // Strip off the jsessionid, if any
216            int jsession = linkString.indexOf(";jsessionid=");
217            if(jsession >= 0) {
218                linkString = linkString.substring(0, jsession);
219            }
220            
221            // Strip off the anchor, if any
222            int anchor = linkString.indexOf('#');
223            if(anchor >= 0) {
224                linkString = linkString.substring(0, anchor);
225            }
226            
227            // Strip off the query string, if any
228            int question = linkString.indexOf('?');
229            if(question >= 0) {
230                linkString = linkString.substring(0, question);
231            }
232            
233            // Unable to establish this link as an action, short circuit
234            if(!(linkString.startsWith(prefix) && linkString.endsWith(suffix))) {
235                continue;
236            }
237            
238            // Chop off prefix and suffix
239            linkString = linkString.substring(prefix.length());
240            linkString = linkString.substring(0, linkString.length() - suffix.length());
241            if(!linkString.startsWith("/")) {
242                linkString = "/" + linkString;
243            }
244            
245            SecureActionMapping secureConfig = (SecureActionMapping) moduleConfig.findActionConfig(linkString);
246            
247            return secureConfig;
248        }
249        
250        return null;
251    }
252    
253    /**
254     * Builds the protocol, server name, and port portion of the new URL
255     * @param request The current request
256     * @param desiredScheme  The scheme (http or https) to be used in the new URL
257     * @param desiredPort The port number to be used in th enew URL
258     * @return The new URL as a StringBuilder
259     */
260    private static StringBuilder startNewUrlString(HttpServletRequest request, String desiredScheme, String desiredPort) {
261        StringBuilder url = new StringBuilder();
262        String serverName = request.getServerName();
263        
264        url.append(desiredScheme).append("://").append(serverName);
265        
266        if((HTTP.equals(desiredScheme) && !STD_HTTP_PORT.equals(desiredPort)) || (HTTPS.equals(desiredScheme) && !STD_HTTPS_PORT.equals(desiredPort))) {
267            url.append(":").append(desiredPort);
268        }
269        
270        return url;
271    }
272    
273    
274    /**
275     * Creates query String from request body parameters
276     * @param aRequest The current request
277     * @return The created query string (with no leading "?")
278     */
279    public static String getRequestParameters(HttpServletRequest aRequest) {
280        Map m = getParameterMap(aRequest);
281        
282        return createQueryStringFromMap(m, "&").toString();
283    }
284    
285    /**
286     * Builds a query string from a given map of parameters
287     * @param m A map of parameters
288     * @param ampersand String to use for ampersands (e.g. "&" or "&amp;" )
289     * @return query string (with no leading "?")
290     */
291    public static StringBuilder createQueryStringFromMap(Map m, String ampersand) {
292        StringBuilder aReturn = new StringBuilder("");
293        Set aEntryS = m.entrySet();
294
295        for(final Object entry : aEntryS) {
296            var aEntry = (Map.Entry)entry;
297            Object value = aEntry.getValue();
298            String[] aValues = new String[1];
299            if(value == null) {
300                aValues[0] = "";
301            } else if(value instanceof List) { // Work around for Weblogic 6.1sp1
302                List aList = (List)value;
303                aValues = (String[])aList.toArray(new String[aList.size()]);
304            } else if(value instanceof String) {  // Single value from Struts tags
305                aValues[0] = (String)value;
306            } else { // String array, the standard returned from request.getParameterMap()
307                aValues = (String[])value;  // This is the standard
308            }
309            for(var aValue : aValues) {
310                append(aEntry.getKey(), aValue, aReturn, ampersand);
311            }
312        }
313        
314        return aReturn;
315    }
316    
317    /**
318     * Appends new key and value pair to query string
319     * @param key parameter name
320     * @param value value of parameter
321     * @param queryString existing query string
322     * @param ampersand string to use for ampersand (e.g. "&" or "&amp;")
323     * @return query string (with no leading "?")
324     */
325    private static StringBuilder append(Object key, Object value, StringBuilder queryString, String ampersand) {
326        TagUtils tagUtils = TagUtils.getInstance();
327        
328        if(queryString.length() > 0) {
329            queryString.append(ampersand);
330        }
331        queryString.append(tagUtils.encodeURL(key.toString()));
332        queryString.append("=");
333        queryString.append(tagUtils.encodeURL(value.toString()));
334        
335        return queryString;
336    }
337    
338    /**
339     * Stores request attributes in session
340     * @param aRequest The current request
341     * @return true, if the attributes were stowed in the session,
342     * false otherwise
343     */
344    public static boolean stowRequestAttributes(HttpServletRequest aRequest) {
345        if(aRequest.getSession().getAttribute(STOWED_REQUEST_ATTRIBS) != null) {
346            return false;
347        }
348        
349        Enumeration enumeration = aRequest.getAttributeNames();
350        Map map = new HashMap();
351        while(enumeration.hasMoreElements()) {
352            String name = (String) enumeration.nextElement();
353            map.put(name, aRequest.getAttribute(name));
354        }
355        aRequest.getSession().setAttribute(STOWED_REQUEST_ATTRIBS, map);
356        
357        return true;
358    }
359    
360    
361    /**
362     * Reclaims request attributes from session to request
363     * @param aRequest The current request
364     * @param doRemove True, if the attributes should be removed after being reclaimed,
365     * false otherwise
366     */
367    public static void reclaimRequestAttributes(HttpServletRequest aRequest,
368    boolean doRemove) {
369        Map map = (Map) aRequest.getSession().getAttribute(STOWED_REQUEST_ATTRIBS);
370        
371        if(map == null) {
372            return;
373        }
374        
375        Iterator itr = map.keySet().iterator();
376        while(itr.hasNext()) {
377            String name = (String) itr.next();
378            
379            aRequest.setAttribute(name, map.get(name));
380        }
381        
382        if(doRemove) {
383            aRequest.getSession().removeAttribute(STOWED_REQUEST_ATTRIBS);
384        }
385    }
386    
387    
388    /**
389     * Creates a redirect URL string if the current request should be redirected
390     * @param request current servlet request
391     * @param application the currecnt ServletContext
392     * @param isSecure "true" if the current request should be transmitted via SSL
393     * "false" if not, "any" if we just don't care if it's SSL or not
394     * @return the URL to redirect to
395     */
396    static public String getRedirectString(HttpServletRequest request, ServletContext application, String isSecure) {
397        String urlString = null;
398        SecurePlugInInterface securePlugin = (SecurePlugInInterface)application.getAttribute(SecurePlugInInterface.SECURE_PLUGIN);
399        String httpPort = securePlugin.getHttpPort();
400        String httpsPort = securePlugin.getHttpsPort();
401        
402        // If sslext disabled, or we don't have a protocol preference,
403        // just return the null value we have so far
404        if(!securePlugin.getSslExtEnable() || SecureActionMapping.ANY.equalsIgnoreCase(isSecure)) {
405            return urlString;
406        }
407        
408        // get the scheme we want to use for this page and
409        // get the scheme used in this request
410        String desiredScheme = Boolean.valueOf(isSecure) ? HTTPS : HTTP;
411        String usingScheme = request.getScheme();
412        
413        // Determine the port number we want to use
414        // and the port number we used in this request
415        String desiredPort = Boolean.valueOf(isSecure) ? httpsPort : httpPort;
416        String usingPort = String.valueOf(request.getServerPort());
417        
418        // Must also check ports, because of IE multiple redirect problem
419        if(!desiredScheme.equals(usingScheme) || !desiredPort.equals(usingPort)) {
420            
421            urlString = buildNewUrlString(request,
422            desiredScheme,
423            desiredPort,
424            securePlugin.getSslExtAddSession());
425            
426            // Temporarily store attributes in session
427            if(!SecureRequestUtils.stowRequestAttributes(request)) {
428                // If request attributes already stored in session, reclaim them
429                // This is a hack for the IE multiple redirect problem
430                SecureRequestUtils.reclaimRequestAttributes(request, false);
431            }
432        } else {
433            // Retrieve attributes from session
434            SecureRequestUtils.reclaimRequestAttributes(request, true);
435        }
436        
437        return urlString;
438    }
439    
440    
441    /**
442     * Builds the URL that we will redirect to
443     * @param request The current request
444     * @param desiredScheme The protocol (http or https) we wish to use in new URL
445     * @param desiredPort The port number we wish to use in new URL
446     * @return the URL we will redirect to, as a String
447     */
448    private static String buildNewUrlString(HttpServletRequest request, String desiredScheme, String desiredPort,
449    boolean addSessionID) {
450        StringBuilder url = startNewUrlString(request, desiredScheme, desiredPort);
451        
452        url.append(request.getRequestURI());
453        
454        String returnUrl = addQueryString(request, url);
455        
456        // If the add session ID feature is enabled, add the session ID when creating a new URL
457        // Could still be added by the calling checkSsl() method if needed due to disabled cookies, etc.
458        if(addSessionID){
459            returnUrl = toEncoded( returnUrl, request.getSession().getId());
460        }
461        
462        return returnUrl;
463    }
464    
465    /**
466     * Adds the query string, if any, to the given URL.  The query string
467     * is either taken from the existing query string or
468     * generated from the posting request body parameters.
469     * @param request The current request
470     * @param url The existing URL we will add the query string to
471     * @return The URL with query string
472     */
473    private static String addQueryString(HttpServletRequest request, StringBuilder url) {
474        // add query string, if any
475        String queryString = request.getQueryString();
476        
477        if(queryString != null && queryString.length() != 0) {
478            url.append("?").append(queryString);
479        } else {
480            queryString = SecureRequestUtils.getRequestParameters(request);
481            if(queryString != null && queryString.length() != 0) {
482                url.append("?").append(queryString);
483            }
484        }
485        
486        return url.toString();
487    }
488    
489    
490    /**
491     * Select the sub-application to which the specified request belongs, and
492     * add corresponding request attributes to this request.
493     *
494     * @param urlPath The requested URL
495     * @param pageContext The ServletContext for this web application
496     * @return The ModuleConfig for the given URL path
497     */
498    public static ModuleConfig selectModule(String urlPath, PageContext pageContext) {
499        // Get the ServletContext
500        ServletContext servletContext = pageContext.getServletContext();
501        
502        // Match against the list of sub-application prefixes
503        String prefix = ModuleUtils.getInstance().getModuleName(urlPath, servletContext);
504        
505        // Expose the resources for this sub-application
506        ModuleConfig config = (ModuleConfig) servletContext.getAttribute(Globals.MODULE_KEY + prefix);
507        
508        return config;
509    }
510    
511    /**
512     *  Creates a map of request parameters where the key is the parameter name and the
513     *  value is the String array of parameter values.
514     *  @param request The current request
515     *  @return The map of parameters and their values
516     */
517    private static Map getParameterMap(HttpServletRequest request) {
518        Map map = new HashMap();
519        Enumeration enumeration = request.getParameterNames();
520        
521        while(enumeration.hasMoreElements()) {
522            String name = (String) enumeration.nextElement();
523            String[] values = request.getParameterValues(name);
524            map.put(name, values);
525        }
526        
527        return map;
528    }
529    
530    /** Checks to see if SSL should be toggled for this
531     *  action
532     *  @param aMapping The mapping object for this Action
533     *  @param aContext The current ServletContext
534     *  @param aRequest The current request object
535     *  @param aResponse The current response object
536     *  @return true, if being redirected, false otherwise
537     */
538    public static boolean checkSsl(SecureActionMapping aMapping, ServletContext aContext, HttpServletRequest aRequest, HttpServletResponse aResponse) {
539        // Build a redirect string if needed
540        String redirectString = SecureRequestUtils.getRedirectString(aRequest, aContext, aMapping.getSecure());
541        
542        // If a redirect string was generated, perform the redirect and return true
543        if(redirectString != null) {
544            try {
545                // Redirect the page to the desired URL
546                aResponse.sendRedirect(aResponse.encodeRedirectURL(redirectString));
547                return true;
548            } catch (Exception ioe) {
549                sLog.error("IOException in redirect" + ioe.getMessage());
550            }
551        }
552        
553        // No redirect performed, return false
554        return false;
555    }
556    
557    
558    /**
559     * Return the specified URL with the specified session identifier
560     * suitably encoded.
561     *
562     * @param url URL to be encoded with the session id
563     * @param sessionId Session id to be included in the encoded URL
564     */
565    private static String toEncoded(String url, String sessionId) {
566        if((url == null) || (sessionId == null))
567            return (url);
568        
569        String path = url;
570        String query = "";
571        String anchor = "";
572        
573        int question = url.indexOf('?');
574        if(question >= 0) {
575            path = url.substring(0, question);
576            query = url.substring(question);
577        }
578        
579        int pound = path.indexOf('#');
580        if(pound >= 0) {
581            anchor = path.substring(pound);
582            path = path.substring(0, pound);
583        }
584        
585        StringBuilder sb = new StringBuilder(path);
586        if(sb.length() > 0) { // jsessionid can't be first.
587            sb.append(";jsessionid=");
588            sb.append(sessionId);
589        }
590        sb.append(anchor);
591        sb.append(query);
592        
593        return sb.toString();
594    }
595    
596}