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:
parent
fedb80f3c2
commit
af44532d26
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
43
src/main/java/com/alttd/forms/form/FormSettingsQuery.java
Normal file
43
src/main/java/com/alttd/forms/form/FormSettingsQuery.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user