001// --------------------------------------------------------------------------------
002// Copyright 2002-2025 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.List;
081import java.util.Map;
082import javax.servlet.ServletContext;
083import javax.servlet.http.HttpServletRequest;
084import javax.servlet.http.HttpServletResponse;
085import javax.servlet.jsp.PageContext;
086import org.apache.commons.logging.Log;
087import org.apache.commons.logging.LogFactory;
088import org.apache.struts.Globals;
089import org.apache.struts.config.ModuleConfig;
090import org.apache.struts.taglib.TagUtils;
091import org.apache.struts.util.MessageResources;
092import org.apache.struts.util.ModuleUtils;
093
094/**
095 * Define some additional utility methods utilized by sslext.
096 */
097public class SecureRequestUtils {
098    
099    /**
100     * The message resources.
101     */
102    protected static MessageResources messages = MessageResources.getMessageResources("org.apache.struts.taglib.html.LocalStrings");
103    
104    private static Log sLog = LogFactory.getLog(SecureRequestUtils.class);
105    
106    private static final String HTTP = "http";
107    private static final String HTTPS = "https";
108    private static final String STD_HTTP_PORT = "80";
109    private static final String STD_HTTPS_PORT = "443";
110    
111    private static final String STOWED_REQUEST_ATTRIBS = "ssl.redirect.attrib.stowed";
112    
113    /**
114     * Compute a hyperlink URL based on the <code>forward</code>,
115     * <code>href</code>, or <code>page</code> parameter that is not null.
116     * The returned URL will have already been passed to
117     * <code>response.encodeURL()</code> for adding a session identifier.
118     *
119     * @param pageContext PageContext for the tag making this call
120     *
121     * @param forward Logical forward name for which to look up
122     *  the context-relative URI (if specified)
123     * @param href URL to be utilized unmodified (if specified)
124     * @param page Context-relative page for which a URL should
125     *  be created (if specified)
126     * @param action a Struts action name
127     *
128     * @param params Map of parameters to be dynamically included (if any)
129     * @param anchor Anchor to be dynamically included (if any)
130     *
131     * @param redirect Is this URL for a <code>response.sendRedirect()</code>?
132     *
133     * @exception MalformedURLException if a URL cannot be created
134     *  for the specified parameters
135     */
136    public static String computeURL(PageContext pageContext, String forward, String href, String page, String action, Map params, String anchor, boolean redirect)
137    throws MalformedURLException {
138        var url = new StringBuilder(TagUtils.getInstance().computeURL(pageContext, forward, href, page, action, null, params, anchor, redirect));
139        var request = (HttpServletRequest)pageContext.getRequest();
140        
141        // Get the action servlet's context, we'll need it later
142        var servletContext = pageContext.getServletContext();
143        var contextPath = request.getContextPath();
144        var securePlugin = (SecurePlugInInterface)servletContext.getAttribute(SecurePlugInInterface.SECURE_PLUGIN);
145        if(securePlugin.getSslExtEnable() && url.toString().startsWith(contextPath)) {
146            
147            // Initialize the scheme and ports we are using
148            var usingScheme = request.getScheme();
149            var usingPort = String.valueOf(request.getServerPort());
150            
151            // Get the servlet context relative link URL
152            var linkString = url.toString().substring(contextPath.length());
153            
154            // See if link references an action somewhere in our app
155            var secureConfig = getActionConfig(pageContext, linkString);
156            
157            // If link is an action, find the desired port and scheme
158            if(secureConfig != null && !SecureActionMapping.ANY.equalsIgnoreCase(secureConfig.getSecure())) {
159
160                var desiredScheme = Boolean.valueOf(secureConfig.getSecure()) ? HTTPS : HTTP;
161                var desiredPort = Boolean.valueOf(secureConfig.getSecure()) ? securePlugin.getHttpsPort() : securePlugin.getHttpPort();
162                
163                // If scheme and port we are using do not match the ones we want
164                if((!desiredScheme.equals(usingScheme) || !desiredPort.equals(usingPort))) {
165                    url.insert(0, startNewUrlString(request, desiredScheme, desiredPort));
166                    
167                    // This is a hack to help us overcome the problem that some
168                    // older browsers do not share sessions between http & https
169                    // If this feature is diabled, session ID could still be added
170                    // the previous call to the RequestUtils.computeURL() method,
171                    // but only if needed due to cookies disabled, etc.
172                    if(securePlugin.getSslExtAddSession() && url.toString().indexOf(";jsessionid=") < 0) {
173                        // Add the session identifier
174                        url = new StringBuilder(toEncoded(url.toString(),
175                        request.getSession().getId()));
176                    }
177                }
178            }
179        }
180        
181        return url.toString();
182    }
183    
184    /**
185     * Finds the configuration definition for the specified action link
186     * @param pageContext the current page context.
187     * @param linkString The action we are searching for, specified as a link. (i.e. may include "..")
188     * @return The SecureActionMapping object entry for this action, or null if not found
189     */
190    private static SecureActionMapping getActionConfig(PageContext pageContext, String linkString) {
191        var moduleConfig = SecureRequestUtils.selectModule(linkString, pageContext);
192        
193        // Strip off the subapp path, if any
194        linkString = linkString.substring(moduleConfig.getPrefix().length());
195        
196        // Get all the servlet mappings for the ActionServlet, loop thru to find
197        // the correct action being specified
198        var servletContext = pageContext.getServletContext();
199        var spi = (SecurePlugInInterface)servletContext.getAttribute(SecurePlugInInterface.SECURE_PLUGIN);
200        var mappingItr = spi.getServletMappings().iterator();
201        
202        while(mappingItr.hasNext()) {
203            var servletMapping = (String) mappingItr.next();
204
205            var starIndex = servletMapping != null ? servletMapping.indexOf('*') : -1;
206            if(starIndex == -1) {
207                continue;
208            }// No servlet mapping or no usable pattern defined, short circuit
209
210            var prefix = servletMapping.substring(0, starIndex);
211            var suffix = servletMapping.substring(starIndex + 1);
212            
213            // Strip off the jsessionid, if any
214            var jsession = linkString.indexOf(";jsessionid=");
215            if(jsession >= 0) {
216                linkString = linkString.substring(0, jsession);
217            }
218            
219            // Strip off the anchor, if any
220            var anchor = linkString.indexOf('#');
221            if(anchor >= 0) {
222                linkString = linkString.substring(0, anchor);
223            }
224            
225            // Strip off the query string, if any
226            var question = linkString.indexOf('?');
227            if(question >= 0) {
228                linkString = linkString.substring(0, question);
229            }
230            
231            // Unable to establish this link as an action, short circuit
232            if(!(linkString.startsWith(prefix) && linkString.endsWith(suffix))) {
233                continue;
234            }
235            
236            // Chop off prefix and suffix
237            linkString = linkString.substring(prefix.length());
238            linkString = linkString.substring(0, linkString.length() - suffix.length());
239            if(!linkString.startsWith("/")) {
240                linkString = "/" + linkString;
241            }
242
243            var secureConfig = (SecureActionMapping) moduleConfig.findActionConfig(linkString);
244            
245            return secureConfig;
246        }
247        
248        return null;
249    }
250    
251    /**
252     * Builds the protocol, server name, and port portion of the new URL
253     * @param request The current request
254     * @param desiredScheme  The scheme (http or https) to be used in the new URL
255     * @param desiredPort The port number to be used in th enew URL
256     * @return The new URL as a StringBuilder
257     */
258    private static StringBuilder startNewUrlString(HttpServletRequest request, String desiredScheme, String desiredPort) {
259        var url = new StringBuilder();
260        var serverName = request.getServerName();
261        
262        url.append(desiredScheme).append("://").append(serverName);
263        
264        if((HTTP.equals(desiredScheme) && !STD_HTTP_PORT.equals(desiredPort)) || (HTTPS.equals(desiredScheme) && !STD_HTTPS_PORT.equals(desiredPort))) {
265            url.append(":").append(desiredPort);
266        }
267        
268        return url;
269    }
270    
271    
272    /**
273     * Creates query String from request body parameters
274     * @param aRequest The current request
275     * @return The created query string (with no leading "?")
276     */
277    public static String getRequestParameters(HttpServletRequest aRequest) {
278        var m = getParameterMap(aRequest);
279        
280        return createQueryStringFromMap(m, "&").toString();
281    }
282    
283    /**
284     * Builds a query string from a given map of parameters
285     * @param m A map of parameters
286     * @param ampersand String to use for ampersands (e.g. "&" or "&amp;" )
287     * @return query string (with no leading "?")
288     */
289    public static StringBuilder createQueryStringFromMap(Map m, String ampersand) {
290        var aReturn = new StringBuilder("");
291        var aEntryS = m.entrySet();
292
293        for(final var entry : aEntryS) {
294            var aEntry = (Map.Entry)entry;
295            var value = aEntry.getValue();
296            var aValues = new String[1];
297            if(value == null) {
298                aValues[0] = "";
299            } else if(value instanceof List aList) { // Work around for Weblogic 6.1sp1
300                aValues = (String[])aList.toArray(new String[aList.size()]);
301            } else if(value instanceof String aString) {  // Single value from Struts tags
302                aValues[0] = aString;
303            } else { // String array, the standard returned from request.getParameterMap()
304                aValues = (String[])value;  // This is the standard
305            }
306            for(var aValue : aValues) {
307                append(aEntry.getKey(), aValue, aReturn, ampersand);
308            }
309        }
310        
311        return aReturn;
312    }
313    
314    /**
315     * Appends new key and value pair to query string
316     * @param key parameter name
317     * @param value value of parameter
318     * @param queryString existing query string
319     * @param ampersand string to use for ampersand (e.g. "&" or "&amp;")
320     * @return query string (with no leading "?")
321     */
322    private static StringBuilder append(Object key, Object value, StringBuilder queryString, String ampersand) {
323        var tagUtils = TagUtils.getInstance();
324        
325        if(queryString.length() > 0) {
326            queryString.append(ampersand);
327        }
328        queryString.append(tagUtils.encodeURL(key.toString()));
329        queryString.append("=");
330        queryString.append(tagUtils.encodeURL(value.toString()));
331        
332        return queryString;
333    }
334    
335    /**
336     * Stores request attributes in session
337     * @param aRequest The current request
338     * @return true, if the attributes were stowed in the session,
339     * false otherwise
340     */
341    public static boolean stowRequestAttributes(HttpServletRequest aRequest) {
342        if(aRequest.getSession().getAttribute(STOWED_REQUEST_ATTRIBS) != null) {
343            return false;
344        }
345        
346        Enumeration enumeration = aRequest.getAttributeNames();
347        Map map = new HashMap();
348        while(enumeration.hasMoreElements()) {
349            var name = (String) enumeration.nextElement();
350            map.put(name, aRequest.getAttribute(name));
351        }
352        aRequest.getSession().setAttribute(STOWED_REQUEST_ATTRIBS, map);
353        
354        return true;
355    }
356    
357    
358    /**
359     * Reclaims request attributes from session to request
360     * @param aRequest The current request
361     * @param doRemove True, if the attributes should be removed after being reclaimed,
362     * false otherwise
363     */
364    public static void reclaimRequestAttributes(HttpServletRequest aRequest,
365    boolean doRemove) {
366        var map = (Map) aRequest.getSession().getAttribute(STOWED_REQUEST_ATTRIBS);
367        
368        if(map == null) {
369            return;
370        }
371
372        var itr = map.keySet().iterator();
373        while(itr.hasNext()) {
374            var name = (String) itr.next();
375            
376            aRequest.setAttribute(name, map.get(name));
377        }
378        
379        if(doRemove) {
380            aRequest.getSession().removeAttribute(STOWED_REQUEST_ATTRIBS);
381        }
382    }
383    
384    
385    /**
386     * Creates a redirect URL string if the current request should be redirected
387     * @param request current servlet request
388     * @param application the currecnt ServletContext
389     * @param isSecure "true" if the current request should be transmitted via SSL
390     * "false" if not, "any" if we just don't care if it's SSL or not
391     * @return the URL to redirect to
392     */
393    static public String getRedirectString(HttpServletRequest request, ServletContext application, String isSecure) {
394        String urlString = null;
395        var securePlugin = (SecurePlugInInterface)application.getAttribute(SecurePlugInInterface.SECURE_PLUGIN);
396        var httpPort = securePlugin.getHttpPort();
397        var httpsPort = securePlugin.getHttpsPort();
398        
399        // If sslext disabled, or we don't have a protocol preference,
400        // just return the null value we have so far
401        if(!securePlugin.getSslExtEnable() || SecureActionMapping.ANY.equalsIgnoreCase(isSecure)) {
402            return urlString;
403        }
404        
405        // get the scheme we want to use for this page and
406        // get the scheme used in this request
407        var desiredScheme = Boolean.valueOf(isSecure) ? HTTPS : HTTP;
408        var usingScheme = request.getScheme();
409        
410        // Determine the port number we want to use
411        // and the port number we used in this request
412        var desiredPort = Boolean.valueOf(isSecure) ? httpsPort : httpPort;
413        var usingPort = String.valueOf(request.getServerPort());
414        
415        // Must also check ports, because of IE multiple redirect problem
416        if(!desiredScheme.equals(usingScheme) || !desiredPort.equals(usingPort)) {
417            
418            urlString = buildNewUrlString(request,
419            desiredScheme,
420            desiredPort,
421            securePlugin.getSslExtAddSession());
422            
423            // Temporarily store attributes in session
424            if(!SecureRequestUtils.stowRequestAttributes(request)) {
425                // If request attributes already stored in session, reclaim them
426                // This is a hack for the IE multiple redirect problem
427                SecureRequestUtils.reclaimRequestAttributes(request, false);
428            }
429        } else {
430            // Retrieve attributes from session
431            SecureRequestUtils.reclaimRequestAttributes(request, true);
432        }
433        
434        return urlString;
435    }
436    
437    
438    /**
439     * Builds the URL that we will redirect to
440     * @param request The current request
441     * @param desiredScheme The protocol (http or https) we wish to use in new URL
442     * @param desiredPort The port number we wish to use in new URL
443     * @return the URL we will redirect to, as a String
444     */
445    private static String buildNewUrlString(HttpServletRequest request, String desiredScheme, String desiredPort,
446    boolean addSessionID) {
447        var url = startNewUrlString(request, desiredScheme, desiredPort);
448        
449        url.append(request.getRequestURI());
450
451        var returnUrl = addQueryString(request, url);
452        
453        // If the add session ID feature is enabled, add the session ID when creating a new URL
454        // Could still be added by the calling checkSsl() method if needed due to disabled cookies, etc.
455        if(addSessionID){
456            returnUrl = toEncoded( returnUrl, request.getSession().getId());
457        }
458        
459        return returnUrl;
460    }
461    
462    /**
463     * Adds the query string, if any, to the given URL.  The query string
464     * is either taken from the existing query string or
465     * generated from the posting request body parameters.
466     * @param request The current request
467     * @param url The existing URL we will add the query string to
468     * @return The URL with query string
469     */
470    private static String addQueryString(HttpServletRequest request, StringBuilder url) {
471        // add query string, if any
472        var queryString = request.getQueryString();
473        
474        if(queryString != null && queryString.length() != 0) {
475            url.append("?").append(queryString);
476        } else {
477            queryString = SecureRequestUtils.getRequestParameters(request);
478            if(queryString != null && queryString.length() != 0) {
479                url.append("?").append(queryString);
480            }
481        }
482        
483        return url.toString();
484    }
485    
486    
487    /**
488     * Select the sub-application to which the specified request belongs, and
489     * add corresponding request attributes to this request.
490     *
491     * @param urlPath The requested URL
492     * @param pageContext The ServletContext for this web application
493     * @return The ModuleConfig for the given URL path
494     */
495    public static ModuleConfig selectModule(String urlPath, PageContext pageContext) {
496        // Get the ServletContext
497        var servletContext = pageContext.getServletContext();
498        
499        // Match against the list of sub-application prefixes
500        var prefix = ModuleUtils.getInstance().getModuleName(urlPath, servletContext);
501        
502        // Expose the resources for this sub-application
503        var config = (ModuleConfig) servletContext.getAttribute(Globals.MODULE_KEY + prefix);
504        
505        return config;
506    }
507    
508    /**
509     *  Creates a map of request parameters where the key is the parameter name and the
510     *  value is the String array of parameter values.
511     *  @param request The current request
512     *  @return The map of parameters and their values
513     */
514    private static Map getParameterMap(HttpServletRequest request) {
515        Map map = new HashMap();
516        Enumeration enumeration = request.getParameterNames();
517        
518        while(enumeration.hasMoreElements()) {
519            var name = (String) enumeration.nextElement();
520            var values = request.getParameterValues(name);
521            map.put(name, values);
522        }
523        
524        return map;
525    }
526    
527    /** Checks to see if SSL should be toggled for this
528     *  action
529     *  @param aMapping The mapping object for this Action
530     *  @param aContext The current ServletContext
531     *  @param aRequest The current request object
532     *  @param aResponse The current response object
533     *  @return true, if being redirected, false otherwise
534     */
535    public static boolean checkSsl(SecureActionMapping aMapping, ServletContext aContext, HttpServletRequest aRequest, HttpServletResponse aResponse) {
536        // Build a redirect string if needed
537        var redirectString = SecureRequestUtils.getRedirectString(aRequest, aContext, aMapping.getSecure());
538        
539        // If a redirect string was generated, perform the redirect and return true
540        if(redirectString != null) {
541            try {
542                // Redirect the page to the desired URL
543                aResponse.sendRedirect(aResponse.encodeRedirectURL(redirectString));
544                return true;
545            } catch (Exception ioe) {
546                sLog.error("IOException in redirect" + ioe.getMessage());
547            }
548        }
549        
550        // No redirect performed, return false
551        return false;
552    }
553    
554    
555    /**
556     * Return the specified URL with the specified session identifier
557     * suitably encoded.
558     *
559     * @param url URL to be encoded with the session id
560     * @param sessionId Session id to be included in the encoded URL
561     */
562    private static String toEncoded(String url, String sessionId) {
563        if((url == null) || (sessionId == null))
564            return (url);
565
566        var path = url;
567        var query = "";
568        var anchor = "";
569
570        var question = url.indexOf('?');
571        if(question >= 0) {
572            path = url.substring(0, question);
573            query = url.substring(question);
574        }
575
576        var pound = path.indexOf('#');
577        if(pound >= 0) {
578            anchor = path.substring(pound);
579            path = path.substring(0, pound);
580        }
581
582        var sb = new StringBuilder(path);
583        if(sb.length() > 0) { // jsessionid can't be first.
584            sb.append(";jsessionid=");
585            sb.append(sessionId);
586        }
587        sb.append(anchor);
588        sb.append(query);
589        
590        return sb.toString();
591    }
592    
593}