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