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}