Skip to content

Commit f027fd4

Browse files
committed
Initial commit (#310).
1 parent 77cd763 commit f027fd4

File tree

1 file changed

+361
-0
lines changed

1 file changed

+361
-0
lines changed
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
package org.gitlab4j.api.utils;
2+
3+
import java.io.BufferedInputStream;
4+
import java.io.ByteArrayOutputStream;
5+
import java.io.FilterOutputStream;
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.io.OutputStream;
9+
import java.net.URI;
10+
import java.nio.charset.Charset;
11+
import java.util.Arrays;
12+
import java.util.Collections;
13+
import java.util.HashSet;
14+
import java.util.List;
15+
import java.util.Map.Entry;
16+
import java.util.Set;
17+
import java.util.TreeSet;
18+
import java.util.concurrent.atomic.AtomicLong;
19+
import java.util.logging.Level;
20+
import java.util.logging.Logger;
21+
22+
import javax.annotation.Priority;
23+
import javax.ws.rs.WebApplicationException;
24+
import javax.ws.rs.client.ClientRequestContext;
25+
import javax.ws.rs.client.ClientRequestFilter;
26+
import javax.ws.rs.client.ClientResponseContext;
27+
import javax.ws.rs.client.ClientResponseFilter;
28+
import javax.ws.rs.core.MediaType;
29+
import javax.ws.rs.core.MultivaluedMap;
30+
import javax.ws.rs.ext.WriterInterceptor;
31+
import javax.ws.rs.ext.WriterInterceptorContext;
32+
33+
import org.glassfish.jersey.message.MessageUtils;
34+
35+
36+
/**
37+
* This class logs request and response info masking HTTP header values that are known to
38+
* contain sensitive information.
39+
*
40+
* This class was patterned after org.glassfish.jersey.logging.LoggingInterceptor, but written in
41+
* such a way that it could be sub-classed and have its behavior modified.
42+
*/
43+
@Priority(Integer.MIN_VALUE)
44+
public class MaskingLoggingFilter implements ClientRequestFilter, ClientResponseFilter, WriterInterceptor {
45+
46+
/**
47+
* Default list of header names that should be masked.
48+
*/
49+
public static final List<String> DEFAULT_MASKED_HEADER_NAMES =
50+
Collections.unmodifiableList(Arrays.asList("PRIVATE-TOKEN", "Authorization"));
51+
52+
/**
53+
* Prefix for request log entries.
54+
*/
55+
protected static final String REQUEST_PREFIX = "> ";
56+
57+
/**
58+
* Prefix for response log entries.
59+
*/
60+
protected static final String RESPONSE_PREFIX = "< ";
61+
62+
/**
63+
* Prefix that marks the beginning of a request or response section.
64+
*/
65+
protected static final String SECTION_PREFIX = "- ";
66+
67+
/**
68+
* Property name for the entity stream property
69+
*/
70+
protected static final String ENTITY_STREAM_PROPERTY = MaskingLoggingFilter.class.getName() + ".entityStream";
71+
72+
/**
73+
* Property name for the logging record id property
74+
*/
75+
protected static final String LOGGING_ID_PROPERTY = MaskingLoggingFilter.class.getName() + ".id";
76+
77+
protected final Logger logger;
78+
protected final Level level;
79+
protected final int maxEntitySize;
80+
protected final AtomicLong _id = new AtomicLong(0);
81+
protected Set<String> maskedHeaderNames = new HashSet<String>();
82+
83+
/**
84+
* Creates a masking logging filter for the specified logger with entity logging disabled.
85+
*
86+
* @param logger the logger to log messages to
87+
* @param level level at which the messages will be logged
88+
*/
89+
public MaskingLoggingFilter(final Logger logger, final Level level) {
90+
this(logger, level, 0, null);
91+
}
92+
93+
/**
94+
* Creates a masking logging filter for the specified logger.
95+
*
96+
* @param logger the logger to log messages to
97+
* @param level level at which the messages will be logged
98+
* @param maxEntitySize maximum number of entity bytes to be logged. When logging if the maxEntitySize
99+
* is reached, the entity logging will be truncated at maxEntitySize and "...more..." will be added at
100+
* the end of the log entry. If maxEntitySize is &lt;= 0, entity logging will be disabled
101+
*/
102+
public MaskingLoggingFilter(final Logger logger, final Level level, final int maxEntitySize) {
103+
this(logger, level, maxEntitySize, null);
104+
}
105+
106+
/**
107+
* Creates a masking logging filter for the specified logger with entity logging disabled.
108+
*
109+
* @param logger the logger to log messages to
110+
* @param level level at which the messages will be logged
111+
* @param maskedHeaderNames a list of header names that should have the values masked
112+
*/
113+
public MaskingLoggingFilter(final Logger logger, final Level level, final List<String> maskedHeaderNames) {
114+
this(logger, level, 0, maskedHeaderNames);
115+
}
116+
117+
/**
118+
* Creates a masking logging filter for the specified logger.
119+
*
120+
* @param logger the logger to log messages to
121+
* @param level level at which the messages will be logged
122+
* @param maxEntitySize maximum number of entity bytes to be logged. When logging if the maxEntitySize
123+
* is reached, the entity logging will be truncated at maxEntitySize and "...more..." will be added at
124+
* the end of the log entry. If maxEntitySize is &lt;= 0, entity logging will be disabled
125+
* @param maskedHeaderNames a list of header names that should have the values masked
126+
*/
127+
public MaskingLoggingFilter(final Logger logger, final Level level, final int maxEntitySize, final List<String> maskedHeaderNames) {
128+
this.logger = logger;
129+
this.level = level;
130+
this.maxEntitySize = maxEntitySize;
131+
132+
if (maskedHeaderNames != null) {
133+
maskedHeaderNames.forEach(h -> this.maskedHeaderNames.add(h.toLowerCase()));
134+
}
135+
}
136+
137+
/**
138+
* Set the list of header names to mask values for. If null, will clear the header names to mask.
139+
*
140+
* @param maskedHeaderNames a list of header names that should have the values masked, if null, will clear
141+
* the header names to mask
142+
*/
143+
public void setMaskedHeaderNames(final List<String> maskedHeaderNames) {
144+
this.maskedHeaderNames.clear();
145+
if (maskedHeaderNames != null) {
146+
maskedHeaderNames.forEach(h -> {
147+
addMaskedHeaderName(h);
148+
});
149+
}
150+
}
151+
152+
/**
153+
* Add a header name to the list of masked header names.
154+
*
155+
* @param maskedHeaderName the masked header name to add
156+
*/
157+
public void addMaskedHeaderName(String maskedHeaderName) {
158+
if (maskedHeaderName != null) {
159+
maskedHeaderName = maskedHeaderName.trim();
160+
if (maskedHeaderName.length() > 0) {
161+
maskedHeaderNames.add(maskedHeaderName.toLowerCase());
162+
}
163+
}
164+
}
165+
166+
protected void log(final StringBuilder sb) {
167+
if (logger != null) {
168+
logger.log(level, sb.toString());
169+
}
170+
}
171+
172+
protected StringBuilder appendId(final StringBuilder sb, final long id) {
173+
sb.append(Long.toString(id)).append(' ');
174+
return (sb);
175+
}
176+
177+
protected void printRequestLine(final StringBuilder sb, final String note, final long id, final String method, final URI uri) {
178+
appendId(sb, id).append(SECTION_PREFIX)
179+
.append(note)
180+
.append(" on thread ").append(Thread.currentThread().getName())
181+
.append('\n');
182+
appendId(sb, id).append(REQUEST_PREFIX).append(method).append(' ')
183+
.append(uri.toASCIIString()).append('\n');
184+
}
185+
186+
protected void printResponseLine(final StringBuilder sb, final String note, final long id, final int status) {
187+
appendId(sb, id).append(SECTION_PREFIX)
188+
.append(note)
189+
.append(" on thread ").append(Thread.currentThread().getName()).append('\n');
190+
appendId(sb, id).append(RESPONSE_PREFIX)
191+
.append(Integer.toString(status))
192+
.append('\n');
193+
}
194+
195+
protected Set<Entry<String, List<String>>> getSortedHeaders(final Set<Entry<String, List<String>>> headers) {
196+
final TreeSet<Entry<String, List<String>>> sortedHeaders = new TreeSet<Entry<String, List<String>>>(
197+
(Entry<String, List<String>> o1, Entry<String, List<String>> o2) -> o1.getKey().compareToIgnoreCase(o2.getKey()));
198+
sortedHeaders.addAll(headers);
199+
return sortedHeaders;
200+
}
201+
202+
/**
203+
* Logs each of the HTTP headers, masking the value of the header if the header key is
204+
* in the list of masked header names.
205+
*
206+
* @param sb the StringBuilder to build up the logging info in
207+
* @param id the ID for the logging line
208+
* @param prefix the logging line prefix character
209+
* @param headers a MultiValue map holding the header keys and values
210+
*/
211+
protected void printHeaders(final StringBuilder sb,
212+
final long id,
213+
final String prefix,
214+
final MultivaluedMap<String, String> headers) {
215+
216+
getSortedHeaders(headers.entrySet()).forEach(h -> {
217+
218+
final List<?> values = h.getValue();
219+
final String header = h.getKey();
220+
final boolean isMaskedHeader = maskedHeaderNames.contains(header.toLowerCase());
221+
222+
if (values.size() == 1) {
223+
String value = (isMaskedHeader ? "********" : values.get(0).toString());
224+
appendId(sb, id).append(prefix).append(header).append(": ").append(value).append('\n');
225+
} else {
226+
227+
final StringBuilder headerBuf = new StringBuilder();
228+
for (final Object value : values) {
229+
if (headerBuf.length() == 0) {
230+
headerBuf.append(", ");
231+
}
232+
233+
headerBuf.append(isMaskedHeader ? "********" : value.toString());
234+
}
235+
236+
appendId(sb, id).append(prefix).append(header).append(": ").append(headerBuf.toString()).append('\n');
237+
}
238+
});
239+
}
240+
241+
protected void buildEntityLogString(StringBuilder sb, byte[] entity, int entitySize, Charset charset) {
242+
243+
sb.append(new String(entity, 0, Math.min(entitySize, maxEntitySize), charset));
244+
if (entitySize > maxEntitySize) {
245+
sb.append("...more...");
246+
}
247+
248+
sb.append('\n');
249+
}
250+
251+
private InputStream logResponseEntity(final StringBuilder sb, InputStream stream, final Charset charset) throws IOException {
252+
253+
if (maxEntitySize <= 0) {
254+
return (stream);
255+
}
256+
257+
if (!stream.markSupported()) {
258+
stream = new BufferedInputStream(stream);
259+
}
260+
261+
stream.mark(maxEntitySize + 1);
262+
final byte[] entity = new byte[maxEntitySize + 1];
263+
final int entitySize = stream.read(entity);
264+
buildEntityLogString(sb, entity, entitySize, charset);
265+
stream.reset();
266+
return stream;
267+
}
268+
269+
@Override
270+
public void filter(ClientRequestContext requestContext) throws IOException {
271+
272+
if (!logger.isLoggable(level)) {
273+
return;
274+
}
275+
276+
final long id = _id.incrementAndGet();
277+
requestContext.setProperty(LOGGING_ID_PROPERTY, id);
278+
279+
final StringBuilder sb = new StringBuilder();
280+
printRequestLine(sb, "Sending client request", id, requestContext.getMethod(), requestContext.getUri());
281+
printHeaders(sb, id, REQUEST_PREFIX, requestContext.getStringHeaders());
282+
283+
if (requestContext.hasEntity() && maxEntitySize > 0) {
284+
final OutputStream stream = new LoggingStream(sb, requestContext.getEntityStream());
285+
requestContext.setEntityStream(stream);
286+
requestContext.setProperty(ENTITY_STREAM_PROPERTY, stream);
287+
} else {
288+
log(sb);
289+
}
290+
}
291+
292+
@Override
293+
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
294+
295+
if (!logger.isLoggable(level)) {
296+
return;
297+
}
298+
299+
final Object requestId = requestContext.getProperty(LOGGING_ID_PROPERTY);
300+
final long id = requestId != null ? (Long) requestId : _id.incrementAndGet();
301+
302+
final StringBuilder sb = new StringBuilder();
303+
printResponseLine(sb, "Received server response", id, responseContext.getStatus());
304+
printHeaders(sb, id, RESPONSE_PREFIX, responseContext.getHeaders());
305+
306+
if (responseContext.hasEntity() && maxEntitySize > 0) {
307+
responseContext.setEntityStream(logResponseEntity(sb, responseContext.getEntityStream(),
308+
MessageUtils.getCharset(responseContext.getMediaType())));
309+
}
310+
311+
log(sb);
312+
}
313+
314+
@Override
315+
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
316+
317+
final LoggingStream stream = (LoggingStream) context.getProperty(ENTITY_STREAM_PROPERTY);
318+
context.proceed();
319+
if (stream == null) {
320+
return;
321+
}
322+
323+
MediaType mediaType = context.getMediaType();
324+
if (mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE) ||
325+
mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) {
326+
log(stream.getStringBuilder(MessageUtils.getCharset(mediaType)));
327+
}
328+
329+
}
330+
331+
/**
332+
* This class is responsible for logging the request entities, it will truncate at maxEntitySize
333+
* and add "...more..." to the end of the entity log string.
334+
*/
335+
protected class LoggingStream extends FilterOutputStream {
336+
337+
private final StringBuilder sb;
338+
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
339+
340+
LoggingStream(StringBuilder sb, OutputStream out) {
341+
super(out);
342+
this.sb = sb;
343+
}
344+
345+
StringBuilder getStringBuilder(Charset charset) {
346+
final byte[] entity = outputStream.toByteArray();
347+
buildEntityLogString(sb, entity, entity.length, charset);
348+
return (sb);
349+
}
350+
351+
@Override
352+
public void write(final int i) throws IOException {
353+
354+
if (outputStream.size() <= maxEntitySize) {
355+
outputStream.write(i);
356+
}
357+
358+
out.write(i);
359+
}
360+
}
361+
}

0 commit comments

Comments
 (0)