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-2014 ForgeRock AS.
016 */
017
018package org.forgerock.openig.servlet;
019
020import static java.lang.String.*;
021import static org.forgerock.json.fluent.JsonValue.*;
022import static org.forgerock.openig.audit.AuditSystem.*;
023import static org.forgerock.openig.audit.decoration.AuditDecorator.*;
024import static org.forgerock.openig.config.Environment.*;
025import static org.forgerock.openig.decoration.capture.CaptureDecorator.*;
026import static org.forgerock.openig.decoration.timer.TimerDecorator.*;
027import static org.forgerock.openig.http.HttpClient.*;
028import static org.forgerock.openig.http.SessionFactory.*;
029import static org.forgerock.openig.io.TemporaryStorage.*;
030import static org.forgerock.openig.log.LogSink.*;
031import static org.forgerock.openig.util.Json.*;
032import static org.forgerock.util.Utils.*;
033
034import java.io.File;
035import java.io.FileNotFoundException;
036import java.io.IOException;
037import java.io.InputStream;
038import java.net.URI;
039import java.net.URISyntaxException;
040import java.net.URL;
041import java.util.Arrays;
042import java.util.Collections;
043import java.util.Enumeration;
044
045import javax.servlet.ServletConfig;
046import javax.servlet.ServletException;
047import javax.servlet.http.HttpServlet;
048import javax.servlet.http.HttpServletRequest;
049import javax.servlet.http.HttpServletResponse;
050
051import org.forgerock.json.fluent.JsonValue;
052import org.forgerock.openig.audit.AuditSystem;
053import org.forgerock.openig.audit.decoration.AuditDecorator;
054import org.forgerock.openig.audit.internal.ForwardingAuditSystem;
055import org.forgerock.openig.config.Environment;
056import org.forgerock.openig.decoration.capture.CaptureDecorator;
057import org.forgerock.openig.decoration.timer.TimerDecorator;
058import org.forgerock.openig.handler.Handler;
059import org.forgerock.openig.handler.HandlerException;
060import org.forgerock.openig.heap.HeapImpl;
061import org.forgerock.openig.heap.Name;
062import org.forgerock.openig.http.Exchange;
063import org.forgerock.openig.http.HttpClient;
064import org.forgerock.openig.http.Request;
065import org.forgerock.openig.http.Session;
066import org.forgerock.openig.http.SessionFactory;
067import org.forgerock.openig.io.BranchingStreamWrapper;
068import org.forgerock.openig.io.TemporaryStorage;
069import org.forgerock.openig.log.ConsoleLogSink;
070import org.forgerock.openig.log.LogSink;
071import org.forgerock.openig.log.Logger;
072import org.forgerock.openig.util.CaseInsensitiveSet;
073import org.forgerock.openig.util.URIUtil;
074
075/**
076 * The main OpenIG HTTP Servlet which is responsible for bootstrapping the configuration and delegating all request
077 * processing to the configured handler implementation (for example, a DispatchHandler).
078 * <p>
079 * <pre>
080 *   {
081 *      "heap": {
082 *          ...
083 *      },
084 *      "handler": "DispatchHandler",
085 *      "baseURI": "http://localhost:8080",
086 *      "logSink":  "myCustomLogSink",
087 *      "temporaryStorage": "myCustomStorage"
088 *   }
089 * </pre>
090 * {@literal handler} is the only mandatory configuration attribute.
091 */
092public class GatewayServlet extends HttpServlet {
093
094    /** Methods that should not include an entity body. */
095    private static final CaseInsensitiveSet NON_ENTITY_METHODS = new CaseInsensitiveSet(Arrays.asList("GET", "HEAD",
096            "TRACE", "DELETE"));
097
098    private static final long serialVersionUID = 1L;
099
100    /**
101     * Default HttpClient heap object declaration.
102     */
103    private static final JsonValue DEFAULT_HTTP_CLIENT = json(object(field("name", HTTP_CLIENT_HEAP_KEY),
104                                                                     field("type", HttpClient.class.getName())));
105
106    private static JsonValue readJson(final URL resource) throws ServletException {
107        InputStream in = null;
108        try {
109            in = resource.openStream();
110            return new JsonValue(readJsonLenient(in));
111        } catch (final FileNotFoundException e) {
112            throw new ServletException(format("File %s does not exists", resource), e);
113        } catch (final IOException e) {
114            throw new ServletException(format("Cannot read/parse content of %s", resource), e);
115        } finally {
116            closeSilently(in);
117        }
118    }
119
120    /** Overrides request URLs constructed by container; making requests relative to a new base URI. */
121    private URI baseURI;
122
123    /**
124     * Environment can be provided by the caller or, if null, it will be based on default policy.
125     * <ol>
126     * <li>{@literal openig-base} servlet init-param</li>
127     * <li>{@literal OPENIG_BASE} environment variable</li>
128     * <li>{@literal openig.base} system property</li>
129     * <li>platform specific default directory ({@literal ~/.openig/} or {@literal $AppData$\openig\})</li>
130     * </ol>
131     */
132    private Environment environment;
133
134    /** The handler to dispatch exchanges to. */
135    private Handler handler;
136
137    /**
138     * Heap of objects (represents the live configuration).
139     */
140    private HeapImpl heap;
141
142    /** Provides methods for various logging activities. */
143    private Logger logger;
144
145    /** Allocates temporary buffers for caching streamed content during request processing. */
146    private TemporaryStorage storage;
147
148    /**
149     * Factory to create OpenIG sessions.
150     */
151    private SessionFactory sessionFactory;
152
153    /**
154     * Default constructor invoked from web container. The servlet will be assumed to be running as a web application
155     * and obtain its configuration from the default web {@linkplain Environment environment}.
156     */
157    public GatewayServlet() {
158        this(null);
159    }
160
161    /**
162     * Creates a new servlet using the provided environment. This constructor should be called when running the servlet
163     * as part of a standalone application.
164     *
165     * @param environment
166     *            The application environment.
167     */
168    public GatewayServlet(final Environment environment) {
169        this.environment = environment;
170    }
171
172    @Override
173    public void destroy() {
174        heap.destroy();
175        environment = null;
176    }
177
178    @Override
179    public void init(final ServletConfig servletConfig) throws ServletException {
180        super.init(servletConfig);
181        if (environment == null) {
182            environment = new WebEnvironment(servletConfig);
183        }
184        try {
185            // Load the configuration
186            final File configuration = new File(environment.getConfigDirectory(), "config.json");
187            final URL configurationURL = configuration.canRead() ? configuration.toURI().toURL() : getClass()
188                    .getResource("default-config.json");
189            final JsonValue config = readJson(configurationURL);
190
191            // Create and configure the heap
192            heap = new HeapImpl(Name.of(configurationURL.toString()));
193            // "Live" objects
194            heap.put("ServletContext", servletConfig.getServletContext());
195            heap.put(ENVIRONMENT_HEAP_KEY, environment);
196
197            AuditSystem auditSystem = new ForwardingAuditSystem();
198
199            // can be overridden in config
200            heap.put(TEMPORARY_STORAGE_HEAP_KEY, new TemporaryStorage());
201            heap.put(LOGSINK_HEAP_KEY, new ConsoleLogSink());
202            heap.put(CAPTURE_HEAP_KEY, new CaptureDecorator(null, false, false));
203            heap.put(TIMER_HEAP_KEY, new TimerDecorator());
204            heap.put(AUDIT_HEAP_KEY, new AuditDecorator(auditSystem));
205            heap.put(AUDIT_SYSTEM_HEAP_KEY, auditSystem);
206            heap.addDefaultDeclaration(DEFAULT_HTTP_CLIENT);
207            heap.init(config, "logSink", "temporaryStorage", "handler", "handlerObject", "baseURI", "globalDecorators");
208
209            // As all heaplets can specify their own storage and logger,
210            // these two lines provide custom logger or storage available.
211            logger = new Logger(heap.resolve(config.get("logSink").defaultTo(LOGSINK_HEAP_KEY),
212                                               LogSink.class, true), Name.of("GatewayServlet"));
213            storage = heap.resolve(config.get("temporaryStorage").defaultTo(TEMPORARY_STORAGE_HEAP_KEY),
214                                             TemporaryStorage.class);
215            // Let the user change the type of session to use
216            sessionFactory = heap.get(SESSION_FACTORY_HEAP_KEY, SessionFactory.class);
217            baseURI = config.get("baseURI").asURI();
218            handler = heap.getHandler();
219        } catch (final ServletException e) {
220            throw e;
221        } catch (final Exception e) {
222            throw new ServletException(e);
223        }
224    }
225
226    /**
227     * Handles a servlet request by dispatching it to a handler. It receives a servlet request, translates it into an
228     * exchange object, dispatches the exchange to a handler, then translates the exchange response into the servlet
229     * response.
230     *
231     * @param request
232     *            the {@link HttpServletRequest} object that will be used to populate the initial OpenIG's
233     *            {@link Request} encapsulated in the {@link Exchange}.
234     * @param response
235     *            the {@link HttpServletResponse} object that contains the response the servlet returns to the client.
236     * @exception IOException
237     *                if an input or output error occurs while the servlet is handling the HTTP request.
238     * @exception ServletException
239     *                if the HTTP request cannot be handled.
240     */
241    @SuppressWarnings("unchecked")
242    @Override
243    public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException,
244            ServletException {
245
246        // Build Exchange
247        URI uri = createRequestUri(request);
248        final Exchange exchange = new Exchange(uri);
249
250        // populate request
251        exchange.request = new Request();
252        exchange.request.setMethod(request.getMethod());
253        exchange.request.setUri(uri);
254        if (baseURI != null) {
255            exchange.request.getUri().rebase(baseURI);
256        }
257
258        // request headers
259        for (final Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements(); ) {
260            final String name = e.nextElement();
261            exchange.request.getHeaders().addAll(name, Collections.list(request.getHeaders(name)));
262        }
263
264        // include request entity if appears to be provided with request
265        if ((request.getContentLength() > 0 || request.getHeader("Transfer-Encoding") != null)
266                && !NON_ENTITY_METHODS.contains(exchange.request.getMethod())) {
267            exchange.request.setEntity(new BranchingStreamWrapper(request.getInputStream(), storage));
268        }
269        exchange.clientInfo = new ServletClientInfo(request);
270        // TODO consider moving this below (when the exchange will be fully configured)
271        exchange.session = newSession(request, exchange);
272        exchange.principal = request.getUserPrincipal();
273        // handy servlet-specific attributes, sure to be abused by downstream filters
274        exchange.put(HttpServletRequest.class.getName(), request);
275        exchange.put(HttpServletResponse.class.getName(), response);
276        try {
277            // handle request
278            try {
279                handler.handle(exchange);
280            } catch (final HandlerException he) {
281                throw new ServletException(he);
282            } finally {
283                // Close the session before writing back the actual response message to the User-Agent
284                closeSilently(exchange.session);
285            }
286            /*
287             * Support for OPENIG-94/95 - The wrapped servlet may have already committed its response w/o creating a new
288             * OpenIG Response instance in the exchange.
289             */
290            if (exchange.response != null) {
291                // response status-code (reason-phrase deprecated in Servlet API)
292                response.setStatus(exchange.response.getStatus());
293
294                // response headers
295                for (final String name : exchange.response.getHeaders().keySet()) {
296                    for (final String value : exchange.response.getHeaders().get(name)) {
297                        if (value != null && value.length() > 0) {
298                            response.addHeader(name, value);
299                        }
300                    }
301                }
302                // response entity (if applicable)
303                exchange.response.getEntity().copyRawContentTo(response.getOutputStream());
304            }
305        } finally {
306            // final cleanup
307            closeSilently(exchange.request, exchange.response);
308        }
309    }
310
311    private static URI createRequestUri(final HttpServletRequest request) throws ServletException {
312        try {
313            return URIUtil.create(request.getScheme(),
314                                  null,
315                                  request.getServerName(),
316                                  request.getServerPort(),
317                                  request.getRequestURI(),
318                                  request.getQueryString(),
319                                  null);
320        } catch (final URISyntaxException use) {
321            throw new ServletException(use);
322        }
323    }
324
325    private Session newSession(final HttpServletRequest request, final Exchange exchange) {
326        if (sessionFactory != null) {
327            return sessionFactory.build(exchange);
328        }
329        return new ServletSession(request);
330    }
331}