Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
696e1bc
updated pom.xml
Ericthilen Feb 11, 2026
de1b6cf
Add HttpRequest (method, path)
Ericthilen Feb 11, 2026
e21bc8c
Add FilterChain to execute Httpfilter pipeline
Ericthilen Feb 11, 2026
78e8df0
Add HttpResponse (status + headers + setHeader)
Ericthilen Feb 11, 2026
0ed2a79
Add RedirectRule (pattern match, targetUrl, 301/302
Ericthilen Feb 11, 2026
3e4c746
Add RedirectRulesLoader.compileSourcePattern (wildcard support
Ericthilen Feb 11, 2026
1a5ce4d
Add RedirectFilter tests (301/302/no match/wildcard)
Ericthilen Feb 11, 2026
f3f39b3
Add RedirectResponse DTO (location + statusCode)
Ericthilen Feb 11, 2026
699c9d7
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen Feb 11, 2026
f48524b
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen Feb 11, 2026
37a8231
feat: improve redirect rule parsing and validation
Ericthilen Feb 11, 2026
30f3b6c
refactor(server): extract TerminalHandler to own file
Ericthilen Feb 11, 2026
5fbd3db
fix(server): make '*' not match '/' in redirect wildcards; update test
Ericthilen Feb 11, 2026
b06171e
extract TerminalHandler from FilterChain into a interface
Ericthilen Feb 11, 2026
172671c
make RedirectRulesLoader wildcard '*' not match '/' (avoid matching s…
Ericthilen Feb 11, 2026
6513e43
removed unused RedirectResponse and .gitkeep placeholders
Ericthilen Feb 11, 2026
101d1f4
Update redirect rules and tests
viktorlindell12 Feb 20, 2026
aebd6de
Merge branch 'Url-Redirect-Filter-Update' into URL-Redirect-Filter
viktorlindell12 Feb 20, 2026
5fdac73
Update RedirectFilter.java
viktorlindell12 Feb 23, 2026
e90052d
Update RedirectRule.java
viktorlindell12 Feb 23, 2026
bb1caff
Update RedirectFilter.java
viktorlindell12 Feb 23, 2026
23df773
Update RedirectRulesLoader.java
viktorlindell12 Feb 23, 2026
ad54bc9
Update RedirectFilterTest.java
viktorlindell12 Feb 23, 2026
1d1f881
Delete pom.xml
viktorlindell12 Feb 23, 2026
ab4d5ef
Delete src/test/java/org/example/server/RedirectFilterTest.java
viktorlindell12 Feb 23, 2026
b7c5eda
Delete src/main/java/org/example/server/HttpResponse.java
viktorlindell12 Feb 23, 2026
8d348a0
Delete src/main/java/org/example/server/HttpRequest.java
viktorlindell12 Feb 23, 2026
ed50be1
Delete src/main/java/org/example/server/FilterChain.java
viktorlindell12 Feb 23, 2026
1f857d2
Delete src/main/java/org/example/server/HttpFilter.java
viktorlindell12 Feb 23, 2026
047e9c1
Delete src/main/java/org/example/server/TerminalHandler.java
viktorlindell12 Feb 23, 2026
ed9c02f
Delete src/main/java/org/example/server/RedirectRule.java
viktorlindell12 Feb 23, 2026
7e35661
Delete src/main/java/org/example/server/RedirectFilter.java
viktorlindell12 Feb 23, 2026
9b041a8
Delete src/main/java/org/example/server/RedirectRulesLoader.java
viktorlindell12 Feb 23, 2026
f40e141
Add redirect filter rules and tests
viktorlindell12 Feb 23, 2026
48bc5f8
Merge remote-tracking branch 'origin/main' into URL-Redirect-Filter
viktorlindell12 Feb 23, 2026
ebfe583
Restore pom.xml from main
viktorlindell12 Feb 23, 2026
7ad19c7
Merge remote-tracking branch 'origin/main' into feature/url-redirect-…
viktorlindell12 Feb 25, 2026
5be82bf
updated pom.xml
Ericthilen Feb 11, 2026
a44524f
Add HttpRequest (method, path)
Ericthilen Feb 11, 2026
0121f64
Add FilterChain to execute Httpfilter pipeline
Ericthilen Feb 11, 2026
ffb5c1c
Add HttpResponse (status + headers + setHeader)
Ericthilen Feb 11, 2026
757e94d
Add RedirectRule (pattern match, targetUrl, 301/302
Ericthilen Feb 11, 2026
4bf1427
Add RedirectRulesLoader.compileSourcePattern (wildcard support
Ericthilen Feb 11, 2026
06266b4
Add RedirectFilter tests (301/302/no match/wildcard)
Ericthilen Feb 11, 2026
d4011bc
Add RedirectResponse DTO (location + statusCode)
Ericthilen Feb 11, 2026
3574f08
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen Feb 11, 2026
0f133da
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen Feb 11, 2026
0abfe57
feat: improve redirect rule parsing and validation
Ericthilen Feb 11, 2026
6420cab
refactor(server): extract TerminalHandler to own file
Ericthilen Feb 11, 2026
2c03c8f
fix(server): make '*' not match '/' in redirect wildcards; update test
Ericthilen Feb 11, 2026
831b1d8
extract TerminalHandler from FilterChain into a interface
Ericthilen Feb 11, 2026
43f0db2
make RedirectRulesLoader wildcard '*' not match '/' (avoid matching s…
Ericthilen Feb 11, 2026
3afa435
removed unused RedirectResponse and .gitkeep placeholders
Ericthilen Feb 11, 2026
a5cf986
Update redirect rules and tests
viktorlindell12 Feb 20, 2026
d85ea46
Update RedirectFilter.java
viktorlindell12 Feb 23, 2026
5e34302
Update RedirectRule.java
viktorlindell12 Feb 23, 2026
96843ae
Update RedirectFilter.java
viktorlindell12 Feb 23, 2026
45d2171
Update RedirectRulesLoader.java
viktorlindell12 Feb 23, 2026
5d36ee2
Update RedirectFilterTest.java
viktorlindell12 Feb 23, 2026
c62ed08
Delete pom.xml
viktorlindell12 Feb 23, 2026
ab6a8d7
Delete src/test/java/org/example/server/RedirectFilterTest.java
viktorlindell12 Feb 23, 2026
05dfe82
Delete src/main/java/org/example/server/HttpResponse.java
viktorlindell12 Feb 23, 2026
76d5bd2
Delete src/main/java/org/example/server/HttpRequest.java
viktorlindell12 Feb 23, 2026
4f6fd85
Delete src/main/java/org/example/server/FilterChain.java
viktorlindell12 Feb 23, 2026
8e29d15
Delete src/main/java/org/example/server/HttpFilter.java
viktorlindell12 Feb 23, 2026
0428e7e
Delete src/main/java/org/example/server/TerminalHandler.java
viktorlindell12 Feb 23, 2026
3ff5c22
Delete src/main/java/org/example/server/RedirectRule.java
viktorlindell12 Feb 23, 2026
371d28d
Delete src/main/java/org/example/server/RedirectFilter.java
viktorlindell12 Feb 23, 2026
7bd0b82
Delete src/main/java/org/example/server/RedirectRulesLoader.java
viktorlindell12 Feb 23, 2026
da8c1d5
Add redirect filter rules and tests
viktorlindell12 Feb 23, 2026
ae73793
Restore pom.xml from main
viktorlindell12 Feb 23, 2026
7c5d1d3
Update RedirectRule.java
viktorlindell12 Feb 25, 2026
3bd0419
Update RedirectRule.java
viktorlindell12 Feb 25, 2026
f63637b
Extend header-injection guard to null-byte characters. Fixed issue fr…
Ericthilen Feb 25, 2026
838a917
Update RedirectRule.java
viktorlindell12 Feb 25, 2026
30fba9c
Update RedirectRule.java
viktorlindell12 Feb 25, 2026
c68a99f
Merge remote-tracking branch 'origin/main' into feature/url-redirect-…
viktorlindell12 Mar 1, 2026
2ca399f
Merge remote-tracking branch 'origin/feature/url-redirect-filter' int…
viktorlindell12 Mar 1, 2026
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
54 changes: 54 additions & 0 deletions src/main/java/org/example/filter/redirect/RedirectFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.example.filter.redirect;

import org.example.filter.Filter;
import org.example.filter.FilterChain;
import org.example.http.HttpResponseBuilder;
import org.example.httpparser.HttpRequest;

import java.util.List;
import java.util.Objects;
import java.util.logging.Logger;

public final class RedirectFilter implements Filter {
private static final Logger LOG = Logger.getLogger(RedirectFilter.class.getName());
private final List<RedirectRule> rules;

public RedirectFilter(List<RedirectRule> rules) {
this.rules = List.copyOf(Objects.requireNonNull(rules, "rules"));
}

@Override
public void init() {
// no-op
}

@Override
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {
String path = request.getPath();

if (path == null) {
chain.doFilter(request, response);
return;
}

for (RedirectRule rule : rules) {
if (rule.matches(path)) { // <-- DENNA RADEN SKA FINNAS
final String sanitizedPath = path.replaceAll("[\\r\\n]", "_");
LOG.info(() -> "Redirecting " + sanitizedPath + " -> "
+ rule.getTargetUrl() + " (" + rule.getStatusCode() + ")");

response.setStatusCode(rule.getStatusCode());
response.setHeader("Location", rule.getTargetUrl());
return; // STOP pipeline
}
}

chain.doFilter(request, response);
}


@Override
public void destroy() {
// no-op
}
}
44 changes: 44 additions & 0 deletions src/main/java/org/example/filter/redirect/RedirectRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.example.filter.redirect;

import java.util.Objects;
import java.util.regex.Pattern;

public final class RedirectRule {
private final Pattern sourcePattern;
private final String targetUrl;
private final int statusCode;

public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) {
this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern");
this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl");
if (this.targetUrl.isBlank()) {
throw new IllegalArgumentException("targetUrl must not be blank");
}
if (this.targetUrl.indexOf('\r') >= 0 || this.targetUrl.indexOf('\n') >= 0
|| this.targetUrl.indexOf('\0') >= 0) {
throw new IllegalArgumentException("targetUrl must not contain CR/LF or null byte");
}

if (statusCode != 301 && statusCode != 302) {
throw new IllegalArgumentException("statusCode must be 301 or 302");
}
this.statusCode = statusCode;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

public Pattern getSourcePattern() { return sourcePattern; }
public String getTargetUrl() { return targetUrl; }
public int getStatusCode() { return statusCode; }

public boolean matches(String requestPath) {
return requestPath != null && sourcePattern.matcher(requestPath).matches();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Override
public String toString() {
return "RedirectRule{" +
"sourcePattern=" + sourcePattern +
", targetUrl='" + targetUrl + '\'' +
", statusCode=" + statusCode +
'}';
}
}
46 changes: 46 additions & 0 deletions src/main/java/org/example/filter/redirect/RedirectRulesLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.example.filter.redirect;

import java.util.regex.Pattern;

public final class RedirectRulesLoader {
private static final String REGEX_PREFIX = "regex:";

private RedirectRulesLoader() {}

public static Pattern compileSourcePattern(String sourcePath) {
if (sourcePath == null || sourcePath.isBlank()) {
throw new IllegalArgumentException("sourcePath must not be blank");
}

String trimmed = sourcePath.trim();

if (trimmed.startsWith(REGEX_PREFIX)) {
String rawRegex = trimmed.substring(REGEX_PREFIX.length()).strip();
if (rawRegex.isBlank()) {
throw new IllegalArgumentException("regex sourcePath must not be blank");
}
return Pattern.compile(rawRegex);
}

String regex;
if (trimmed.contains("*")) {
regex = wildcardToRegex(trimmed);
} else {
regex = Pattern.quote(trimmed);
}
return Pattern.compile("^" + regex + "$");
}

private static String wildcardToRegex(String wildcard) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < wildcard.length(); i++) {
char c = wildcard.charAt(i);
if (c == '*') {
sb.append("[^/]*");
} else {
sb.append(Pattern.quote(String.valueOf(c)));
}
}
return sb.toString();
}
}
Empty file removed src/main/resources/.gitkeep
Empty file.
109 changes: 109 additions & 0 deletions src/test/java/org/example/filter/redirect/RedirectFilterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.example.filter.redirect;

import org.example.filter.FilterChain;
import org.example.http.HttpResponseBuilder;
import org.example.httpparser.HttpRequest;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class RedirectFilterTest {

private static HttpRequest request(String path) {
return new HttpRequest("GET", path, "HTTP/1.1", Map.of(), null);
}

private static String responseAsString(HttpResponseBuilder response) {
return new String(response.build(), StandardCharsets.UTF_8);
}

@Test
void returns_301_redirect_and_stops_pipeline() {
RedirectFilter filter = new RedirectFilter(List.of(
new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301)
));

AtomicBoolean chainCalled = new AtomicBoolean(false);
FilterChain chain = (req, res) -> chainCalled.set(true);

HttpResponseBuilder res = new HttpResponseBuilder();

filter.doFilter(request("/old-page"), res, chain);

String raw = responseAsString(res);
assertThat(raw).contains("HTTP/1.1 301 Moved Permanently");
assertThat(raw).contains("Location: /new-page");
assertThat(chainCalled.get()).isFalse();
}

@Test
void returns_302_redirect_and_stops_pipeline() {
RedirectFilter filter = new RedirectFilter(List.of(
new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302)
));

AtomicBoolean chainCalled = new AtomicBoolean(false);
FilterChain chain = (req, res) -> chainCalled.set(true);

HttpResponseBuilder res = new HttpResponseBuilder();

filter.doFilter(request("/temp"), res, chain);

String raw = responseAsString(res);
assertThat(raw).contains("HTTP/1.1 302 Found");
assertThat(raw).contains("Location: https://example.com/temporary");
assertThat(chainCalled.get()).isFalse();
}

@Test
void no_matching_rule_calls_next_in_chain() {
RedirectFilter filter = new RedirectFilter(List.of(
new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301)
));

AtomicBoolean chainCalled = new AtomicBoolean(false);
FilterChain chain = (req, res) -> {
chainCalled.set(true);
res.setStatusCode(200);
res.setBody("terminal");
};

HttpResponseBuilder res = new HttpResponseBuilder();

filter.doFilter(request("/nope"), res, chain);

String raw = responseAsString(res);
assertThat(chainCalled.get()).isTrue();
assertThat(raw).contains("HTTP/1.1 200 OK");
assertThat(raw).doesNotContain("Location:");
}

@Test
void wildcard_matching_docs_star() {
var p = RedirectRulesLoader.compileSourcePattern("/docs/*");
assertThat(p.matcher("/docs/test").matches()).isTrue();
assertThat(p.matcher("/docs/any/path").matches()).isFalse();
assertThat(p.matcher("/doc/test").matches()).isFalse();
}

@Test
void regex_matching_via_loader_prefix() {
var p = RedirectRulesLoader.compileSourcePattern("regex:^/docs/(v1|v2)$");
assertThat(p.matcher("/docs/v1").matches()).isTrue();
assertThat(p.matcher("/docs/v2").matches()).isTrue();
assertThat(p.matcher("/docs/v3").matches()).isFalse();
}

@Test
void redirect_rule_rejects_invalid_status_code() {
assertThatThrownBy(() -> new RedirectRule(Pattern.compile("^/x$"), "/y", 307))
.isInstanceOf(IllegalArgumentException.class);
}
}
Empty file removed src/test/resources/.gitkeep
Empty file.