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 "&" ) 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 "&") 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}