Skip to content

Commit 84bbae4

Browse files
committed
Introduce support for pcap traffic capturing of network traffic
1 parent 5066fa4 commit 84bbae4

15 files changed

+243
-1
lines changed

src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, EventBu
129129
obj.setOpenSslEngineOptions(new io.vertx.core.net.OpenSSLEngineOptions((io.vertx.core.json.JsonObject)member.getValue()));
130130
}
131131
break;
132+
case "pcapCaptureFile":
133+
if (member.getValue() instanceof String) {
134+
obj.setPcapCaptureFile((String)member.getValue());
135+
}
136+
break;
132137
case "pemKeyCertOptions":
133138
if (member.getValue() instanceof JsonObject) {
134139
obj.setPemKeyCertOptions(new io.vertx.core.net.PemKeyCertOptions((io.vertx.core.json.JsonObject)member.getValue()));
@@ -319,6 +324,9 @@ static void toJson(EventBusOptions obj, java.util.Map<String, Object> json) {
319324
if (obj.getOpenSslEngineOptions() != null) {
320325
json.put("openSslEngineOptions", obj.getOpenSslEngineOptions().toJson());
321326
}
327+
if (obj.getPcapCaptureFile() != null) {
328+
json.put("pcapCaptureFile", obj.getPcapCaptureFile());
329+
}
322330
if (obj.getPemKeyCertOptions() != null) {
323331
json.put("pemKeyCertOptions", obj.getPemKeyCertOptions().toJson());
324332
}

src/main/generated/io/vertx/core/net/NetworkOptionsConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, Network
2525
obj.setLogActivity((Boolean)member.getValue());
2626
}
2727
break;
28+
case "pcapCaptureFile":
29+
if (member.getValue() instanceof String) {
30+
obj.setPcapCaptureFile((String)member.getValue());
31+
}
32+
break;
2833
case "receiveBufferSize":
2934
if (member.getValue() instanceof Number) {
3035
obj.setReceiveBufferSize(((Number)member.getValue()).intValue());
@@ -60,6 +65,9 @@ static void toJson(NetworkOptions obj, JsonObject json) {
6065

6166
static void toJson(NetworkOptions obj, java.util.Map<String, Object> json) {
6267
json.put("logActivity", obj.getLogActivity());
68+
if (obj.getPcapCaptureFile() != null) {
69+
json.put("pcapCaptureFile", obj.getPcapCaptureFile());
70+
}
6371
json.put("receiveBufferSize", obj.getReceiveBufferSize());
6472
json.put("reuseAddress", obj.isReuseAddress());
6573
json.put("reusePort", obj.isReusePort());

src/main/java/io/vertx/core/datagram/DatagramSocketOptions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ public DatagramSocketOptions setReusePort(boolean reusePort) {
135135
return (DatagramSocketOptions) super.setReusePort(reusePort);
136136
}
137137

138+
@Override
139+
public DatagramSocketOptions setPcapCaptureFile(String pcapCaptureFile) {
140+
return (DatagramSocketOptions) super.setPcapCaptureFile(pcapCaptureFile);
141+
}
142+
138143
@Override
139144
public int getTrafficClass() {
140145
return super.getTrafficClass();

src/main/java/io/vertx/core/eventbus/EventBusOptions.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,12 @@ public EventBusOptions setReusePort(boolean reusePort) {
440440
return this;
441441
}
442442

443+
@Override
444+
public EventBusOptions setPcapCaptureFile(String pcapCaptureFile) {
445+
super.setPcapCaptureFile(pcapCaptureFile);
446+
return this;
447+
}
448+
443449
@Override
444450
public EventBusOptions setSendBufferSize(int sendBufferSize) {
445451
super.setSendBufferSize(sendBufferSize);

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

Lines changed: 14 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
@@ -412,6 +420,12 @@ public HttpClientOptions setReusePort(boolean reusePort) {
412420
return this;
413421
}
414422

423+
@Override
424+
public HttpClientOptions setPcapCaptureFile(String pcapCaptureFile) {
425+
super.setPcapCaptureFile(pcapCaptureFile);
426+
return this;
427+
}
428+
415429
@Override
416430
public HttpClientOptions setTrafficClass(int trafficClass) {
417431
super.setTrafficClass(trafficClass);

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

Lines changed: 14 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
@@ -283,6 +291,12 @@ public HttpServerOptions setReusePort(boolean reusePort) {
283291
return this;
284292
}
285293

294+
@Override
295+
public HttpServerOptions setPcapCaptureFile(String pcapCaptureFile) {
296+
super.setPcapCaptureFile(pcapCaptureFile);
297+
return this;
298+
}
299+
286300
@Override
287301
public HttpServerOptions setTrafficClass(int trafficClass) {
288302
super.setTrafficClass(trafficClass);

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+
}

src/main/java/io/vertx/core/net/ClientOptionsBase.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,11 @@ public ClientOptionsBase setReusePort(boolean reusePort) {
361361
return (ClientOptionsBase) super.setReusePort(reusePort);
362362
}
363363

364+
@Override
365+
public ClientOptionsBase setPcapCaptureFile(String pcapCaptureFile) {
366+
return (ClientOptionsBase) super.setPcapCaptureFile(pcapCaptureFile);
367+
}
368+
364369
@Override
365370
public ClientOptionsBase setTrafficClass(int trafficClass) {
366371
return (ClientOptionsBase) super.setTrafficClass(trafficClass);

0 commit comments

Comments
 (0)