Merge branch 'master' into enable-patterns

# Conflicts:
#	src/main/java/ru/serega6531/packmate/model/enums/SubscriptionMessageType.java
#	src/main/java/ru/serega6531/packmate/service/PatternService.java
This commit is contained in:
serega6531
2020-04-07 00:41:00 +03:00
27 changed files with 492 additions and 87 deletions

View File

@@ -1,20 +0,0 @@
FROM openjdk:13-jdk-alpine
RUN apk add libpcap npm
COPY ./ /app/
WORKDIR /app/frontend/
RUN npm install && npm run build
RUN mkdir -p ../src/main/resources/static/ \
&& cp -rf ./dist/* ../src/main/resources/static/
WORKDIR /app/
RUN ./gradlew --no-daemon build
RUN cp build/libs/packmate-*.jar app.jar
EXPOSE 65000:65000

View File

@@ -19,15 +19,16 @@
* Конкатенирует смежные пакеты * Конкатенирует смежные пакеты
* Автоматически проводит urldecode * Автоматически проводит urldecode
* Разархивирует GZIP в HTTP на лету * Разархивирует GZIP в HTTP на лету
* Разархивирует сжатые WebSockets
![Скриншот главного окна](screenshots/Screenshot.png) ![Скриншот главного окна](screenshots/Screenshot.png)
## Клонирование ## Клонирование
Поскольку этот репозиторий содержит фронтенд как git submodule, его необходимо клонировать так: Поскольку этот репозиторий содержит фронтенд как git submodule, его необходимо клонировать так:
```bash ```bash
git clone --recurse-submodules https://gitlab.com/binarybears_ctf/Packmate.git git clone --recurse-submodules https://gitlab.com/packmate/Packmate.git
# Или, на старых версиях git # Или, на старых версиях git
git clone --recursive https://gitlab.com/binarybears_ctf/Packmate.git git clone --recursive https://gitlab.com/packmate/Packmate.git
``` ```
Если репозиторий уже был склонирован без подмодулей, необходимо выполнить: Если репозиторий уже был склонирован без подмодулей, необходимо выполнить:

View File

@@ -14,20 +14,21 @@ Advanced network traffic flow analyzer for A/D CTFs.
* Binary substring * Binary substring
* Can make certain streams favorite and show only favorite streams * Can make certain streams favorite and show only favorite streams
* Supports several simultaneous services, can show streams for a specific service or pattern * Supports several simultaneous services, can show streams for a specific service or pattern
* Allows to navigate streams using shortcuts * Allows navigating streams using shortcuts
* Has the option to copy packet content in the required format * Has the option to copy packet content in the required format
* Can concatenate adjacent packets * Can concatenate adjacent packets
* Can urldecode text automatically * Can urldecode text automatically
* Can automatically decode GZIPed HTTP * Can automatically decompress GZIPed HTTP
* Can automatically deflate WebSockets with permessages-deflate extension
![Main window](screenshots/Screenshot.png) ![Main window](screenshots/Screenshot.png)
## Cloning ## Cloning
As this repository contains frontend part as git submodule, it has to be cloned like this: As this repository contains frontend part as a git submodule, it has to be cloned like this:
```bash ```bash
git clone --recurse-submodules https://gitlab.com/binarybears_ctf/Packmate.git git clone --recurse-submodules https://gitlab.com/packmate/Packmate.git
# Or if you have older git # Or if you have older git
git clone --recursive https://gitlab.com/binarybears_ctf/Packmate.git git clone --recursive https://gitlab.com/packmate/Packmate.git
``` ```
If the repository was already cloned without submodule, just run: If the repository was already cloned without submodule, just run:
@@ -39,23 +40,23 @@ git submodule update --init --recursive
## Preparation ## Preparation
This program uses Docker and docker-compose. This program uses Docker and docker-compose.
`packmate-db` will listen port 65001 at localhost. `packmate-db` will listen to port 65001 at localhost.
Database files do not mount as volume, so upon container recreation all data will be lost. Database files do not mount as volume, so upon container recreation, all data will be lost.
### Settings ### Settings
This program retreives settings from environment variables, This program retrieves settings from environment variables,
so it would be convenient to create env file; so it would be convenient to create an env file;
It must be called `.env` and located at the root of the project. It must be called `.env` and located at the root of the project.
Contents of the file: Contents of the file:
```bash ```bash
# Interface to capture on # Interface to capture on
PACKMATE_INTERFACE=wlan0 PACKMATE_INTERFACE=wlan0
# Local ip on said interface to tell incoming packets from outgoing # Local IP on said interface to tell incoming packets from outgoing
PACKMATE_LOCAL_IP=192.168.1.124 PACKMATE_LOCAL_IP=192.168.1.124
# Username for web interface # Username for the web interface
PACKMATE_WEB_LOGIN=SomeUser PACKMATE_WEB_LOGIN=SomeUser
# Password for web interface # Password for the web interface
PACKMATE_WEB_PASSWORD=SomeSecurePassword PACKMATE_WEB_PASSWORD=SomeSecurePassword
``` ```
@@ -67,8 +68,8 @@ sudo docker-compose up --build -d
If everything went fine, Packmate will be available on port `65000` from any host If everything went fine, Packmate will be available on port `65000` from any host
### Accessing web interface ### Accessing the web interface
When you open web interface for the first time, you will be asked for login and password When you open a web interface for the first time, you will be asked for a login and password
you specified in the env file. you specified in the env file.
After entering the credentials, open the settings by clicking on the cogs After entering the credentials, open the settings by clicking on the cogs
in the top right corner and enter login and password again. in the top right corner and enter login and password again.
@@ -76,18 +77,18 @@ in the top right corner and enter login and password again.
![Settings](screenshots/Screenshot_Settings.png) ![Settings](screenshots/Screenshot_Settings.png)
All settings are saved in the local storage and will be All settings are saved in the local storage and will be
lost only upon changing server ip or port. lost only upon changing server IP or port.
## Usage ## Usage
First of all you should create game services. First of all, you should create game services.
To do that click `+` in the navbar, To do that click `+` in the navbar,
then fill in service name, port and optimization to perform. then fill in the service name, port, and optimization to perform.
System will start automatically capture streams and show them in a sidebar. The system will start automatically capture streams and show them in a sidebar.
Click at stream to view a list of packets; Click at a stream to view a list of packets;
you can click a button in the sidebar to switch between binary and text views. you can click a button in the sidebar to switch between binary and text views.
For a simple monitoring of flags there is a system of patterns. For simple monitoring of flags, there is a system of patterns.
To create a pattern open `Patterns` dropdown menu, press `+`, then To create a pattern open `Patterns` dropdown menu, press `+`, then
specify the type of pattern, the pattern itself, highlight color and other things. specify the type of pattern, the pattern itself, highlight color and other things.

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

@@ -14,7 +14,7 @@ services:
container_name: packmate-app container_name: packmate-app
build: build:
context: . context: .
dockerfile: Dockerfile_app dockerfile: docker/Dockerfile_app
network_mode: "host" network_mode: "host"
image: packmate-app:v1 image: packmate-app:v1
command: [ command: [
@@ -31,7 +31,7 @@ services:
container_name: packmate-db container_name: packmate-db
build: build:
context: . context: .
dockerfile: Dockerfile_db dockerfile: docker/Dockerfile_db
args: args:
POSTGRES_USER: ${PACKMATE_DB_USER:-packmate} POSTGRES_USER: ${PACKMATE_DB_USER:-packmate}
POSTGRES_PASSWORD: ${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb} POSTGRES_PASSWORD: ${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb}

20
docker/Dockerfile_app Normal file
View File

@@ -0,0 +1,20 @@
FROM openjdk:15-ea-jdk-alpine
RUN apk --no-cache add libpcap npm
COPY ./ /app/
WORKDIR /app/frontend/
RUN npm install && npm run build && npm cache clean --force \
&& mkdir -p ../src/main/resources/static/ \
&& mv ./dist/* ../src/main/resources/static/ \
&& rm -rf node_modules
WORKDIR /app/
RUN ./gradlew --no-daemon --no-build-cache build \
&& cp build/libs/packmate-*.jar app.jar \
&& ./gradlew --no-daemon clean
EXPOSE 65000:65000

View File

@@ -8,7 +8,7 @@ ENV POSTGRES_USER ${POSTGRES_USER}
ENV POSTGRES_PASSWORD ${POSTGRES_PASSWORD} ENV POSTGRES_PASSWORD ${POSTGRES_PASSWORD}
ENV POSTGRES_DB ${POSTGRES_DB} ENV POSTGRES_DB ${POSTGRES_DB}
COPY postgresql.conf /tmp/postgresql.conf COPY docker/postgresql.conf /tmp/postgresql.conf
COPY update_db_config.sh /docker-entrypoint-initdb.d/_update_db_config.sh COPY docker/update_db_config.sh /docker-entrypoint-initdb.d/_update_db_config.sh
EXPOSE 65001:65001 EXPOSE 65001:65001

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

@@ -140,7 +140,7 @@ public class PcapWorker implements PacketListener {
UnfinishedStream stream = addNewPacket(sourceIp, destIp, time, sourcePort, destPort, ttl, content, Protocol.TCP); UnfinishedStream stream = addNewPacket(sourceIp, destIp, time, sourcePort, destPort, ttl, content, Protocol.TCP);
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("tcp {} {}:{} -> {}:{}, номер пакета {}", log.debug("tcp {} {}:{} -> {}:{}, packet number {}",
serviceOptional.get(), sourceIpString, sourcePort, destIpString, destPort, serviceOptional.get(), sourceIpString, sourcePort, destIpString, destPort,
unfinishedTcpStreams.get(stream).size()); unfinishedTcpStreams.get(stream).size());
} }
@@ -179,7 +179,7 @@ public class PcapWorker implements PacketListener {
UnfinishedStream stream = addNewPacket(sourceIp, destIp, time, sourcePort, destPort, ttl, content, Protocol.UDP); UnfinishedStream stream = addNewPacket(sourceIp, destIp, time, sourcePort, destPort, ttl, content, Protocol.UDP);
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("udp {} {}:{} -> {}:{}, номер пакета {}", log.debug("udp {} {}:{} -> {}:{}, packet number {}",
serviceOptional.get(), sourceIpString, sourcePort, destIpString, destPort, serviceOptional.get(), sourceIpString, sourcePort, destIpString, destPort,
unfinishedUdpStreams.get(stream).size()); unfinishedUdpStreams.get(stream).size());
} }
@@ -207,7 +207,7 @@ public class PcapWorker implements PacketListener {
final var streams = (protocol == Protocol.TCP) ? this.unfinishedTcpStreams : this.unfinishedUdpStreams; final var streams = (protocol == Protocol.TCP) ? this.unfinishedTcpStreams : this.unfinishedUdpStreams;
if (!streams.containsKey(stream)) { if (!streams.containsKey(stream)) {
log.debug("Начат новый стрим"); log.debug("New stream started");
} }
streams.put(stream, packet); streams.put(stream, packet);

View File

@@ -30,12 +30,12 @@ public class TimeoutStreamsSaver {
public void saveStreams() { public void saveStreams() {
int streamsClosed = pcapWorker.closeTimeoutStreams(Protocol.UDP, udpStreamTimeoutMillis); int streamsClosed = pcapWorker.closeTimeoutStreams(Protocol.UDP, udpStreamTimeoutMillis);
if (streamsClosed > 0) { if (streamsClosed > 0) {
log.info("Закрыто {} udp стримов", streamsClosed); log.info("{} udp streams closed", streamsClosed);
} }
streamsClosed = pcapWorker.closeTimeoutStreams(Protocol.TCP, tcpStreamTimeoutMillis); streamsClosed = pcapWorker.closeTimeoutStreams(Protocol.TCP, tcpStreamTimeoutMillis);
if (streamsClosed > 0) { if (streamsClosed > 0) {
log.info("Закрыто {} tcp стримов", streamsClosed); log.info("{} tcp streams closed", streamsClosed);
} }
} }

View File

@@ -5,15 +5,15 @@ import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import ru.serega6531.packmate.service.StreamSubscriptionService; import ru.serega6531.packmate.service.SubscriptionService;
@Component @Component
public class WebSocketHandler extends TextWebSocketHandler { public class WebSocketHandler extends TextWebSocketHandler {
private final StreamSubscriptionService subscriptionService; private final SubscriptionService subscriptionService;
@Autowired @Autowired
public WebSocketHandler(StreamSubscriptionService subscriptionService) { public WebSocketHandler(SubscriptionService subscriptionService) {
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
} }

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

@@ -1,5 +1,5 @@
package ru.serega6531.packmate.model.enums; package ru.serega6531.packmate.model.enums;
public enum SubscriptionMessageType { public enum SubscriptionMessageType {
SAVE_SERVICE, SAVE_PATTERN, DELETE_SERVICE, ENABLE_PATTERN, DISABLE_PATTERN, NEW_STREAM SAVE_SERVICE, SAVE_PATTERN, DELETE_SERVICE, ENABLE_PATTERN, DISABLE_PATTERN, NEW_STREAM, COUNTERS_UPDATE
} }

View File

@@ -0,0 +1,18 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Getter;
@Getter
public class Counter {
private int value = 0;
public void increment() {
value++;
}
public void increment(int num) {
value += num;
}
}

View File

@@ -0,0 +1,23 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Getter;
import java.util.Map;
@Getter
public class CountersHolder {
private Map<Integer, Integer> servicesPackets;
private Map<Integer, Integer> servicesStreams;
private int totalPackets;
private int totalStreams;
public CountersHolder(Map<Integer, Integer> servicesPackets, Map<Integer, Integer> servicesStreams,
int totalPackets, int totalStreams) {
this.servicesPackets = servicesPackets;
this.servicesStreams = servicesStreams;
this.totalPackets = totalPackets;
this.totalStreams = totalStreams;
}
}

View File

@@ -0,0 +1,63 @@
package ru.serega6531.packmate.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.Counter;
import ru.serega6531.packmate.model.pojo.CountersHolder;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class CountingService {
private final SubscriptionService subscriptionService;
private Map<Integer, Counter> servicesPackets = new HashMap<>();
private Map<Integer, Counter> servicesStreams = new HashMap<>();
private Counter totalPackets = new Counter();
private Counter totalStreams = new Counter();
@Autowired
public CountingService(SubscriptionService subscriptionService) {
this.subscriptionService = subscriptionService;
}
void countStream(int serviceId, int packets) {
getCounter(servicesPackets, serviceId).increment(packets);
getCounter(servicesStreams, serviceId).increment();
totalPackets.increment(packets);
totalStreams.increment();
}
@Scheduled(cron = "0 * * ? * *")
public void sendCounters() {
subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.COUNTERS_UPDATE,
new CountersHolder(
toIntegerMap(servicesPackets), toIntegerMap(servicesStreams),
totalPackets.getValue(), totalStreams.getValue())));
servicesPackets.clear();
servicesStreams.clear();
totalPackets = new Counter();
totalStreams = new Counter();
}
private Map<Integer, Integer> toIntegerMap(Map<Integer, Counter> map) {
return map.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
ent -> ent.getValue().getValue()));
}
private Counter getCounter(Map<Integer, Counter> counters, int serviceId) {
return counters.computeIfAbsent(serviceId, c -> new Counter());
}
}

View File

@@ -20,13 +20,13 @@ import java.util.stream.Collectors;
public class PatternService { public class PatternService {
private final PatternRepository repository; private final PatternRepository repository;
private final StreamSubscriptionService subscriptionService; private final SubscriptionService subscriptionService;
private final Map<Integer, Pattern> patterns = new HashMap<>(); private final Map<Integer, Pattern> patterns = new HashMap<>();
@Autowired @Autowired
public PatternService(PatternRepository repository, public PatternService(PatternRepository repository,
StreamSubscriptionService subscriptionService) { SubscriptionService subscriptionService) {
this.repository = repository; this.repository = repository;
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
@@ -56,7 +56,7 @@ public class PatternService {
pattern.setEnabled(enabled); pattern.setEnabled(enabled);
repository.save(pattern); repository.save(pattern);
if(enabled) { if (enabled) {
log.info("Включен паттерн {} со значением {}", pattern.getName(), pattern.getValue()); log.info("Включен паттерн {} со значением {}", pattern.getName(), pattern.getValue());
subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.ENABLE_PATTERN, id)); subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.ENABLE_PATTERN, id));
} else { } else {
@@ -77,7 +77,7 @@ public class PatternService {
final Pattern saved = repository.save(pattern); final Pattern saved = repository.save(pattern);
patterns.put(saved.getId(), saved); patterns.put(saved.getId(), saved);
log.info("Добавлен новый паттерн {} со значением {}", pattern.getName(), pattern.getValue()); log.info("Added new pattern {} with value {}", pattern.getName(), pattern.getValue());
subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.SAVE_PATTERN, saved)); subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.SAVE_PATTERN, saved));
return saved; return saved;
} }

View File

@@ -22,7 +22,7 @@ import java.util.Optional;
public class ServicesService { public class ServicesService {
private final ServiceRepository repository; private final ServiceRepository repository;
private final StreamSubscriptionService subscriptionService; private final SubscriptionService subscriptionService;
private final InetAddress localIp; private final InetAddress localIp;
@@ -30,7 +30,7 @@ public class ServicesService {
@Autowired @Autowired
public ServicesService(ServiceRepository repository, public ServicesService(ServiceRepository repository,
StreamSubscriptionService subscriptionService, SubscriptionService subscriptionService,
@Value("${local-ip}") String localIpString) throws UnknownHostException { @Value("${local-ip}") String localIpString) throws UnknownHostException {
this.repository = repository; this.repository = repository;
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
@@ -59,14 +59,14 @@ public class ServicesService {
} }
public void deleteByPort(int port) { public void deleteByPort(int port) {
log.info("Удален сервис на порту {}", port); log.info("Removed service at port {}", port);
services.remove(port); services.remove(port);
repository.deleteById(port); repository.deleteById(port);
subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.DELETE_SERVICE, port)); subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.DELETE_SERVICE, port));
} }
public CtfService save(CtfService service) { public CtfService save(CtfService service) {
log.info("Добавлен или изменен сервис {} на порту {}", service.getName(), service.getPort()); log.info("Added or edited service {} at port {}", service.getName(), service.getPort());
final CtfService saved = repository.save(service); final CtfService saved = repository.save(service);
services.put(saved.getPort(), saved); services.put(saved.getPort(), saved);
subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.SAVE_SERVICE, saved)); subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.SAVE_SERVICE, saved));

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/");
@@ -209,16 +217,17 @@ public class StreamOptimizer {
IOUtils.copy(gzipStream, out); IOUtils.copy(gzipStream, out);
byte[] newContent = ArrayUtils.addAll(httpHeader, out.toByteArray()); byte[] newContent = ArrayUtils.addAll(httpHeader, out.toByteArray());
log.debug("Разархивирован gzip: {} -> {} байт", gzipBytes.length, out.size()); log.debug("GZIP decompressed: {} -> {} bytes", gzipBytes.length, out.size());
return Packet.builder() return Packet.builder()
.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) {
log.warn("Не удалось разархивировать gzip, оставляем как есть", e); log.warn("Failed to decompress gzip, leaving as it is", e);
} catch (IOException e) { } catch (IOException e) {
log.error("decompress gzip", e); log.error("decompress gzip", 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

@@ -30,7 +30,8 @@ public class StreamService {
private final StreamRepository repository; private final StreamRepository repository;
private final PatternService patternService; private final PatternService patternService;
private final ServicesService servicesService; private final ServicesService servicesService;
private final StreamSubscriptionService subscriptionService; private final CountingService countingService;
private final SubscriptionService subscriptionService;
private final boolean ignoreEmptyPackets; private final boolean ignoreEmptyPackets;
@@ -40,11 +41,13 @@ public class StreamService {
public StreamService(StreamRepository repository, public StreamService(StreamRepository repository,
PatternService patternService, PatternService patternService,
ServicesService servicesService, ServicesService servicesService,
StreamSubscriptionService subscriptionService, CountingService countingService,
SubscriptionService subscriptionService,
@Value("${ignore-empty-packets}") boolean ignoreEmptyPackets) { @Value("${ignore-empty-packets}") boolean ignoreEmptyPackets) {
this.repository = repository; this.repository = repository;
this.patternService = patternService; this.patternService = patternService;
this.servicesService = servicesService; this.servicesService = servicesService;
this.countingService = countingService;
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
this.ignoreEmptyPackets = ignoreEmptyPackets; this.ignoreEmptyPackets = ignoreEmptyPackets;
} }
@@ -62,7 +65,7 @@ public class StreamService {
); );
if (serviceOptional.isEmpty()) { if (serviceOptional.isEmpty()) {
log.warn("Не удалось сохранить стрим: сервиса на порту {} или {} не существует", log.warn("Failed to save the stream: service at port {} or {} does not exist",
unfinishedStream.getFirstPort(), unfinishedStream.getSecondPort()); unfinishedStream.getFirstPort(), unfinishedStream.getSecondPort());
return false; return false;
} }
@@ -72,7 +75,7 @@ public class StreamService {
packets.removeIf(packet -> packet.getContent().length == 0); packets.removeIf(packet -> packet.getContent().length == 0);
if (packets.isEmpty()) { if (packets.isEmpty()) {
log.debug("Стрим состоит только из пустых пакетов и не будет сохранен"); log.debug("Stream consists only of empty packets and will not be saved");
return false; return false;
} }
} }
@@ -88,7 +91,9 @@ public class StreamService {
stream.setEndTimestamp(packets.get(packets.size() - 1).getTimestamp()); stream.setEndTimestamp(packets.get(packets.size() - 1).getTimestamp());
stream.setService(service.getPort()); stream.setService(service.getPort());
new StreamOptimizer(service, packets).optimizeStream(); countingService.countStream(service.getPort(), packets.size());
packets = new StreamOptimizer(service, packets).optimizeStream();
processUserAgent(packets, stream); processUserAgent(packets, stream);
Stream savedStream = save(stream); Stream savedStream = save(stream);
@@ -105,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);
@@ -149,7 +154,7 @@ public class StreamService {
Stream saved; Stream saved;
if (stream.getId() == null) { if (stream.getId() == null) {
saved = repository.save(stream); saved = repository.save(stream);
log.debug("Создан стрим с id {}", saved.getId()); log.debug("Saved stream with id {}", saved.getId());
} else { } else {
saved = repository.save(stream); saved = repository.save(stream);
} }

View File

@@ -18,25 +18,25 @@ import java.util.Objects;
@Service @Service
@Slf4j @Slf4j
public class StreamSubscriptionService { public class SubscriptionService {
private final List<WebSocketSession> subscribers = Collections.synchronizedList(new ArrayList<>()); private final List<WebSocketSession> subscribers = Collections.synchronizedList(new ArrayList<>());
private final ObjectMapper mapper; private final ObjectMapper mapper;
@Autowired @Autowired
public StreamSubscriptionService(ObjectMapper mapper) { public SubscriptionService(ObjectMapper mapper) {
this.mapper = mapper; this.mapper = mapper;
} }
public void addSubscriber(WebSocketSession session) { public void addSubscriber(WebSocketSession session) {
subscribers.add(session); subscribers.add(session);
log.info("Подписан пользователь {}", Objects.requireNonNull(session.getRemoteAddress()).getHostName()); log.info("User subscribed: {}", Objects.requireNonNull(session.getRemoteAddress()).getHostName());
} }
public void removeSubscriber(WebSocketSession session) { public void removeSubscriber(WebSocketSession session) {
subscribers.remove(session); subscribers.remove(session);
log.info("Отписан пользователь {}", Objects.requireNonNull(session.getRemoteAddress()).getHostName()); log.info("User unsubscribed: {}", Objects.requireNonNull(session.getRemoteAddress()).getHostName());
} }
void broadcast(SubscriptionMessage message) { void broadcast(SubscriptionMessage message) {

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);