30 Commits
v1.0 ... v3.2

Author SHA1 Message Date
Sergey Shkurov
fcd7918125 Update frontend to use dark theme 2023-05-01 23:23:11 +02:00
Sergey Shkurov
c88ca8abbd Update frontend 2023-05-01 21:18:26 +02:00
Sergey
15206188a2 Merge branch 'display-stream-size' into 'master'
Display stream size

See merge request packmate/Packmate!20
2023-04-30 22:22:01 +00:00
Sergey
4346445af9 Display stream size 2023-04-30 22:22:01 +00:00
Sergey
f1d67f696d Merge branch 'pattern-updates' into 'master'
Pattern updates

Closes #32

See merge request packmate/Packmate!19
2023-04-30 00:08:15 +00:00
Sergey Shkurov
4b45f7dee7 Update frontend 2023-04-30 01:50:05 +02:00
Sergey Shkurov
a8ee7363d4 Revert adding field 2023-04-29 04:51:57 +02:00
Sergey Shkurov
25d0921aed Update frontend 2023-04-29 04:40:46 +02:00
Sergey Shkurov
73fa5b1373 Add support for pattern updating 2023-04-28 04:08:16 +02:00
Sergey Shkurov
40136ad9d9 Update ServiceController endpoints 2023-04-28 03:59:01 +02:00
Sergey Shkurov
0b50f202fc Move dto transformation into services 2023-04-28 03:27:28 +02:00
Sergey Shkurov
288d24fffc Send pattern ids instead of patterns in streams 2023-04-28 02:02:28 +02:00
Sergey
40b42934b6 Merge branch 'pattern-removal' into 'master'
Implement pattern removal

Closes #29

See merge request packmate/Packmate!18
2023-04-27 23:19:16 +00:00
Sergey
4cd5e72fee Implement pattern removal 2023-04-27 23:19:16 +00:00
Sergey
145f3e63c8 Merge branch 'update-versions' into 'master'
Update versions

See merge request packmate/Packmate!17
2023-04-27 21:22:40 +00:00
Sergey Shkurov
6ea53719fd Remove DISTINCT 2023-04-27 23:19:19 +02:00
Sergey Shkurov
8bbd135e96 Refactor code 2023-04-27 22:35:03 +02:00
Sergey Shkurov
79315c3c18 Update jna dependency for MacOS 2023-04-27 22:35:02 +02:00
Sergey Shkurov
67c5462018 Fix a possible bug 2023-04-27 22:35:02 +02:00
Sergey Shkurov
4e2473a3cc Update libraries 2023-04-27 22:35:02 +02:00
Sergey Shkurov
ea45f1b9e5 Use gradle.kts 2023-04-27 22:35:02 +02:00
Sergey Shkurov
93ec39b561 Prepare to move to gradle.kts 2023-04-27 22:35:02 +02:00
Sergey Shkurov
7878ecebfc Fix hashtag symbols becoming links 2023-04-27 22:35:02 +02:00
Sergey Shkurov
7afb9dc5fb Update Spring Boot 2 2023-04-27 22:35:02 +02:00
Sergey Shkurov
8d33c6a6e1 Update gradle version 2023-04-27 22:35:02 +02:00
Sergey
1b6e619475 Merge branch 'failure-analyzer' into 'master'
Add failure analyzers

Closes #30

See merge request packmate/Packmate!16
2023-04-25 16:19:49 +00:00
Sergey Shkurov
0d756ec39c Add failure analyzer for incorrect interface name 2023-04-25 11:28:28 +02:00
Sergey Shkurov
eef33308a5 Add failure analyzer for incorrect pcap file 2023-04-24 02:20:21 +03:00
Sergey
5be73b4b61 Merge branch 'update-docs' into 'master'
Update docs

See merge request packmate/Packmate!15
2023-04-14 00:58:44 +00:00
Sergey
872e27b926 Update docs 2023-04-14 00:58:44 +00:00
58 changed files with 966 additions and 397 deletions

View File

@@ -25,7 +25,14 @@
* Расшифровывает TLS на RSA при наличии приватного ключа * Расшифровывает TLS на RSA при наличии приватного ключа
![Скриншот главного окна](screenshots/Screenshot.png) ![Скриншот главного окна](screenshots/Screenshot.png)
## Клонирование
## Быстрый запуск
Для быстрого запуска Packmate следует использовать [этот стартер](https://gitlab.com/packmate/starter/-/blob/master/README.md).
## Полный запуск
Ниже следует инструкция для тех, кто хочет собрать Packmate самостоятельно.
### Клонирование
Поскольку этот репозиторий содержит фронтенд как git submodule, его необходимо клонировать так: Поскольку этот репозиторий содержит фронтенд как git submodule, его необходимо клонировать так:
```bash ```bash
git clone --recurse-submodules https://gitlab.com/packmate/Packmate.git git clone --recurse-submodules https://gitlab.com/packmate/Packmate.git
@@ -40,102 +47,20 @@ git pull # Забираем свежую версию мастер-репы и
git submodule update --init --recursive git submodule update --init --recursive
``` ```
## Подготовка
В этом ПО используется Docker и docker-compose. В образ `packmate-app` пробрасывается
сетевой интерфейс хоста, его название указывается переменной окружения (об этом ниже).
`packmate-db` настроен на прослушивание порта 65001 с локальным IP.
Файлы БД сохраняются в ./data, поэтому для обнуления базы нужно удалить эту папку.
### Настройка ### Настройка
Программа берет основные настройки из переменных окружения, поэтому для удобства [Инструкция](docs/SETUP.md)
можно создать env-файл.
Он должен называться `.env` и лежать в корневой директории проекта.
В файле необходимо прописать:
```dotenv
# Локальный IP сервера на указанном интерфейсе или в pcap файле
PACKMATE_LOCAL_IP=192.168.1.124
# Имя пользователя для web-авторизации
PACKMATE_WEB_LOGIN=SomeUser
# Пароль для web-авторизации
PACKMATE_WEB_PASSWORD=SomeSecurePassword
```
Если мы перехватываем трафик сервера (лучший вариант, если есть возможность):
```dotenv
# Режим работы - перехват
PACKMATE_MODE=LIVE
# Интерфейс, на котором производится перехват трафика
PACKMATE_INTERFACE=wlan0
```
Если мы анализируем pcap дамп:
```dotenv
# Режим работы - анализ файла
PACKMATE_MODE=FILE
# Путь до файла от корня проекта
PACKMATE_PCAP_FILE=dump.pcap
```
Или если мы хотим посмотреть уже обработанный трафик (например, для разбора после игры):
```dotenv
PACKMATE_MODE=VIEW
```
При захвате живого трафика рекомендуется включать удаление старых стримов, иначе ближе к концу
соревнования анализатор будет медленнее работать.
```dotenv
PACKMATE_OLD_STREAMS_CLEANUP_ENABLED=true
# Интервал удаления старых стримов (в минутах).
# Лучше ставить маленькое число, чтобы стримы удалялись маленькими кусками, и это не нагружало систему
PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL=1
# Насколько старым стрим должен быть для удаления (в минутах от текущего времени)
PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```
Чтобы использовать расшифровку TLS, нужно положить соответствующий приватный ключ, который
использовался для генерации сертификата, в папку `rsa_keys`.
### Запуск ### Запуск
После указания нужных настроек в env-файле, можно запустить приложение: После указания нужных настроек в env-файле, можно запустить приложение:
```bash ```bash
sudo docker-compose up --build -d sudo docker compose up --build -d
``` ```
При успешном запуске Packmate будет видно с любого хоста на порту `65000`. При успешном запуске Packmate будет видно с любого хоста на порту `65000`.
БД будет слушать на порту 65001, но будет разрешать подключения только с localhost.
### Начало работы
При попытке зайти в web-интерфейс впервые, браузер спросит логин и пароль,
который указывался в env-файле.
При необходимости можно настроить дополнительные параметры по кнопке с шестеренками в верхнем
правом углу экрана.
![Скриншот настроек](screenshots/Screenshot_Settings.png)
Все настройки сохраняются в local storage и теряются только при смене IP-адреса или порта сервера.
## Использование ## Использование
Сначала нужно создать сервисы, находящиеся в игре. [Инструкция](docs/USAGE.md)
Для этого вызывается диалоговое окно по нажатию кнопки `+` в навбаре,
где можно указать название и порт сервиса, а также дополнительные опции.
Для удобного отлова флагов в приложении существует система паттернов.
Чтобы создать паттерн, нужно открыть выпадающее меню `Patterns` и нажать кнопку `+`,
затем указать нужный тип поиска, сам паттерн, цвет подсветки в тексте и прочее.
Если выбрать тип паттерна IGNORE, то стримы, попадающие под шаблон, автоматически будут удаляться.
Это может пригодиться, чтобы не засорять БД трафиком с эксплоитами, которые уже были запатчены.
В режиме LIVE система начнет автоматически захватывать стримы и отображать их в сайдбаре.
В режиме FILE для начала обработки файла нужно нажать соответствующую кнопку в сайдбаре.
При нажатии на стрим в главном контейнере выводится список пакетов;
между бинарным и текстовым представлением можно переключиться по кнопке в сайдбаре.
### Горячие клавиши
Для быстрой навигации по стримам можно использовать следующие горячие клавиши:
* `Ctrl+Up` -- переместиться на один стрим выше
* `Ctrl+Down` -- переместиться на один стрим ниже
* `Ctrl+Home` -- перейти на последний стрим
* `Ctrl+End` -- перейти на первый стрим
<div align="right"> <div align="right">

View File

@@ -25,7 +25,14 @@ Advanced network traffic flow analyzer for A/D CTFs.
* Can automatically decrypt TLS with RSA using given private key (like Wireshark) * Can automatically decrypt TLS with RSA using given private key (like Wireshark)
![Main window](screenshots/Screenshot.png) ![Main window](screenshots/Screenshot.png)
## Cloning
## Quick Start
To quickly start using Packmate, use [this starter](https://gitlab.com/packmate/starter/-/blob/master/README_EN.md).
## Full Build
Below are the instructions for those who want to build Packmate on their own.
### Cloning
As this repository contains frontend part as a 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/packmate/Packmate.git git clone --recurse-submodules https://gitlab.com/packmate/Packmate.git
@@ -40,54 +47,8 @@ git pull
git submodule update --init --recursive git submodule update --init --recursive
``` ```
## Preparation ### Setup
This program uses Docker and docker-compose. [Instructions](docs/SETUP_EN.md)
`packmate-db` will listen to port 65001 at localhost.
Database files are saved in ./data, so in order to reset database you'll have to delete that directory.
### Settings
This program retrieves settings from environment variables,
so it would be convenient to create an env file;
It must be called `.env` and located at the root of the project.
Contents of the file:
```bash
# Local IP on network interface or in pcap file to tell incoming packets from outgoing
PACKMATE_LOCAL_IP=192.168.1.124
# Username for the web interface
PACKMATE_WEB_LOGIN=SomeUser
# Password for the web interface
PACKMATE_WEB_PASSWORD=SomeSecurePassword
```
If we are capturing live traffic (best option if possible):
```bash
# Mode: capturing
PACKMATE_MODE=LIVE
# Interface to capture on
PACKMATE_INTERFACE=wlan0
```
If we are analyzing pcap dump:
```bash
# Mode: dump analyzing
PACKMATE_MODE=FILE
# Path to pcap file from project root
PACKMATE_PCAP_FILE=dump.pcap
```
When capturing live traffic it's better to turn on old streams removal. Otherwise, after some time Packmate
will start working slower.
```dotenv
PACKMATE_OLD_STREAMS_CLEANUP_ENABLED=true
# Old streams removal interval (in minutes).
# It's better to use small numbers so the streams are removed in small chunks and don't overload the server.
PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL=1
# How old the stream must be to be removed (in minutes before current time)
PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```
To decrypt TLS, put the private key used to generate a certificate into the `rsa_keys` folder.
### Launch ### Launch
After filling in env file you can launch the app: After filling in env file you can launch the app:
@@ -95,42 +56,11 @@ After filling in env file you can launch the app:
sudo docker-compose up --build -d 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.
Database with listen on port 65001, but will only accept connections from localhost.
### Accessing the web interface
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.
After entering the credentials, open the settings by clicking the cogs
in the top right corner and modify additional parameters.
![Settings](screenshots/Screenshot_Settings.png)
All settings are saved in the local storage and will be
lost only upon changing server IP or port.
## Usage ## Usage
First of all, you should create game services. [Instructions](docs/USAGE_EN.md)
To do that, click `+` in the navbar,
then fill in the service name, port, and optimizations to perform on streams.
For a simple monitoring of flags, there is a system of patterns.
To create a pattern, open `Patterns` dropdown menu, press `+`, then
specify the type of pattern, the pattern itself, highlight color and other things.
If you choose IGNORE as the type of a pattern, all matching streams will be automatically deleted.
This can be useful to filter out exploits you have already patched against.
In LIVE mode the system will automatically capture streams and show them in a sidebar.
In FILE mode you'll have to press appropriate button in a sidebar to start processing a file.
Note that you should only do that after all services are created.
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.
### Shortcuts
To quickly navigate streams you can use the following shortcuts:
* `Ctrl+Up` -- go to the next stream
* `Ctrl+Down` -- go to the previous stream
* `Ctrl+Home` -- go to the latest stream
* `Ctrl+End` -- go to the first stream
<div align="right"> <div align="right">

View File

@@ -1,50 +0,0 @@
plugins {
id 'org.springframework.boot' version '2.6.3'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'ru.serega6531'
version = '1.0-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.boot:spring-boot-starter-websocket"
implementation 'org.springframework.session:spring-session-core'
implementation 'com.github.jmnarloch:modelmapper-spring-boot-starter:1.1.0'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'
implementation 'org.pcap4j:pcap4j-core:1.8.2'
implementation 'org.pcap4j:pcap4j-packetfactory-static:1.8.2'
implementation group: 'com.google.guava', name: 'guava', version: '31.0.1-jre'
implementation group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.1'
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69'
implementation group: 'org.bouncycastle', name: 'bctls-jdk15on', version: '1.70'
implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.4.5'
compileOnly 'org.jetbrains:annotations:22.0.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
}
test {
useJUnitPlatform()
}

59
build.gradle.kts Normal file
View File

@@ -0,0 +1,59 @@
plugins {
id("org.springframework.boot") version "3.0.6"
id("java")
id("io.spring.dependency-management") version "1.1.0"
}
group = "ru.serega6531"
version = "1.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
configurations {
get("compileOnly").apply {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.springframework.session:spring-session-core")
implementation(group = "org.apache.commons", name = "commons-lang3", version = "3.12.0")
implementation(group = "commons-io", name = "commons-io", version = "2.11.0")
implementation("org.pcap4j:pcap4j-core:1.8.2")
implementation("org.pcap4j:pcap4j-packetfactory-static:1.8.2")
constraints {
implementation("net.java.dev.jna:jna:5.13.0") {
because("upgraded version required to run on MacOS")
// https://stackoverflow.com/questions/70368863/unsatisfiedlinkerror-for-m1-macs-while-running-play-server-locally
}
}
implementation(group = "com.google.guava", name = "guava", version = "31.1-jre")
implementation(group = "org.java-websocket", name = "Java-WebSocket", version = "1.5.3")
implementation(group = "org.bouncycastle", name = "bcprov-jdk15on", version = "1.70")
implementation(group = "org.bouncycastle", name = "bctls-jdk15on", version = "1.70")
implementation(group = "org.modelmapper", name = "modelmapper", version = "3.1.1")
compileOnly("org.jetbrains:annotations:24.0.1")
compileOnly("org.projectlombok:lombok")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("org.postgresql:postgresql")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
tasks.getByName<Test>("test") {
useJUnitPlatform()
}

View File

@@ -2,7 +2,6 @@ services:
packmate: # port = 65000 packmate: # port = 65000
environment: environment:
DB_PASSWORD: ${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb} DB_PASSWORD: ${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb}
DB_NAME: ${PACKMATE_DB_NAME:-packmate}
INTERFACE: ${PACKMATE_INTERFACE:-} INTERFACE: ${PACKMATE_INTERFACE:-}
LOCAL_IP: ${PACKMATE_LOCAL_IP} LOCAL_IP: ${PACKMATE_LOCAL_IP}
MODE: ${PACKMATE_MODE:-LIVE} MODE: ${PACKMATE_MODE:-LIVE}
@@ -20,16 +19,9 @@ services:
dockerfile: docker/Dockerfile_app dockerfile: docker/Dockerfile_app
network_mode: "host" network_mode: "host"
image: registry.gitlab.com/packmate/packmate:${BUILD_TAG:-latest} image: registry.gitlab.com/packmate/packmate:${BUILD_TAG:-latest}
command: [ volumes:
"java", "-Djava.net.preferIPv4Stack=true", "-Djava.net.preferIPv4Addresses=true", - "./pcaps/:/app/pcaps/:ro"
"-jar", "/app/app.jar", "--spring.datasource.url=jdbc:postgresql://127.0.0.1:65001/$${DB_NAME}", - "./rsa_keys/:/app/rsa_keys/:ro"
"--spring.datasource.password=$${DB_PASSWORD}",
"--capture-mode=$${MODE}", "--pcap-file=$${PCAP_FILE}",
"--interface-name=$${INTERFACE}", "--local-ip=$${LOCAL_IP}", "--account-login=$${WEB_LOGIN}",
"--old-streams-cleanup-enabled=$${OLD_STREAMS_CLEANUP_ENABLED}", "--cleanup-interval=$${OLD_STREAMS_CLEANUP_INTERVAL}",
"--old-streams-threshold=$${OLD_STREAMS_CLEANUP_THRESHOLD}",
"--account-password=$${WEB_PASSWORD}", "--server.port=65000", "--server.address=0.0.0.0"
]
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@@ -38,7 +30,7 @@ services:
environment: environment:
POSTGRES_USER: packmate POSTGRES_USER: packmate
POSTGRES_PASSWORD: ${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb} POSTGRES_PASSWORD: ${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb}
POSTGRES_DB: ${PACKMATE_DB_NAME:-packmate} POSTGRES_DB: packmate
env_file: env_file:
- .env - .env
volumes: volumes:

View File

@@ -13,4 +13,15 @@ FROM eclipse-temurin:17-jre
WORKDIR /app WORKDIR /app
RUN apt update && apt install -y libpcap0.8 && rm -rf /var/lib/apt/lists/* RUN apt update && apt install -y libpcap0.8 && rm -rf /var/lib/apt/lists/*
COPY --from=1 /tmp/compile/build/libs/packmate-*-SNAPSHOT.jar app.jar COPY --from=1 /tmp/compile/build/libs/packmate-*-SNAPSHOT.jar app.jar
CMD [ "java", "-Djava.net.preferIPv4Stack=true", "-Djava.net.preferIPv4Addresses=true", \
"-jar", "/app/app.jar", "--spring.datasource.url=jdbc:postgresql://127.0.0.1:65001/packmate", \
"--spring.datasource.password=${DB_PASSWORD}", \
"--capture-mode=${MODE}", "--pcap-file=${PCAP_FILE}", \
"--interface-name=${INTERFACE}", "--local-ip=${LOCAL_IP}", "--account-login=${WEB_LOGIN}", \
"--old-streams-cleanup-enabled=${OLD_STREAMS_CLEANUP_ENABLED}", "--cleanup-interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \
"--old-streams-threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \
"--account-password=${WEB_PASSWORD}", "--server.port=65000", "--server.address=0.0.0.0" \
]
EXPOSE 65000 EXPOSE 65000

89
docs/SETUP.md Normal file
View File

@@ -0,0 +1,89 @@
## Настройка
Packmate использует настройки из файла `.env` (в той же папке, что и `docker-compose.yml`)
### Основные настройки
```dotenv
# Локальный IP сервера, на который приходит игровой трафик
PACKMATE_LOCAL_IP=10.20.1.1
# Имя пользователя для web-авторизации
PACKMATE_WEB_LOGIN=SomeUser
# Пароль для web-авторизации
PACKMATE_WEB_PASSWORD=SomeSecurePassword
```
### Режим работы
Packmate поддерживает три основных режима работы: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - это основной режим работы во время CTF. Packmate обрабатывает живой трафик и сразу выводит результаты.
2. `FILE` - обрабатывает трафик из pcap файлов. Полезен для анализа трафика с прошедших CTF, где не был запущен Packmate, или тех, где невозможно запустить его на вулнбоксе.
3. `VIEW` - Packmate не обрабатывает трафик, а только показывает уже обработанные стримы. Полезен для разборов после завершения CTF.
<details>
<summary>Настройка LIVE</summary>
Необходимо указать интерфейс, через который проходит игровой трафик.
На этом же интерфейсе должен располагаться ip, указанный в параметре `PACKMATE_LOCAL_IP`
```dotenv
# Режим работы - перехват
PACKMATE_MODE=LIVE
# Интерфейс, на котором производится перехват трафика
PACKMATE_INTERFACE=game
```
</details>
<details>
<summary>Настройка FILE</summary>
Необходимо указать название pcap файла, лежащего в папке pcaps.
После запуска в веб-интерфейсе появится кнопка, активирующая чтение файла.
Важно, чтобы к этому моменту уже были созданы сервисы и паттерны (см. раздел Использование).
```dotenv
# Режим работы - анализ файла
PACKMATE_MODE=FILE
# Название файла в папке pcaps
PACKMATE_PCAP_FILE=dump.pcap
```
</details>
<details>
<summary>Настройка VIEW</summary>
В этом режиме Packmate просто показывает уже имеющиеся данные.
```dotenv
# Режим работы - просмотр
PACKMATE_MODE=VIEW
```
</details>
### Очистка БД
На крупных CTF через какое-то время накапливается большое количество трафика. Это замедляет работу Packmate и занимает много места на диске.
Для оптимизации работы, рекомендуется включить регулярную очистку БД от старых стримов. Это будет работать только в режиме `LIVE`.
```dotenv
PACKMATE_OLD_STREAMS_CLEANUP_ENABLED=true
# Интервал удаления старых стримов (в минутах).
# Лучше ставить маленькое число, чтобы стримы удалялись маленькими кусками, и это не нагружало систему
PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL=1
# Насколько старым стрим должен быть для удаления (в минутах от текущего времени)
PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```
### Дополнительные настройки
```dotenv
# Пароль от БД. Из-за того, что БД принимает подключения только с localhost, менять его необязательно, но можно, для дополнительной безопасности.
PACKMATE_DB_PASSWORD=K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb
# Версия Packmate. Можно изменить, если нужно использовать другой образ из docker registry.
BUILD_TAG=latest
```
Чтобы использовать расшифровку TLS (с RSA), нужно положить соответствующий приватный ключ, который
использовался для генерации сертификата, в папку `rsa_keys`.
Файлы БД сохраняются в ./data, поэтому для обнуления базы нужно удалить эту папку.

88
docs/SETUP_EN.md Normal file
View File

@@ -0,0 +1,88 @@
## Setup
Packmate uses properties from the `.env` file (in the same directory as `docker-compose.yml`)
### Primary settings
```dotenv
# Local IP of a server on which the traffic in directed. Used to tell incoming packets from outgoing.
PACKMATE_LOCAL_IP=10.20.1.1
# Username for the web interface
PACKMATE_WEB_LOGIN=SomeUser
# Password for the web interface
PACKMATE_WEB_PASSWORD=SomeSecurePassword
```
### Modes of operation
Packmate supports 3 modes of operation: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - the usual mode during a CTF. Packmate processes live traffic and instantly displays the results.
2. `FILE` - processes traffic from pcap files. Useful to analyze traffic from past CTFs where Packmate wasn't launched, or CTFs where it's impossible to use it on the vulnbox.
3. `VIEW` - Packmate does not process any traffic, but simply shows already processed streams. Useful for post-game analyses.
<details>
<summary>LIVE setup</summary>
Set the interface through which the game traffic passes.
IP address from `PACKMATE_LOCAL_IP` should be bound to the same interface.
```dotenv
# Mode: capturing
PACKMATE_MODE=LIVE
# Interface to capture on
PACKMATE_INTERFACE=game
```
</details>
<details>
<summary>FILE setup</summary>
Set the name of the pcap file in the `pcaps` directory.
After the startup, in the web interface, you will see the button that activates the file processing.
It's important that by this moment all services and patterns are already created (see Usage).
```dotenv
# Mode: pcap file anysis
PACKMATE_MODE=FILE
# Path to pcap file in the pcaps directory
PACKMATE_PCAP_FILE=dump.pcap
```
</details>
<details>
<summary>VIEW setup</summary>
In that mode, Packmate simply shows already existing data.
```dotenv
# Mode: viewing the data
PACKMATE_MODE=VIEW
```
</details>
### Database cleanup
On large CTFsб after some time a lot of traffic will pile up. This can slow Packmate down and take a lot of drive space.
To optimize the workflow, it is recommended to enable periodical database cleanup of old streams. It will only work in the `LIVE` mode.
```dotenv
PACKMATE_OLD_STREAMS_CLEANUP_ENABLED=true
# Old streams removal interval (in minutes).
# It's better to use small numbers so the streams are removed in small chunks and don't overload the server.
PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL=1
# How old the stream must be to be removed (in minutes before current time)
PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```
### Additional settings
```dotenv
# Database password. Considering it only listens on localhost, it's not mandatory to change it, but you can do it for additional security.
PACKMATE_DB_PASSWORD=K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb
# Packmate version. Change it if you want to use a different version from the docker registry.
BUILD_TAG=latest
```
To use the TLS decryption, you have to put the matching private key in the `rsa_keys` directory.
Database files are being saved in `./data`, so to reset the database, you need to delete this directory.

117
docs/USAGE.md Normal file
View File

@@ -0,0 +1,117 @@
## Использование
### Настройки
При попытке зайти в web-интерфейс впервые, браузер спросит логин и пароль,
который указывался в env-файле.
При необходимости можно настроить дополнительные параметры по кнопке с шестеренками в верхнем
правом углу экрана.
<img alt="Скриншот настроек" width="400" src="../screenshots/Screenshot_Settings.png"/>
### Создание сервисов
Сначала нужно создать сервисы, находящиеся в игре. Если не сделать этого, то никакие стримы не будут сохраняться!
Для этого вызывается диалоговое окно по нажатию кнопки `+` в навбаре,
где можно указать название и порт сервиса, а также дополнительные опции.
<img alt="Скриншот окна создания сервиса" src="../screenshots/Screenshot_Service.png" width="600"/>
#### Параметры сервиса:
1. Имя
2. Порт (если сервис использует несколько портов, нужно создать по сервису на каждый порт)
3. Chunked transfer encoding: автоматически раскодировать [подобные](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#chunked_encoding) http пакеты
4. Urldecode: автоматически проводить urldecode пакетов. Стоит включать по умолчанию в http сервисах.
5. Merge adjacent packets: автоматически склеивать соседние пакеты в одном направлении. Стоит включать по умолчанию в небинарных сервисах.
6. Inflate WebSockets: автоматически разархивировать [сжатые](https://www.rfc-editor.org/rfc/rfc7692) websocket-пакеты.
7. Decrypt TLS: автоматически расшифровывать TLS-трафик (HTTPS).
Работает только с типами шифрования TLS_RSA_WITH_AES_*, и при наличии приватного ключа, который использовался в сертификате сервера (как Wireshark).
### Создание паттернов
Для удобного отлова эксплоитов в приложении существует система паттернов.
Чтобы создать паттерн, нужно открыть выпадающее меню `Patterns` и нажать кнопку `+`,
затем указать параметры паттерна и сохранить.
Важно: паттерн начнет действовать только на стримы, захваченные после его создания. Но можно использовать Lookback, чтобы проанализировать и стримы в прошлом.
<img alt="Скриншот окна создания паттерна" src="../screenshots/Screenshot_Pattern.png" width="400"/>
#### Параметры паттерна:
1. Имя: оно отображается в списке на стримах, которые содержат этот паттерн.
2. Паттерн: само содержимое паттерна. Может быть строкой, регулярным выражением или hex-строкой в зависимости от типа паттерна.
3. Действие паттерна
1. Highlight подсветит найденный паттерн. Пример: поиск флагов.
2. Ignore удалит стрим, содержащий этот паттерн.
Пример: вы запатчили сервис от определенной уязвимости и больше не хотите видеть определенный эксплоит в трафике. Можно добавить этот эксплоит как паттерн с типом IGNORE, и он больше не будет сохраняться.
4. Цвет: этим цветом будут подсвечиваться паттерны с типом Highlight
5. Метод поиска: подстрока, регулярное выражение, бинарная подстрока
6. Направление поиска: везде, только в запросах, только в ответах
7. Сервис: искать в трафике всех сервисов или в каком-то конкретном
### Начало игры
В режиме LIVE система начнет автоматически захватывать стримы и отображать их в сайдбаре.
В режиме FILE для начала обработки файла нужно нажать соответствующую кнопку в сайдбаре.
При нажатии на стрим в главном окне выводится список пакетов;
между бинарным и текстовым представлением можно переключиться по кнопке в сайдбаре.
### Обзор навбара
![Скриншот навбара](../screenshots/Screenshot_Navbar.png)
1. Заголовок
2. Счетчик SPM - Streams Per Minute, стримов в минуту
3. Счетчик PPS - Packets Per Stream, среднее количество пакетов в стриме
4. Кнопка открытия списка паттернов
5. Список сервисов. В каждом сервисе:
1. Название
2. Порт
3. Счетчик SPM сервиса - позволяет определить наиболее популярные сервисы
4. Кнопка редактирования сервиса
6. Кнопка добавления нового сервиса
7. Кнопка открытия настроек
### Обзор сайдбара
![Скриншот сайдбара](../screenshots/Screenshot_Sidebar.png)
В левой панели Packmate находятся стримы выбранного сервиса.
Отображается номер стрима, протокол, TTL, сервис, время, хэш User-Agent (для http сервисов) и найденные паттерны.
Совет: иногда на CTF забывают перезаписать TTL пакетов внутри сети. В таком случае по TTL можно отличить запросы от чекеров и от других команд.
Совет #&#8203;2: по User-Agent можно отличать запросы из разных источников. К примеру, можно предположить, что на скриншоте выше запросы 4 и 5 пришли из разных источников.
Совет #&#8203;3: нажимайте на звездочку, чтобы добавить интересный стрим в избранное. Этот стрим будет выделен в списке, и появится в списке избранных стримов.
#### Управление просмотром
<img alt="Панель управления" src="../screenshots/Screenshot_Sidebar_header.png" width="400"/>
1. Пауза: Остановить/возобновить показ новых стримов на экране. Не останавливает перехват стримов и показ другим пользователям! Полезно, если стримы летят слишком быстро.
2. Избранные: показать только стримы, отмеченные как избранные
3. Переключить текстовый/hexdump вид
4. Начать анализ: появляется только при запуске в режиме `FILE`
5. Промотать список стримов до самого нового
### Обзор меню паттернов
![Список паттернов](../screenshots/Screenshot_Patterns.png)
1. Кнопка добавления паттерна
2. Выбор всех стримов (убрать фильтрацию по паттерну)
3. Список паттернов. В каждой строчке:
1. Описание паттерна
2. Кнопка Lookback - позволяет применить паттерн к стримам, обработанным до создания паттерна.
3. Пауза - паттерн нельзя удалить, но можно поставить на паузу. После этого он не будет применяться к новым стримам.
Совет: создавайте отдельные паттерны для входящих и исходящих флагов. Так легче отличать чекер, кладущий флаги, от эксплоитов.
Совет #&#8203;2: используйте Lookback для исследования найденных эксплоитов.
Пример: вы обнаружили, что сервис только что отдал флаг пользователю `abc123` без видимых причин.
Можно предположить, что атакующая команда создала этого пользователя и подготовила эксплоит в другом стриме.
Но в игре слишком много трафика, чтобы найти этот стрим вручную.
Тогда можно создать `SUBSTRING` паттерн со значением `abc123` и активировать Lookback на несколько минут назад.
После этого со включенным фильтром по паттерну будут отображаться только стримы, где упоминался этот пользователь.
### Горячие клавиши
Для быстрой навигации по стримам можно использовать следующие горячие клавиши:
* `Ctrl+Up` -- переместиться на один стрим выше
* `Ctrl+Down` -- переместиться на один стрим ниже
* `Ctrl+Home` -- перейти на последний стрим
* `Ctrl+End` -- перейти на первый стрим

110
docs/USAGE_EN.md Normal file
View File

@@ -0,0 +1,110 @@
## Usage
### Settings
When attempting to access the web interface for the first time, your browser will prompt for a login and password, which were specified in the env file.
If necessary, additional parameters can be configured via the gear icon in the top right corner of the screen.
<img alt="Screenshot of settings" src="../screenshots/Screenshot_Settings.png" width="400"/>
### Creating Services
First, you need to create services that are present in the game. If you don't do this, no streams will be saved!
To do this, a dialog box is called by clicking the `+` button in the navbar,
where you can specify the name and port of the service, as well as additional options.
<img alt="Screenshot of service creation window" src="../screenshots/Screenshot_Service.png" width="600"/>
#### Service Parameters:
1. Name
2. Port (if the service uses multiple ports, you need to create a Packmate service for each port)
3. Chunked transfer encoding: automatically decode [chunked](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#chunked_encoding) HTTP packets
4. Urldecode: automatically perform URL decoding of packets. Should be enabled by default for HTTP services.
5. Merge adjacent packets: automatically merge adjacent packets in the same direction. Should be enabled by default for non-binary services.
6. Inflate WebSockets: automatically decompress [compressed](https://www.rfc-editor.org/rfc/rfc7692) WebSocket packets.
7. Decrypt TLS: automatically decrypt TLS traffic (HTTPS). Only works with TLS_RSA_WITH_AES_* cipher suites and requires the private key used in the server's certificate (just like Wireshark).
### Creating Patterns
To conveniently capture exploits in the application, a pattern system exists.
To create a pattern, open the dropdown menu `Patterns` and click the `+` button,
then specify the pattern parameters and save.
Important: the pattern will only apply to streams captured after its creation. But you can use Lookback to analyze past streams.
<img alt="Screenshot of pattern creation window" src="../screenshots/Screenshot_Pattern.png" width="400"/>
#### Pattern Parameters:
1. Name: it will be displayed in the list on streams that contain this pattern.
2. Pattern: the content of the pattern itself. It can be a string, a regular expression, or a hex string depending on the pattern type.
3. Pattern ation:
1. Highlight will highlight the found pattern. Example: searching for flags.
2. Ignore will delete the stream containing this pattern.
Example: you patched a service from a certain vulnerability and no longer want to see a specific exploit in the traffic. You can add this exploit as a pattern with IGNORE type, and it will no longer be saved.
4. Color: the color with which patterns of Highlight type will be highlighted.
5. Search method: substring, regular expression, binary substring
6. Search type: everywhere, only in requests, only in responses
7. Service: search in the traffic of all services or in a specific one.
### Game Start
In LIVE mode, the system will automatically capture streams and display them in the sidebar.
In FILE mode, click the corresponding button in the sidebar to start processing a file.
When you click on a stream in the main window, a list of packets is displayed;
you can switch between binary and text representation using the button in the sidebar.
### Navbar Overview
![Navbar screenshot](../screenshots/Screenshot_Navbar.png)
1. Title
2. SPM counter - Streams Per Minute
3. PPS counter - (average number of) Packets Per Stream
4. Button to open the list of patterns
5. List of services. In each service:
1. Name
2. Port
3. SPM counter for the service - allows you to determine the most popular services
4. Service edit button
6. Button to add a new service
7. Button to open settings
### Sidebar Overview
![Sidebar screenshot](../screenshots/Screenshot_Sidebar.png)
Tip: Sometimes during CTFs, admins forget to overwrite the TTL of packets inside the network. In such cases, you can differentiate requests from checkers and other teams based on TTL.
Tip #&#8203;2: User-Agent can be used to differentiate requests from different sources. For example, in the screenshot above, requests 4 and 5 may have come from different sources.
Tip #&#8203;3: Click on the star icon to add an interesting stream to your favorites. This stream will be highlighted in the list and will appear in the list of favorite streams.
#### Control Panel
<img alt="Control Panel" src="../screenshots/Screenshot_Sidebar_header.png" width="400"/>
1. Pause: Stop/resume displaying new streams on the screen. It does not stop intercepting streams or showing them to other users! Useful if streams are flying by too quickly.
2. Favorites: Show only streams marked as favorites.
3. Switch text/hexdump view.
4. Start analysis: Only appears when running in `FILE` mode.
5. Scroll stream list to the newest.
### Pattern Menu Overview
![Pattern List](../screenshots/Screenshot_Patterns.png)
1. Add Pattern Button
2. Select All Streams (do not filter by pattern)
3. Pattern List. Each line contains:
1. Pattern Description
2. Lookback Button - applies the pattern to streams processed before the pattern creation.
3. Pause - pattern cannot be deleted, but can be paused. It will not be applied to new streams after pausing.
Tip: Create separate patterns for incoming and outgoing flags to easily distinguish between flag checkers and exploits.
Tip #&#8203;2: Use Lookback to investigate discovered exploits.
Example: You found that the service just handed out a flag to user `abc123` without an apparent reason.
You can assume that the attacking team created this user and prepared an exploit in another stream.
But there is too much traffic in the game to manually find this stream.
Then you can create a `SUBSTRING` pattern with the value `abc123` and activate Lookback for a few minutes back.
After that, with the pattern filter enabled, only streams mentioning this user will be displayed.
### Hotkeys
Use the following hotkeys for quick navigation through streams:
* `Ctrl+Up` -- Move one stream up.
* `Ctrl+Down` -- Move one stream down.
* `Ctrl+Home` -- Go to the last stream.
* `Ctrl+End` -- Go to the first stream.

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-7.3.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

BIN
pcaps/dump.pcap Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -3,4 +3,4 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
} }
} }
rootProject.name = 'packmate' rootProject.name = "packmate"

View File

@@ -1,5 +1,8 @@
package ru.serega6531.packmate.configuration; package ru.serega6531.packmate.configuration;
import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import org.modelmapper.TypeMap;
import org.pcap4j.core.PcapNativeException; import org.pcap4j.core.PcapNativeException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -7,9 +10,10 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import ru.serega6531.packmate.model.Pattern;
import org.springframework.security.crypto.password.PasswordEncoder; import ru.serega6531.packmate.model.Stream;
import ru.serega6531.packmate.model.enums.CaptureMode; import ru.serega6531.packmate.model.enums.CaptureMode;
import ru.serega6531.packmate.model.pojo.StreamDto;
import ru.serega6531.packmate.pcap.FilePcapWorker; import ru.serega6531.packmate.pcap.FilePcapWorker;
import ru.serega6531.packmate.pcap.LivePcapWorker; import ru.serega6531.packmate.pcap.LivePcapWorker;
import ru.serega6531.packmate.pcap.NoOpPcapWorker; import ru.serega6531.packmate.pcap.NoOpPcapWorker;
@@ -19,6 +23,8 @@ import ru.serega6531.packmate.service.StreamService;
import ru.serega6531.packmate.service.SubscriptionService; import ru.serega6531.packmate.service.SubscriptionService;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Set;
import java.util.stream.Collectors;
@Configuration @Configuration
@EnableScheduling @EnableScheduling
@@ -36,14 +42,33 @@ public class ApplicationConfiguration {
@Value("${capture-mode}") CaptureMode captureMode) throws PcapNativeException, UnknownHostException { @Value("${capture-mode}") CaptureMode captureMode) throws PcapNativeException, UnknownHostException {
return switch (captureMode) { return switch (captureMode) {
case LIVE -> new LivePcapWorker(servicesService, streamService, localIpString, interfaceName); case LIVE -> new LivePcapWorker(servicesService, streamService, localIpString, interfaceName);
case FILE -> new FilePcapWorker(servicesService, streamService, subscriptionService, localIpString, filename); case FILE ->
new FilePcapWorker(servicesService, streamService, subscriptionService, localIpString, filename);
case VIEW -> new NoOpPcapWorker(); case VIEW -> new NoOpPcapWorker();
}; };
} }
@Bean @Bean
public PasswordEncoder passwordEncoder() { public ModelMapper modelMapper() {
return new BCryptPasswordEncoder(); ModelMapper modelMapper = new ModelMapper();
addStreamMapper(modelMapper);
return modelMapper;
}
private void addStreamMapper(ModelMapper modelMapper) {
TypeMap<Stream, StreamDto> streamMapper = modelMapper.createTypeMap(Stream.class, StreamDto.class);
Converter<Set<Pattern>, Set<Integer>> patternSetToIdSet = ctx -> ctx.getSource()
.stream()
.map(Pattern::getId)
.collect(Collectors.toSet());
streamMapper.addMappings(mapping ->
mapping.using(patternSetToIdSet)
.map(Stream::getFoundPatterns, StreamDto::setFoundPatternsIds)
);
} }
} }

View File

@@ -1,21 +1,24 @@
package ru.serega6531.packmate.configuration; package ru.serega6531.packmate.configuration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@Slf4j @Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { public class SecurityConfiguration {
@Value("${account-login}") @Value("${account-login}")
private String login; private String login;
@@ -23,35 +26,39 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Value("${account-password}") @Value("${account-password}")
private String password; private String password;
private final PasswordEncoder passwordEncoder; @Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
@Autowired UserDetails user = User.builder()
public SecurityConfiguration(PasswordEncoder passwordEncoder) { .username(login)
this.passwordEncoder = passwordEncoder;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(login)
.password(passwordEncoder.encode(password)) .password(passwordEncoder.encode(password))
.authorities("ROLE_USER"); .roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
} }
@Override @Bean
protected void configure(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf() return http.csrf()
.disable() .disable()
.authorizeRequests() .authorizeHttpRequests((auth) ->
.antMatchers("/site.webmanifest") auth.requestMatchers("/site.webmanifest")
.permitAll() .permitAll()
.anyRequest().authenticated() .anyRequest()
.and() .authenticated()
)
.httpBasic() .httpBasic()
.and() .and()
.headers() .headers()
.frameOptions() .frameOptions()
.sameOrigin(); .sameOrigin()
.and()
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
} }
@EventListener @EventListener

View File

@@ -1,14 +1,16 @@
package ru.serega6531.packmate.controller; package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.PathVariable;
import ru.serega6531.packmate.model.Packet; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.PacketDto; import ru.serega6531.packmate.model.pojo.PacketDto;
import ru.serega6531.packmate.model.pojo.PacketPagination; import ru.serega6531.packmate.model.pojo.PacketPagination;
import ru.serega6531.packmate.service.StreamService; import ru.serega6531.packmate.service.StreamService;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/packet/") @RequestMapping("/api/packet/")
@@ -23,10 +25,7 @@ public class PacketController {
@PostMapping("/{streamId}") @PostMapping("/{streamId}")
public List<PacketDto> getPacketsForStream(@PathVariable long streamId, @RequestBody PacketPagination pagination) { public List<PacketDto> getPacketsForStream(@PathVariable long streamId, @RequestBody PacketPagination pagination) {
List<Packet> packets = streamService.getPackets(streamId, pagination.getStartingFrom(), pagination.getPageSize()); return streamService.getPackets(streamId, pagination.getStartingFrom(), pagination.getPageSize());
return packets.stream()
.map(streamService::packetToDto)
.collect(Collectors.toList());
} }
} }

View File

@@ -1,13 +1,20 @@
package ru.serega6531.packmate.controller; package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.DeleteMapping;
import ru.serega6531.packmate.model.Pattern; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.PatternCreateDto;
import ru.serega6531.packmate.model.pojo.PatternDto; import ru.serega6531.packmate.model.pojo.PatternDto;
import ru.serega6531.packmate.model.pojo.PatternUpdateDto;
import ru.serega6531.packmate.service.PatternService; import ru.serega6531.packmate.service.PatternService;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/pattern/") @RequestMapping("/api/pattern/")
@@ -24,14 +31,19 @@ public class PatternController {
public List<PatternDto> getPatterns() { public List<PatternDto> getPatterns() {
return service.findAll() return service.findAll()
.stream().map(service::toDto) .stream().map(service::toDto)
.collect(Collectors.toList()); .toList();
} }
@PostMapping("/{id}") @PostMapping("/{id}/enable")
public void enable(@PathVariable int id, @RequestParam boolean enabled) { public void enable(@PathVariable int id, @RequestParam boolean enabled) {
service.enable(id, enabled); service.enable(id, enabled);
} }
@DeleteMapping("/{id}")
public void delete(@PathVariable int id) {
service.delete(id);
}
@PostMapping("/{id}/lookback") @PostMapping("/{id}/lookback")
public void lookBack(@PathVariable int id, @RequestBody int minutes) { public void lookBack(@PathVariable int id, @RequestBody int minutes) {
if (minutes < 1) { if (minutes < 1) {
@@ -42,11 +54,13 @@ public class PatternController {
} }
@PostMapping @PostMapping
public PatternDto addPattern(@RequestBody PatternDto dto) { public PatternDto addPattern(@RequestBody PatternCreateDto dto) {
dto.setEnabled(true); return service.create(dto);
Pattern pattern = service.fromDto(dto); }
Pattern saved = service.save(pattern);
return service.toDto(saved); @PostMapping("/{id}")
public PatternDto updatePattern(@PathVariable int id, @RequestBody PatternUpdateDto dto) {
return service.update(id, dto);
} }
} }

View File

@@ -1,13 +1,19 @@
package ru.serega6531.packmate.controller; package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.DeleteMapping;
import ru.serega6531.packmate.model.CtfService; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.ServiceCreateDto;
import ru.serega6531.packmate.model.pojo.ServiceDto; import ru.serega6531.packmate.model.pojo.ServiceDto;
import ru.serega6531.packmate.model.pojo.ServiceUpdateDto;
import ru.serega6531.packmate.service.ServicesService; import ru.serega6531.packmate.service.ServicesService;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/service/") @RequestMapping("/api/service/")
@@ -22,9 +28,7 @@ public class ServiceController {
@GetMapping @GetMapping
public List<ServiceDto> getServices() { public List<ServiceDto> getServices() {
return service.findAll().stream() return service.findAll();
.map(service::toDto)
.collect(Collectors.toList());
} }
@DeleteMapping("/{port}") @DeleteMapping("/{port}")
@@ -33,9 +37,13 @@ public class ServiceController {
} }
@PostMapping @PostMapping
public CtfService addService(@RequestBody ServiceDto dto) { public ServiceDto addService(@RequestBody ServiceCreateDto dto) {
CtfService newService = this.service.fromDto(dto); return this.service.create(dto);
return this.service.save(newService); }
@PostMapping("/{port}")
public ServiceDto updateService(@PathVariable int port, @RequestBody ServiceUpdateDto dto) {
return this.service.update(port, dto);
} }
} }

View File

@@ -1,14 +1,17 @@
package ru.serega6531.packmate.controller; package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.PathVariable;
import ru.serega6531.packmate.model.pojo.StreamPagination; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.StreamDto; import ru.serega6531.packmate.model.pojo.StreamDto;
import ru.serega6531.packmate.model.pojo.StreamPagination;
import ru.serega6531.packmate.service.StreamService; import ru.serega6531.packmate.service.StreamService;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/stream/") @RequestMapping("/api/stream/")
@@ -23,16 +26,12 @@ public class StreamController {
@PostMapping("/all") @PostMapping("/all")
public List<StreamDto> getStreams(@RequestBody StreamPagination pagination) { public List<StreamDto> getStreams(@RequestBody StreamPagination pagination) {
return service.findAll(pagination, Optional.empty(), pagination.isFavorites()).stream() return service.findAll(pagination, Optional.empty(), pagination.isFavorites());
.map(service::streamToDto)
.collect(Collectors.toList());
} }
@PostMapping("/{port}") @PostMapping("/{port}")
public List<StreamDto> getStreams(@PathVariable int port, @RequestBody StreamPagination pagination) { public List<StreamDto> getStreams(@PathVariable int port, @RequestBody StreamPagination pagination) {
return service.findAll(pagination, Optional.of(port), pagination.isFavorites()).stream() return service.findAll(pagination, Optional.of(port), pagination.isFavorites());
.map(service::streamToDto)
.collect(Collectors.toList());
} }
@PostMapping("/{id}/favorite") @PostMapping("/{id}/favorite")

View File

@@ -0,0 +1,15 @@
package ru.serega6531.packmate.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.File;
@EqualsAndHashCode(callSuper = true)
@Data
public class PcapFileNotFoundException extends RuntimeException {
private final File file;
private final File directory;
}

View File

@@ -0,0 +1,15 @@
package ru.serega6531.packmate.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class PcapInterfaceNotFoundException extends RuntimeException {
private final String requestedInterface;
private final List<String> existingInterfaces;
}

View File

@@ -0,0 +1,42 @@
package ru.serega6531.packmate.exception.analyzer;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import ru.serega6531.packmate.exception.PcapFileNotFoundException;
import java.io.File;
import java.util.Arrays;
import java.util.List;
public class PcapFileNotFoundFailureAnalyzer extends AbstractFailureAnalyzer<PcapFileNotFoundException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PcapFileNotFoundException cause) {
String description = "The file " + cause.getFile().getAbsolutePath() + " was not found";
String existingFilesMessage;
File[] existingFiles = cause.getDirectory().listFiles();
if (existingFiles == null) {
return new FailureAnalysis(
description,
"Make sure you've put the pcap file to the ./pcaps directory, not the root directory. " +
"The directory currently does not exist",
cause
);
}
if (existingFiles.length == 0) {
existingFilesMessage = "The pcaps directory is currently empty";
} else {
List<String> existingFilesNames = Arrays.stream(existingFiles).map(File::getName).toList();
existingFilesMessage = "The files present in " + cause.getDirectory().getAbsolutePath() + " are: " + existingFilesNames;
}
return new FailureAnalysis(
description,
"Please verify the file name. Make sure you've put the pcap file to the ./pcaps directory, not the root directory.\n" +
existingFilesMessage,
cause
);
}
}

View File

@@ -0,0 +1,16 @@
package ru.serega6531.packmate.exception.analyzer;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import ru.serega6531.packmate.exception.PcapInterfaceNotFoundException;
public class PcapInterfaceNotFoundFailureAnalyzer extends AbstractFailureAnalyzer<PcapInterfaceNotFoundException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PcapInterfaceNotFoundException cause) {
return new FailureAnalysis(
"The interface \"" + cause.getRequestedInterface() + "\" was not found",
"Check the interface name in the config. Existing interfaces are: " + cause.getExistingInterfaces(),
cause
);
}
}

View File

@@ -3,10 +3,10 @@ package ru.serega6531.packmate.model;
import lombok.*; import lombok.*;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import javax.persistence.Column; import jakarta.persistence.Column;
import javax.persistence.Entity; import jakarta.persistence.Entity;
import javax.persistence.Id; import jakarta.persistence.Id;
import javax.persistence.Table; import jakarta.persistence.Table;
import java.util.Objects; import java.util.Objects;
@Getter @Getter

View File

@@ -5,7 +5,7 @@ import org.hibernate.Hibernate;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter; import org.hibernate.annotations.Parameter;
import javax.persistence.*; import jakarta.persistence.*;
import java.util.Objects; import java.util.Objects;
@Entity @Entity

View File

@@ -5,7 +5,7 @@ import org.hibernate.Hibernate;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter; import org.hibernate.annotations.Parameter;
import javax.persistence.*; import jakarta.persistence.*;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;

View File

@@ -1,5 +1,10 @@
package ru.serega6531.packmate.model; package ru.serega6531.packmate.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -11,14 +16,13 @@ import ru.serega6531.packmate.model.enums.PatternActionType;
import ru.serega6531.packmate.model.enums.PatternDirectionType; import ru.serega6531.packmate.model.enums.PatternDirectionType;
import ru.serega6531.packmate.model.enums.PatternSearchType; import ru.serega6531.packmate.model.enums.PatternSearchType;
import javax.persistence.*;
import java.util.Objects; import java.util.Objects;
@Getter @Getter
@Setter @Setter
@RequiredArgsConstructor @RequiredArgsConstructor
@ToString @ToString
@Entity @Entity(name = "pattern")
@GenericGenerator( @GenericGenerator(
name = "pattern_generator", name = "pattern_generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
@@ -34,8 +38,12 @@ public class Pattern {
@GeneratedValue(generator = "pattern_generator") @GeneratedValue(generator = "pattern_generator")
private Integer id; private Integer id;
@Column(nullable = false)
private boolean enabled; private boolean enabled;
@Column(nullable = false)
private boolean deleted = false;
@Column(nullable = false) @Column(nullable = false)
private String name; private String name;

View File

@@ -9,7 +9,7 @@ import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter; import org.hibernate.annotations.Parameter;
import ru.serega6531.packmate.model.enums.Protocol; import ru.serega6531.packmate.model.enums.Protocol;
import javax.persistence.*; import jakarta.persistence.*;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -53,7 +53,7 @@ public class Stream {
private long endTimestamp; private long endTimestamp;
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany
@JoinTable( @JoinTable(
name = "stream_found_patterns", name = "stream_found_patterns",
joinColumns = @JoinColumn(name = "stream_id"), joinColumns = @JoinColumn(name = "stream_id"),
@@ -70,6 +70,12 @@ public class Stream {
@Column(columnDefinition = "char(3)") @Column(columnDefinition = "char(3)")
private String userAgentHash; private String userAgentHash;
@Column(name = "size_bytes", nullable = false)
private Integer sizeBytes;
@Column(name = "packets_count", nullable = false)
private Integer packetsCount;
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@@ -2,7 +2,7 @@ package ru.serega6531.packmate.model.enums;
public enum SubscriptionMessageType { public enum SubscriptionMessageType {
SAVE_SERVICE, SAVE_PATTERN, SAVE_SERVICE, SAVE_PATTERN,
DELETE_SERVICE, DELETE_PATTERN, DELETE_SERVICE,
NEW_STREAM, NEW_STREAM,
FINISH_LOOKBACK, FINISH_LOOKBACK,
COUNTERS_UPDATE, COUNTERS_UPDATE,

View File

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

View File

@@ -0,0 +1,19 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
import ru.serega6531.packmate.model.enums.PatternActionType;
import ru.serega6531.packmate.model.enums.PatternDirectionType;
import ru.serega6531.packmate.model.enums.PatternSearchType;
@Data
public class PatternCreateDto {
private String name;
private String value;
private String color;
private PatternSearchType searchType;
private PatternDirectionType directionType;
private PatternActionType actionType;
private Integer serviceId;
}

View File

@@ -10,6 +10,7 @@ public class PatternDto {
private int id; private int id;
private boolean enabled; private boolean enabled;
private boolean deleted;
private String name; private String name;
private String value; private String value;
private String color; private String color;

View File

@@ -0,0 +1,11 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
@Data
public class PatternUpdateDto {
private String name;
private String color;
}

View File

@@ -0,0 +1,17 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
@Data
public class ServiceCreateDto {
private int port;
private String name;
private boolean decryptTls;
private boolean processChunkedEncoding;
private boolean ungzipHttp;
private boolean urldecodeHttpRequests;
private boolean mergeAdjacentPackets;
private boolean parseWebSockets;
}

View File

@@ -0,0 +1,17 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
@Data
public class ServiceUpdateDto {
private int port;
private String name;
private boolean decryptTls;
private boolean processChunkedEncoding;
private boolean ungzipHttp;
private boolean urldecodeHttpRequests;
private boolean mergeAdjacentPackets;
private boolean parseWebSockets;
}

View File

@@ -13,9 +13,11 @@ public class StreamDto {
private Protocol protocol; private Protocol protocol;
private long startTimestamp; private long startTimestamp;
private long endTimestamp; private long endTimestamp;
private Set<PatternDto> foundPatterns; private Set<Integer> foundPatternsIds;
private boolean favorite; private boolean favorite;
private int ttl; private int ttl;
private String userAgentHash; private String userAgentHash;
private int sizeBytes;
private int packetsCount;
} }

View File

@@ -1,29 +1,18 @@
package ru.serega6531.packmate.model.pojo; package ru.serega6531.packmate.model.pojo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import ru.serega6531.packmate.model.enums.Protocol; import ru.serega6531.packmate.model.enums.Protocol;
import java.net.InetAddress; import java.net.InetAddress;
@AllArgsConstructor public record UnfinishedStream(InetAddress firstIp, InetAddress secondIp, int firstPort, int secondPort,
@Getter Protocol protocol) {
public class UnfinishedStream {
private final InetAddress firstIp;
private final InetAddress secondIp;
private final int firstPort;
private final int secondPort;
private final Protocol protocol;
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (!(obj instanceof UnfinishedStream)) { if (!(obj instanceof UnfinishedStream o)) {
return false; return false;
} }
UnfinishedStream o = (UnfinishedStream) obj;
boolean ipEq1 = firstIp.equals(o.firstIp) && secondIp.equals(o.secondIp); boolean ipEq1 = firstIp.equals(o.firstIp) && secondIp.equals(o.secondIp);
boolean ipEq2 = firstIp.equals(o.secondIp) && secondIp.equals(o.firstIp); boolean ipEq2 = firstIp.equals(o.secondIp) && secondIp.equals(o.firstIp);
boolean portEq1 = firstPort == o.firstPort && secondPort == o.secondPort; boolean portEq1 = firstPort == o.firstPort && secondPort == o.secondPort;

View File

@@ -6,6 +6,7 @@ import org.apache.tomcat.util.threads.InlineExecutorService;
import org.pcap4j.core.PcapNativeException; import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.Pcaps; import org.pcap4j.core.Pcaps;
import org.pcap4j.packet.Packet; import org.pcap4j.packet.Packet;
import ru.serega6531.packmate.exception.PcapFileNotFoundException;
import ru.serega6531.packmate.model.enums.Protocol; import ru.serega6531.packmate.model.enums.Protocol;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType; import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage; import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
@@ -31,9 +32,10 @@ public class FilePcapWorker extends AbstractPcapWorker {
super(servicesService, streamService, localIpString); super(servicesService, streamService, localIpString);
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
file = new File(filename); File directory = new File("pcaps");
file = new File(directory, filename);
if (!file.exists()) { if (!file.exists()) {
throw new IllegalArgumentException("File " + file.getAbsolutePath() + " does not exist"); throw new PcapFileNotFoundException(file, directory);
} }
processorExecutorService = new InlineExecutorService(); processorExecutorService = new InlineExecutorService();

View File

@@ -6,10 +6,12 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.pcap4j.core.PcapNativeException; import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface; import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.Pcaps; import org.pcap4j.core.Pcaps;
import ru.serega6531.packmate.exception.PcapInterfaceNotFoundException;
import ru.serega6531.packmate.service.ServicesService; import ru.serega6531.packmate.service.ServicesService;
import ru.serega6531.packmate.service.StreamService; import ru.serega6531.packmate.service.StreamService;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -27,8 +29,8 @@ public class LivePcapWorker extends AbstractPcapWorker {
device = Pcaps.getDevByName(interfaceName); device = Pcaps.getDevByName(interfaceName);
if (device == null) { if (device == null) {
log.info("Existing devices: {}", Pcaps.findAllDevs().stream().map(PcapNetworkInterface::getName).toList()); List<String> existingInterfaces = Pcaps.findAllDevs().stream().map(PcapNetworkInterface::getName).toList();
throw new IllegalArgumentException("Device " + interfaceName + " does not exist"); throw new PcapInterfaceNotFoundException(interfaceName, existingInterfaces);
} }
BasicThreadFactory factory = new BasicThreadFactory.Builder() BasicThreadFactory factory = new BasicThreadFactory.Builder()

View File

@@ -1,11 +1,10 @@
package ru.serega6531.packmate.pcap; package ru.serega6531.packmate.pcap;
import org.pcap4j.core.PcapNativeException;
import ru.serega6531.packmate.model.enums.Protocol; import ru.serega6531.packmate.model.enums.Protocol;
public class NoOpPcapWorker implements PcapWorker { public class NoOpPcapWorker implements PcapWorker {
@Override @Override
public void start() throws PcapNativeException { public void start() {
} }
@Override @Override

View File

@@ -1,11 +1,13 @@
package ru.serega6531.packmate.repository; package ru.serega6531.packmate.repository;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.*; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import ru.serega6531.packmate.model.Packet; import ru.serega6531.packmate.model.Packet;
import ru.serega6531.packmate.model.Stream; import ru.serega6531.packmate.model.Stream;
import javax.persistence.QueryHint;
import java.util.List; import java.util.List;
public interface StreamRepository extends JpaRepository<Stream, Long>, JpaSpecificationExecutor<Stream> { public interface StreamRepository extends JpaRepository<Stream, Long>, JpaSpecificationExecutor<Stream> {
@@ -16,13 +18,12 @@ public interface StreamRepository extends JpaRepository<Stream, Long>, JpaSpecif
long deleteByEndTimestampBeforeAndFavoriteIsFalse(long threshold); long deleteByEndTimestampBeforeAndFavoriteIsFalse(long threshold);
@Query("SELECT DISTINCT p FROM Packet p " + @Query("SELECT p FROM Packet p " +
"LEFT JOIN FETCH p.matches " + "LEFT JOIN FETCH p.matches " +
"WHERE p.stream.id = :streamId " + "WHERE p.stream.id = :streamId " +
"AND (:startingFrom IS NULL OR p.id > :startingFrom) " + "AND (:startingFrom IS NULL OR p.id > :startingFrom) " +
"ORDER BY p.id" "ORDER BY p.id"
) )
@QueryHints(@QueryHint(name = org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false"))
List<Packet> getPackets(long streamId, Long startingFrom, Pageable pageable); List<Packet> getPackets(long streamId, Long startingFrom, Pageable pageable);
} }

View File

@@ -1,25 +1,31 @@
package ru.serega6531.packmate.service; package ru.serega6531.packmate.service;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper; import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.serega6531.packmate.model.CtfService; import ru.serega6531.packmate.model.CtfService;
import ru.serega6531.packmate.model.FoundPattern; import ru.serega6531.packmate.model.FoundPattern;
import ru.serega6531.packmate.model.Pattern; import ru.serega6531.packmate.model.Pattern;
import ru.serega6531.packmate.model.enums.PatternActionType; import ru.serega6531.packmate.model.enums.PatternActionType;
import ru.serega6531.packmate.model.enums.PatternDirectionType; import ru.serega6531.packmate.model.enums.PatternDirectionType;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType; import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.PatternCreateDto;
import ru.serega6531.packmate.model.pojo.PatternDto; import ru.serega6531.packmate.model.pojo.PatternDto;
import ru.serega6531.packmate.model.pojo.PatternUpdateDto;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage; import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import ru.serega6531.packmate.repository.PatternRepository; import ru.serega6531.packmate.repository.PatternRepository;
import javax.annotation.PostConstruct;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@@ -59,11 +65,11 @@ public class PatternService {
public Set<FoundPattern> findMatches(byte[] bytes, CtfService service, PatternDirectionType directionType, PatternActionType actionType) { public Set<FoundPattern> findMatches(byte[] bytes, CtfService service, PatternDirectionType directionType, PatternActionType actionType) {
final List<Pattern> list = patterns.values().stream() final List<Pattern> list = patterns.values().stream()
.filter(Pattern::isEnabled) .filter(pattern -> pattern.isEnabled() && !pattern.isDeleted())
.filter(p -> p.getServiceId() == null || p.getServiceId() == service.getPort()) .filter(p -> p.getServiceId() == null || p.getServiceId().equals(service.getPort()))
.filter(p -> p.getActionType() == actionType) .filter(p -> p.getActionType() == actionType)
.filter(p -> p.getDirectionType() == directionType || p.getDirectionType() == PatternDirectionType.BOTH) .filter(p -> p.getDirectionType() == directionType || p.getDirectionType() == PatternDirectionType.BOTH)
.collect(Collectors.toList()); .toList();
return new PatternMatcher(bytes, list).findMatches(); return new PatternMatcher(bytes, list).findMatches();
} }
@@ -88,15 +94,47 @@ public class PatternService {
} }
} }
public Pattern save(Pattern pattern) { public void delete(int id) {
final Pattern pattern = find(id);
if (pattern != null) {
pattern.setDeleted(true);
final Pattern saved = repository.save(pattern);
patterns.put(id, saved);
log.info("Deleted pattern '{}' with value '{}'", pattern.getName(), pattern.getValue());
subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.SAVE_PATTERN, toDto(saved)));
}
}
@Transactional
public PatternDto create(PatternCreateDto dto) {
Pattern pattern = fromDto(dto);
pattern.setEnabled(true);
pattern.setDeleted(false);
pattern.setSearchStartTimestamp(System.currentTimeMillis());
Pattern saved = save(pattern);
return toDto(saved);
}
@Transactional
public PatternDto update(int id, PatternUpdateDto dto) {
Pattern pattern = repository.findById(id).orElseThrow();
modelMapper.map(dto, pattern);
Pattern saved = save(pattern);
return toDto(saved);
}
private Pattern save(Pattern pattern) {
try { try {
PatternMatcher.compilePattern(pattern); PatternMatcher.compilePattern(pattern);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException(e.getMessage()); throw new IllegalArgumentException(e.getMessage());
} }
pattern.setSearchStartTimestamp(System.currentTimeMillis());
final Pattern saved = repository.save(pattern); final Pattern saved = repository.save(pattern);
patterns.put(saved.getId(), saved); patterns.put(saved.getId(), saved);
@@ -121,12 +159,11 @@ public class PatternService {
} }
} }
public Pattern fromDto(PatternDto dto) { public Pattern fromDto(PatternCreateDto dto) {
return modelMapper.map(dto, Pattern.class); return modelMapper.map(dto, Pattern.class);
} }
public PatternDto toDto(Pattern pattern) { public PatternDto toDto(Pattern pattern) {
return modelMapper.map(pattern, PatternDto.class); return modelMapper.map(pattern, PatternDto.class);
} }
} }

View File

@@ -4,8 +4,8 @@ import lombok.extern.slf4j.Slf4j;
import org.pcap4j.core.PcapNativeException; import org.pcap4j.core.PcapNativeException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.serega6531.packmate.model.CtfService;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType; import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.ServiceDto;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage; import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import ru.serega6531.packmate.pcap.NoOpPcapWorker; import ru.serega6531.packmate.pcap.NoOpPcapWorker;
import ru.serega6531.packmate.pcap.PcapWorker; import ru.serega6531.packmate.pcap.PcapWorker;
@@ -40,14 +40,14 @@ public class PcapService {
} }
} }
public void updateFilter(Collection<CtfService> services) { public void updateFilter(Collection<ServiceDto> services) {
String filter; String filter;
if (services.isEmpty()) { if (services.isEmpty()) {
filter = "tcp or udp"; filter = "tcp or udp";
} else { } else {
final String ports = services.stream() final String ports = services.stream()
.map(CtfService::getPort) .map(ServiceDto::getPort)
.map(p -> "port " + p) .map(p -> "port " + p)
.collect(Collectors.joining(" or ")); .collect(Collectors.joining(" or "));

View File

@@ -6,13 +6,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.serega6531.packmate.model.CtfService; import ru.serega6531.packmate.model.CtfService;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType; import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.ServiceCreateDto;
import ru.serega6531.packmate.model.pojo.ServiceDto; import ru.serega6531.packmate.model.pojo.ServiceDto;
import ru.serega6531.packmate.model.pojo.ServiceUpdateDto;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage; import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import ru.serega6531.packmate.repository.ServiceRepository; import ru.serega6531.packmate.repository.ServiceRepository;
import javax.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.*; import java.util.*;
@@ -67,8 +70,11 @@ public class ServicesService {
return Optional.ofNullable(services.get(port)); return Optional.ofNullable(services.get(port));
} }
public Collection<CtfService> findAll() { public List<ServiceDto> findAll() {
return services.values(); return services.values()
.stream()
.map(this::toDto)
.toList();
} }
public void deleteByPort(int port) { public void deleteByPort(int port) {
@@ -82,9 +88,31 @@ public class ServicesService {
updateFilter(); updateFilter();
} }
public CtfService save(CtfService service) { @Transactional
log.info("Added or edited service '{}' at port {}", service.getName(), service.getPort()); public ServiceDto create(ServiceCreateDto dto) {
if (repository.existsById(dto.getPort())) {
throw new IllegalArgumentException("Service already exists");
}
CtfService service = fromDto(dto);
log.info("Added service '{}' at port {}", service.getName(), service.getPort());
return save(service);
}
@Transactional
public ServiceDto update(int port, ServiceUpdateDto dto) {
CtfService service = repository.findById(port).orElseThrow();
log.info("Edited service '{}' at port {}", service.getName(), service.getPort());
modelMapper.map(dto, service);
service.setPort(port);
return save(service);
}
private ServiceDto save(CtfService service) {
final CtfService saved = repository.save(service); final CtfService saved = repository.save(service);
services.put(saved.getPort(), saved); services.put(saved.getPort(), saved);
@@ -92,18 +120,18 @@ public class ServicesService {
updateFilter(); updateFilter();
return saved; return toDto(saved);
} }
public void updateFilter() { public void updateFilter() {
pcapService.updateFilter(findAll()); pcapService.updateFilter(findAll());
} }
public ServiceDto toDto(CtfService service) { private ServiceDto toDto(CtfService service) {
return modelMapper.map(service, ServiceDto.class); return modelMapper.map(service, ServiceDto.class);
} }
public CtfService fromDto(ServiceDto dto) { private CtfService fromDto(ServiceCreateDto dto) {
return modelMapper.map(dto, CtfService.class); return modelMapper.map(dto, CtfService.class);
} }

View File

@@ -13,11 +13,19 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.serega6531.packmate.model.*; import ru.serega6531.packmate.model.CtfService;
import ru.serega6531.packmate.model.FoundPattern;
import ru.serega6531.packmate.model.Packet;
import ru.serega6531.packmate.model.Pattern;
import ru.serega6531.packmate.model.Stream;
import ru.serega6531.packmate.model.enums.PatternActionType; import ru.serega6531.packmate.model.enums.PatternActionType;
import ru.serega6531.packmate.model.enums.PatternDirectionType; import ru.serega6531.packmate.model.enums.PatternDirectionType;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType; import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.*; import ru.serega6531.packmate.model.pojo.PacketDto;
import ru.serega6531.packmate.model.pojo.StreamDto;
import ru.serega6531.packmate.model.pojo.StreamPagination;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import ru.serega6531.packmate.model.pojo.UnfinishedStream;
import ru.serega6531.packmate.repository.StreamRepository; import ru.serega6531.packmate.repository.StreamRepository;
import ru.serega6531.packmate.service.optimization.RsaKeysHolder; import ru.serega6531.packmate.service.optimization.RsaKeysHolder;
import ru.serega6531.packmate.service.optimization.StreamOptimizer; import ru.serega6531.packmate.service.optimization.StreamOptimizer;
@@ -28,7 +36,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@@ -41,7 +48,6 @@ public class StreamService {
private final SubscriptionService subscriptionService; private final SubscriptionService subscriptionService;
private final RsaKeysHolder keysHolder; private final RsaKeysHolder keysHolder;
private final ModelMapper modelMapper; private final ModelMapper modelMapper;
private final boolean ignoreEmptyPackets; private final boolean ignoreEmptyPackets;
private final java.util.regex.Pattern userAgentPattern = java.util.regex.Pattern.compile("User-Agent: (.+)\\r\\n"); private final java.util.regex.Pattern userAgentPattern = java.util.regex.Pattern.compile("User-Agent: (.+)\\r\\n");
@@ -71,15 +77,15 @@ public class StreamService {
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
public boolean saveNewStream(UnfinishedStream unfinishedStream, List<Packet> packets) { public boolean saveNewStream(UnfinishedStream unfinishedStream, List<Packet> packets) {
final var serviceOptional = servicesService.findService( final var serviceOptional = servicesService.findService(
unfinishedStream.getFirstIp(), unfinishedStream.firstIp(),
unfinishedStream.getFirstPort(), unfinishedStream.firstPort(),
unfinishedStream.getSecondIp(), unfinishedStream.secondIp(),
unfinishedStream.getSecondPort() unfinishedStream.secondPort()
); );
if (serviceOptional.isEmpty()) { if (serviceOptional.isEmpty()) {
log.warn("Failed to save the stream: service at port {} or {} does not exist", log.warn("Failed to save the stream: service at port {} or {} does not exist",
unfinishedStream.getFirstPort(), unfinishedStream.getSecondPort()); unfinishedStream.firstPort(), unfinishedStream.secondPort());
return false; return false;
} }
CtfService service = serviceOptional.get(); CtfService service = serviceOptional.get();
@@ -95,6 +101,9 @@ public class StreamService {
countingService.countStream(service.getPort(), packets.size()); countingService.countStream(service.getPort(), packets.size());
int packetsSize = packets.stream().mapToInt(p -> p.getContent().length).sum();
int packetsCount = packets.size();
List<Packet> optimizedPackets = new StreamOptimizer(keysHolder, service, packets).optimizeStream(); List<Packet> optimizedPackets = new StreamOptimizer(keysHolder, service, packets).optimizeStream();
if (isStreamIgnored(optimizedPackets, service)) { if (isStreamIgnored(optimizedPackets, service)) {
@@ -107,7 +116,7 @@ public class StreamService {
.findFirst(); .findFirst();
final Stream stream = new Stream(); final Stream stream = new Stream();
stream.setProtocol(unfinishedStream.getProtocol()); stream.setProtocol(unfinishedStream.protocol());
stream.setTtl(firstIncoming.map(Packet::getTtl).orElse(0)); stream.setTtl(firstIncoming.map(Packet::getTtl).orElse(0));
stream.setStartTimestamp(packets.get(0).getTimestamp()); stream.setStartTimestamp(packets.get(0).getTimestamp());
stream.setEndTimestamp(packets.get(packets.size() - 1).getTimestamp()); stream.setEndTimestamp(packets.get(packets.size() - 1).getTimestamp());
@@ -116,6 +125,9 @@ public class StreamService {
String userAgentHash = getUserAgentHash(optimizedPackets); String userAgentHash = getUserAgentHash(optimizedPackets);
stream.setUserAgentHash(userAgentHash); stream.setUserAgentHash(userAgentHash);
stream.setSizeBytes(packetsSize);
stream.setPacketsCount(packetsCount);
Set<Pattern> foundPatterns = matchPatterns(optimizedPackets, service); Set<Pattern> foundPatterns = matchPatterns(optimizedPackets, service);
stream.setFoundPatterns(foundPatterns); stream.setFoundPatterns(foundPatterns);
stream.setPackets(optimizedPackets); stream.setPackets(optimizedPackets);
@@ -190,7 +202,7 @@ public class StreamService {
foundPatterns.addAll(matches.stream() foundPatterns.addAll(matches.stream()
.map(FoundPattern::getPatternId) .map(FoundPattern::getPatternId)
.map(patternService::find) .map(patternService::find)
.collect(Collectors.toList())); .toList());
} }
return foundPatterns; return foundPatterns;
@@ -244,9 +256,12 @@ public class StreamService {
return saved; return saved;
} }
public List<Packet> getPackets(long streamId, @Nullable Long startingFrom, int pageSize) { @Transactional
// long safeStartingFrom = startingFrom != null ? startingFrom : 0; public List<PacketDto> getPackets(long streamId, @Nullable Long startingFrom, int pageSize) {
return repository.getPackets(streamId, startingFrom, Pageable.ofSize(pageSize)); return repository.getPackets(streamId, startingFrom, Pageable.ofSize(pageSize))
.stream()
.map(this::packetToDto)
.toList();
} }
/** /**
@@ -262,7 +277,8 @@ public class StreamService {
repository.setFavorite(id, favorite); repository.setFavorite(id, favorite);
} }
public List<Stream> findAll(StreamPagination pagination, Optional<Integer> service, boolean onlyFavorites) { @Transactional
public List<StreamDto> findAll(StreamPagination pagination, Optional<Integer> service, boolean onlyFavorites) {
PageRequest page = PageRequest.of(0, pagination.getPageSize(), Sort.Direction.DESC, "id"); PageRequest page = PageRequest.of(0, pagination.getPageSize(), Sort.Direction.DESC, "id");
Specification<Stream> spec = Specification.where(null); Specification<Stream> spec = Specification.where(null);
@@ -283,7 +299,11 @@ public class StreamService {
spec = spec.and(streamPatternsContains(pagination.getPattern())); spec = spec.and(streamPatternsContains(pagination.getPattern()));
} }
return repository.findAll(spec, page).getContent(); return repository.findAll(spec, page)
.getContent()
.stream()
.map(this::streamToDto)
.toList();
} }
public List<Stream> findAllBetweenTimestamps(long start, long end) { public List<Stream> findAllBetweenTimestamps(long start, long end) {

View File

@@ -107,7 +107,7 @@ public class TlsDecryptor {
int blockCipherSize = Integer.parseInt(blockCipherParts[1]); int blockCipherSize = Integer.parseInt(blockCipherParts[1]);
String blockCipherMode = blockCipherParts[2]; String blockCipherMode = blockCipherParts[2];
if (!blockCipherAlgo.equals("AES")) { //TODO использовать не только AES256 if (!blockCipherAlgo.equals("AES")) {
return; return;
} }

View File

@@ -2,7 +2,7 @@ package ru.serega6531.packmate.tasks;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.serega6531.packmate.service.StreamService; import ru.serega6531.packmate.service.StreamService;
@@ -12,7 +12,7 @@ import java.time.temporal.ChronoUnit;
@Component @Component
@Slf4j @Slf4j
@ConditionalOnProperty(name = "old-streams-cleanup-enabled", havingValue = "true") @ConditionalOnExpression("${old-streams-cleanup-enabled:false} && '${capture-mode}' == 'LIVE'")
public class OldStreamsCleanupTask { public class OldStreamsCleanupTask {
private final StreamService service; private final StreamService service;

View File

@@ -0,0 +1,3 @@
org.springframework.boot.diagnostics.FailureAnalyzer=\
ru.serega6531.packmate.exception.analyzer.PcapFileNotFoundFailureAnalyzer,\
ru.serega6531.packmate.exception.analyzer.PcapInterfaceNotFoundFailureAnalyzer

View File

@@ -12,8 +12,6 @@ spring:
jdbc: jdbc:
batch_size: 20 batch_size: 20
order_inserts: true order_inserts: true
temp:
use_jdbc_metadata_defaults: false
database-platform: org.hibernate.dialect.PostgreSQLDialect database-platform: org.hibernate.dialect.PostgreSQLDialect

View File

@@ -3,6 +3,7 @@ package ru.serega6531.packmate;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import ru.serega6531.packmate.model.Packet; import ru.serega6531.packmate.model.Packet;
import ru.serega6531.packmate.service.optimization.HttpChunksProcessor;
import ru.serega6531.packmate.service.optimization.HttpGzipProcessor; import ru.serega6531.packmate.service.optimization.HttpGzipProcessor;
import ru.serega6531.packmate.service.optimization.HttpUrldecodeProcessor; import ru.serega6531.packmate.service.optimization.HttpUrldecodeProcessor;
import ru.serega6531.packmate.service.optimization.PacketsMerger; import ru.serega6531.packmate.service.optimization.PacketsMerger;
@@ -67,6 +68,18 @@ class StreamOptimizerTest {
assertEquals(2, list.get(3).getContent().length); assertEquals(2, list.get(3).getContent().length);
} }
@Test
void testChunkedTransferEncoding() {
String content = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" +
"6\r\nChunk1\r\n6\r\nChunk2\r\n0\r\n\r\n";
List<Packet> packets = new ArrayList<>(List.of(createPacket(content.getBytes(), false)));
new HttpChunksProcessor(packets).processChunkedEncoding();
assertEquals(1, packets.size());
assertTrue(packets.get(0).getContentString().contains("Chunk1Chunk2"));
}
private Packet createPacket(int content, boolean incoming) { private Packet createPacket(int content, boolean incoming) {
return createPacket(new byte[] {(byte) content}, incoming); return createPacket(new byte[] {(byte) content}, incoming);
} }