Skip to content

Commit 46352c8

Browse files
Add URL redirect filter (301/302) (#64)
* Add `RedirectRule` class to handle URL redirection logic with support for exact and wildcard path matching * Add `RedirectFilter` to handle HTTP URL redirection logic using configurable rules * WIP: Save current work * Fix HttpResponse empty constructor to initialize fields Initialize headers map and body array in empty constructor to prevent NullPointerException when setHeader() or setBody() are called. Before: HttpResponse() {} // headers = null, body = null After: HttpResponse() { this.headers = new LinkedHashMap<>(); this.body = new byte[0]; } This fix is required for RedirectFilter and any other code that creates empty HttpResponse objects and modifies them using setters. Fixes crashes in RedirectFilterTest. * Improve wildcard matching in `RedirectRule` by using `Pattern.quote` for safer and more precise regex generation. * Update `RedirectFilterTest` to include client IP address in test request creation --------- Co-authored-by: Kristina M <kristina0x7@gmail.com>
1 parent 97381b0 commit 46352c8

File tree

5 files changed

+386
-1
lines changed

5 files changed

+386
-1
lines changed

src/main/java/org/juv25d/App.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import org.juv25d.plugin.StaticFilesPlugin;
1010
import org.juv25d.router.SimpleRouter; // New import
1111
import org.juv25d.util.ConfigLoader;
12+
import org.juv25d.filter.RedirectFilter;
13+
import org.juv25d.filter.RedirectRule;
14+
import java.util.List;
1215

1316
import java.util.Set;
1417
import java.util.logging.Logger;
@@ -20,6 +23,14 @@ public static void main(String[] args) {
2023
HttpParser httpParser = new HttpParser();
2124

2225
Pipeline pipeline = new Pipeline();
26+
// Configure redirect rules
27+
List<RedirectRule> redirectRules = List.of(
28+
new RedirectRule("/old-page", "/new-page", 301),
29+
new RedirectRule("/temp", "https://example.com/temporary", 302),
30+
new RedirectRule("/docs/*", "/documentation/", 301)
31+
);
32+
pipeline.addGlobalFilter(new RedirectFilter(redirectRules), 0);
33+
2334

2435
// IP filter is enabled but configured with open access during development
2536
// White/blacklist can be tightened when specific IP restrictions are decided
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.juv25d.filter;
2+
3+
import org.juv25d.http.HttpRequest;
4+
import org.juv25d.http.HttpResponse;
5+
6+
import java.io.IOException;
7+
import java.util.List;
8+
import java.util.logging.Logger;
9+
10+
/**
11+
* Filter that handles URL redirects based on configurable rules.
12+
*
13+
* When a request matches a redirect rule:
14+
* - The pipeline is stopped (no further processing)
15+
* - A redirect response is returned with the appropriate Location header
16+
* - Status code is either 301 (Moved Permanently) or 302 (Found/Temporary)
17+
*
18+
* Rules are evaluated in order - the first matching rule wins.
19+
*
20+
* Example usage:
21+
* <pre>
22+
* List<RedirectRule> rules = List.of(
23+
* new RedirectRule("/old-page", "/new-page", 301),
24+
* new RedirectRule("/temp", "https://example.com", 302),
25+
* new RedirectRule("/docs/*", "/documentation/", 301)
26+
* );
27+
* pipeline.addFilter(new RedirectFilter(rules));
28+
* </pre>
29+
*/
30+
public class RedirectFilter implements Filter {
31+
private final List<RedirectRule> rules;
32+
private final Logger logger;
33+
34+
/**
35+
* Creates a redirect filter with the given rules.
36+
*
37+
* @param rules List of redirect rules (evaluated in order)
38+
*/
39+
public RedirectFilter(List<RedirectRule> rules) {
40+
this.rules = rules;
41+
this.logger = Logger.getLogger(RedirectFilter.class.getName());
42+
}
43+
44+
@Override
45+
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws IOException {
46+
String requestPath = request.path();
47+
48+
// Check each rule in order - first match wins
49+
for (RedirectRule rule : rules) {
50+
if (rule.matches(requestPath)) {
51+
logger.info("Redirecting: " + requestPath + " -> " + rule.getTargetUrl()
52+
+ " (" + rule.getStatusCode() + ")");
53+
54+
performRedirect(response, rule);
55+
56+
// Stop pipeline - don't call chain.doFilter()
57+
return;
58+
}
59+
}
60+
61+
// No matching rule - continue pipeline
62+
chain.doFilter(request, response);
63+
}
64+
65+
/**
66+
* Sets up the redirect response with appropriate headers.
67+
*
68+
* @param response The HTTP response to modify
69+
* @param rule The redirect rule to apply
70+
*/
71+
private void performRedirect(HttpResponse response, RedirectRule rule) {
72+
int statusCode = rule.getStatusCode();
73+
String statusText = statusCode == 301 ? "Moved Permanently" : "Found";
74+
75+
response.setStatusCode(statusCode);
76+
response.setStatusText(statusText);
77+
response.setHeader("Location", rule.getTargetUrl());
78+
response.setHeader("Content-Length", "0");
79+
80+
// Empty body for redirects
81+
response.setBody(new byte[0]);
82+
}
83+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package org.juv25d.filter;
2+
3+
import java.util.regex.Pattern;
4+
5+
/**
6+
* Represents a URL redirect rule.
7+
*
8+
* A redirect rule consists of:
9+
* - sourcePath: The path to match (supports exact match or wildcards with *)
10+
* - targetUrl: The URL to redirect to
11+
* - statusCode: HTTP status code (301 for permanent, 302 for temporary)
12+
*
13+
* Examples:
14+
* - new RedirectRule("/old-page", "/new-page", 301)
15+
* - new RedirectRule("/temp", "https://example.com", 302)
16+
* - new RedirectRule("/docs/*", "/documentation/", 301)
17+
*/
18+
public class RedirectRule {
19+
private final String sourcePath;
20+
private final String targetUrl;
21+
private final int statusCode;
22+
23+
/**
24+
* Creates a redirect rule with exact path matching.
25+
*
26+
* @param sourcePath The path to match (e.g., "/old-page" or "/docs/*" for wildcard)
27+
* @param targetUrl The URL to redirect to
28+
* @param statusCode HTTP status code (301 or 302)
29+
*/
30+
public RedirectRule(String sourcePath, String targetUrl, int statusCode) {
31+
validateStatusCode(statusCode);
32+
this.sourcePath = sourcePath;
33+
this.targetUrl = targetUrl;
34+
this.statusCode = statusCode;
35+
}
36+
37+
/**
38+
* Checks if the given request path matches this rule.
39+
*
40+
* Supports:
41+
* - Exact matching: "/old-page" matches exactly "/old-page"
42+
* - Wildcard matching: "/docs/*" matches "/docs/api", "/docs/guide", etc.
43+
*
44+
* @param requestPath The request path to check
45+
* @return true if the path matches this rule
46+
*/
47+
public boolean matches(String requestPath) {
48+
if (requestPath == null) {
49+
return false;
50+
}
51+
52+
// Check for wildcard matching
53+
if (sourcePath.contains("*")) {
54+
// Split on wildcard, escape each literal segment, then rejoin with ".*"
55+
String[] parts = sourcePath.split("\\*", -1);
56+
StringBuilder sb = new StringBuilder();
57+
for (int i = 0; i < parts.length; i++) {
58+
sb.append(Pattern.quote(parts[i]));
59+
if (i < parts.length - 1) {
60+
sb.append(".*");
61+
}
62+
}
63+
return requestPath.matches(sb.toString());
64+
}
65+
66+
// Exact match
67+
return requestPath.equals(sourcePath);
68+
}
69+
70+
public String getSourcePath() {
71+
return sourcePath;
72+
}
73+
74+
public String getTargetUrl() {
75+
return targetUrl;
76+
}
77+
78+
public int getStatusCode() {
79+
return statusCode;
80+
}
81+
82+
private void validateStatusCode(int statusCode) {
83+
if (statusCode != 301 && statusCode != 302) {
84+
throw new IllegalArgumentException(
85+
"Status code must be 301 (Moved Permanently) or 302 (Found). Got: " + statusCode
86+
);
87+
}
88+
}
89+
90+
@Override
91+
public String toString() {
92+
return "RedirectRule{" +
93+
"sourcePath='" + sourcePath + '\'' +
94+
", targetUrl='" + targetUrl + '\'' +
95+
", statusCode=" + statusCode +
96+
'}';
97+
}
98+
}

src/main/java/org/juv25d/http/HttpResponse.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ public class HttpResponse {
1414
private Map<String, String> headers;
1515
private byte[] body;
1616

17-
public HttpResponse(){}
17+
public HttpResponse(){
18+
this.headers = new LinkedHashMap<>();
19+
this.body = new byte[0];
20+
}
1821

1922
public HttpResponse(int statusCode, String statusText, Map<String, String> headers, byte[] body) {
2023
this.statusCode = statusCode;

0 commit comments

Comments
 (0)