-
Notifications
You must be signed in to change notification settings - Fork 0
Url redirect filter #33 #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
696e1bc
updated pom.xml
Ericthilen de1b6cf
Add HttpRequest (method, path)
Ericthilen e21bc8c
Add FilterChain to execute Httpfilter pipeline
Ericthilen 78e8df0
Add HttpResponse (status + headers + setHeader)
Ericthilen 0ed2a79
Add RedirectRule (pattern match, targetUrl, 301/302
Ericthilen ab1d054
Add RedirectRulesLoader.compileSourcePattern (wildcard support
Ericthilen 9e0610a
Add RedirectFilter tests (301/302/no match/wildcard)
Ericthilen 8e144de
Add RedirectResponse DTO (location + statusCode)
Ericthilen 53e11a3
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen 58882cc
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen aaa4748
feat: improve redirect rule parsing and validation
Ericthilen 8c9a173
refactor(server): extract TerminalHandler to own file
Ericthilen a0cefab
fix(server): make '*' not match '/' in redirect wildcards; update test
Ericthilen e5f50ef
extract TerminalHandler from FilterChain into a interface
Ericthilen 512627d
make RedirectRulesLoader wildcard '*' not match '/' (avoid matching s…
Ericthilen 42f23cd
removed unused RedirectResponse and .gitkeep placeholders
Ericthilen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package org.example.server; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public final class FilterChain { | ||
|
|
||
| private final HttpFilter[] filters; | ||
| private final TerminalHandler terminal; | ||
| private int index = 0; | ||
|
|
||
| public FilterChain(HttpFilter[] filters, TerminalHandler terminal) { | ||
| this.filters = Objects.requireNonNull(filters, "filters"); | ||
| this.terminal = Objects.requireNonNull(terminal, "terminal"); | ||
| } | ||
|
|
||
| public void doFilter(HttpRequest request, HttpResponse response) { | ||
| if (index < filters.length) { | ||
| HttpFilter current = filters[index++]; | ||
| current.doFilter(request, response, this); | ||
| return; | ||
| } | ||
| terminal.handle(request, response); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package org.example.server; | ||
|
|
||
| @FunctionalInterface | ||
| public interface HttpFilter { | ||
| void doFilter(HttpRequest request, HttpResponse response, FilterChain chain); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package org.example.server; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public final class HttpRequest { | ||
| private final String method; | ||
| private final String path; | ||
|
|
||
| public HttpRequest(String method, String path) { | ||
| this.method = Objects.requireNonNull(method, "method"); | ||
| this.path = Objects.requireNonNull(path, "path"); | ||
| } | ||
|
|
||
| public String method() { | ||
| return method; | ||
| } | ||
|
|
||
| public String path() { | ||
| return path; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package org.example.server; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
|
|
||
| public final class HttpResponse { | ||
| private int status = 200; | ||
| private final Map<String, String> headers = new LinkedHashMap<>(); | ||
|
|
||
| public int status() { | ||
| return status; | ||
| } | ||
|
|
||
| public void setStatus(int status) { | ||
| this.status = status; | ||
| } | ||
|
|
||
| public Map<String, String> headers() { | ||
| return Collections.unmodifiableMap(headers); | ||
| } | ||
|
|
||
| public void setHeader(String name, String value) { | ||
| headers.put(name, value); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package org.example.server; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.logging.Logger; | ||
|
|
||
| public final class RedirectFilter implements HttpFilter { | ||
| 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 doFilter(HttpRequest request, HttpResponse response, FilterChain chain) { | ||
| String path = request.path(); | ||
|
|
||
| for (RedirectRule rule : rules) { | ||
| if (rule.matches(path)) { | ||
| LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); | ||
| response.setStatus(rule.getStatusCode()); | ||
| response.setHeader("Location", rule.getTargetUrl()); | ||
| return; // STOP pipeline | ||
| } | ||
| } | ||
|
|
||
| chain.doFilter(request, response); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package org.example.server; | ||
|
|
||
| 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 (statusCode != 301 && statusCode != 302) { | ||
| throw new IllegalArgumentException("statusCode must be 301 or 302"); | ||
| } | ||
| this.statusCode = statusCode; | ||
| } | ||
|
|
||
| public Pattern getSourcePattern() { return sourcePattern; } | ||
| public String getTargetUrl() { return targetUrl; } | ||
| public int getStatusCode() { return statusCode; } | ||
|
|
||
| public boolean matches(String requestPath) { | ||
| return sourcePattern.matcher(requestPath).matches(); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "RedirectRule{" + | ||
| "sourcePattern=" + sourcePattern + | ||
| ", targetUrl='" + targetUrl + '\'' + | ||
| ", statusCode=" + statusCode + | ||
| '}'; | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package org.example.server; | ||
|
|
||
| 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()); | ||
| 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(); | ||
| } | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package org.example.server; | ||
|
|
||
| @FunctionalInterface | ||
| public interface TerminalHandler { | ||
| void handle(HttpRequest request, HttpResponse response); | ||
| } |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package org.example.server; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.util.List; | ||
| 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 { | ||
|
|
||
| @Test | ||
| void returns_301_redirect_and_stops_pipeline() { | ||
| RedirectFilter filter = new RedirectFilter(List.of( | ||
| new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) | ||
| )); | ||
|
|
||
| AtomicBoolean terminalCalled = new AtomicBoolean(false); | ||
| FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); | ||
|
|
||
| HttpRequest req = new HttpRequest("GET", "/old-page"); | ||
| HttpResponse res = new HttpResponse(); | ||
|
|
||
| chain.doFilter(req, res); | ||
|
|
||
| assertThat(res.status()).isEqualTo(301); | ||
| assertThat(res.headers()).containsEntry("Location", "/new-page"); | ||
| assertThat(terminalCalled.get()).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void returns_302_redirect() { | ||
| RedirectFilter filter = new RedirectFilter(List.of( | ||
| new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) | ||
| )); | ||
|
|
||
| FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> res.setStatus(200)); | ||
|
|
||
| HttpRequest req = new HttpRequest("GET", "/temp"); | ||
| HttpResponse res = new HttpResponse(); | ||
|
|
||
| chain.doFilter(req, res); | ||
|
|
||
| assertThat(res.status()).isEqualTo(302); | ||
| assertThat(res.headers()).containsEntry("Location", "https://example.com/temporary"); | ||
| } | ||
|
|
||
| @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 terminalCalled = new AtomicBoolean(false); | ||
| FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); | ||
|
|
||
| HttpRequest req = new HttpRequest("GET", "/nope"); | ||
| HttpResponse res = new HttpResponse(); | ||
|
|
||
| chain.doFilter(req, res); | ||
|
|
||
| assertThat(terminalCalled.get()).isTrue(); | ||
| assertThat(res.status()).isEqualTo(200); | ||
| assertThat(res.headers()).doesNotContainKey("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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.