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 2014-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.decoration.capture; 018 019import static groovy.json.JsonOutput.prettyPrint; 020import static groovy.json.JsonOutput.toJson; 021 022import java.io.IOException; 023import java.io.PrintWriter; 024import java.io.StringWriter; 025import java.io.UnsupportedEncodingException; 026import java.nio.charset.IllegalCharsetNameException; 027import java.nio.charset.UnsupportedCharsetException; 028import java.util.Arrays; 029import java.util.HashSet; 030import java.util.LinkedHashMap; 031import java.util.Map; 032import java.util.Set; 033 034import org.forgerock.http.header.ContentTypeHeader; 035import org.forgerock.http.protocol.Header; 036import org.forgerock.http.protocol.Message; 037import org.forgerock.http.protocol.Request; 038import org.forgerock.http.protocol.Response; 039import org.forgerock.openig.log.Logger; 040import org.forgerock.services.context.AttributesContext; 041import org.forgerock.services.context.Context; 042 043/** 044 * Capture a message. 045 */ 046public class MessageCapture { 047 048 /** Set of common textual content with non-text content-types to capture. */ 049 private static final Set<String> TEXT_TYPES = new HashSet<>( 050 Arrays.asList("application/atom+xml", "application/javascript", "application/json", 051 "application/rss+xml", "application/xhtml+xml", "application/xml", "application/xml-dtd", 052 "application/x-www-form-urlencoded") 053 ); // make all entries lower case 054 055 private final Logger logger; 056 private final boolean captureEntity; 057 private final boolean captureContext; 058 059 /** 060 * Builds a MessageCapture that will prints messages in the provided {@code logger}. 061 * 062 * @param logger 063 * where to write captured messages 064 * @param captureEntity 065 * capture the entity content (if not binary) 066 */ 067 public MessageCapture(final Logger logger, final boolean captureEntity) { 068 this(logger, captureEntity, false); 069 } 070 071 /** 072 * Builds a MessageCapture that will prints messages in the provided {@code logger}. 073 * 074 * @param logger 075 * where to write captured messages 076 * @param captureEntity 077 * capture the entity content (if not binary) 078 * @param captureContext 079 * capture the context content (excluding request and response object) as json 080 */ 081 public MessageCapture(final Logger logger, final boolean captureEntity, final boolean captureContext) { 082 this.logger = logger; 083 this.captureEntity = captureEntity; 084 this.captureContext = captureContext; 085 } 086 087 /** 088 * Captures the given request, in the given mode. 089 * 090 * @param context 091 * Captured message's {@link Context} 092 * @param request 093 * Captured message 094 * @param mode 095 * one of {@link CapturePoint#REQUEST}, {@link CapturePoint#FILTERED_REQUEST} 096 */ 097 void capture(final Context context, final Request request, final CapturePoint mode) { 098 StringWriter out = new StringWriter(); 099 PrintWriter writer = new PrintWriter(out); 100 String id = context.getId(); 101 switch (mode) { 102 case REQUEST: 103 captureRequest(writer, request, id); 104 break; 105 case FILTERED_REQUEST: 106 captureFilteredRequest(writer, request, id); 107 break; 108 default: 109 throw new IllegalArgumentException("The given mode is not accepted: " + mode.name()); 110 } 111 112 // Prints the context if required 113 if (captureContext) { 114 writer.println("Context's content as JSON:"); 115 captureContextAsJson(writer, context); 116 } 117 118 // Print the message 119 logger.info(out.toString()); 120 } 121 122 /** 123 * Captures the given response, in the given mode. 124 * 125 * @param context 126 * Captured message's {@link Context} 127 * @param response 128 * Captured message 129 * @param mode 130 * one of {@link CapturePoint#FILTERED_RESPONSE} or {@link CapturePoint#RESPONSE} 131 */ 132 void capture(final Context context, final Response response, final CapturePoint mode) { 133 StringWriter out = new StringWriter(); 134 PrintWriter writer = new PrintWriter(out); 135 String id = context.getId(); 136 switch (mode) { 137 case RESPONSE: 138 captureResponse(writer, response, id); 139 break; 140 case FILTERED_RESPONSE: 141 captureFilteredResponse(writer, response, id); 142 break; 143 default: 144 throw new IllegalArgumentException("The given mode is not accepted: " + mode.name()); 145 } 146 147 // Prints the context if required 148 if (captureContext) { 149 writer.println("Context's content as JSON:"); 150 captureContextAsJson(writer, context); 151 } 152 153 // Print the message 154 logger.info(out.toString()); 155 } 156 157 private void captureContextAsJson(final PrintWriter writer, final Context context) { 158 // TODO we restrict ourselves to attributes only here, we should pretty print the chain of contexts instead 159 AttributesContext attributesContext = context.asContext(AttributesContext.class); 160 Map<String, Object> map = new LinkedHashMap<>(attributesContext.getAttributes()); 161 map.remove("javax.servlet.http.HttpServletRequest"); 162 map.remove("javax.servlet.http.HttpServletResponse"); 163 writer.println(prettyPrint(toJson(map))); 164 } 165 166 private void captureRequest(PrintWriter writer, Request request, String id) { 167 writer.printf("%n%n--- (request) id:%s --->%n%n", id); 168 if (request != null) { 169 captureRequestMessage(writer, request); 170 } 171 } 172 173 private void captureFilteredRequest(PrintWriter writer, Request request, String id) { 174 writer.printf("%n%n--- (filtered-request) id:%s --->%n%n", id); 175 if (request != null) { 176 captureRequestMessage(writer, request); 177 } 178 } 179 180 private void captureResponse(PrintWriter writer, Response response, String id) { 181 writer.printf("%n%n<--- (response) id:%s ---%n%n", id); 182 if (response != null) { 183 captureResponseMessage(writer, response); 184 } 185 } 186 187 private void captureFilteredResponse(PrintWriter writer, Response response, String id) { 188 writer.printf("%n%n<--- (filtered-response) id:%s ---%n%n", id); 189 if (response != null) { 190 captureResponseMessage(writer, response); 191 } 192 } 193 194 private void captureRequestMessage(final PrintWriter writer, Request request) { 195 writer.println(request.getMethod() + " " + request.getUri() + " " + request.getVersion()); 196 writeHeaders(writer, request); 197 writeEntity(writer, request); 198 writer.flush(); 199 } 200 201 private void captureResponseMessage(final PrintWriter writer, Response response) { 202 writer.print(response.getVersion() + " "); 203 if (response.getStatus() != null) { 204 writer.print(response.getStatus().getCode() + " "); 205 writer.print(response.getStatus().getReasonPhrase()); 206 } 207 writer.println(); 208 writeHeaders(writer, response); 209 writeEntity(writer, response); 210 writer.flush(); 211 } 212 213 private void writeHeaders(final PrintWriter writer, Message message) { 214 for (Map.Entry<String, Header> entry : message.getHeaders().asMapOfHeaders().entrySet()) { 215 for (String value : entry.getValue().getValues()) { 216 writer.println(entry.getKey() + ": " + value); 217 } 218 } 219 } 220 221 private void writeEntity(final PrintWriter writer, Message message) { 222 ContentTypeHeader contentType = ContentTypeHeader.valueOf(message); 223 if (message.getEntity() == null || contentType.getType() == null) { 224 return; 225 } 226 writer.println(); 227 if (!captureEntity) { 228 // simply show presence of an entity 229 writer.println("[entity]"); 230 return; 231 } 232 if (!isTextualContent(contentType)) { 233 writer.println("[binary entity]"); 234 return; 235 } 236 try { 237 message.getEntity().push(); 238 try { 239 message.getEntity().copyDecodedContentTo(writer); 240 } finally { 241 message.getEntity().pop(); 242 } 243 } catch (UnsupportedEncodingException uee) { 244 writer.println("[entity contains data in unsupported encoding]"); 245 } catch (UnsupportedCharsetException uce) { 246 writer.println("[entity contains characters in unsupported character set]"); 247 } catch (IllegalCharsetNameException icne) { 248 writer.println("[entity contains characters in illegal character set]"); 249 } catch (IOException e) { 250 writer.println("[IOException during entity writing] - " + e.getMessage()); 251 } 252 // entity may not terminate with new line, so here it is 253 writer.println(); 254 } 255 256 /** 257 * Decide if the given content-type is printable or not. The entity represents a textual/printable content if: <ul> 258 * <li>there is a charset associated to the content-type, we'll be able to print it correctly</li> <li>the content 259 * type is in the category 'text' or it is an accepted type</li> </ul> 260 * 261 * @param contentType 262 * the message's content-type 263 * @return {@literal true} if the content-type represents a textual content 264 */ 265 private static boolean isTextualContent(final ContentTypeHeader contentType) { 266 String type = (contentType.getType() != null ? contentType.getType().toLowerCase() : null); 267 return contentType.getCharset() != null 268 // text or white-listed type 269 || (type != null && (TEXT_TYPES.contains(type) || type.startsWith("text/"))); 270 } 271 272}