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.filter;
019
020import static org.forgerock.openig.util.Json.*;
021import static org.forgerock.util.Utils.*;
022
023import java.io.File;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.io.OutputStreamWriter;
027import java.io.PrintWriter;
028import java.io.UnsupportedEncodingException;
029import java.nio.charset.Charset;
030import java.nio.charset.IllegalCharsetNameException;
031import java.nio.charset.UnsupportedCharsetException;
032import java.util.Arrays;
033import java.util.HashSet;
034import java.util.Set;
035import java.util.concurrent.atomic.AtomicLong;
036
037import org.forgerock.json.fluent.JsonValue;
038import org.forgerock.openig.el.Expression;
039import org.forgerock.openig.handler.Handler;
040import org.forgerock.openig.handler.HandlerException;
041import org.forgerock.openig.header.ContentTypeHeader;
042import org.forgerock.openig.heap.GenericHeaplet;
043import org.forgerock.openig.heap.HeapException;
044import org.forgerock.openig.http.Exchange;
045import org.forgerock.openig.http.Message;
046import org.forgerock.openig.http.Request;
047import org.forgerock.openig.http.Response;
048
049/**
050 * Captures request and response messages for further analysis.
051 * @deprecated since OpenIG 3.1
052 */
053@Deprecated
054public class CaptureFilter extends GenericFilter {
055
056    /**
057     * Provides an abstraction to make PrintWriter plugable.
058     */
059    public static interface WriterProvider {
060        /**
061         * Returns a valid PrintWriter.
062         * @return a valid PrintWriter.
063         * @throws IOException when a writer cannot be produced.
064         */
065        PrintWriter getWriter() throws IOException;
066    }
067
068    /**
069     * Provides a {@link java.io.PrintWriter} instance based on a {@link java.io.File}.
070     */
071    public static class FileWriterProvider implements WriterProvider {
072        /** File where captured output should be written. */
073        private final File file;
074
075        /** Character set to encode captured output with (default: UTF-8). */
076        private final Charset charset;
077
078        private PrintWriter writer;
079
080        /**
081         * Construct a new {@code FileWriterProvider} using the given file as
082         * destination. Calling this constructor is equivalent to calling
083         * {@link FileWriterProvider#FileWriterProvider(java.io.File, java.nio.charset.Charset)}
084         * with {@literal UTF-8} as {@link java.nio.charset.Charset}.
085         *
086         * @param file
087         *            specify where the output will be flushed.
088         */
089        public FileWriterProvider(final File file) {
090            this(file, Charset.forName("UTF-8"));
091        }
092
093        /**
094         * Construct a new {@code FileWriterProvider} using the given file as destination and the given Charset.
095         * @param file specify where the output will be flushed.
096         * @param charset specify the {@link java.nio.charset.Charset} to use.
097         */
098        public FileWriterProvider(final File file, final Charset charset) {
099            this.file = file;
100            this.charset = charset;
101        }
102
103        @Override
104        public PrintWriter getWriter() throws IOException {
105            if (writer == null || !file.exists()) {
106                if (writer != null) {
107                    // file was removed while open
108                    closeSilently(writer);
109                }
110                writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), charset));
111            }
112            return writer;
113        }
114    }
115
116
117    /** Set of common textual content with non-text content-types to capture. */
118    private static final Set<String> TEXT_TYPES = new HashSet<String>(
119            Arrays.asList("application/atom+xml", "application/javascript", "application/json",
120                    "application/rss+xml", "application/xhtml+xml", "application/xml", "application/xml-dtd",
121                    "application/x-www-form-urlencoded")
122    ); // make all entries lower case
123
124    private Expression condition = null;
125
126    private boolean captureEntity = true;
127
128    /** Used to assign each exchange a monotonically increasing number. */
129    private AtomicLong sequence = new AtomicLong(0L);
130
131    /** Used to write captured output to a target destination (file, ...). */
132    private WriterProvider provider;
133
134    /**
135     * Assign the given provider.
136     * @param provider provider to be used.
137     */
138    public void setWriterProvider(final WriterProvider provider) {
139        this.provider = provider;
140    }
141
142    /**
143     * Used to conditionally capture the exchange. If the given expression evaluates to {@literal true},
144     * both the request and the response will be captured.
145     * Notice that the condition is evaluated when the request flows in this filter.
146     * @param condition expression that evaluates to a {@link java.lang.Boolean}
147     */
148    public synchronized void setCondition(final Expression condition) {
149        this.condition = condition;
150    }
151
152    /**
153     * If set to {@literal true}, the message's entity will be captured as part of the output.
154     * Notice that only entities with a text-based {@literal Content-Type} will be captured.
155     * @param captureEntity capture the entity if possible
156     */
157    public void setCaptureEntity(final boolean captureEntity) {
158        this.captureEntity = captureEntity;
159    }
160
161    @Override
162    public synchronized void filter(final Exchange exchange, final Handler next) throws HandlerException, IOException {
163        Object eval = (condition != null ? condition.eval(exchange) : Boolean.TRUE);
164        boolean doCapture = (eval instanceof Boolean && (Boolean) eval);
165        long id = 0;
166        if (doCapture) {
167            id = sequence.incrementAndGet();
168            captureRequest(exchange.request, id);
169        }
170        next.handle(exchange);
171        if (doCapture) {
172            captureResponse(exchange.response, id);
173        }
174    }
175
176    private void captureRequest(Request request, long id) throws IOException {
177        PrintWriter writer = provider.getWriter();
178        writer.println();
179        writer.println("--- REQUEST " + id + " --->");
180        writer.println();
181        writer.println(request.getMethod() + " " + request.getUri() + " " + request.getVersion());
182        writeHeaders(writer, request);
183        writeEntity(writer, request);
184        writer.flush();
185    }
186
187    private void captureResponse(Response response, long id) throws IOException {
188        PrintWriter writer = provider.getWriter();
189        writer.println();
190        writer.println("<--- RESPONSE " + id + " ---");
191        writer.println();
192        writer.println(response.getVersion() + " " + response.getStatus() + " " + response.getReason());
193        writeHeaders(writer, response);
194        writeEntity(writer, response);
195        writer.flush();
196    }
197
198    private void writeHeaders(final PrintWriter writer, Message<?> message) {
199        for (String key : message.getHeaders().keySet()) {
200            for (String value : message.getHeaders().get(key)) {
201                writer.println(key + ": " + value);
202            }
203        }
204    }
205
206    private void writeEntity(final PrintWriter writer, Message<?> message) throws IOException {
207        ContentTypeHeader contentType = new ContentTypeHeader(message);
208        if (message.getEntity() == null || contentType.getType() == null) {
209            return;
210        }
211        writer.println();
212        if (!captureEntity) {
213            // simply show presence of an entity
214            writer.println("[entity]");
215            return;
216        }
217        if (!isTextualContent(contentType)) {
218            writer.println("[binary entity]");
219            return;
220        }
221        try {
222            message.getEntity().push();
223            try {
224                message.getEntity().copyDecodedContentTo(writer);
225            } finally {
226                message.getEntity().pop();
227            }
228        } catch (UnsupportedEncodingException uee) {
229            writer.println("[entity contains data in unsupported encoding]");
230        } catch (UnsupportedCharsetException uce) {
231            writer.println("[entity contains characters in unsupported character set]");
232        } catch (IllegalCharsetNameException icne) {
233            writer.println("[entity contains characters in illegal character set]");
234        }
235        // entity may not terminate with new line, so here it is
236        writer.println();
237    }
238
239    /**
240     * Decide if the given content-type is printable or not.
241     * The entity represents a textual/printable content if:
242     * <ul>
243     *     <li>there is a charset associated to the content-type, we'll be able to print it correctly</li>
244     *     <li>the content type is in the category 'text' or it is an accepted type</li>
245     * </ul>
246     *
247     * @param contentType the message's content-type
248     * @return {@literal true} if the content-type represents a textual content
249     */
250    private boolean isTextualContent(final ContentTypeHeader contentType) {
251        String type = (contentType.getType() != null ? contentType.getType().toLowerCase() : null);
252        return contentType.getCharset() != null
253                // text or white-listed type
254                || (type != null && (TEXT_TYPES.contains(type) || type.startsWith("text/")));
255    }
256
257    /** Creates and initializes a capture filter in a heap environment. */
258    public static class Heaplet extends GenericHeaplet {
259        @Override
260        public Object create() throws HeapException {
261            CaptureFilter filter = new CaptureFilter();
262            filter.setWriterProvider(buildFileProvider(config));
263            filter.setCondition(asExpression(config.get("condition")));
264            JsonValue capture = config.get("captureEntity");
265            filter.setCaptureEntity(capture.defaultTo(filter.captureEntity).asBoolean());
266            return filter;
267        }
268
269        private WriterProvider buildFileProvider(final JsonValue config) {
270            File file = new File(evaluate(config.get("file").required()));
271            Charset charset = config.get("charset").defaultTo("UTF-8").asCharset();
272            return new FileWriterProvider(file, charset);
273        }
274    }
275}