Skip to content

Commit 0a1a8b6

Browse files
committed
Introduce support for pcap traffic capturing of HTTP traffic
1 parent 5066fa4 commit 0a1a8b6

File tree

7 files changed

+189
-1
lines changed

7 files changed

+189
-1
lines changed

src/main/generated/io/vertx/core/http/HttpClientOptionsConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpCli
140140
obj.setName((String)member.getValue());
141141
}
142142
break;
143+
case "pcapCaptureFile":
144+
if (member.getValue() instanceof String) {
145+
obj.setPcapCaptureFile((String)member.getValue());
146+
}
147+
break;
143148
case "pipelining":
144149
if (member.getValue() instanceof Boolean) {
145150
obj.setPipelining((Boolean)member.getValue());
@@ -264,6 +269,9 @@ static void toJson(HttpClientOptions obj, java.util.Map<String, Object> json) {
264269
if (obj.getName() != null) {
265270
json.put("name", obj.getName());
266271
}
272+
if (obj.getPcapCaptureFile() != null) {
273+
json.put("pcapCaptureFile", obj.getPcapCaptureFile());
274+
}
267275
json.put("pipelining", obj.isPipelining());
268276
json.put("pipeliningLimit", obj.getPipeliningLimit());
269277
json.put("poolCleanerPeriod", obj.getPoolCleanerPeriod());

src/main/generated/io/vertx/core/http/HttpServerOptionsConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpSer
100100
obj.setMaxWebSocketMessageSize(((Number)member.getValue()).intValue());
101101
}
102102
break;
103+
case "pcapCaptureFile":
104+
if (member.getValue() instanceof String) {
105+
obj.setPcapCaptureFile((String)member.getValue());
106+
}
107+
break;
103108
case "perFrameWebSocketCompressionSupported":
104109
if (member.getValue() instanceof Boolean) {
105110
obj.setPerFrameWebSocketCompressionSupported((Boolean)member.getValue());
@@ -175,6 +180,9 @@ static void toJson(HttpServerOptions obj, java.util.Map<String, Object> json) {
175180
json.put("maxInitialLineLength", obj.getMaxInitialLineLength());
176181
json.put("maxWebSocketFrameSize", obj.getMaxWebSocketFrameSize());
177182
json.put("maxWebSocketMessageSize", obj.getMaxWebSocketMessageSize());
183+
if (obj.getPcapCaptureFile() != null) {
184+
json.put("pcapCaptureFile", obj.getPcapCaptureFile());
185+
}
178186
json.put("perFrameWebSocketCompressionSupported", obj.getPerFrameWebSocketCompressionSupported());
179187
json.put("perMessageWebSocketCompressionSupported", obj.getPerMessageWebSocketCompressionSupported());
180188
if (obj.getTracingPolicy() != null) {

src/main/java/io/vertx/core/http/HttpClientOptions.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ public class HttpClientOptions extends ClientOptionsBase {
224224
*/
225225
public static final String DEFAULT_NAME = "__vertx.DEFAULT";
226226

227+
/**
228+
* Default pcap file name where capturing of HTTP traffic is recorded. When no file is set, capturing is disabled.
229+
*/
230+
public static final String DEFAULT_PCAP_CAPTURE_FILE = "";
231+
227232
private boolean verifyHost = true;
228233
private int maxPoolSize;
229234
private boolean keepAlive;
@@ -267,6 +272,7 @@ public class HttpClientOptions extends ClientOptionsBase {
267272

268273
private boolean shared;
269274
private String name;
275+
private String pcapCaptureFile;
270276

271277
/**
272278
* Default constructor
@@ -322,6 +328,7 @@ public HttpClientOptions(HttpClientOptions other) {
322328
this.tracingPolicy = other.tracingPolicy;
323329
this.shared = other.shared;
324330
this.name = other.name;
331+
this.pcapCaptureFile = other.pcapCaptureFile;
325332
}
326333

327334
/**
@@ -386,6 +393,7 @@ private void init() {
386393
tracingPolicy = DEFAULT_TRACING_POLICY;
387394
shared = DEFAULT_SHARED;
388395
name = DEFAULT_NAME;
396+
pcapCaptureFile = DEFAULT_PCAP_CAPTURE_FILE;
389397
}
390398

391399
@Override
@@ -1434,4 +1442,16 @@ public HttpClientOptions setName(String name) {
14341442
this.name = name;
14351443
return this;
14361444
}
1445+
1446+
/**
1447+
* @return the name of the PCAP file where the HTTP traffic will be written to.
1448+
*/
1449+
public String getPcapCaptureFile() {
1450+
return pcapCaptureFile;
1451+
}
1452+
1453+
public HttpClientOptions setPcapCaptureFile(String pcapCaptureFile) {
1454+
this.pcapCaptureFile = pcapCaptureFile;
1455+
return this;
1456+
}
14371457
}

src/main/java/io/vertx/core/http/HttpServerOptions.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ public class HttpServerOptions extends NetServerOptions {
146146
*/
147147
public static final TracingPolicy DEFAULT_TRACING_POLICY = TracingPolicy.ALWAYS;
148148

149+
/**
150+
* Default pcap file name where capturing of HTTP traffic is recorded. When no file is set, capturing is disabled.
151+
*/
152+
public static final String DEFAULT_PCAP_CAPTURE_FILE = "";
153+
149154
private boolean compressionSupported;
150155
private int compressionLevel;
151156
private int maxWebSocketFrameSize;
@@ -169,6 +174,7 @@ public class HttpServerOptions extends NetServerOptions {
169174
private boolean webSocketPreferredClientNoContext;
170175
private int webSocketClosingTimeout;
171176
private TracingPolicy tracingPolicy;
177+
private String pcapCaptureFile;
172178

173179
/**
174180
* Default constructor
@@ -209,6 +215,7 @@ public HttpServerOptions(HttpServerOptions other) {
209215
this.webSocketAllowServerNoContext = other.webSocketAllowServerNoContext;
210216
this.webSocketClosingTimeout = other.webSocketClosingTimeout;
211217
this.tracingPolicy = other.tracingPolicy;
218+
this.pcapCaptureFile = other.pcapCaptureFile;
212219
}
213220

214221
/**
@@ -257,6 +264,7 @@ private void init() {
257264
webSocketAllowServerNoContext = DEFAULT_WEBSOCKET_ALLOW_SERVER_NO_CONTEXT;
258265
webSocketClosingTimeout = DEFAULT_WEBSOCKET_CLOSING_TIMEOUT;
259266
tracingPolicy = DEFAULT_TRACING_POLICY;
267+
pcapCaptureFile = DEFAULT_PCAP_CAPTURE_FILE;
260268
}
261269

262270
@Override
@@ -977,4 +985,16 @@ public HttpServerOptions setTracingPolicy(TracingPolicy tracingPolicy) {
977985
this.tracingPolicy = tracingPolicy;
978986
return this;
979987
}
988+
989+
/**
990+
* @return the name of the PCAP file where the HTTP traffic will be written to.
991+
*/
992+
public String getPcapCaptureFile() {
993+
return pcapCaptureFile;
994+
}
995+
996+
public HttpServerOptions setPcapCaptureFile(String pcapCaptureFile) {
997+
this.pcapCaptureFile = pcapCaptureFile;
998+
return this;
999+
}
9801000
}

src/main/java/io/vertx/core/http/impl/HttpChannelConnector.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import io.vertx.core.net.SocketAddress;
3333
import io.vertx.core.net.impl.NetClientImpl;
3434
import io.vertx.core.net.impl.NetSocketImpl;
35-
import io.vertx.core.net.impl.NetSocketInternal;
3635
import io.vertx.core.net.impl.VertxHandler;
3736
import io.vertx.core.spi.metrics.ClientMetrics;
3837
import io.vertx.core.spi.metrics.HttpClientMetrics;
@@ -58,6 +57,7 @@ public class HttpChannelConnector {
5857
private final HttpVersion version;
5958
private final SocketAddress peerAddress;
6059
private final SocketAddress server;
60+
private final boolean enablePcapCapture;
6161

6262
public HttpChannelConnector(HttpClientImpl client,
6363
NetClientImpl netClient,
@@ -78,6 +78,7 @@ public HttpChannelConnector(HttpClientImpl client,
7878
this.version = version;
7979
this.peerAddress = peerAddress;
8080
this.server = server;
81+
this.enablePcapCapture = (options.getPcapCaptureFile() != null) && !options.getPcapCaptureFile().isEmpty();
8182
}
8283

8384
public SocketAddress server() {
@@ -165,6 +166,9 @@ private void applyHttp1xConnectionOptions(ChannelPipeline pipeline) {
165166
if (options.getLogActivity()) {
166167
pipeline.addLast("logging", new LoggingHandler());
167168
}
169+
if (enablePcapCapture) {
170+
pipeline.addLast("pcapCapturing", new VertxPcapWriteHandler(options.getPcapCaptureFile()));
171+
}
168172
pipeline.addLast("codec", new HttpClientCodec(
169173
options.getMaxInitialLineLength(),
170174
options.getMaxHeaderSize(),

src/main/java/io/vertx/core/http/impl/HttpServerWorker.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
import io.vertx.core.net.impl.HAProxyMessageCompletionHandler;
3636
import io.vertx.core.spi.metrics.HttpServerMetrics;
3737

38+
import java.io.File;
39+
import java.io.FileNotFoundException;
40+
import java.io.FileOutputStream;
41+
import java.io.UncheckedIOException;
3842
import java.nio.charset.StandardCharsets;
3943
import java.util.function.Supplier;
4044

@@ -55,6 +59,7 @@ public class HttpServerWorker implements Handler<Channel> {
5559
private final String serverOrigin;
5660
private final boolean logEnabled;
5761
private final boolean disableH2C;
62+
private final boolean enablePcapCapture;
5863
final Handler<HttpServerConnection> connectionHandler;
5964
private final Handler<Throwable> exceptionHandler;
6065

@@ -77,6 +82,7 @@ public HttpServerWorker(EventLoopContext context,
7782
this.serverOrigin = serverOrigin;
7883
this.logEnabled = options.getLogActivity();
7984
this.disableH2C = disableH2C;
85+
this.enablePcapCapture = (options.getPcapCaptureFile() != null) && !options.getPcapCaptureFile().isEmpty();
8086
this.connectionHandler = connectionHandler;
8187
this.exceptionHandler = exceptionHandler;
8288
}
@@ -252,6 +258,9 @@ private void configureHttp1OrH2C(ChannelPipeline pipeline) {
252258
if (logEnabled) {
253259
pipeline.addLast("logging", new LoggingHandler());
254260
}
261+
if (enablePcapCapture) {
262+
pipeline.addLast("pcapCapturing", new VertxPcapWriteHandler(options.getPcapCaptureFile()));
263+
}
255264
if (HttpServerImpl.USE_FLASH_POLICY_HANDLER) {
256265
pipeline.addLast("flashpolicy", new FlashPolicyHandler());
257266
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package io.vertx.core.http.impl;
2+
3+
import io.netty.channel.ChannelDuplexHandler;
4+
import io.netty.channel.ChannelHandlerContext;
5+
import io.netty.channel.ChannelPromise;
6+
import io.netty.handler.pcap.PcapWriteHandler;
7+
import io.vertx.core.impl.logging.Logger;
8+
import io.vertx.core.impl.logging.LoggerFactory;
9+
import java.io.Closeable;
10+
import java.io.FileNotFoundException;
11+
import java.io.FileOutputStream;
12+
import java.io.IOException;
13+
import java.io.OutputStream;
14+
import java.util.concurrent.ConcurrentHashMap;
15+
import java.util.concurrent.ConcurrentMap;
16+
import java.util.concurrent.atomic.AtomicInteger;
17+
18+
/**
19+
* A handler that simply delegates to the built in {@link PcapWriteHandler}.
20+
* Vert.x needs this because the handler might not have been added to the processing pipeline
21+
* when the {@code channelRead} method is invoked, and thus the necessary setup is performed
22+
* in the {@code channelRegistered} method.
23+
* Furthermore, we want to support capturing the output of multiple Netty pipelines into a single file,
24+
* so for example both the output of an HTTP server and an HTTP Client can be inspected via the same file.
25+
*/
26+
public class VertxPcapWriteHandler extends ChannelDuplexHandler implements Closeable {
27+
28+
private static final Logger log = LoggerFactory.getLogger(VertxPcapWriteHandler.class);
29+
30+
/**
31+
* The idea of this map is to control the usage of each throughout the entire Vert.x application.
32+
* When the same file is configured for multiple pipelines, we want each pipeline to write to the same
33+
* OutputStream, but we only want to close it when the last pipeline has been closed.
34+
*/
35+
private static final ConcurrentMap<String, Metadata> fileToMetadata = new ConcurrentHashMap<>();
36+
37+
private final PcapWriteHandler delegate;
38+
private final String pcapCaptureFile;
39+
40+
public VertxPcapWriteHandler(String pcapCaptureFile) {
41+
this.pcapCaptureFile = pcapCaptureFile;
42+
Metadata metadata = fileToMetadata.computeIfAbsent(pcapCaptureFile, Metadata::new);
43+
// pcap contains a global header section that should only be written by the first handler
44+
int openedCount = metadata.openedCount.getAndIncrement();
45+
this.delegate = new PcapWriteHandler(metadata.outputStream, false, openedCount == 0);
46+
}
47+
48+
@Override
49+
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
50+
delegate.channelActive(ctx);
51+
}
52+
53+
@Override
54+
public void channelActive(ChannelHandlerContext ctx) throws Exception {
55+
delegate.channelActive(ctx);
56+
}
57+
58+
@Override
59+
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
60+
delegate.channelRead(ctx, msg);
61+
}
62+
63+
@Override
64+
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
65+
delegate.write(ctx, msg, promise);
66+
}
67+
68+
@Override
69+
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
70+
int openedCount = fileToMetadata.get(pcapCaptureFile).openedCount.decrementAndGet();
71+
if (openedCount == 0) {
72+
delegate.handlerRemoved(ctx);
73+
}
74+
}
75+
76+
@Override
77+
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
78+
delegate.exceptionCaught(ctx, cause);
79+
}
80+
81+
@Override
82+
public void close() throws IOException {
83+
delegate.close();
84+
}
85+
86+
private static class Metadata {
87+
88+
final AtomicInteger openedCount;
89+
final OutputStream outputStream;
90+
91+
Metadata(String pcapFile) {
92+
openedCount = new AtomicInteger(0);
93+
outputStream = getOutputStream(pcapFile);
94+
}
95+
96+
private OutputStream getOutputStream(String pcapFile) {
97+
try {
98+
return new FileOutputStream(pcapFile);
99+
} catch (FileNotFoundException e) {
100+
log.warn("Unable to open capture file for writing, so no capture information will be recorded.", e);
101+
return NullOutputStream.INSTANCE;
102+
}
103+
}
104+
105+
private static class NullOutputStream extends OutputStream {
106+
107+
static final NullOutputStream INSTANCE = new NullOutputStream();
108+
109+
private NullOutputStream() {
110+
}
111+
112+
@Override
113+
public void write(int b) throws IOException {
114+
115+
}
116+
}
117+
}
118+
119+
}

0 commit comments

Comments
 (0)