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}