Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Empty file.
Binary file added medplat-android/.gradle/checksums/checksums.lock
Binary file not shown.
Empty file.
6 changes: 6 additions & 0 deletions medplat-web/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@
<artifactId>xstream</artifactId>
<version>1.4.20</version>
</dependency>
<!-- Rate Limiting -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.1.1</version>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.argusoft.medplat.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RateLimitConfig {

@Bean
public FilterRegistrationBean<RateLimitingFilter> rateLimitingFilter() {
FilterRegistrationBean<RateLimitingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RateLimitingFilter());
registration.addUrlPatterns("/api/*");
registration.setName("rateLimitingFilter");
registration.setOrder(1);
return registration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.argusoft.medplat.config;

import org.hibernate.SessionFactory;
import org.hibernate.query.NativeQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

@Repository
@Transactional
public class RateLimitDao {

@Autowired
private SessionFactory sessionFactory;

// Read config value from DB (e.g. RATE_LIMIT_GLOBAL)
public String getConfigValue(String key, String defaultValue) {
try {
String sql = "SELECT config_value FROM rate_limit_config WHERE config_key = :key";
NativeQuery<String> query = sessionFactory.getCurrentSession().createNativeQuery(sql);
query.setParameter("key", key);
String result = query.uniqueResult();
return result != null ? result : defaultValue;
} catch (Exception e) {
return defaultValue;
}
}

// Read sensitive endpoints list from DB
public List<String> getSensitiveEndpoints() {
try {
String sql = "SELECT endpoint_pattern FROM rate_limit_sensitive_endpoints";
NativeQuery<String> query = sessionFactory.getCurrentSession().createNativeQuery(sql);
return query.list();
} catch (Exception e) {
return List.of("/login", "/users", "/oauth", "/password");
}
}

// Save violation log to DB
public void saveViolationLog(String ipAddress, String endpoint, boolean isSensitive) {
try {
String sql = "INSERT INTO rate_limit_violation_log " +
"(ip_address, endpoint, is_sensitive, violated_at) " +
"VALUES (:ip, :endpoint, :isSensitive, :violatedAt)";
NativeQuery<?> query = sessionFactory.getCurrentSession().createNativeQuery(sql);
query.setParameter("ip", ipAddress);
query.setParameter("endpoint", endpoint);
query.setParameter("isSensitive", isSensitive);
query.setParameter("violatedAt", new Date());
query.executeUpdate();
} catch (Exception e) {
// fallback log to console if DB fails
System.err.println("[RATE LIMIT LOG FAILED] " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.argusoft.medplat.config;

import javax.persistence.*;
import java.util.Date;

@Entity
@Table(name = "rate_limit_violation_log")
public class RateLimitViolationLog {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Column(name = "ip_address")
private String ipAddress;

@Column(name = "endpoint")
private String endpoint;

@Column(name = "is_sensitive")
private Boolean isSensitive;

@Column(name = "violated_at")
@Temporal(TemporalType.TIMESTAMP)
private Date violatedAt;

public RateLimitViolationLog() {}

public RateLimitViolationLog(String ipAddress, String endpoint, Boolean isSensitive) {
this.ipAddress = ipAddress;
this.endpoint = endpoint;
this.isSensitive = isSensitive;
this.violatedAt = new Date();
}

public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public String getEndpoint() { return endpoint; }
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public Boolean getIsSensitive() { return isSensitive; }
public void setIsSensitive(Boolean isSensitive) { this.isSensitive = isSensitive; }
public Date getViolatedAt() { return violatedAt; }
public void setViolatedAt(Date violatedAt) { this.violatedAt = violatedAt; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.argusoft.medplat.config;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Order(1)
public class RateLimitingFilter implements Filter {

private static final Logger log = LoggerFactory.getLogger(RateLimitingFilter.class);

@Autowired
private RateLimitDao rateLimitDao;

private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

private Bucket createBucket(int limit) {
return Bucket.builder()
.addLimit(Bandwidth.classic(limit,
Refill.intervally(limit, Duration.ofMinutes(1))))
.build();
}

private boolean isSensitivePath(String path) {
try {
List<String> sensitiveEndpoints = rateLimitDao.getSensitiveEndpoints();
return sensitiveEndpoints.stream().anyMatch(path::contains);
} catch (Exception e) {
// fallback if DB not available
return path.contains("/login") || path.contains("/users") ||
path.contains("/oauth") || path.contains("/password");
}
}

private int getLimit(String key, String defaultValue) {
try {
String value = rateLimitDao.getConfigValue(key, defaultValue);
return Integer.parseInt(value);
} catch (Exception e) {
return Integer.parseInt(defaultValue);
}
}

@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

String ip = request.getRemoteAddr();
String path = request.getRequestURI();
boolean sensitive = isSensitivePath(path);

int limit = sensitive
? getLimit("RATE_LIMIT_SENSITIVE", "10")
: getLimit("RATE_LIMIT_GLOBAL", "100");

String bucketKey = ip + ":" + (sensitive ? "sensitive" : "global");
Bucket bucket = buckets.computeIfAbsent(bucketKey, k -> createBucket(limit));

if (bucket.tryConsume(1)) {
chain.doFilter(req, res);
} else {
// Log violation to DB
log.warn("[RATE LIMIT VIOLATED] IP={} | Path={} | Sensitive={}", ip, path, sensitive);
rateLimitDao.saveViolationLog(ip, path, sensitive);

response.setStatus(429);
response.setContentType("application/json");
response.getWriter().write(
"{\"status\": 429, " +
"\"error\": \"Too Many Requests\", " +
"\"message\": \"Rate limit exceeded. Please try again later.\"}"
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Table to store rate limit configurations
CREATE TABLE IF NOT EXISTS rate_limit_config (
id SERIAL PRIMARY KEY,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(100) NOT NULL,
description VARCHAR(255)
);

-- Default values
INSERT INTO rate_limit_config (config_key, config_value, description)
VALUES
('RATE_LIMIT_GLOBAL', '100', 'Max requests per minute per IP for all endpoints'),
('RATE_LIMIT_SENSITIVE', '10', 'Max requests per minute per IP for sensitive endpoints')
ON CONFLICT (config_key) DO NOTHING;

-- Sensitive endpoints stored in DB
CREATE TABLE IF NOT EXISTS rate_limit_sensitive_endpoints (
id SERIAL PRIMARY KEY,
endpoint_pattern VARCHAR(255) NOT NULL UNIQUE
);

INSERT INTO rate_limit_sensitive_endpoints (endpoint_pattern)
VALUES ('/login'), ('/users'), ('/oauth'), ('/password')
ON CONFLICT (endpoint_pattern) DO NOTHING;

-- Table to log rate limit violations
CREATE TABLE IF NOT EXISTS rate_limit_violation_log (
id SERIAL PRIMARY KEY,
ip_address VARCHAR(100),
endpoint VARCHAR(255),
is_sensitive BOOLEAN,
violated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
4 changes: 3 additions & 1 deletion medplat-web/src/main/resources/root-config.properties
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
appName=medplat
appName=medplat
RATE_LIMIT_GLOBAL=100
RATE_LIMIT_SENSITIVE=10