001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2010–2011 ApexIdentity Inc. 015 * Portions Copyright 2011-2015 ForgeRock AS. 016 */ 017 018package org.forgerock.http.servlet; 019 020import static java.util.Collections.*; 021import static org.forgerock.http.handler.Handlers.chainOf; 022import static org.forgerock.http.io.IO.*; 023import static org.forgerock.util.Utils.*; 024 025import java.io.File; 026import java.io.IOException; 027import java.net.URISyntaxException; 028import java.security.cert.X509Certificate; 029import java.util.Arrays; 030import java.util.Collections; 031import java.util.Enumeration; 032import java.util.ServiceLoader; 033 034import javax.servlet.ServletConfig; 035import javax.servlet.ServletContext; 036import javax.servlet.ServletException; 037import javax.servlet.http.HttpServlet; 038import javax.servlet.http.HttpServletRequest; 039import javax.servlet.http.HttpServletResponse; 040 041import org.forgerock.http.Handler; 042import org.forgerock.http.HttpApplication; 043import org.forgerock.http.HttpApplicationException; 044import org.forgerock.http.filter.TransactionIdInboundFilter; 045import org.forgerock.http.io.Buffer; 046import org.forgerock.http.protocol.Request; 047import org.forgerock.http.protocol.Response; 048import org.forgerock.http.protocol.Status; 049import org.forgerock.http.routing.UriRouterContext; 050import org.forgerock.http.session.Session; 051import org.forgerock.http.session.SessionContext; 052import org.forgerock.http.util.CaseInsensitiveSet; 053import org.forgerock.http.util.Uris; 054import org.forgerock.services.context.AttributesContext; 055import org.forgerock.services.context.ClientContext; 056import org.forgerock.services.context.Context; 057import org.forgerock.services.context.RequestAuditContext; 058import org.forgerock.services.context.RootContext; 059import org.forgerock.util.Factory; 060import org.forgerock.util.promise.NeverThrowsException; 061import org.forgerock.util.promise.Promise; 062import org.forgerock.util.promise.ResultHandler; 063import org.forgerock.util.promise.RuntimeExceptionHandler; 064 065/** 066 * <p> 067 * An HTTP servlet implementation which provides integration between the Servlet 068 * API and the common HTTP Framework. 069 * </p> 070 * <p> 071 * A {@link HttpApplication} implementation must be registered in the 072 * {@link ServiceLoader} framework 073 * </p> 074 * 075 * @see HttpApplication 076 */ 077public final class HttpFrameworkServlet extends HttpServlet { 078 079 private static final long serialVersionUID = 3524182656424860912L; 080 081 /** 082 * Standard specified request attribute name for retrieving X509 Certificates. 083 */ 084 private static final String SERVLET_REQUEST_X509_ATTRIBUTE = "javax.servlet.request.X509Certificate"; 085 086 /** Methods that should not include an entity body. */ 087 private static final CaseInsensitiveSet NON_ENTITY_METHODS = new CaseInsensitiveSet( 088 Arrays.asList("GET", "HEAD", "TRACE", "DELETE")); 089 090 /** 091 * Servlet init-param for configuring the routing base for the 092 * {@link HttpApplication}. 093 * 094 * @see ServletRoutingBase 095 */ 096 public static final String ROUTING_BASE_INIT_PARAM_NAME = "routing-base"; 097 098 private ServletVersionAdapter adapter; 099 private HttpApplication application; 100 private Factory<Buffer> storage; 101 private Handler handler; 102 private ServletRoutingBase routingBase; 103 104 /** 105 * Default constructor for use via web.xml declaration. 106 */ 107 public HttpFrameworkServlet() { 108 } 109 110 /** 111 * Creates a new {@code HttpFrameworkServlet} programmatically using the 112 * specified {@link HttpApplication}. 113 * 114 * @param application The {@code HttpApplication} instance. 115 */ 116 public HttpFrameworkServlet(HttpApplication application) { 117 this.application = application; 118 } 119 120 @Override 121 public void init() throws ServletException { 122 adapter = getAdapter(getServletContext()); 123 routingBase = selectRoutingBase(getServletConfig()); 124 if (application == null) { 125 HttpApplicationLoader applicationLoader = getApplicationLoader(getServletConfig()); 126 application = getApplication(applicationLoader, getServletConfig()); 127 } 128 storage = application.getBufferFactory(); 129 if (storage == null) { 130 final File tmpDir = (File) getServletContext().getAttribute(ServletContext.TEMPDIR); 131 storage = newTemporaryStorage(tmpDir); 132 } 133 try { 134 handler = chainOf(application.start(), new TransactionIdInboundFilter()); 135 } catch (HttpApplicationException e) { 136 throw new ServletException("Failed to start HTTP Application", e); 137 } 138 } 139 140 private ServletVersionAdapter getAdapter(ServletContext servletContext) throws ServletException { 141 switch (servletContext.getMajorVersion()) { 142 case 1: 143 // FIXME: i18n. 144 throw new ServletException("Unsupported Servlet version " 145 + servletContext.getMajorVersion()); 146 case 2: 147 return new Servlet2Adapter(); 148 default: 149 return new Servlet3Adapter(); 150 } 151 } 152 153 private ServletRoutingBase selectRoutingBase(ServletConfig servletConfig) throws ServletException { 154 String routingModeParam = servletConfig.getInitParameter(ROUTING_BASE_INIT_PARAM_NAME); 155 if (routingModeParam == null) { 156 return ServletRoutingBase.SERVLET_PATH; 157 } 158 try { 159 return ServletRoutingBase.valueOf(routingModeParam.toUpperCase()); 160 } catch (IllegalArgumentException e) { 161 throw new ServletException("Invalid routing mode: " + routingModeParam); 162 } 163 } 164 165 private HttpApplicationLoader getApplicationLoader(ServletConfig config) throws ServletException { 166 String applicationLoaderParam = config.getInitParameter("application-loader"); 167 if (applicationLoaderParam == null) { 168 return HttpApplicationLoader.SERVICE_LOADER; 169 } 170 try { 171 return HttpApplicationLoader.valueOf(applicationLoaderParam.toUpperCase()); 172 } catch (IllegalArgumentException e) { 173 throw new ServletException("Invalid HTTP application loader: " + applicationLoaderParam); 174 } 175 } 176 177 private HttpApplication getApplication(HttpApplicationLoader applicationLoader, ServletConfig config) 178 throws ServletException { 179 return applicationLoader.load(config); 180 } 181 182 @Override 183 protected void service(final HttpServletRequest req, final HttpServletResponse resp) 184 throws ServletException, IOException { 185 final Session session = new ServletSession(req); 186 final SessionContext sessionContext = new SessionContext(new RootContext(), session); 187 188 final Request request; 189 final UriRouterContext uriRouterContext; 190 try { 191 request = createRequest(req); 192 uriRouterContext = createRouterContext(sessionContext, req, request); 193 } catch (URISyntaxException e) { 194 Response response = new Response(Status.BAD_REQUEST); 195 response.setEntity(e.getMessage()); 196 writeResponse(response, resp, sessionContext); 197 return; 198 } 199 200 final AttributesContext attributesContext = new AttributesContext(new RequestAuditContext(uriRouterContext)); 201 202 /* TODO 203 * add comment on why this was added as probably shouldn't stick around as 204 * only to fix AM's case of forwarding the request from a different servlet?.... 205 */ 206 Enumeration<String> attributeNames = req.getAttributeNames(); 207 while (attributeNames.hasMoreElements()) { 208 String attributeName = attributeNames.nextElement(); 209 attributesContext.getAttributes().put(attributeName, req.getAttribute(attributeName)); 210 } 211 212 //FIXME ideally we don't want to expose the HttpServlet Request and Response 213 // handy servlet-specific attributes, sure to be abused by downstream filters 214 attributesContext.getAttributes().put(HttpServletRequest.class.getName(), req); 215 attributesContext.getAttributes().put(HttpServletResponse.class.getName(), resp); 216 217 Context context = createClientContext(attributesContext, req); 218 219 // handle request 220 final ServletSynchronizer sync = adapter.createServletSynchronizer(req, resp); 221 try { 222 final Promise<Response, NeverThrowsException> promise = 223 handler.handle(context, request) 224 .thenOnResult(new ResultHandler<Response>() { 225 @Override 226 public void handleResult(Response response) { 227 writeResponse(request, response, resp, sessionContext, sync); 228 } 229 }); 230 promise.thenOnRuntimeException(new RuntimeExceptionHandler() { 231 @Override 232 public void handleRuntimeException(RuntimeException e) { 233 log("RuntimeException caught", e); 234 writeResponse(request, new Response(Status.INTERNAL_SERVER_ERROR), resp, sessionContext, sync); 235 } 236 }); 237 238 sync.setAsyncListener(new Runnable() { 239 @Override 240 public void run() { 241 promise.cancel(true); 242 } 243 }); 244 } catch (Throwable throwable) { 245 // Guard against any kind of Throwable that may be thrown synchronously (not caught by promise 246 // RuntimeExceptionHandler), possibly leaving a stale response in the web container :'( 247 // Servlet specification indicates that it's the responsibility of the Servlet implementer to call 248 // AsyncContext.complete() 249 log("Throwable caught", throwable); 250 writeResponse(request, new Response(Status.INTERNAL_SERVER_ERROR), resp, sessionContext, sync); 251 } 252 253 try { 254 sync.awaitIfNeeded(); 255 } catch (InterruptedException e) { 256 throw new ServletException("Awaiting asynchronous request was interrupted.", e); 257 } 258 } 259 260 private Request createRequest(HttpServletRequest req) throws IOException, URISyntaxException { 261 // populate request 262 Request request = new Request(); 263 request.setMethod(req.getMethod()); 264 265 /* CHF-81: containers are generally quite tolerant of invalid query strings, so we'll try to be as well 266 * by decoding the query string and re-encoding it correctly before constructing the URI. */ 267 request.setUri(Uris.createNonStrict(req.getScheme(), 268 null, 269 req.getServerName(), 270 req.getServerPort(), 271 req.getRequestURI(), 272 req.getQueryString(), 273 null)); 274 275 // request headers 276 for (Enumeration<String> e = req.getHeaderNames(); e.hasMoreElements();) { 277 String name = e.nextElement(); 278 request.getHeaders().add(name, list(req.getHeaders(name))); 279 } 280 281 // include request entity if appears to be provided with request 282 if ((req.getContentLength() > 0 || req.getHeader("Transfer-Encoding") != null) 283 && !NON_ENTITY_METHODS.contains(request.getMethod())) { 284 request.setEntity(newBranchingInputStream(req.getInputStream(), storage)); 285 } 286 287 return request; 288 } 289 290 private ClientContext createClientContext(Context parent, HttpServletRequest req) { 291 return ClientContext.buildExternalClientContext(parent) 292 .remoteUser(req.getRemoteUser()) 293 .remoteAddress(req.getRemoteAddr()) 294 .remotePort(req.getRemotePort()) 295 .certificates((X509Certificate[]) req.getAttribute(SERVLET_REQUEST_X509_ATTRIBUTE)) 296 .userAgent(req.getHeader("User-Agent")) 297 .secure("https".equalsIgnoreCase(req.getScheme())) 298 .localAddress(req.getLocalAddr()) 299 .localPort(req.getLocalPort()) 300 .build(); 301 } 302 303 private UriRouterContext createRouterContext(Context parent, HttpServletRequest req, final Request request) 304 throws URISyntaxException { 305 String matchedUri = routingBase.extractMatchedUri(req); 306 final String requestURI = req.getRequestURI(); 307 String remaining = requestURI.substring(requestURI.indexOf(matchedUri) + matchedUri.length()); 308 return new UriRouterContext(parent, matchedUri, remaining, Collections.<String, String>emptyMap(), 309 request.getUri().asURI()); 310 } 311 312 private void writeResponse(Request request, Response response, HttpServletResponse servletResponse, 313 SessionContext sessionContext, ServletSynchronizer synchronizer) { 314 try { 315 writeResponse(response, servletResponse, sessionContext); 316 } finally { 317 closeSilently(request); 318 synchronizer.signalAndComplete(); 319 } 320 } 321 322 private void writeResponse(final Response response, final HttpServletResponse servletResponse, 323 final SessionContext sessionContext) { 324 try { 325 /* 326 * Support for OPENIG-94/95 - The wrapped servlet may have already 327 * committed its response w/o creating a new OpenIG Response instance in 328 * the exchange. 329 */ 330 if (response != null) { 331 // response status-code (reason-phrase deprecated in Servlet API) 332 servletResponse.setStatus(response.getStatus().getCode()); 333 334 // ensure that the session has been written back to the response 335 sessionContext.getSession().save(response); 336 337 // response headers 338 for (String name : response.getHeaders().keySet()) { 339 for (String value : response.getHeaders().get(name).getValues()) { 340 if (value != null && value.length() > 0) { 341 servletResponse.addHeader(name, value); 342 } 343 } 344 } 345 // response entity (if applicable) 346 // TODO does this also set content length? 347 response.getEntity().copyRawContentTo(servletResponse.getOutputStream()); 348 } 349 } catch (IOException e) { 350 log("Failed to write response", e); 351 } finally { 352 closeSilently(response); 353 } 354 } 355 356 @Override 357 public void destroy() { 358 application.stop(); 359 } 360}