33 Commits

Author SHA1 Message Date
Sergey Shkurov
db8ffbfcdd Store dates in Instant 2023-08-01 21:40:23 +04:00
Sergey
938031f1de Use RawHTTP library to process HTTP streams (packmate/Packmate!23) 2023-07-31 15:42:17 +00:00
Sergey
7986658bd1 Update configuration 2023-07-26 18:21:49 +00:00
Sergey
4fed53244d Merge branch 'update-frontend' into 'master'
Update frontend to show packet offsets

See merge request packmate/Packmate!21
2023-07-24 22:30:47 +00:00
Sergey Shkurov
37fd548364 Update frontend to show packet offsets 2023-07-25 02:28:56 +04:00
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
60 changed files with 813 additions and 754 deletions

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()
}

61
build.gradle.kts Normal file
View File

@@ -0,0 +1,61 @@
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")
implementation("com.athaydes.rawhttp:rawhttp-core:2.5.2")
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

@@ -17,11 +17,13 @@ COPY --from=1 /tmp/compile/build/libs/packmate-*-SNAPSHOT.jar app.jar
CMD [ "java", "-Djava.net.preferIPv4Stack=true", "-Djava.net.preferIPv4Addresses=true", \ 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", \ "-jar", "/app/app.jar", "--spring.datasource.url=jdbc:postgresql://127.0.0.1:65001/packmate", \
"--spring.datasource.password=${DB_PASSWORD}", \ "--spring.datasource.password=${DB_PASSWORD}", \
"--capture-mode=${MODE}", "--pcap-file=${PCAP_FILE}", \ "--packmate.capture-mode=${MODE}", "--packmate.pcap-file=${PCAP_FILE}", \
"--interface-name=${INTERFACE}", "--local-ip=${LOCAL_IP}", "--account-login=${WEB_LOGIN}", \ "--packmate.interface-name=${INTERFACE}", "--packmate.local-ip=${LOCAL_IP}", \
"--old-streams-cleanup-enabled=${OLD_STREAMS_CLEANUP_ENABLED}", "--cleanup-interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \ "--packmate.web.account-login=${WEB_LOGIN}", "--packmate.web.account-password=${WEB_PASSWORD}", \
"--old-streams-threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \ "--packmate.cleanup.enabled=${OLD_STREAMS_CLEANUP_ENABLED}", \
"--account-password=${WEB_PASSWORD}", "--server.port=65000", "--server.address=0.0.0.0" \ "--packmate.cleanup.interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \
"--packmate.cleanup.threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \
"--server.port=65000", "--server.address=0.0.0.0" \
] ]
EXPOSE 65000 EXPOSE 65000

View File

@@ -75,9 +75,9 @@
Совет: иногда на CTF забывают перезаписать TTL пакетов внутри сети. В таком случае по TTL можно отличить запросы от чекеров и от других команд. Совет: иногда на CTF забывают перезаписать TTL пакетов внутри сети. В таком случае по TTL можно отличить запросы от чекеров и от других команд.
Совет #2: по User-Agent можно отличать запросы из разных источников. К примеру, можно предположить, что на скриншоте выше запросы 4 и 5 пришли из разных источников. Совет #&#8203;2: по User-Agent можно отличать запросы из разных источников. К примеру, можно предположить, что на скриншоте выше запросы 4 и 5 пришли из разных источников.
Совет #3: нажимайте на звездочку, чтобы добавить интересный стрим в избранное. Этот стрим будет выделен в списке, и появится в списке избранных стримов. Совет #&#8203;3: нажимайте на звездочку, чтобы добавить интересный стрим в избранное. Этот стрим будет выделен в списке, и появится в списке избранных стримов.
#### Управление просмотром #### Управление просмотром
@@ -101,7 +101,7 @@
Совет: создавайте отдельные паттерны для входящих и исходящих флагов. Так легче отличать чекер, кладущий флаги, от эксплоитов. Совет: создавайте отдельные паттерны для входящих и исходящих флагов. Так легче отличать чекер, кладущий флаги, от эксплоитов.
Совет #2: используйте Lookback для исследования найденных эксплоитов. Совет #&#8203;2: используйте Lookback для исследования найденных эксплоитов.
Пример: вы обнаружили, что сервис только что отдал флаг пользователю `abc123` без видимых причин. Пример: вы обнаружили, что сервис только что отдал флаг пользователю `abc123` без видимых причин.
Можно предположить, что атакующая команда создала этого пользователя и подготовила эксплоит в другом стриме. Можно предположить, что атакующая команда создала этого пользователя и подготовила эксплоит в другом стриме.

View File

@@ -68,9 +68,9 @@ you can switch between binary and text representation using the button in the si
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: 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 #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;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 #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. 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 #### Control Panel
@@ -94,7 +94,7 @@ Tip #3: Click on the star icon to add an interesting stream to your favorites. T
Tip: Create separate patterns for incoming and outgoing flags to easily distinguish between flag checkers and exploits. Tip: Create separate patterns for incoming and outgoing flags to easily distinguish between flag checkers and exploits.
Tip #2: Use Lookback to investigate discovered 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. 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. You can assume that the attacking team created this user and prepared an exploit in another 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

View File

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

View File

@@ -1,28 +1,35 @@
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.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean; 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.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;
import ru.serega6531.packmate.pcap.PcapWorker; import ru.serega6531.packmate.pcap.PcapWorker;
import ru.serega6531.packmate.properties.PackmateProperties;
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 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
@EnableAsync @EnableAsync
@ConfigurationPropertiesScan("ru.serega6531.packmate.properties")
public class ApplicationConfiguration { public class ApplicationConfiguration {
@Bean(destroyMethod = "stop") @Bean(destroyMethod = "stop")
@@ -30,20 +37,37 @@ public class ApplicationConfiguration {
public PcapWorker pcapWorker(ServicesService servicesService, public PcapWorker pcapWorker(ServicesService servicesService,
StreamService streamService, StreamService streamService,
SubscriptionService subscriptionService, SubscriptionService subscriptionService,
@Value("${local-ip}") String localIpString, PackmateProperties properties
@Value("${interface-name}") String interfaceName, ) throws PcapNativeException, UnknownHostException {
@Value("${pcap-file}") String filename, return switch (properties.captureMode()) {
@Value("${capture-mode}") CaptureMode captureMode) throws PcapNativeException, UnknownHostException { case LIVE -> new LivePcapWorker(servicesService, streamService, properties.localIp(), properties.interfaceName());
return switch (captureMode) { case FILE ->
case LIVE -> new LivePcapWorker(servicesService, streamService, localIpString, interfaceName); new FilePcapWorker(servicesService, streamService, subscriptionService, properties.localIp(), properties.pcapFile());
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,57 +1,58 @@
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.context.annotation.Bean;
import org.springframework.beans.factory.annotation.Value;
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;
import ru.serega6531.packmate.properties.PackmateProperties;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@Slf4j @Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { public class SecurityConfiguration {
@Value("${account-login}") @Bean
private String login; public InMemoryUserDetailsManager userDetailsService(PackmateProperties properties, PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username(properties.web().accountLogin())
.password(passwordEncoder.encode(properties.web().accountPassword()))
.roles("USER")
.build();
@Value("${account-password}") return new InMemoryUserDetailsManager(user);
private String password;
private final PasswordEncoder passwordEncoder;
@Autowired
public SecurityConfiguration(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
} }
@Autowired @Bean
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
auth.inMemoryAuthentication() return http.csrf()
.withUser(login)
.password(passwordEncoder.encode(password))
.authorities("ROLE_USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
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
@@ -25,9 +25,7 @@ public class CtfService {
private boolean decryptTls; private boolean decryptTls;
private boolean processChunkedEncoding; private boolean http;
private boolean ungzipHttp;
private boolean urldecodeHttpRequests; private boolean urldecodeHttpRequests;

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,9 @@ 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.time.Instant;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@@ -24,7 +26,7 @@ import java.util.Set;
} }
) )
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder(toBuilder = true)
@Table(indexes = { @Index(name = "stream_id_index", columnList = "stream_id") }) @Table(indexes = { @Index(name = "stream_id_index", columnList = "stream_id") })
public class Packet { public class Packet {
@@ -45,15 +47,17 @@ public class Packet {
@OneToMany(mappedBy = "packet", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "packet", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<FoundPattern> matches; private Set<FoundPattern> matches;
private long timestamp; private Instant timestamp;
private boolean incoming; // true если от клиента к серверу, иначе false private boolean incoming; // true если от клиента к серверу, иначе false
private boolean ungzipped; private boolean httpProcessed = false;
private boolean webSocketParsed; private boolean webSocketParsed = false;
private boolean tlsDecrypted; private boolean tlsDecrypted = false;
private boolean hasHttpBody = false;
@Column(nullable = false) @Column(nullable = false)
private byte[] content; private byte[] content;

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,9 @@ 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.time.Instant;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -49,11 +51,11 @@ public class Stream {
@ToString.Exclude @ToString.Exclude
private List<Packet> packets; private List<Packet> packets;
private long startTimestamp; private Instant startTimestamp;
private long endTimestamp; private Instant 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 +72,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

@@ -1,7 +1,9 @@
package ru.serega6531.packmate.model.pojo; package ru.serega6531.packmate.model.pojo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import java.time.Instant;
import java.util.Set; import java.util.Set;
@Data @Data
@@ -9,11 +11,13 @@ public class PacketDto {
private Long id; private Long id;
private Set<FoundPatternDto> matches; private Set<FoundPatternDto> matches;
private long timestamp; @JsonFormat(shape = JsonFormat.Shape.NUMBER, without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
private Instant timestamp;
private boolean incoming; private boolean incoming;
private boolean ungzipped; private boolean ungzipped;
private boolean webSocketParsed; private boolean webSocketParsed;
private boolean tlsDecrypted; private boolean tlsDecrypted;
private boolean hasHttpBody;
private byte[] content; private byte[] content;
} }

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,16 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
@Data
public class ServiceCreateDto {
private int port;
private String name;
private boolean decryptTls;
private boolean http;
private boolean urldecodeHttpRequests;
private boolean mergeAdjacentPackets;
private boolean parseWebSockets;
}

View File

@@ -8,8 +8,7 @@ public class ServiceDto {
private int port; private int port;
private String name; private String name;
private boolean decryptTls; private boolean decryptTls;
private boolean processChunkedEncoding; private boolean http;
private boolean ungzipHttp;
private boolean urldecodeHttpRequests; private boolean urldecodeHttpRequests;
private boolean mergeAdjacentPackets; private boolean mergeAdjacentPackets;
private boolean parseWebSockets; private boolean parseWebSockets;

View File

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

View File

@@ -1,8 +1,10 @@
package ru.serega6531.packmate.model.pojo; package ru.serega6531.packmate.model.pojo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import ru.serega6531.packmate.model.enums.Protocol; import ru.serega6531.packmate.model.enums.Protocol;
import java.time.Instant;
import java.util.Set; import java.util.Set;
@Data @Data
@@ -11,11 +13,15 @@ public class StreamDto {
private Long id; private Long id;
private int service; private int service;
private Protocol protocol; private Protocol protocol;
private long startTimestamp; @JsonFormat(shape = JsonFormat.Shape.NUMBER, without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
private long endTimestamp; private Instant startTimestamp;
private Set<PatternDto> foundPatterns; @JsonFormat(shape = JsonFormat.Shape.NUMBER, without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
private Instant endTimestamp;
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

@@ -18,6 +18,8 @@ import ru.serega6531.packmate.service.StreamService;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -52,11 +54,11 @@ public abstract class AbstractPcapWorker implements PcapWorker, PacketListener {
protected AbstractPcapWorker(ServicesService servicesService, protected AbstractPcapWorker(ServicesService servicesService,
StreamService streamService, StreamService streamService,
String localIpString) throws UnknownHostException { InetAddress localIp) throws UnknownHostException {
this.servicesService = servicesService; this.servicesService = servicesService;
this.streamService = streamService; this.streamService = streamService;
this.localIp = InetAddress.getByName(localIpString); this.localIp = localIp;
BasicThreadFactory factory = new BasicThreadFactory.Builder() BasicThreadFactory factory = new BasicThreadFactory.Builder()
.namingPattern("pcap-loop").build(); .namingPattern("pcap-loop").build();
@@ -82,7 +84,7 @@ public abstract class AbstractPcapWorker implements PcapWorker, PacketListener {
return; return;
} }
final long time = pcap.getTimestamp().getTime(); final Instant time = pcap.getTimestamp().toInstant();
if (rawPacket.contains(TcpPacket.class)) { if (rawPacket.contains(TcpPacket.class)) {
final TcpPacket packet = rawPacket.get(TcpPacket.class); final TcpPacket packet = rawPacket.get(TcpPacket.class);
@@ -93,7 +95,7 @@ public abstract class AbstractPcapWorker implements PcapWorker, PacketListener {
} }
} }
private void gotTcpPacket(TcpPacket packet, InetAddress sourceIp, InetAddress destIp, int ttl, long time) { private void gotTcpPacket(TcpPacket packet, InetAddress sourceIp, InetAddress destIp, int ttl, Instant time) {
final TcpPacket.TcpHeader tcpHeader = packet.getHeader(); final TcpPacket.TcpHeader tcpHeader = packet.getHeader();
int sourcePort = tcpHeader.getSrcPort().valueAsInt(); int sourcePort = tcpHeader.getSrcPort().valueAsInt();
int destPort = tcpHeader.getDstPort().valueAsInt(); int destPort = tcpHeader.getDstPort().valueAsInt();
@@ -127,7 +129,7 @@ public abstract class AbstractPcapWorker implements PcapWorker, PacketListener {
} }
} }
private void gotUdpPacket(UdpPacket packet, InetAddress sourceIp, InetAddress destIp, int ttl, long time) { private void gotUdpPacket(UdpPacket packet, InetAddress sourceIp, InetAddress destIp, int ttl, Instant time) {
final UdpPacket.UdpHeader udpHeader = packet.getHeader(); final UdpPacket.UdpHeader udpHeader = packet.getHeader();
int sourcePort = udpHeader.getSrcPort().valueAsInt(); int sourcePort = udpHeader.getSrcPort().valueAsInt();
int destPort = udpHeader.getDstPort().valueAsInt(); int destPort = udpHeader.getDstPort().valueAsInt();
@@ -156,7 +158,7 @@ public abstract class AbstractPcapWorker implements PcapWorker, PacketListener {
} }
} }
private UnfinishedStream addNewPacket(InetAddress sourceIp, InetAddress destIp, long time, private UnfinishedStream addNewPacket(InetAddress sourceIp, InetAddress destIp, Instant time,
int sourcePort, int destPort, int ttl, byte[] content, Protocol protocol) { int sourcePort, int destPort, int ttl, byte[] content, Protocol protocol) {
var incoming = destIp.equals(localIp); var incoming = destIp.equals(localIp);
var stream = new UnfinishedStream(sourceIp, destIp, sourcePort, destPort, protocol); var stream = new UnfinishedStream(sourceIp, destIp, sourcePort, destPort, protocol);
@@ -222,17 +224,17 @@ public abstract class AbstractPcapWorker implements PcapWorker, PacketListener {
@Override @Override
@SneakyThrows @SneakyThrows
public int closeTimeoutStreams(Protocol protocol, long timeoutMillis) { public int closeTimeoutStreams(Protocol protocol, Duration timeout) {
return processorExecutorService.submit(() -> { return processorExecutorService.submit(() -> {
int streamsClosed = 0; int streamsClosed = 0;
final long time = System.currentTimeMillis(); final Instant time = Instant.now();
final var streams = (protocol == Protocol.TCP) ? this.unfinishedTcpStreams : this.unfinishedUdpStreams; final var streams = (protocol == Protocol.TCP) ? this.unfinishedTcpStreams : this.unfinishedUdpStreams;
final var oldStreams = Multimaps.asMap(streams).entrySet().stream() final var oldStreams = Multimaps.asMap(streams).entrySet().stream()
.filter(entry -> { .filter(entry -> {
final var packets = entry.getValue(); final var packets = entry.getValue();
return time - packets.get(packets.size() - 1).getTimestamp() > timeoutMillis; return Duration.between(packets.get(packets.size() - 1).getTimestamp(), time).compareTo(timeout) > 0;
}) })
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

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;
@@ -15,28 +16,27 @@ import ru.serega6531.packmate.service.SubscriptionService;
import java.io.EOFException; import java.io.EOFException;
import java.io.File; import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Arrays;
@Slf4j @Slf4j
public class FilePcapWorker extends AbstractPcapWorker { public class FilePcapWorker extends AbstractPcapWorker {
private final File directory = new File("pcaps");
private final SubscriptionService subscriptionService; private final SubscriptionService subscriptionService;
private final File file; private final File file;
public FilePcapWorker(ServicesService servicesService, public FilePcapWorker(ServicesService servicesService,
StreamService streamService, StreamService streamService,
SubscriptionService subscriptionService, SubscriptionService subscriptionService,
String localIpString, InetAddress localIp,
String filename) throws UnknownHostException { String filename) throws UnknownHostException {
super(servicesService, streamService, localIpString); super(servicesService, streamService, localIp);
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
file = new File("pcaps", filename); file = new File(directory, filename);
if (!file.exists()) { validateFileExists();
log.info("Existing files: " + Arrays.toString(new File("pcaps").listFiles()));
throw new IllegalArgumentException("File " + file.getAbsolutePath() + " does not exist");
}
processorExecutorService = new InlineExecutorService(); processorExecutorService = new InlineExecutorService();
} }
@@ -86,4 +86,10 @@ public class FilePcapWorker extends AbstractPcapWorker {
public String getExecutorState() { public String getExecutorState() {
return "inline"; return "inline";
} }
private void validateFileExists() {
if (!file.exists()) {
throw new PcapFileNotFoundException(file, directory);
}
}
} }

View File

@@ -6,10 +6,13 @@ 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.InetAddress;
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;
@@ -21,14 +24,14 @@ public class LivePcapWorker extends AbstractPcapWorker {
public LivePcapWorker(ServicesService servicesService, public LivePcapWorker(ServicesService servicesService,
StreamService streamService, StreamService streamService,
String localIpString, InetAddress localIp,
String interfaceName) throws PcapNativeException, UnknownHostException { String interfaceName) throws PcapNativeException, UnknownHostException {
super(servicesService, streamService, localIpString); super(servicesService, streamService, localIp);
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,12 @@
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;
import java.time.Duration;
public class NoOpPcapWorker implements PcapWorker { public class NoOpPcapWorker implements PcapWorker {
@Override @Override
public void start() throws PcapNativeException { public void start() {
} }
@Override @Override
@@ -17,7 +18,7 @@ public class NoOpPcapWorker implements PcapWorker {
} }
@Override @Override
public int closeTimeoutStreams(Protocol protocol, long timeoutMillis) { public int closeTimeoutStreams(Protocol protocol, Duration timeout) {
return 0; return 0;
} }

View File

@@ -3,6 +3,8 @@ package ru.serega6531.packmate.pcap;
import org.pcap4j.core.PcapNativeException; import org.pcap4j.core.PcapNativeException;
import ru.serega6531.packmate.model.enums.Protocol; import ru.serega6531.packmate.model.enums.Protocol;
import java.time.Duration;
public interface PcapWorker { public interface PcapWorker {
void start() throws PcapNativeException; void start() throws PcapNativeException;
@@ -16,7 +18,7 @@ public interface PcapWorker {
/** /**
* Выполняется в потоке обработчика * Выполняется в потоке обработчика
*/ */
int closeTimeoutStreams(Protocol protocol, long timeoutMillis); int closeTimeoutStreams(Protocol protocol, Duration timeout);
void setFilter(String filter); void setFilter(String filter);

View File

@@ -0,0 +1,39 @@
package ru.serega6531.packmate.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import ru.serega6531.packmate.model.enums.CaptureMode;
import java.net.InetAddress;
import java.time.Duration;
@ConfigurationProperties("packmate")
public record PackmateProperties(
CaptureMode captureMode,
String interfaceName,
String pcapFile,
InetAddress localIp,
WebProperties web,
TimeoutProperties timeout,
CleanupProperties cleanup,
boolean ignoreEmptyPackets
) {
public record WebProperties(
String accountLogin,
String accountPassword
) {}
public record TimeoutProperties(
Duration udpStreamTimeout,
Duration tcpStreamTimeout,
int checkInterval
){}
public record CleanupProperties(
boolean enabled,
int threshold,
int interval
){}
}

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

@@ -1,21 +1,26 @@
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.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.properties.PackmateProperties;
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 java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.util.HashMap;
import java.util.*; import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service @Service
@Slf4j @Slf4j
@@ -35,12 +40,12 @@ public class ServicesService {
SubscriptionService subscriptionService, SubscriptionService subscriptionService,
@Lazy PcapService pcapService, @Lazy PcapService pcapService,
ModelMapper modelMapper, ModelMapper modelMapper,
@Value("${local-ip}") String localIpString) throws UnknownHostException { PackmateProperties properties) {
this.repository = repository; this.repository = repository;
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
this.pcapService = pcapService; this.pcapService = pcapService;
this.modelMapper = modelMapper; this.modelMapper = modelMapper;
this.localIp = InetAddress.getByName(localIpString); this.localIp = properties.localIp();
} }
@PostConstruct @PostConstruct
@@ -67,8 +72,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 +90,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 +122,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

@@ -4,7 +4,6 @@ import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
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.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@@ -13,11 +12,20 @@ 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.properties.PackmateProperties;
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");
@@ -54,7 +60,7 @@ public class StreamService {
SubscriptionService subscriptionService, SubscriptionService subscriptionService,
RsaKeysHolder keysHolder, RsaKeysHolder keysHolder,
ModelMapper modelMapper, ModelMapper modelMapper,
@Value("${ignore-empty-packets}") boolean ignoreEmptyPackets) { PackmateProperties properties) {
this.repository = repository; this.repository = repository;
this.patternService = patternService; this.patternService = patternService;
this.servicesService = servicesService; this.servicesService = servicesService;
@@ -62,7 +68,7 @@ public class StreamService {
this.subscriptionService = subscriptionService; this.subscriptionService = subscriptionService;
this.keysHolder = keysHolder; this.keysHolder = keysHolder;
this.modelMapper = modelMapper; this.modelMapper = modelMapper;
this.ignoreEmptyPackets = ignoreEmptyPackets; this.ignoreEmptyPackets = properties.ignoreEmptyPackets();
} }
/** /**
@@ -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

@@ -1,180 +0,0 @@
package ru.serega6531.packmate.service.optimization;
import com.google.common.primitives.Bytes;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import ru.serega6531.packmate.model.Packet;
import ru.serega6531.packmate.utils.BytesUtils;
import ru.serega6531.packmate.utils.PacketUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
public class HttpChunksProcessor {
private static final String CHUNKED_HTTP_HEADER = "transfer-encoding: chunked\r\n";
private final List<Packet> packets;
private int position;
private boolean chunkStarted = false;
private final List<Packet> chunkPackets = new ArrayList<>();
public void processChunkedEncoding() {
int start = -1;
for (position = 0; position < packets.size(); position++) {
Packet packet = packets.get(position);
if (!packet.isIncoming()) {
String content = packet.getContentString();
boolean http = content.startsWith("HTTP/");
int contentPos = content.indexOf("\r\n\r\n");
if (http && contentPos != -1) { // начало body
String headers = content.substring(0, contentPos + 2); // захватываем первые \r\n
boolean chunked = headers.toLowerCase().contains(CHUNKED_HTTP_HEADER);
if (chunked) {
chunkStarted = true;
start = position;
chunkPackets.add(packet);
checkCompleteChunk(chunkPackets, start);
} else {
chunkStarted = false;
chunkPackets.clear();
}
} else if (chunkStarted) {
chunkPackets.add(packet);
checkCompleteChunk(chunkPackets, start);
}
}
}
}
private void checkCompleteChunk(List<Packet> packets, int start) {
boolean end = Arrays.equals(packets.get(packets.size() - 1).getContent(), "0\r\n\r\n".getBytes()) ||
BytesUtils.endsWith(packets.get(packets.size() - 1).getContent(), "\r\n0\r\n\r\n".getBytes());
if (end) {
processChunk(packets, start);
}
}
@SneakyThrows
private void processChunk(List<Packet> packets, int start) {
//noinspection OptionalGetWithoutIsPresent
final byte[] content = PacketUtils.mergePackets(packets).get();
ByteArrayOutputStream output = new ByteArrayOutputStream(content.length);
final int contentStart = Bytes.indexOf(content, "\r\n\r\n".getBytes()) + 4;
output.write(content, 0, contentStart);
ByteBuffer buf = ByteBuffer.wrap(Arrays.copyOfRange(content, contentStart, content.length));
while (true) {
final int chunkSize = readChunkSize(buf);
switch (chunkSize) {
case -1 -> {
log.warn("Failed to merge chunks, next chunk size not found");
resetChunk();
return;
}
case 0 -> {
buildWholePacket(packets, start, output);
return;
}
default -> {
if (!readChunk(buf, chunkSize, output)) return;
if (!readTrailer(buf)) return;
}
}
}
}
private boolean readChunk(ByteBuffer buf, int chunkSize, ByteArrayOutputStream output) throws IOException {
if (chunkSize > buf.remaining()) {
log.warn("Failed to merge chunks, chunk size too big: {} + {} > {}",
buf.position(), chunkSize, buf.capacity());
resetChunk();
return false;
}
byte[] chunk = new byte[chunkSize];
buf.get(chunk);
output.write(chunk);
return true;
}
private boolean readTrailer(ByteBuffer buf) {
if (buf.remaining() < 2) {
log.warn("Failed to merge chunks, chunk doesn't end with \\r\\n");
resetChunk();
return false;
}
int c1 = buf.get();
int c2 = buf.get();
if (c1 != '\r' || c2 != '\n') {
log.warn("Failed to merge chunks, chunk trailer is not equal to \\r\\n");
resetChunk();
return false;
}
return true;
}
private void buildWholePacket(List<Packet> packets, int start, ByteArrayOutputStream output) {
Packet result = Packet.builder()
.incoming(false)
.timestamp(packets.get(0).getTimestamp())
.ungzipped(false)
.webSocketParsed(false)
.tlsDecrypted(packets.get(0).isTlsDecrypted())
.content(output.toByteArray())
.build();
this.packets.removeAll(packets);
this.packets.add(start, result);
resetChunk();
position = start + 1;
}
private void resetChunk() {
chunkStarted = false;
chunkPackets.clear();
}
private int readChunkSize(ByteBuffer buf) {
StringBuilder sb = new StringBuilder();
while (buf.remaining() > 2) {
byte b = buf.get();
if ((b >= '0' && b <= '9') || (b >= 'a' && b <= 'f')) {
sb.append((char) b);
} else if (b == '\r') {
if (buf.get() == '\n') {
return Integer.parseInt(sb.toString(), 16);
} else {
return -1; // после \r не идет \n
}
} else {
return -1;
}
}
return -1;
}
}

View File

@@ -1,121 +0,0 @@
package ru.serega6531.packmate.service.optimization;
import com.google.common.primitives.Bytes;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import ru.serega6531.packmate.model.Packet;
import ru.serega6531.packmate.utils.PacketUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipException;
@Slf4j
@RequiredArgsConstructor
public class HttpGzipProcessor {
private static final String GZIP_HTTP_HEADER = "content-encoding: gzip\r\n";
private static final byte[] GZIP_HEADER = {0x1f, (byte) 0x8b, 0x08};
private final List<Packet> packets;
boolean gzipStarted = false;
private int position;
/**
* Попытаться распаковать GZIP из исходящих http пакетов. <br>
* GZIP поток начинается на найденном HTTP пакете с заголовком Content-Encoding: gzip
* (при этом заголовок HTTP может быть в другом пакете)<br>
* Поток заканчивается при обнаружении нового HTTP заголовка,
* при смене стороны передачи или при окончании всего стрима
*/
public void unpackGzip() {
int gzipStartPacket = 0;
for (position = 0; position < packets.size(); position++) {
Packet packet = packets.get(position);
if (packet.isIncoming() && gzipStarted) { // поток gzip закончился
extractGzip(gzipStartPacket, position - 1);
} else if (!packet.isIncoming()) {
String content = packet.getContentString();
int contentPos = content.indexOf("\r\n\r\n");
boolean http = content.startsWith("HTTP/");
if (http && gzipStarted) { // начался новый http пакет, заканчиваем старый gzip поток
extractGzip(gzipStartPacket, position - 1);
}
if (contentPos != -1) { // начало body
String headers = content.substring(0, contentPos + 2); // захватываем первые \r\n
boolean gziped = headers.toLowerCase().contains(GZIP_HTTP_HEADER);
if (gziped) {
gzipStarted = true;
gzipStartPacket = position;
}
}
}
}
if (gzipStarted) { // стрим закончился gzip пакетом
extractGzip(gzipStartPacket, packets.size() - 1);
}
}
/**
* Попытаться распаковать кусок пакетов с gzip body и вставить результат на их место
*/
private void extractGzip(int gzipStartPacket, int gzipEndPacket) {
List<Packet> cut = packets.subList(gzipStartPacket, gzipEndPacket + 1);
Packet decompressed = decompressGzipPackets(cut);
if (decompressed != null) {
packets.removeAll(cut);
packets.add(gzipStartPacket, decompressed);
gzipStarted = false;
position = gzipStartPacket + 1; // продвигаем указатель на следующий после склеенного блок
}
}
private Packet decompressGzipPackets(List<Packet> cut) {
//noinspection OptionalGetWithoutIsPresent
final byte[] content = PacketUtils.mergePackets(cut).get();
final int gzipStart = Bytes.indexOf(content, GZIP_HEADER);
byte[] httpHeader = Arrays.copyOfRange(content, 0, gzipStart);
byte[] gzipBytes = Arrays.copyOfRange(content, gzipStart, content.length);
try {
final GZIPInputStream gzipStream = new GZIPInputStream(new ByteArrayInputStream(gzipBytes));
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(gzipStream, out);
byte[] newContent = ArrayUtils.addAll(httpHeader, out.toByteArray());
log.debug("GZIP decompressed: {} -> {} bytes", gzipBytes.length, out.size());
return Packet.builder()
.incoming(false)
.timestamp(cut.get(0).getTimestamp())
.ungzipped(true)
.webSocketParsed(false)
.tlsDecrypted(cut.get(0).isTlsDecrypted())
.content(newContent)
.build();
} catch (ZipException e) {
log.warn("Failed to decompress gzip, leaving as it is: {}", e.getMessage());
} catch (IOException e) {
log.error("decompress gzip", e);
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
package ru.serega6531.packmate.service.optimization;
import lombok.extern.slf4j.Slf4j;
import rawhttp.core.HttpMessage;
import rawhttp.core.RawHttp;
import rawhttp.core.RawHttpOptions;
import rawhttp.core.body.BodyReader;
import rawhttp.core.errors.InvalidHttpHeader;
import rawhttp.core.errors.InvalidHttpRequest;
import rawhttp.core.errors.InvalidHttpResponse;
import rawhttp.core.errors.InvalidMessageFrame;
import rawhttp.core.errors.UnknownEncodingException;
import ru.serega6531.packmate.model.Packet;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
@Slf4j
public class HttpProcessor {
private static final RawHttp rawHttp = new RawHttp(RawHttpOptions.strict());
public void process(List<Packet> packets) {
packets.stream()
.filter(p -> !p.isWebSocketParsed())
.forEach(this::processPacket);
}
private void processPacket(Packet packet) {
try {
ByteArrayInputStream contentStream = new ByteArrayInputStream(packet.getContent());
HttpMessage message;
if (packet.isIncoming()) {
message = rawHttp.parseRequest(contentStream).eagerly();
} else {
message = rawHttp.parseResponse(contentStream).eagerly();
}
packet.setContent(getDecodedMessage(message));
packet.setHasHttpBody(message.getBody().isPresent());
} catch (IOException | InvalidHttpRequest | InvalidHttpResponse | InvalidHttpHeader | InvalidMessageFrame |
UnknownEncodingException e) {
log.warn("Could not parse http packet", e);
}
}
private byte[] getDecodedMessage(HttpMessage message) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream(256);
message.getStartLine().writeTo(os);
message.getHeaders().writeTo(os);
Optional<? extends BodyReader> body = message.getBody();
if (body.isPresent()) {
body.get().writeDecodedTo(os, 256);
}
return os.toByteArray();
}
}

View File

@@ -1,6 +1,5 @@
package ru.serega6531.packmate.service.optimization; package ru.serega6531.packmate.service.optimization;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import ru.serega6531.packmate.model.Packet; import ru.serega6531.packmate.model.Packet;
@@ -9,17 +8,14 @@ import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
@AllArgsConstructor
@Slf4j @Slf4j
public class HttpUrldecodeProcessor { public class HttpUrldecodeProcessor {
private final List<Packet> packets;
/** /**
* Декодирование urlencode с http пакета до смены стороны или окончания стрима * Декодирование urlencode с http пакета до смены стороны или окончания стрима
*/ */
@SneakyThrows @SneakyThrows
public void urldecodeRequests() { public void urldecodeRequests(List<Packet> packets) {
boolean httpStarted = false; boolean httpStarted = false;
for (Packet packet : packets) { for (Packet packet : packets) {

View File

@@ -1,30 +1,26 @@
package ru.serega6531.packmate.service.optimization; package ru.serega6531.packmate.service.optimization;
import lombok.AllArgsConstructor;
import ru.serega6531.packmate.model.Packet; import ru.serega6531.packmate.model.Packet;
import ru.serega6531.packmate.utils.PacketUtils; import ru.serega6531.packmate.utils.PacketUtils;
import java.time.Instant;
import java.util.List; import java.util.List;
@AllArgsConstructor
public class PacketsMerger { public class PacketsMerger {
private final List<Packet> packets;
/** /**
* Сжать соседние пакеты в одном направлении в один. * Сжать соседние пакеты в одном направлении в один. Не склеивает WS и не-WS пакеты.
* Выполняется после других оптимизаций чтобы правильно определять границы пакетов.
*/ */
public void mergeAdjacentPackets() { public void mergeAdjacentPackets(List<Packet> packets) {
int start = 0; int start = 0;
int packetsInRow = 0; int packetsInRow = 0;
boolean incoming = true; Packet previous = null;
for (int i = 0; i < packets.size(); i++) { for (int i = 0; i < packets.size(); i++) {
Packet packet = packets.get(i); Packet packet = packets.get(i);
if (packet.isIncoming() != incoming) { if (previous == null || !shouldBeInSameBatch(packet, previous)) {
if (packetsInRow > 1) { if (packetsInRow > 1) {
compress(start, i); compress(packets, start, i);
i = start + 1; // продвигаем указатель на следующий после склеенного блок i = start + 1; // продвигаем указатель на следующий после склеенного блок
} }
@@ -34,36 +30,40 @@ public class PacketsMerger {
packetsInRow++; packetsInRow++;
} }
incoming = packet.isIncoming(); previous = packet;
} }
if (packetsInRow > 1) { if (packetsInRow > 1) {
compress(start, packets.size()); compress(packets, start, packets.size());
} }
} }
/** /**
* Сжать кусок со start по end в один пакет * Сжать кусок со start по end в один пакет
*/ */
private void compress(int start, int end) { private void compress(List<Packet> packets, int start, int end) {
final List<Packet> cut = packets.subList(start, end); final List<Packet> cut = packets.subList(start, end);
final long timestamp = cut.get(0).getTimestamp(); final Instant timestamp = cut.get(0).getTimestamp();
final boolean ungzipped = cut.stream().anyMatch(Packet::isUngzipped); final boolean httpProcessed = cut.stream().anyMatch(Packet::isHttpProcessed);
final boolean webSocketParsed = cut.stream().anyMatch(Packet::isWebSocketParsed); final boolean webSocketParsed = cut.stream().anyMatch(Packet::isWebSocketParsed);
final boolean tlsDecrypted = cut.get(0).isTlsDecrypted(); final boolean tlsDecrypted = cut.get(0).isTlsDecrypted();
final boolean incoming = cut.get(0).isIncoming(); final boolean incoming = cut.get(0).isIncoming();
//noinspection OptionalGetWithoutIsPresent final byte[] content = PacketUtils.mergePackets(cut);
final byte[] content = PacketUtils.mergePackets(cut).get();
packets.removeAll(cut); packets.removeAll(cut);
packets.add(start, Packet.builder() packets.add(start, Packet.builder()
.incoming(incoming) .incoming(incoming)
.timestamp(timestamp) .timestamp(timestamp)
.ungzipped(ungzipped) .httpProcessed(httpProcessed)
.webSocketParsed(webSocketParsed) .webSocketParsed(webSocketParsed)
.tlsDecrypted(tlsDecrypted) .tlsDecrypted(tlsDecrypted)
.content(content) .content(content)
.build()); .build());
} }
private boolean shouldBeInSameBatch(Packet p1, Packet p2) {
return p1.isIncoming() == p2.isIncoming() &&
p1.isWebSocketParsed() == p2.isWebSocketParsed();
}
} }

View File

@@ -1,7 +1,6 @@
package ru.serega6531.packmate.service.optimization; package ru.serega6531.packmate.service.optimization;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import ru.serega6531.packmate.model.CtfService; import ru.serega6531.packmate.model.CtfService;
import ru.serega6531.packmate.model.Packet; import ru.serega6531.packmate.model.Packet;
@@ -16,6 +15,11 @@ public class StreamOptimizer {
private final CtfService service; private final CtfService service;
private List<Packet> packets; private List<Packet> packets;
private final PacketsMerger merger = new PacketsMerger();
private final HttpUrldecodeProcessor urldecodeProcessor = new HttpUrldecodeProcessor();
private final HttpProcessor httpProcessor = new HttpProcessor();
/** /**
* Вызвать для выполнения оптимизаций на переданном списке пакетов. * Вызвать для выполнения оптимизаций на переданном списке пакетов.
*/ */
@@ -29,51 +33,42 @@ public class StreamOptimizer {
} }
} }
if (service.isProcessChunkedEncoding()) {
try {
processChunkedEncoding();
} catch (Exception e) {
log.warn("Error optimizing stream (chunks)", e);
return packets;
}
}
if (service.isUngzipHttp()) {
try {
unpackGzip();
} catch (Exception e) {
log.warn("Error optimizing stream (gzip)", e);
return packets;
}
}
if (service.isParseWebSockets()) { if (service.isParseWebSockets()) {
try { try {
parseWebSockets(); parseWebSockets();
} catch (Exception e) { } catch (Exception e) {
log.warn("Error optimizing stream (websocketss)", e); log.warn("Error optimizing stream (websockets)", e);
return packets; return packets;
} }
} }
if (service.isUrldecodeHttpRequests()) { if (service.isUrldecodeHttpRequests()) {
try { try {
urldecodeRequests(); urldecodeProcessor.urldecodeRequests(packets);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error optimizing stream (urldecode)", e); log.warn("Error optimizing stream (urldecode)", e);
return packets; return packets;
} }
} }
if (service.isMergeAdjacentPackets()) { if (service.isMergeAdjacentPackets() || service.isHttp()) {
try { try {
mergeAdjacentPackets(); merger.mergeAdjacentPackets(packets);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error optimizing stream (adjacent)", e); log.warn("Error optimizing stream (adjacent)", e);
return packets; return packets;
} }
} }
if (service.isHttp()) {
try {
httpProcessor.process(packets);
} catch (Exception e) {
log.warn("Error optimizing stream (http)", e);
return packets;
}
}
return packets; return packets;
} }
@@ -86,44 +81,6 @@ public class StreamOptimizer {
} }
} }
/**
* Сжать соседние пакеты в одном направлении в один.
* Выполняется после других оптимизаций чтобы правильно определять границы пакетов.
*/
private void mergeAdjacentPackets() {
final PacketsMerger merger = new PacketsMerger(packets);
merger.mergeAdjacentPackets();
}
/**
* Декодирование urlencode с http пакета до смены стороны или окончания стрима
*/
@SneakyThrows
private void urldecodeRequests() {
final HttpUrldecodeProcessor processor = new HttpUrldecodeProcessor(packets);
processor.urldecodeRequests();
}
/**
* <a href="https://ru.wikipedia.org/wiki/Chunked_transfer_encoding">Chunked transfer encoding</a>
*/
private void processChunkedEncoding() {
HttpChunksProcessor processor = new HttpChunksProcessor(packets);
processor.processChunkedEncoding();
}
/**
* Попытаться распаковать GZIP из исходящих http пакетов. <br>
* GZIP поток начинается на найденном HTTP пакете с заголовком Content-Encoding: gzip
* (при этом заголовок HTTP может быть в другом пакете)<br>
* Поток заканчивается при обнаружении нового HTTP заголовка,
* при смене стороны передачи или при окончании всего стрима
*/
private void unpackGzip() {
final HttpGzipProcessor processor = new HttpGzipProcessor(packets);
processor.unpackGzip();
}
private void parseWebSockets() { private void parseWebSockets() {
if (!packets.get(0).getContentString().contains("HTTP/")) { if (!packets.get(0).getContentString().contains("HTTP/")) {
return; return;

View File

@@ -182,15 +182,12 @@ public class TlsDecryptor {
decoded = clearDecodedData(decoded); decoded = clearDecodedData(decoded);
result.add(Packet.builder() result.add(
packet.toBuilder()
.content(decoded) .content(decoded)
.incoming(packet.isIncoming())
.timestamp(packet.getTimestamp())
.ungzipped(false)
.webSocketParsed(false)
.tlsDecrypted(true) .tlsDecrypted(true)
.ttl(packet.getTtl()) .build()
.build()); );
} }
} }
} }

View File

@@ -120,14 +120,9 @@ public class WebSocketsParser {
} }
private Packet mimicPacket(Packet packet, byte[] content, boolean ws) { private Packet mimicPacket(Packet packet, byte[] content, boolean ws) {
return Packet.builder() return packet.toBuilder()
.content(content) .content(content)
.incoming(packet.isIncoming())
.timestamp(packet.getTimestamp())
.ttl(packet.getTtl())
.ungzipped(packet.isUngzipped())
.webSocketParsed(ws) .webSocketParsed(ws)
.tlsDecrypted(packet.isTlsDecrypted())
.build(); .build();
} }
@@ -138,8 +133,7 @@ public class WebSocketsParser {
for (List<Packet> side : sides) { for (List<Packet> side : sides) {
final Packet lastPacket = side.get(0); final Packet lastPacket = side.get(0);
//noinspection OptionalGetWithoutIsPresent final byte[] wsContent = PacketUtils.mergePackets(side);
final byte[] wsContent = PacketUtils.mergePackets(side).get();
final ByteBuffer buffer = ByteBuffer.wrap(wsContent); final ByteBuffer buffer = ByteBuffer.wrap(wsContent);
List<Framedata> frames; List<Framedata> frames;
@@ -153,14 +147,10 @@ public class WebSocketsParser {
for (Framedata frame : frames) { for (Framedata frame : frames) {
if (frame instanceof DataFrame) { if (frame instanceof DataFrame) {
parsedPackets.add(Packet.builder() parsedPackets.add(
lastPacket.toBuilder()
.content(frame.getPayloadData().array()) .content(frame.getPayloadData().array())
.incoming(lastPacket.isIncoming())
.timestamp(lastPacket.getTimestamp())
.ttl(lastPacket.getTtl())
.ungzipped(lastPacket.isUngzipped())
.webSocketParsed(true) .webSocketParsed(true)
.tlsDecrypted(lastPacket.isTlsDecrypted())
.build() .build()
); );
} }
@@ -179,13 +169,10 @@ public class WebSocketsParser {
} }
private String getHandshake(final List<Packet> packets) { private String getHandshake(final List<Packet> packets) {
final String handshake = PacketUtils.mergePackets(packets) final String handshake = new String(PacketUtils.mergePackets(packets));
.map(String::new)
.orElse(null);
if (handshake == null || if (!handshake.toLowerCase().contains(WEBSOCKET_CONNECTION_HEADER)
!handshake.toLowerCase().contains(WEBSOCKET_CONNECTION_HEADER) || || !handshake.toLowerCase().contains(WEBSOCKET_UPGRADE_HEADER)) {
!handshake.toLowerCase().contains(WEBSOCKET_UPGRADE_HEADER)) {
return null; return null;
} }

View File

@@ -1,31 +1,30 @@
package ru.serega6531.packmate.tasks; 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.boot.autoconfigure.condition.ConditionalOnExpression; 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.properties.PackmateProperties;
import ru.serega6531.packmate.service.StreamService; import ru.serega6531.packmate.service.StreamService;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
@Component @Component
@Slf4j @Slf4j
@ConditionalOnExpression("${old-streams-cleanup-enabled:false} && '${capture-mode}' == 'LIVE'") @ConditionalOnExpression("${packmate.cleanup.enabled:false} && '${packmate.capture-mode}' == 'LIVE'")
public class OldStreamsCleanupTask { public class OldStreamsCleanupTask {
private final StreamService service; private final StreamService service;
private final int oldStreamsThreshold; private final int oldStreamsThreshold;
public OldStreamsCleanupTask(StreamService service, @Value("${old-streams-threshold}") int oldStreamsThreshold) { public OldStreamsCleanupTask(StreamService service, PackmateProperties properties) {
this.service = service; this.service = service;
this.oldStreamsThreshold = oldStreamsThreshold; this.oldStreamsThreshold = properties.cleanup().threshold();
} }
@Scheduled(fixedDelayString = "PT${cleanup-interval}M", initialDelayString = "PT1M") @Scheduled(fixedDelayString = "PT${packmate.cleanup.interval}M", initialDelayString = "PT1M")
public void cleanup() { public void cleanup() {
ZonedDateTime before = ZonedDateTime.now().minus(oldStreamsThreshold, ChronoUnit.MINUTES); ZonedDateTime before = ZonedDateTime.now().minusMinutes(oldStreamsThreshold);
log.info("Cleaning up old non-favorite streams (before {})", before); log.info("Cleaning up old non-favorite streams (before {})", before);
long deleted = service.cleanupOldStreams(before); long deleted = service.cleanupOldStreams(before);
log.info("Deleted {} rows", deleted); log.info("Deleted {} rows", deleted);

View File

@@ -1,10 +1,10 @@
package ru.serega6531.packmate.tasks; package ru.serega6531.packmate.tasks;
import org.pcap4j.core.PcapNativeException; import org.pcap4j.core.PcapNativeException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.serega6531.packmate.properties.PackmateProperties;
import ru.serega6531.packmate.model.enums.CaptureMode; import ru.serega6531.packmate.model.enums.CaptureMode;
import ru.serega6531.packmate.service.PcapService; import ru.serega6531.packmate.service.PcapService;
import ru.serega6531.packmate.service.ServicesService; import ru.serega6531.packmate.service.ServicesService;
@@ -12,29 +12,23 @@ import ru.serega6531.packmate.service.ServicesService;
@Component @Component
public class StartupListener { public class StartupListener {
@Value("${enable-capture}") private final PackmateProperties packmateProperties;
private boolean enableCapture;
@Value("${capture-mode}")
private CaptureMode captureMode;
private final PcapService pcapService; private final PcapService pcapService;
private final ServicesService servicesService; private final ServicesService servicesService;
public StartupListener(PcapService pcapService, ServicesService servicesService) { public StartupListener(PcapService pcapService, ServicesService servicesService, PackmateProperties packmateProperties) {
this.pcapService = pcapService; this.pcapService = pcapService;
this.servicesService = servicesService; this.servicesService = servicesService;
this.packmateProperties = packmateProperties;
} }
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
public void afterStartup() throws PcapNativeException { public void afterStartup() throws PcapNativeException {
if (enableCapture) {
servicesService.updateFilter(); servicesService.updateFilter();
if (captureMode == CaptureMode.LIVE) { if (packmateProperties.captureMode() == CaptureMode.LIVE) {
pcapService.start(); pcapService.start();
} }
} }
}
} }

View File

@@ -2,34 +2,33 @@ package ru.serega6531.packmate.tasks;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.model.enums.Protocol; import ru.serega6531.packmate.model.enums.Protocol;
import ru.serega6531.packmate.pcap.PcapWorker; import ru.serega6531.packmate.pcap.PcapWorker;
import ru.serega6531.packmate.properties.PackmateProperties;
import java.util.concurrent.TimeUnit; import java.time.Duration;
@Component @Component
@Slf4j @Slf4j
@ConditionalOnProperty(name = "capture-mode", havingValue = "LIVE") @ConditionalOnProperty(name = "packmate.capture-mode", havingValue = "LIVE")
public class TimeoutStreamsSaver { public class TimeoutStreamsSaver {
private final PcapWorker pcapWorker; private final PcapWorker pcapWorker;
private final long udpStreamTimeoutMillis; private final Duration udpStreamTimeoutMillis;
private final long tcpStreamTimeoutMillis; private final Duration tcpStreamTimeoutMillis;
@Autowired @Autowired
public TimeoutStreamsSaver(PcapWorker pcapWorker, public TimeoutStreamsSaver(PcapWorker pcapWorker,
@Value("${udp-stream-timeout}") int udpStreamTimeout, PackmateProperties properties) {
@Value("${tcp-stream-timeout}") int tcpStreamTimeout) {
this.pcapWorker = pcapWorker; this.pcapWorker = pcapWorker;
this.udpStreamTimeoutMillis = TimeUnit.SECONDS.toMillis(udpStreamTimeout); this.udpStreamTimeoutMillis = properties.timeout().udpStreamTimeout();
this.tcpStreamTimeoutMillis = TimeUnit.SECONDS.toMillis(tcpStreamTimeout); this.tcpStreamTimeoutMillis = properties.timeout().tcpStreamTimeout();
} }
@Scheduled(fixedRateString = "PT${timeout-stream-check-interval}S", initialDelayString = "PT${timeout-stream-check-interval}S") @Scheduled(fixedRateString = "PT${packmate.timeout.check-interval}S", initialDelayString = "PT${packmate.timeout.check-interval}S")
public void saveStreams() { public void saveStreams() {
int streamsClosed = pcapWorker.closeTimeoutStreams(Protocol.UDP, udpStreamTimeoutMillis); int streamsClosed = pcapWorker.closeTimeoutStreams(Protocol.UDP, udpStreamTimeoutMillis);
if (streamsClosed > 0) { if (streamsClosed > 0) {

View File

@@ -1,20 +1,28 @@
package ru.serega6531.packmate.utils; package ru.serega6531.packmate.utils;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.ArrayUtils;
import ru.serega6531.packmate.model.Packet; import ru.serega6531.packmate.model.Packet;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
@UtilityClass @UtilityClass
public class PacketUtils { public class PacketUtils {
public Optional<byte[]> mergePackets(List<Packet> cut) { public byte[] mergePackets(List<Packet> cut) {
return cut.stream() int size = cut.stream()
.map(Packet::getContent) .map(Packet::getContent)
.reduce(ArrayUtils::addAll); .mapToInt(c -> c.length)
.sum();
ByteArrayOutputStream os = new ByteArrayOutputStream(size);
cut.stream()
.map(Packet::getContent)
.forEach(os::writeBytes);
return os.toByteArray();
} }
public List<List<Packet>> sliceToSides(List<Packet> packets) { public List<List<Packet>> sliceToSides(List<Packet> packets) {

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

@@ -1,6 +1,6 @@
spring: spring:
datasource: datasource:
url: "jdbc:postgresql://localhost/packmate" url: "jdbc:postgresql://localhost:5432/packmate"
username: "packmate" username: "packmate"
password: "123456" password: "123456"
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
@@ -12,22 +12,27 @@ 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
server:
compression:
enabled: true
min-response-size: 1KB
enable-capture: true packmate:
capture-mode: LIVE # LIVE, FILE, VIEW capture-mode: LIVE # LIVE, FILE, VIEW
interface-name: enp0s31f6 interface-name: enp0s31f6
pcap-file: file.pcap pcap-file: file.pcap
local-ip: "192.168.0.125" local-ip: "192.168.0.125"
account-login: BinaryBears web:
account-password: 123456 account-login: BinaryBears
udp-stream-timeout: 20 # seconds account-password: 123456
tcp-stream-timeout: 40 # seconds timeout:
timeout-stream-check-interval: 10 # seconds udp-stream-timeout: 20S
old-streams-cleanup-enabled: true tcp-stream-timeout: 40S
old-streams-threshold: 240 # minutes check-interval: 10 # seconds
cleanup-interval: 5 # minutes cleanup:
ignore-empty-packets: true enabled: true
threshold: 240 # minutes
interval: 5 # minutes
ignore-empty-packets: true

View File

@@ -3,8 +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.HttpProcessor;
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;
@@ -27,18 +26,18 @@ class StreamOptimizerTest {
List<Packet> list = new ArrayList<>(); List<Packet> list = new ArrayList<>();
list.add(p); list.add(p);
new HttpGzipProcessor(list).unpackGzip(); new HttpProcessor().process(list);
final String processed = list.get(0).getContentString(); final String processed = list.get(0).getContentString();
assertTrue(processed.contains("aaabbb")); assertTrue(processed.contains("aaabbb"));
} }
@Test @Test
void testUrldecodeRequests() { void testUrldecodeRequests() {
Packet p = createPacket("GET /?q=%D0%B0+%D0%B1 HTTP/1.1\r\n\r\n".getBytes(), true); Packet p = createPacket("GET /?q=%D0%B0+%D0%B1 HTTP/1.1\r\nHost: localhost:8080\r\n\r\n".getBytes(), true);
List<Packet> list = new ArrayList<>(); List<Packet> list = new ArrayList<>();
list.add(p); list.add(p);
new HttpUrldecodeProcessor(list).urldecodeRequests(); new HttpUrldecodeProcessor().urldecodeRequests(list);
final String processed = list.get(0).getContentString(); final String processed = list.get(0).getContentString();
assertTrue(processed.contains("а б")); assertTrue(processed.contains("а б"));
} }
@@ -60,7 +59,7 @@ class StreamOptimizerTest {
list.add(p5); list.add(p5);
list.add(p6); list.add(p6);
new PacketsMerger(list).mergeAdjacentPackets(); new PacketsMerger().mergeAdjacentPackets(list);
assertEquals(4, list.size()); assertEquals(4, list.size());
assertEquals(2, list.get(1).getContent().length); assertEquals(2, list.get(1).getContent().length);
@@ -74,7 +73,7 @@ class StreamOptimizerTest {
"6\r\nChunk1\r\n6\r\nChunk2\r\n0\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))); List<Packet> packets = new ArrayList<>(List.of(createPacket(content.getBytes(), false)));
new HttpChunksProcessor(packets).processChunkedEncoding(); new HttpProcessor().process(packets);
assertEquals(1, packets.size()); assertEquals(1, packets.size());
assertTrue(packets.get(0).getContentString().contains("Chunk1Chunk2")); assertTrue(packets.get(0).getContentString().contains("Chunk1Chunk2"));