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.
This commit is contained in:
Teriuihi 2024-08-10 02:28:18 +02:00
parent fedb80f3c2
commit af44532d26
6 changed files with 196 additions and 0 deletions

View File

@ -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<ResponseEntity<String>> 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");
}
});
}
}

View File

@ -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;
}

View File

@ -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<GlobalRateLimitingFilter> rateLimitingFilter() {
FilterRegistrationBean<GlobalRateLimitingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new GlobalRateLimitingFilter());
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}
}

View File

@ -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<String, RequestCounter> 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;
}
}
}

View File

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

View File

@ -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<Optional<Boolean>> 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();
}
});
}
}