-
Notifications
You must be signed in to change notification settings - Fork 2
Add RedirectFilter with configurable 301/302 rules #9
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
Changes from all commits
98b40a1
c1b5f70
bf4d977
148411e
696e1bc
de1b6cf
e21bc8c
78e8df0
0ed2a79
ab1d054
9e0610a
8e144de
53e11a3
58882cc
aaa4748
8c9a173
a0cefab
e5f50ef
512627d
42f23cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: Java CI with Maven | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ "main" ] | ||
| pull_request: | ||
| branches: [ "main" ] | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Get Java Version | ||
| run: | | ||
| JAVA_VERSION=$(mvn help:evaluate "-Dexpression=maven.compiler.release" -q -DforceStdout) | ||
| echo "JAVA_VERSION=$JAVA_VERSION" >> $GITHUB_ENV | ||
|
|
||
| - name: Set up JDK ${{ env.JAVA_VERSION }} | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: ${{ env.JAVA_VERSION }} | ||
| distribution: 'temurin' | ||
| cache: maven | ||
|
|
||
| - name: Compile with Maven | ||
| run: mvn -B compile --file pom.xml | ||
|
|
||
| - name: Test with Maven | ||
| run: mvn -B test --file pom.xml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| name: Publish Docker Image to Github Packages on Release | ||
| on: | ||
| release: | ||
| types: | ||
| - published | ||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| packages: write | ||
| steps: | ||
| - uses: actions/checkout@v6.0.2 | ||
| - uses: docker/setup-qemu-action@v3.7.0 | ||
| - uses: docker/setup-buildx-action@v3.12.0 | ||
| - name: Log in to GHCR | ||
| uses: docker/login-action@v3.7.0 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Extract metadata | ||
| id: meta | ||
| uses: docker/metadata-action@v5.10.0 | ||
| with: | ||
| images: ghcr.io/ithsjava25/webserver | ||
| - name: Build and push | ||
| uses: docker/build-push-action@v6.18.0 | ||
| with: | ||
| context: . | ||
| push: true | ||
| platforms: linux/amd64, linux/arm64 | ||
| tags: ${{ steps.meta.outputs.tags }} | ||
| labels: ${{ steps.meta.outputs.labels }} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||||||||||||
| package org.example; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||
| import java.net.ServerSocket; | ||||||||||||||||||||||
| import java.net.Socket; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public class TcpServer { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private final int port; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public TcpServer(int port) { | ||||||||||||||||||||||
| this.port = port; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public void start() { | ||||||||||||||||||||||
| System.out.println("Starting TCP server on port " + port); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try (ServerSocket serverSocket = new ServerSocket(port)) { | ||||||||||||||||||||||
| while (true) { | ||||||||||||||||||||||
| Socket clientSocket = serverSocket.accept(); // block | ||||||||||||||||||||||
| System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress()); | ||||||||||||||||||||||
| clientSocket.close(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+19
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Client socket is not closed in a try-with-resources — potential leak. If anything throws between Proposed fix while (true) {
- Socket clientSocket = serverSocket.accept(); // block
- System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
- clientSocket.close();
+ try (Socket clientSocket = serverSocket.accept()) {
+ System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } catch (IOException e) { | ||||||||||||||||||||||
| throw new RuntimeException("Failed to start TCP server", e); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| 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); | ||
| } | ||
| } |
| 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); | ||
| } |
| 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; | ||
| } | ||
| } |
| 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); | ||
| } | ||
| } |
| 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); | ||
| } | ||
| } |
| 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(); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛡️ Proposed fix public boolean matches(String requestPath) {
- return sourcePattern.matcher(requestPath).matches();
+ if (requestPath == null) return false;
+ return sourcePattern.matcher(requestPath).matches();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| @Override | ||||||||||||||||
| public String toString() { | ||||||||||||||||
| return "RedirectRule{" + | ||||||||||||||||
| "sourcePattern=" + sourcePattern + | ||||||||||||||||
| ", targetUrl='" + targetUrl + '\'' + | ||||||||||||||||
| ", statusCode=" + statusCode + | ||||||||||||||||
| '}'; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.