1 /*
2  * Copyright (C) 2015 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package okhttp3.logging;
17
18 import java.io.EOFException;
19 import java.io.IOException;
20 import java.nio.charset.Charset;
21 import java.util.Collections;
22 import java.util.Set;
23 import java.util.TreeSet;
24 import java.util.concurrent.TimeUnit;
25 import okhttp3.Connection;
26 import okhttp3.Headers;
27 import okhttp3.Interceptor;
28 import okhttp3.MediaType;
29 import okhttp3.OkHttpClient;
30 import okhttp3.Request;
31 import okhttp3.RequestBody;
32 import okhttp3.Response;
33 import okhttp3.ResponseBody;
34 import okhttp3.internal.http.HttpHeaders;
35 import okhttp3.internal.platform.Platform;
36 import okio.Buffer;
37 import okio.BufferedSource;
38 import okio.GzipSource;
39
40 import static okhttp3.internal.platform.Platform.INFO;
41
42 /**
43  * An OkHttp interceptor which logs request and response information. Can be applied as an
44  * {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain
45  * OkHttpClient#networkInterceptors() network interceptor}. <p> The format of the logs created by
46  * this class should not be considered stable and may change slightly between releases. If you need
47  * a stable logging format, use your own interceptor.
48  */

49 public final class HttpLoggingInterceptor implements Interceptor {
50   private static final Charset UTF8 = Charset.forName("UTF-8");
51
52   public enum Level {
53     /** No logs. */
54     NONE,
55     /**
56      * Logs request and response lines.
57      *
58      * <p>Example:
59      * <pre>{@code
60      * --> POST /greeting http/1.1 (3-byte body)
61      *
62      * <-- 200 OK (22ms, 6-byte body)
63      * }</pre>
64      */

65     BASIC,
66     /**
67      * Logs request and response lines and their respective headers.
68      *
69      * <p>Example:
70      * <pre>{@code
71      * --> POST /greeting http/1.1
72      * Host: example.com
73      * Content-Type: plain/text
74      * Content-Length: 3
75      * --> END POST
76      *
77      * <-- 200 OK (22ms)
78      * Content-Type: plain/text
79      * Content-Length: 6
80      * <-- END HTTP
81      * }</pre>
82      */

83     HEADERS,
84     /**
85      * Logs request and response lines and their respective headers and bodies (if present).
86      *
87      * <p>Example:
88      * <pre>{@code
89      * --> POST /greeting http/1.1
90      * Host: example.com
91      * Content-Type: plain/text
92      * Content-Length: 3
93      *
94      * Hi?
95      * --> END POST
96      *
97      * <-- 200 OK (22ms)
98      * Content-Type: plain/text
99      * Content-Length: 6
100      *
101      * Hello!
102      * <-- END HTTP
103      * }</pre>
104      */

105     BODY
106   }
107
108   public interface Logger {
109     void log(String message);
110
111     /** A {@link Logger} defaults output appropriate for the current platform. */
112     Logger DEFAULT = message -> Platform.get().log(INFO, message, null);
113   }
114
115   public HttpLoggingInterceptor() {
116     this(Logger.DEFAULT);
117   }
118
119   public HttpLoggingInterceptor(Logger logger) {
120     this.logger = logger;
121   }
122
123   private final Logger logger;
124
125   private volatile Set<String> headersToRedact = Collections.emptySet();
126
127   public void redactHeader(String name) {
128     Set<String> newHeadersToRedact = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
129     newHeadersToRedact.addAll(headersToRedact);
130     newHeadersToRedact.add(name);
131     headersToRedact = newHeadersToRedact;
132   }
133
134   private volatile Level level = Level.NONE;
135
136   /** Change the level at which this interceptor logs. */
137   public HttpLoggingInterceptor setLevel(Level level) {
138     if (level == nullthrow new NullPointerException("level == null. Use Level.NONE instead.");
139     this.level = level;
140     return this;
141   }
142
143   public Level getLevel() {
144     return level;
145   }
146
147   @Override public Response intercept(Chain chain) throws IOException {
148     Level level = this.level;
149
150     Request request = chain.request();
151     if (level == Level.NONE) {
152       return chain.proceed(request);
153     }
154
155     boolean logBody = level == Level.BODY;
156     boolean logHeaders = logBody || level == Level.HEADERS;
157
158     RequestBody requestBody = request.body();
159     boolean hasRequestBody = requestBody != null;
160
161     Connection connection = chain.connection();
162     String requestStartMessage = "--> "
163         + request.method()
164         + ' ' + request.url()
165         + (connection != null ? " " + connection.protocol() : "");
166     if (!logHeaders && hasRequestBody) {
167       requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
168     }
169     logger.log(requestStartMessage);
170
171     if (logHeaders) {
172       if (hasRequestBody) {
173         // Request body headers are only present when installed as a network interceptor. Force
174         // them to be included (when available) so there values are known.
175         if (requestBody.contentType() != null) {
176           logger.log("Content-Type: " + requestBody.contentType());
177         }
178         if (requestBody.contentLength() != -1) {
179           logger.log("Content-Length: " + requestBody.contentLength());
180         }
181       }
182
183       Headers headers = request.headers();
184       for (int i = 0, count = headers.size(); i < count; i++) {
185         String name = headers.name(i);
186         // Skip headers from the request body as they are explicitly logged above.
187         if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
188           logHeader(headers, i);
189         }
190       }
191
192       if (!logBody || !hasRequestBody) {
193         logger.log("--> END " + request.method());
194       } else if (bodyHasUnknownEncoding(request.headers())) {
195         logger.log("--> END " + request.method() + " (encoded body omitted)");
196       } else if (requestBody.isDuplex()) {
197         logger.log("--> END " + request.method() + " (duplex request body omitted)");
198       } else {
199         Buffer buffer = new Buffer();
200         requestBody.writeTo(buffer);
201
202         Charset charset = UTF8;
203         MediaType contentType = requestBody.contentType();
204         if (contentType != null) {
205           charset = contentType.charset(UTF8);
206         }
207
208         logger.log("");
209         if (isPlaintext(buffer)) {
210           logger.log(buffer.readString(charset));
211           logger.log("--> END " + request.method()
212               + " (" + requestBody.contentLength() + "-byte body)");
213         } else {
214           logger.log("--> END " + request.method() + " (binary "
215               + requestBody.contentLength() + "-byte body omitted)");
216         }
217       }
218     }
219
220     long startNs = System.nanoTime();
221     Response response;
222     try {
223       response = chain.proceed(request);
224     } catch (Exception e) {
225       logger.log("<-- HTTP FAILED: " + e);
226       throw e;
227     }
228     long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
229
230     ResponseBody responseBody = response.body();
231     long contentLength = responseBody.contentLength();
232     String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
233     logger.log("<-- "
234         + response.code()
235         + (response.message().isEmpty() ? "" : ' ' + response.message())
236         + ' ' + response.request().url()
237         + " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')');
238
239     if (logHeaders) {
240       Headers headers = response.headers();
241       for (int i = 0, count = headers.size(); i < count; i++) {
242         logHeader(headers, i);
243       }
244
245       if (!logBody || !HttpHeaders.hasBody(response)) {
246         logger.log("<-- END HTTP");
247       } else if (bodyHasUnknownEncoding(response.headers())) {
248         logger.log("<-- END HTTP (encoded body omitted)");
249       } else {
250         BufferedSource source = responseBody.source();
251         source.request(Long.MAX_VALUE); // Buffer the entire body.
252         Buffer buffer = source.getBuffer();
253
254         Long gzippedLength = null;
255         if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
256           gzippedLength = buffer.size();
257           try (GzipSource gzippedResponseBody = new GzipSource(buffer.clone())) {
258             buffer = new Buffer();
259             buffer.writeAll(gzippedResponseBody);
260           }
261         }
262
263         Charset charset = UTF8;
264         MediaType contentType = responseBody.contentType();
265         if (contentType != null) {
266           charset = contentType.charset(UTF8);
267         }
268
269         if (!isPlaintext(buffer)) {
270           logger.log("");
271           logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
272           return response;
273         }
274
275         if (contentLength != 0) {
276           logger.log("");
277           logger.log(buffer.clone().readString(charset));
278         }
279
280         if (gzippedLength != null) {
281             logger.log("<-- END HTTP (" + buffer.size() + "-byte, "
282                 + gzippedLength + "-gzipped-byte body)");
283         } else {
284             logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
285         }
286       }
287     }
288
289     return response;
290   }
291
292   private void logHeader(Headers headers, int i) {
293     String value = headersToRedact.contains(headers.name(i)) ? "██" : headers.value(i);
294     logger.log(headers.name(i) + ": " + value);
295   }
296
297   /**
298    * Returns true if the body in question probably contains human readable text. Uses a small sample
299    * of code points to detect unicode control characters commonly used in binary file signatures.
300    */

301   static boolean isPlaintext(Buffer buffer) {
302     try {
303       Buffer prefix = new Buffer();
304       long byteCount = buffer.size() < 64 ? buffer.size() : 64;
305       buffer.copyTo(prefix, 0, byteCount);
306       for (int i = 0; i < 16; i++) {
307         if (prefix.exhausted()) {
308           break;
309         }
310         int codePoint = prefix.readUtf8CodePoint();
311         if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
312           return false;
313         }
314       }
315       return true;
316     } catch (EOFException e) {
317       return false// Truncated UTF-8 sequence.
318     }
319   }
320
321   private static boolean bodyHasUnknownEncoding(Headers headers) {
322     String contentEncoding = headers.get("Content-Encoding");
323     return contentEncoding != null
324         && !contentEncoding.equalsIgnoreCase("identity")
325         && !contentEncoding.equalsIgnoreCase("gzip");
326   }
327 }
328