Merge branch 'unpack-permessage-deflate' into 'master'

Unpack permessage deflate

See merge request packmate/Packmate!3
This commit is contained in:
Sergey
2020-04-04 22:55:05 +00:00
9 changed files with 307 additions and 13 deletions

View File

@@ -19,6 +19,9 @@ configurations {
repositories { repositories {
mavenCentral() mavenCentral()
// удалить после выхода стабильной версии Java-WebSocket
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
} }
dependencies { dependencies {
@@ -32,6 +35,7 @@ dependencies {
compile 'org.pcap4j:pcap4j-core:1.8.2' compile 'org.pcap4j:pcap4j-core:1.8.2'
compile 'org.pcap4j:pcap4j-packetfactory-static:1.8.2' compile 'org.pcap4j:pcap4j-packetfactory-static:1.8.2'
compile group: 'com.google.guava', name: 'guava', version: '28.2-jre' compile group: 'com.google.guava', name: 'guava', version: '28.2-jre'
compile group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.0-SNAPSHOT'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'org.postgresql:postgresql'

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -22,4 +22,6 @@ public class CtfService {
private boolean mergeAdjacentPackets; private boolean mergeAdjacentPackets;
private boolean inflateWebSockets;
} }

View File

@@ -52,6 +52,18 @@ public class Packet {
private boolean ungzipped; private boolean ungzipped;
private boolean webSocketInflated;
private byte[] content; private byte[] content;
@Transient
@JsonIgnore
public String getContentString() {
return new String(content);
}
public String toString() {
return "Packet(id=" + id + ", content=" + getContentString() + ")";
}
} }

View File

@@ -24,18 +24,22 @@ import java.util.zip.ZipException;
public class StreamOptimizer { public class StreamOptimizer {
private final CtfService service; private final CtfService service;
private final List<Packet> packets; private List<Packet> packets;
private static final byte[] GZIP_HEADER = {0x1f, (byte) 0x8b, 0x08}; private static final byte[] GZIP_HEADER = {0x1f, (byte) 0x8b, 0x08};
/** /**
* Вызвать для выполнения оптимизаций на переданном списке пакетов. * Вызвать для выполнения оптимизаций на переданном списке пакетов.
*/ */
public void optimizeStream() { public List<Packet> optimizeStream() {
if (service.isUngzipHttp()) { if (service.isUngzipHttp()) {
unpackGzip(); unpackGzip();
} }
if (service.isInflateWebSockets()) {
inflateWebSocket();
}
if (service.isUrldecodeHttpRequests()) { if (service.isUrldecodeHttpRequests()) {
urldecodeRequests(); urldecodeRequests();
} }
@@ -43,6 +47,8 @@ public class StreamOptimizer {
if (service.isMergeAdjacentPackets()) { if (service.isMergeAdjacentPackets()) {
mergeAdjacentPackets(); mergeAdjacentPackets();
} }
return packets;
} }
/** /**
@@ -83,6 +89,7 @@ public class StreamOptimizer {
final List<Packet> cut = packets.subList(start, end); final List<Packet> cut = packets.subList(start, end);
final long timestamp = cut.get(0).getTimestamp(); final long timestamp = cut.get(0).getTimestamp();
final boolean ungzipped = cut.stream().anyMatch(Packet::isUngzipped); final boolean ungzipped = cut.stream().anyMatch(Packet::isUngzipped);
final boolean webSocketInflated = cut.stream().anyMatch(Packet::isWebSocketInflated);
boolean incoming = cut.get(0).isIncoming(); boolean incoming = cut.get(0).isIncoming();
//noinspection OptionalGetWithoutIsPresent //noinspection OptionalGetWithoutIsPresent
final byte[] content = cut.stream() final byte[] content = cut.stream()
@@ -95,6 +102,7 @@ public class StreamOptimizer {
.incoming(incoming) .incoming(incoming)
.timestamp(timestamp) .timestamp(timestamp)
.ungzipped(ungzipped) .ungzipped(ungzipped)
.webSocketInflated(webSocketInflated)
.content(content) .content(content)
.build()); .build());
} }
@@ -108,7 +116,7 @@ public class StreamOptimizer {
for (Packet packet : packets) { for (Packet packet : packets) {
if (packet.isIncoming()) { if (packet.isIncoming()) {
String content = new String(packet.getContent()); String content = packet.getContentString();
if (content.contains("HTTP/")) { if (content.contains("HTTP/")) {
httpStarted = true; httpStarted = true;
} }
@@ -145,7 +153,7 @@ public class StreamOptimizer {
i = gzipStartPacket + 1; // продвигаем указатель на следующий после склеенного блок i = gzipStartPacket + 1; // продвигаем указатель на следующий после склеенного блок
} }
} else if (!packet.isIncoming()) { } else if (!packet.isIncoming()) {
String content = new String(packet.getContent()); String content = packet.getContentString();
int contentPos = content.indexOf("\r\n\r\n"); int contentPos = content.indexOf("\r\n\r\n");
boolean http = content.startsWith("HTTP/"); boolean http = content.startsWith("HTTP/");
@@ -215,6 +223,7 @@ public class StreamOptimizer {
.incoming(false) .incoming(false)
.timestamp(cut.get(0).getTimestamp()) .timestamp(cut.get(0).getTimestamp())
.ungzipped(true) .ungzipped(true)
.webSocketInflated(false)
.content(newContent) .content(newContent)
.build(); .build();
} catch (ZipException e) { } catch (ZipException e) {
@@ -226,4 +235,17 @@ public class StreamOptimizer {
return null; return null;
} }
private void inflateWebSocket() {
if (!packets.get(0).getContentString().contains("HTTP/")) {
return;
}
final WebSocketsParser parser = new WebSocketsParser(packets);
if(!parser.isParsed()) {
return;
}
packets = parser.getParsedPackets();
}
} }

View File

@@ -93,7 +93,7 @@ public class StreamService {
countingService.countStream(service.getPort(), packets.size()); countingService.countStream(service.getPort(), packets.size());
new StreamOptimizer(service, packets).optimizeStream(); packets = new StreamOptimizer(service, packets).optimizeStream();
processUserAgent(packets, stream); processUserAgent(packets, stream);
Stream savedStream = save(stream); Stream savedStream = save(stream);
@@ -110,7 +110,7 @@ public class StreamService {
private void processUserAgent(List<Packet> packets, Stream stream) { private void processUserAgent(List<Packet> packets, Stream stream) {
String ua = null; String ua = null;
for (Packet packet : packets) { for (Packet packet : packets) {
String content = new String(packet.getContent()); String content = packet.getContentString();
final Matcher matcher = userAgentPattern.matcher(content); final Matcher matcher = userAgentPattern.matcher(content);
if (matcher.find()) { if (matcher.find()) {
ua = matcher.group(1); ua = matcher.group(1);

View File

@@ -0,0 +1,254 @@
package ru.serega6531.packmate.service;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.exceptions.InvalidDataException;
import org.java_websocket.exceptions.InvalidHandshakeException;
import org.java_websocket.extensions.permessage_deflate.PerMessageDeflateExtension;
import org.java_websocket.framing.DataFrame;
import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.HandshakeImpl1Client;
import org.java_websocket.handshake.HandshakeImpl1Server;
import ru.serega6531.packmate.model.Packet;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
@Slf4j
public class WebSocketsParser {
private static final java.util.regex.Pattern WEBSOCKET_KEY_PATTERN =
java.util.regex.Pattern.compile("Sec-WebSocket-Key: (.+)\\r\\n");
private static final java.util.regex.Pattern WEBSOCKET_EXTENSIONS_PATTERN =
java.util.regex.Pattern.compile("Sec-WebSocket-Extensions?: (.+)\\r\\n");
private static final java.util.regex.Pattern WEBSOCKET_VERSION_PATTERN =
java.util.regex.Pattern.compile("Sec-WebSocket-Version: (\\d+)\\r\\n");
private static final java.util.regex.Pattern WEBSOCKET_ACCEPT_PATTERN =
java.util.regex.Pattern.compile("Sec-WebSocket-Accept: (.+)\\r\\n");
private static final String WEBSOCKET_EXTENSION_HEADER = "Sec-WebSocket-Extension: permessage-deflate";
private static final String WEBSOCKET_EXTENSIONS_HEADER = "Sec-WebSocket-Extensions: permessage-deflate";
private static final String WEBSOCKET_UPGRADE_HEADER = "upgrade: websocket\r\n";
private static final String WEBSOCKET_CONNECTION_HEADER = "connection: upgrade\r\n";
private final List<Packet> packets;
@Getter
private boolean parsed = false;
private List<Packet> parsedPackets;
public WebSocketsParser(List<Packet> packets) {
this.packets = packets;
detectWebSockets();
}
private void detectWebSockets() {
final List<Packet> clientHandshakePackets = packets.stream()
.takeWhile(Packet::isIncoming)
.collect(Collectors.toList());
final String clientHandshake = getHandshake(clientHandshakePackets);
if (clientHandshake == null) {
return;
}
int httpEnd = -1;
for (int i = clientHandshakePackets.size(); i < packets.size(); i++) {
if (packets.get(i).getContentString().endsWith("\r\n\r\n")) {
httpEnd = i + 1;
break;
}
}
if (httpEnd == -1) {
return;
}
final List<Packet> serverHandshakePackets = packets.subList(clientHandshakePackets.size(), httpEnd);
final String serverHandshake = getHandshake(serverHandshakePackets);
if (serverHandshake == null) {
return;
}
HandshakeImpl1Server serverHandshakeImpl = fillServerHandshake(serverHandshake);
HandshakeImpl1Client clientHandshakeImpl = fillClientHandshake(clientHandshake);
if (serverHandshakeImpl == null || clientHandshakeImpl == null) {
return;
}
Draft_6455 draft = new Draft_6455(new PerMessageDeflateExtension());
try {
draft.acceptHandshakeAsServer(clientHandshakeImpl);
draft.acceptHandshakeAsClient(clientHandshakeImpl, serverHandshakeImpl);
} catch (InvalidHandshakeException e) {
log.warn("WebSocket handshake", e);
return;
}
final List<Packet> wsPackets = packets.subList(
httpEnd,
packets.size());
if(wsPackets.isEmpty()) {
return;
}
final List<Packet> handshakes = packets.subList(0, httpEnd);
parse(wsPackets, handshakes, draft);
parsed = true;
}
private void parse(final List<Packet> wsPackets, final List<Packet> handshakes, Draft_6455 draft) {
List<List<Packet>> sides = sliceToSides(wsPackets);
parsedPackets = new ArrayList<>(handshakes);
for (List<Packet> side : sides) {
final Packet lastPacket = side.get(0);
final byte[] wsContent = side.stream()
.map(Packet::getContent)
.reduce(ArrayUtils::addAll)
.get();
final ByteBuffer buffer = ByteBuffer.wrap(wsContent);
List<Framedata> frames;
try {
frames = draft.translateFrame(buffer);
} catch (InvalidDataException e) {
log.warn("WebSocket data", e);
return;
}
for (Framedata frame : frames) {
if(frame instanceof DataFrame) {
parsedPackets.add(Packet.builder()
.content(frame.getPayloadData().array())
.incoming(lastPacket.isIncoming())
.timestamp(lastPacket.getTimestamp())
.ttl(lastPacket.getTtl())
.ungzipped(lastPacket.isUngzipped())
.webSocketInflated(true)
.build()
);
}
}
}
}
public List<Packet> getParsedPackets() {
if (!parsed) {
throw new IllegalStateException("WS is not parsed");
}
return parsedPackets;
}
private List<List<Packet>> sliceToSides(List<Packet> packets) {
List<List<Packet>> result = new ArrayList<>();
List<Packet> side = new ArrayList<>();
boolean incoming = true;
for (Packet packet : packets) {
if(packet.isIncoming() != incoming) {
incoming = packet.isIncoming();
if(!side.isEmpty()) {
result.add(side);
side = new ArrayList<>();
}
}
side.add(packet);
}
if(!side.isEmpty()) {
result.add(side);
}
return result;
}
private String getHandshake(final List<Packet> packets) {
final String handshake = packets.stream()
.map(Packet::getContent)
.reduce(ArrayUtils::addAll)
.map(String::new)
.orElse(null);
if (handshake == null ||
!handshake.toLowerCase().contains(WEBSOCKET_CONNECTION_HEADER) ||
!handshake.toLowerCase().contains(WEBSOCKET_UPGRADE_HEADER)) {
return null;
}
if (!handshake.contains(WEBSOCKET_EXTENSION_HEADER) &&
!handshake.contains(WEBSOCKET_EXTENSIONS_HEADER)) {
return null;
}
return handshake;
}
private HandshakeImpl1Client fillClientHandshake(String clientHandshake) {
Matcher matcher = WEBSOCKET_VERSION_PATTERN.matcher(clientHandshake);
if (!matcher.find()) {
return null;
}
String version = matcher.group(1);
matcher = WEBSOCKET_KEY_PATTERN.matcher(clientHandshake);
if (!matcher.find()) {
return null;
}
String key = matcher.group(1);
matcher = WEBSOCKET_EXTENSIONS_PATTERN.matcher(clientHandshake);
if (!matcher.find()) {
return null;
}
String extensions = matcher.group(1);
HandshakeImpl1Client clientHandshakeImpl = new HandshakeImpl1Client();
clientHandshakeImpl.put("Upgrade", "websocket");
clientHandshakeImpl.put("Connection", "Upgrade");
clientHandshakeImpl.put("Sec-WebSocket-Version", version);
clientHandshakeImpl.put("Sec-WebSocket-Key", key);
clientHandshakeImpl.put("Sec-WebSocket-Extensions", extensions);
return clientHandshakeImpl;
}
private HandshakeImpl1Server fillServerHandshake(String serverHandshake) {
Matcher matcher = WEBSOCKET_ACCEPT_PATTERN.matcher(serverHandshake);
if (!matcher.find()) {
return null;
}
String accept = matcher.group(1);
matcher = WEBSOCKET_EXTENSIONS_PATTERN.matcher(serverHandshake);
if (!matcher.find()) {
return null;
}
String extensions = matcher.group(1);
HandshakeImpl1Server serverHandshakeImpl = new HandshakeImpl1Server();
serverHandshakeImpl.put("Upgrade", "websocket");
serverHandshakeImpl.put("Connection", "Upgrade");
serverHandshakeImpl.put("Sec-WebSocket-Accept", accept);
serverHandshakeImpl.put("Sec-WebSocket-Extensions", extensions);
return serverHandshakeImpl;
}
}

View File

@@ -28,8 +28,8 @@ class StreamOptimizerTest {
List<Packet> list = new ArrayList<>(); List<Packet> list = new ArrayList<>();
list.add(p); list.add(p);
new StreamOptimizer(service, list).optimizeStream(); list = new StreamOptimizer(service, list).optimizeStream();
final String processed = new String(list.get(0).getContent()); final String processed = list.get(0).getContentString();
assertTrue(processed.contains("aaabbb")); assertTrue(processed.contains("aaabbb"));
} }
@@ -42,8 +42,8 @@ class StreamOptimizerTest {
List<Packet> list = new ArrayList<>(); List<Packet> list = new ArrayList<>();
list.add(p); list.add(p);
new StreamOptimizer(service, list).optimizeStream(); list = new StreamOptimizer(service, list).optimizeStream();
final String processed = new String(list.get(0).getContent()); final String processed = list.get(0).getContentString();
assertTrue(processed.contains("а б")); assertTrue(processed.contains("а б"));
} }
@@ -67,7 +67,7 @@ class StreamOptimizerTest {
list.add(p5); list.add(p5);
list.add(p6); list.add(p6);
new StreamOptimizer(service, list).optimizeStream(); list = new StreamOptimizer(service, list).optimizeStream();
assertEquals(4, list.size()); assertEquals(4, list.size());
assertEquals(2, list.get(1).getContent().length); assertEquals(2, list.get(1).getContent().length);