-
Notifications
You must be signed in to change notification settings - Fork 2
Feature/mime type detection #10
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
9ac7b57
875d1ef
9289c7d
6bdb1ef
781e34a
6a424d5
bf44e60
47cecbd
cc93f34
511b5ee
aaeba6d
524f33c
73447ee
016b324
37fe80a
11cdfea
758f8c4
0de1e14
5a685f3
8cc69d8
c0e3de6
0c8cd60
5162d75
a401ba6
0e83b32
5b51837
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,11 @@ | ||
| FROM maven:3-eclipse-temurin-25-alpine AS build | ||
| WORKDIR /build | ||
| COPY src/ src/ | ||
| COPY pom.xml pom.xml | ||
| RUN mvn compile | ||
|
|
||
| FROM eclipse-temurin:25-jre-alpine | ||
| RUN addgroup -S appgroup && adduser -S appuser -G appgroup | ||
| COPY --from=build /build/target/classes/ /app/ | ||
| ENTRYPOINT ["java", "-classpath", "/app", "org.example.App"] | ||
| USER appuser | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,13 +9,21 @@ | |
| <version>1.0-SNAPSHOT</version> | ||
|
|
||
| <properties> | ||
| <maven.compiler.release>23</maven.compiler.release> | ||
| <maven.compiler.release>25</maven.compiler.release> | ||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| <junit.jupiter.version>6.0.2</junit.jupiter.version> | ||
| <assertj.core.version>3.27.7</assertj.core.version> | ||
| <mockito.version>5.21.0</mockito.version> | ||
| <bucket4j.version>8.14.0</bucket4j.version> | ||
|
|
||
| </properties> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>com.bucket4j</groupId> | ||
| <artifactId>bucket4j_jdk17-core</artifactId> | ||
| <version>${bucket4j.version}</version> | ||
| </dependency> | ||
|
Comment on lines
+22
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. 🧩 Analysis chain🏁 Script executed: rg -i 'bucket4j' --type=javaRepository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template Length of output: 90 Remove This dependency is not used anywhere in the codebase and appears to be added prematurely. If it's intended for future rate-limiting work, add it in the PR where it's actually implemented to keep the dependency tree lean. 🤖 Prompt for AI Agents |
||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter</artifactId> | ||
|
|
@@ -28,12 +36,36 @@ | |
| <version>${assertj.core.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-core</artifactId> | ||
| <version>${mockito.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-junit-jupiter</artifactId> | ||
| <version>${mockito.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.awaitility</groupId> | ||
| <artifactId>awaitility</artifactId> | ||
| <version>4.3.0</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <dependency> | ||
| <groupId>tools.jackson.core</groupId> | ||
| <artifactId>jackson-databind</artifactId> | ||
| <version>3.0.3</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>tools.jackson.dataformat</groupId> | ||
| <artifactId>jackson-dataformat-yaml</artifactId> | ||
| <version>3.0.3</version> | ||
| </dependency> | ||
|
|
||
| </dependencies> | ||
| <build> | ||
| <plugins> | ||
|
|
@@ -118,6 +150,39 @@ | |
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| <plugin> | ||
| <groupId>org.pitest</groupId> | ||
| <artifactId>pitest-maven</artifactId> | ||
| <version>1.22.0</version> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.pitest</groupId> | ||
| <artifactId>pitest-junit5-plugin</artifactId> | ||
| <version>1.2.2</version> | ||
| </dependency> | ||
| </dependencies> | ||
| </plugin> | ||
| <plugin> | ||
| <groupId>com.diffplug.spotless</groupId> | ||
| <artifactId>spotless-maven-plugin</artifactId> | ||
| <version>3.2.1</version> | ||
| <configuration> | ||
| <java> | ||
| <removeUnusedImports/> | ||
| <formatAnnotations/> | ||
| </java> | ||
| </configuration> | ||
| <executions> | ||
| <execution> | ||
| <phase>verify</phase> | ||
| <goals> | ||
| <goal> | ||
| check | ||
| </goal> | ||
| </goals> | ||
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package org.example; | ||
|
|
||
| import org.example.httpparser.HttpParser; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.Socket; | ||
|
|
||
| public class ConnectionHandler implements AutoCloseable { | ||
|
|
||
| Socket client; | ||
| String uri; | ||
|
|
||
| public ConnectionHandler(Socket client) { | ||
| this.client = client; | ||
| } | ||
|
|
||
| public void runConnectionHandler() throws IOException { | ||
| StaticFileHandler sfh = new StaticFileHandler(); | ||
| HttpParser parser = new HttpParser(); | ||
| parser.setReader(client.getInputStream()); | ||
| parser.parseRequest(); | ||
| parser.parseHttp(); | ||
| resolveTargetFile(parser.getUri()); | ||
| sfh.sendGetRequest(client.getOutputStream(), uri); | ||
| } | ||
|
|
||
| private void resolveTargetFile(String uri) { | ||
| if (uri.matches("/$")) { //matches(/) | ||
| this.uri = "index.html"; | ||
| } else if (uri.matches("^(?!.*\\.html$).*$")) { | ||
| this.uri = uri.concat(".html"); | ||
| } else { | ||
| this.uri = uri; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public void close() throws Exception { | ||
| client.close(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package org.example; | ||
|
|
||
| import org.example.http.HttpResponseBuilder; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import java.nio.file.Files; | ||
|
|
||
| public class StaticFileHandler { | ||
| private final String WEB_ROOT; | ||
| private byte[] fileBytes; | ||
| private int statusCode; | ||
|
|
||
| // Constructor for production | ||
| public StaticFileHandler() { | ||
| WEB_ROOT = "www"; | ||
| } | ||
|
|
||
| // Constructor for tests, otherwise the www folder won't be seen | ||
| public StaticFileHandler(String webRoot) { | ||
| WEB_ROOT = webRoot; | ||
| } | ||
|
|
||
| private void handleGetRequest(String uri) throws IOException { | ||
| // Security: Prevent path traversal attacks (e.g. GET /../../etc/passwd) | ||
| File root = new File(WEB_ROOT).getCanonicalFile(); | ||
| File file = new File(root, uri).getCanonicalFile(); | ||
|
|
||
| if (!file.toPath().startsWith(root.toPath())) { | ||
| fileBytes = "403 Forbidden".getBytes(); | ||
| statusCode = 403; | ||
| return; | ||
| } | ||
|
|
||
| if (file.exists()) { | ||
| fileBytes = Files.readAllBytes(file.toPath()); | ||
| statusCode = 200; | ||
| } else { | ||
| File errorFile = new File(WEB_ROOT, "pageNotFound.html"); | ||
| if (errorFile.exists()) { | ||
| fileBytes = Files.readAllBytes(errorFile.toPath()); | ||
| } else { | ||
| fileBytes = "404 Not Found".getBytes(); | ||
| } | ||
| statusCode = 404; | ||
| } | ||
| } | ||
|
|
||
| public void sendGetRequest(OutputStream outputStream, String uri) throws IOException { | ||
| handleGetRequest(uri); | ||
|
|
||
| HttpResponseBuilder response = new HttpResponseBuilder(); | ||
| response.setStatusCode(statusCode); | ||
| // Use MimeTypeDetector instead of hardcoded text/html | ||
| response.setContentTypeFromFilename(uri); | ||
| response.setBody(fileBytes); | ||
|
|
||
| outputStream.write(response.build()); | ||
| outputStream.flush(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||||||||||||||||||
| 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()); | ||||||||||||||||||||||
| Thread.ofVirtual().start(() -> handleClient(clientSocket)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
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. Use try-with-resources for If 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); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void handleClient(Socket client) { | ||||||||||||||||||||||
| try (ConnectionHandler connectionHandler = new ConnectionHandler(client)) { | ||||||||||||||||||||||
| connectionHandler.runConnectionHandler(); | ||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||
| throw new RuntimeException("Error handling client connection " + e); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package org.example.config; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public record AppConfig( | ||
| @JsonProperty("server") ServerConfig server, | ||
| @JsonProperty("logging") LoggingConfig logging | ||
| ) { | ||
| public static AppConfig defaults() { | ||
| return new AppConfig(ServerConfig.defaults(), LoggingConfig.defaults()); | ||
| } | ||
|
|
||
| public AppConfig withDefaultsApplied() { | ||
| ServerConfig serverConfig = (server == null ? ServerConfig.defaults() : server.withDefaultsApplied()); | ||
| LoggingConfig loggingConfig = (logging == null ? LoggingConfig.defaults() : logging.withDefaultsApplied()); | ||
| return new AppConfig(serverConfig, loggingConfig); | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public record ServerConfig( | ||
| @JsonProperty("port") Integer port, | ||
| @JsonProperty("rootDir") String rootDir | ||
| ) { | ||
| public static ServerConfig defaults() { | ||
| return new ServerConfig(8080, "./www"); | ||
| } | ||
|
|
||
| public ServerConfig withDefaultsApplied() { | ||
| int p = (port == null ? 8080 : port); | ||
| if (p < 1 || p > 65535) { | ||
| throw new IllegalArgumentException("Invalid port number: " + p + ". Port must be between 1 and 65535"); | ||
| } | ||
| String rd = (rootDir == null || rootDir.isBlank()) ? "./www" : rootDir; | ||
| return new ServerConfig(p, rd); | ||
| } | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public record LoggingConfig( | ||
| @JsonProperty("level") String level | ||
| ) { | ||
| public static LoggingConfig defaults() { | ||
| return new LoggingConfig("INFO"); | ||
| } | ||
|
|
||
| public LoggingConfig withDefaultsApplied() { | ||
| String lvl = (level == null || level.isBlank()) ? "INFO" : level; | ||
| return new LoggingConfig(lvl); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Classpath includes only compiled classes — runtime dependencies will be missing.
The
COPYonly brings/build/target/classes/into the image. If any non-test dependency (e.g.,bucket4j) is used at runtime, it won't be on the classpath and the app will fail withClassNotFoundException. Consider usingmaven-dependency-plugin:copy-dependenciesor packaging as a fat JAR instead.Alternative: use maven-jar-plugin with dependency copy
In the build stage, add a step to copy dependencies:
RUN mvn compile +RUN mvn dependency:copy-dependencies -DoutputDirectory=target/dependency -DincludeScope=runtime🤖 Prompt for AI Agents