From af44532d260db5915ca2800a7933541e8dc30d6a Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 10 Aug 2024 02:28:18 +0200 Subject: [PATCH] Add form activity checking and global rate limiting Implemented new features to track form activity and enforce global rate limits. Added a `form_active` table and created endpoints to check form activity. Also, introduced a rate-limiting filter to restrict API requests to 30 per minute per IP. --- .../form_active/FormActiveController.java | 41 ++++++++++++ .../form_active/FormActiveData.java | 18 +++++ .../global_rate_limiter/FilterConfig.java | 17 +++++ .../GlobalRateLimitingFilter.java | 67 +++++++++++++++++++ .../com/alttd/forms/database/Database.java | 10 +++ .../alttd/forms/form/FormSettingsQuery.java | 43 ++++++++++++ 6 files changed, 196 insertions(+) create mode 100644 src/main/java/com/alttd/forms/controlers/form_active/FormActiveController.java create mode 100644 src/main/java/com/alttd/forms/controlers/form_active/FormActiveData.java create mode 100644 src/main/java/com/alttd/forms/controlers/global_rate_limiter/FilterConfig.java create mode 100644 src/main/java/com/alttd/forms/controlers/global_rate_limiter/GlobalRateLimitingFilter.java create mode 100644 src/main/java/com/alttd/forms/form/FormSettingsQuery.java diff --git a/src/main/java/com/alttd/forms/controlers/form_active/FormActiveController.java b/src/main/java/com/alttd/forms/controlers/form_active/FormActiveController.java new file mode 100644 index 0000000..8919e96 --- /dev/null +++ b/src/main/java/com/alttd/forms/controlers/form_active/FormActiveController.java @@ -0,0 +1,41 @@ +package com.alttd.forms.controlers.form_active; + +import com.alttd.forms.form.FormSettingsQuery; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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 java.util.concurrent.CompletableFuture; + +@RestController +@RequestMapping("/api/checks") +public class FormActiveController { + + private static final Logger logger = LoggerFactory.getLogger(FormActiveController.class); + + @PostMapping("/formActive") + public CompletableFuture> formActiveRequest(@Valid @RequestBody FormActiveData formData, HttpServletRequest request) { + logger.debug("formActive"); + logger.trace(formData.toString()); + + FormSettingsQuery formSettingsQuery = new FormSettingsQuery(); + return formSettingsQuery + .isActive(formData.formName) + .thenApply(result -> { + if (result.isEmpty()) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to check if the form is active"); + } else if (result.get()) { + return ResponseEntity.ok("yes"); + } else { + return ResponseEntity.ok("no"); + } + }); + } +} diff --git a/src/main/java/com/alttd/forms/controlers/form_active/FormActiveData.java b/src/main/java/com/alttd/forms/controlers/form_active/FormActiveData.java new file mode 100644 index 0000000..52af249 --- /dev/null +++ b/src/main/java/com/alttd/forms/controlers/form_active/FormActiveData.java @@ -0,0 +1,18 @@ +package com.alttd.forms.controlers.form_active; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import org.hibernate.validator.constraints.Length; + +public class FormActiveData { + + public FormActiveData(String formName) { + this.formName = formName; + } + + @NotEmpty(message = "You have to provide a form name") + @Length(min = 1, max = 64, message = "Usernames have to be between 3 and 16 characters") + @Pattern(regexp = "[a-zA-Z]{1,64}", message = "This is an invalid form name") + public final String formName; + +} \ No newline at end of file diff --git a/src/main/java/com/alttd/forms/controlers/global_rate_limiter/FilterConfig.java b/src/main/java/com/alttd/forms/controlers/global_rate_limiter/FilterConfig.java new file mode 100644 index 0000000..7cdf38b --- /dev/null +++ b/src/main/java/com/alttd/forms/controlers/global_rate_limiter/FilterConfig.java @@ -0,0 +1,17 @@ +package com.alttd.forms.controlers.global_rate_limiter; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean rateLimitingFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new GlobalRateLimitingFilter()); + registrationBean.addUrlPatterns("/api/*"); + return registrationBean; + } +} \ No newline at end of file diff --git a/src/main/java/com/alttd/forms/controlers/global_rate_limiter/GlobalRateLimitingFilter.java b/src/main/java/com/alttd/forms/controlers/global_rate_limiter/GlobalRateLimitingFilter.java new file mode 100644 index 0000000..db06e40 --- /dev/null +++ b/src/main/java/com/alttd/forms/controlers/global_rate_limiter/GlobalRateLimitingFilter.java @@ -0,0 +1,67 @@ +package com.alttd.forms.controlers.global_rate_limiter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class GlobalRateLimitingFilter implements Filter { + + private static final int MAX_REQUESTS_PER_MINUTE = 30; + private final Map ipRequestMap = new ConcurrentHashMap<>(); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + String clientIp = httpRequest.getRemoteAddr(); + + RequestCounter requestCounter = ipRequestMap.computeIfAbsent(clientIp, k -> new RequestCounter(0, Instant.now().getEpochSecond())); + + synchronized (requestCounter) { + long currentTime = Instant.now().getEpochSecond(); + if (currentTime - requestCounter.timestamp > 60) { + // Reset counter every 60 seconds + requestCounter.timestamp = currentTime; + requestCounter.count.set(0); + } + + if (requestCounter.count.incrementAndGet() > MAX_REQUESTS_PER_MINUTE) { + httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + return; + } + } + + chain.doFilter(request, response); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void destroy() { + } + + private static class RequestCounter { + AtomicInteger count; + long timestamp; + + public RequestCounter(int count, long timestamp) { + this.count = new AtomicInteger(count); + this.timestamp = timestamp; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/alttd/forms/database/Database.java b/src/main/java/com/alttd/forms/database/Database.java index 06b610b..370aeb9 100644 --- a/src/main/java/com/alttd/forms/database/Database.java +++ b/src/main/java/com/alttd/forms/database/Database.java @@ -40,6 +40,16 @@ public class Database { mail VARCHAR(256), PRIMARY KEY(id) ) + """, + // language=SQL + """ + CREATE TABLE IF NOT EXISTS form_active( + id INT AUTO_INCREMENT, + name VARCHAR(64) NOT NULL, + active_from TIMESTAMP NOT NULL, + active_until TIMESTAMP NOT NULL, + PRIMARY KEY(id) + ) """ }; Connection connection = DatabaseConnection.getConnection(); diff --git a/src/main/java/com/alttd/forms/form/FormSettingsQuery.java b/src/main/java/com/alttd/forms/form/FormSettingsQuery.java new file mode 100644 index 0000000..15c5705 --- /dev/null +++ b/src/main/java/com/alttd/forms/form/FormSettingsQuery.java @@ -0,0 +1,43 @@ +package com.alttd.forms.form; + +import com.alttd.forms.database.DatabaseConnection; +import jakarta.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class FormSettingsQuery { + + private static final Logger logger = LoggerFactory.getLogger(FormSettingsQuery.class); + + public CompletableFuture> isActive(@NotNull String formName) { + return CompletableFuture.supplyAsync(() -> { + Connection connection = DatabaseConnection.getConnection(); + String sql = "SELECT active_from, active_until FROM form_active WHERE name = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, formName); + ResultSet resultSet = stmt.executeQuery(); + if (!resultSet.next()) { + return Optional.of(true); + } + Instant activeFrom = resultSet.getTimestamp("active_from").toInstant(); + Instant activeUntil = resultSet.getTimestamp("active_until").toInstant(); + Instant now = Instant.now(); + if (activeFrom.isAfter(now) || activeUntil.isBefore(now)) + return Optional.of(false); + return Optional.of(true); + } catch (SQLException e) { + logger.error("Failed to check if the {} form is active", formName ,e); + return Optional.empty(); + } + }); + } + +}