Skip to content

Commit 3497f8a

Browse files
authored
Merge branch 'main' into issue36
2 parents d4227b0 + 7652687 commit 3497f8a

16 files changed

Lines changed: 818 additions & 37 deletions

Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ FROM eclipse-temurin:25-jre-alpine
99
EXPOSE 8080
1010
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
1111
WORKDIR /app/
12-
COPY --from=build /build/target/classes/ /
13-
COPY --from=build /build/target/dependency/ /dependencies/
14-
COPY /www/ /www/
12+
COPY --from=build /build/target/classes/ /app/
13+
COPY --from=build /build/target/dependency/ /app/dependencies/
14+
COPY www/ ./www/
1515
USER appuser
16-
ENTRYPOINT ["java", "-classpath", "/app:/dependencies/*", "org.example.App"]
16+
ENTRYPOINT ["java", "-classpath", "/app:/app/dependencies/*", "org.example.App"]

PortConfigurationGuide.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Konfiguration: port (CLI → config-fil → default)
2+
3+
Det här projektet väljer vilken port servern ska starta på enligt följande prioritet:
4+
5+
1. **CLI-argument** (`--port <port>`) – högst prioritet
6+
2. **Config-fil** (`application.yml`: `server.port`)
7+
3. **Default** (`8080`) – används om port saknas i config eller om config-filen saknas
8+
9+
---
10+
11+
## 1) Default-värde
12+
13+
Om varken CLI eller config anger port används:
14+
15+
- **8080** (default för `server.port` i `AppConfig`)
16+
17+
---
18+
19+
## 2) Config-fil: `application.yml`
20+
21+
### Var ska filen ligga?
22+
Standard:
23+
- `src/main/resources/application.yml`
24+
25+
### Exempel
26+
```yaml
27+
server:
28+
port: 9090
29+
```
30+
31+
---
32+
33+
## 3) CLI-argument
34+
35+
CLI kan användas för att override:a config:
36+
37+
```bash
38+
java -cp target/classes org.example.App --port 8000
39+
```
40+
41+
---
42+
43+
## 4) Sammanfattning
44+
45+
Prioritet:
46+
47+
1. CLI (`--port`)
48+
2. `application.yml` (`server.port`)
49+
3. Default (`8080`)

src/main/java/org/example/App.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,50 @@
66
import java.nio.file.Path;
77

88
public class App {
9+
10+
private static final String PORT_FLAG = "--port";
11+
912
public static void main(String[] args) {
1013
Path configPath = Path.of("src/main/resources/application.yml");
1114

1215
AppConfig appConfig = ConfigLoader.loadOnce(configPath);
13-
int port = appConfig.server().port();
16+
17+
int port = resolvePort(args, appConfig.server().port());
18+
1419
new TcpServer(port).start();
1520
}
21+
22+
static int resolvePort(String[] args, int configPort) {
23+
Integer cliPort = parsePortFromCli(args);
24+
if (cliPort != null) {
25+
return validatePort(cliPort, "CLI argument " + PORT_FLAG);
26+
}
27+
return validatePort(configPort, "configuration server.port");
28+
}
29+
30+
static Integer parsePortFromCli(String[] args) {
31+
if (args == null) return null;
32+
33+
for (int i = 0; i < args.length; i++) {
34+
if (PORT_FLAG.equals(args[i])) {
35+
int valueIndex = i + 1;
36+
if (valueIndex >= args.length) {
37+
throw new IllegalArgumentException("Missing value after " + PORT_FLAG);
38+
}
39+
try {
40+
return Integer.parseInt(args[valueIndex]);
41+
} catch (NumberFormatException e) {
42+
throw new IllegalArgumentException("Invalid port value after " + PORT_FLAG + ": " + args[valueIndex], e);
43+
}
44+
}
45+
}
46+
return null;
47+
}
48+
49+
static int validatePort(int port, String source) {
50+
if (port < 1 || port > 65535) {
51+
throw new IllegalArgumentException("Port out of range (1-65535) from " + source + ": " + port);
52+
}
53+
return port;
54+
}
1655
}

src/main/java/org/example/ConnectionHandler.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
package org.example;
22

3+
import org.example.config.AppConfig;
4+
import org.example.filter.IpFilter;
35
import org.example.httpparser.HttpParser;
6+
import org.example.httpparser.HttpRequest;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import org.example.filter.Filter;
10+
import org.example.filter.FilterChainImpl;
11+
import org.example.http.HttpResponseBuilder;
12+
import org.example.config.ConfigLoader;
413

514
import java.io.IOException;
615
import java.net.Socket;
@@ -9,21 +18,66 @@ public class ConnectionHandler implements AutoCloseable {
918

1019
Socket client;
1120
String uri;
21+
private final List<Filter> filters;
1222

1323
public ConnectionHandler(Socket client) {
1424
this.client = client;
25+
this.filters = buildFilters();
1526
}
27+
private List<Filter> buildFilters() {
28+
List<Filter> list = new ArrayList<>();
29+
AppConfig config = ConfigLoader.get();
30+
AppConfig.IpFilterConfig ipFilterConfig = config.ipFilter();
31+
if (Boolean.TRUE.equals(ipFilterConfig.enabled())) {
32+
list.add(createIpFilterFromConfig(ipFilterConfig));
33+
}
34+
// Add more filters here...
35+
return list;
36+
}
37+
1638

1739
public void runConnectionHandler() throws IOException {
18-
StaticFileHandler sfh = new StaticFileHandler();
1940
HttpParser parser = new HttpParser();
2041
parser.setReader(client.getInputStream());
2142
parser.parseRequest();
2243
parser.parseHttp();
44+
45+
HttpRequest request = new HttpRequest(
46+
parser.getMethod(),
47+
parser.getUri(),
48+
parser.getVersion(),
49+
parser.getHeadersMap(),
50+
""
51+
);
52+
53+
String clientIp = client.getInetAddress().getHostAddress();
54+
request.setAttribute("clientIp", clientIp);
55+
56+
HttpResponseBuilder response = applyFilters(request);
57+
58+
int statusCode = response.getStatusCode();
59+
if (statusCode == HttpResponseBuilder.SC_FORBIDDEN ||
60+
statusCode == HttpResponseBuilder.SC_BAD_REQUEST) {
61+
byte[] responseBytes = response.build();
62+
client.getOutputStream().write(responseBytes);
63+
client.getOutputStream().flush();
64+
return;
65+
}
66+
2367
resolveTargetFile(parser.getUri());
68+
StaticFileHandler sfh = new StaticFileHandler();
2469
sfh.sendGetRequest(client.getOutputStream(), uri);
2570
}
2671

72+
private HttpResponseBuilder applyFilters(HttpRequest request) {
73+
HttpResponseBuilder response = new HttpResponseBuilder();
74+
75+
FilterChainImpl chain = new FilterChainImpl(filters);
76+
chain.doFilter(request, response);
77+
78+
return response;
79+
}
80+
2781
private void resolveTargetFile(String uri) {
2882
if (uri.matches("/$")) { //matches(/)
2983
this.uri = "index.html";
@@ -39,4 +93,28 @@ private void resolveTargetFile(String uri) {
3993
public void close() throws Exception {
4094
client.close();
4195
}
96+
97+
private IpFilter createIpFilterFromConfig(AppConfig.IpFilterConfig config) {
98+
IpFilter filter = new IpFilter();
99+
100+
// Set mode
101+
if ("ALLOWLIST".equalsIgnoreCase(config.mode())) {
102+
filter.setMode(IpFilter.FilterMode.ALLOWLIST);
103+
} else {
104+
filter.setMode(IpFilter.FilterMode.BLOCKLIST);
105+
}
106+
107+
// Add blocked IPs
108+
for (String ip : config.blockedIps()) {
109+
filter.addBlockedIp(ip);
110+
}
111+
112+
// Add allowed IPs
113+
for (String ip : config.allowedIps()) {
114+
filter.addAllowedIp(ip);
115+
}
116+
117+
filter.init();
118+
return filter;
119+
}
42120
}

src/main/java/org/example/StaticFileHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.example;
22

33
import org.example.http.HttpResponseBuilder;
4+
import static org.example.http.HttpResponseBuilder.*;
45

56
import java.io.File;
67
import java.io.IOException;

src/main/java/org/example/config/AppConfig.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
@JsonIgnoreProperties(ignoreUnknown = true)
77
public record AppConfig(
88
@JsonProperty("server") ServerConfig server,
9-
@JsonProperty("logging") LoggingConfig logging
9+
@JsonProperty("logging") LoggingConfig logging,
10+
@JsonProperty("ipFilter") IpFilterConfig ipFilter
1011
) {
1112
public static AppConfig defaults() {
12-
return new AppConfig(ServerConfig.defaults(), LoggingConfig.defaults());
13+
return new AppConfig(
14+
ServerConfig.defaults(),
15+
LoggingConfig.defaults(),
16+
IpFilterConfig.defaults()
17+
);
1318
}
1419

1520
public AppConfig withDefaultsApplied() {
1621
ServerConfig serverConfig = (server == null ? ServerConfig.defaults() : server.withDefaultsApplied());
1722
LoggingConfig loggingConfig = (logging == null ? LoggingConfig.defaults() : logging.withDefaultsApplied());
18-
return new AppConfig(serverConfig, loggingConfig);
23+
IpFilterConfig ipFilterConfig = (ipFilter == null ? IpFilterConfig.defaults() : ipFilter.withDefaultsApplied()); // ← LÄGG TILL
24+
return new AppConfig(serverConfig, loggingConfig, ipFilterConfig); // ← UPPDATERA DENNA RAD
1925
}
2026

2127
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -50,4 +56,24 @@ public LoggingConfig withDefaultsApplied() {
5056
return new LoggingConfig(lvl);
5157
}
5258
}
59+
60+
@JsonIgnoreProperties(ignoreUnknown = true)
61+
public record IpFilterConfig(
62+
@JsonProperty("enabled") Boolean enabled,
63+
@JsonProperty("mode") String mode,
64+
@JsonProperty("blockedIps") java.util.List<String> blockedIps,
65+
@JsonProperty("allowedIps") java.util.List<String> allowedIps
66+
) {
67+
public static IpFilterConfig defaults() {
68+
return new IpFilterConfig(false, "BLOCKLIST", java.util.List.of(), java.util.List.of());
69+
}
70+
71+
public IpFilterConfig withDefaultsApplied() {
72+
Boolean e = (enabled == null) ? false : enabled;
73+
String m = (mode == null || mode.isBlank()) ? "BLOCKLIST" : mode;
74+
java.util.List<String> blocked = (blockedIps == null) ? java.util.List.of() : blockedIps;
75+
java.util.List<String> allowed = (allowedIps == null) ? java.util.List.of() : allowedIps;
76+
return new IpFilterConfig(e, m, blocked, allowed);
77+
}
78+
}
5379
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.example.filter;
2+
3+
4+
import org.example.http.HttpResponseBuilder;
5+
import org.example.httpparser.HttpRequest;
6+
7+
import java.util.Set;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
10+
/**
11+
* A filter that allows or blocks HTTP requests based on the client's IP address.
12+
* The filter supports two modes:
13+
* ALLOWLIST – only IP addresses in the allowlist are permitted
14+
* BLOCKLIST – all IP addresses are permitted except those in the blocklist
15+
*/
16+
public class IpFilter implements Filter {
17+
18+
private final Set<String> blockedIps = ConcurrentHashMap.newKeySet();
19+
private final Set<String> allowedIps = ConcurrentHashMap.newKeySet();
20+
private volatile FilterMode mode = FilterMode.BLOCKLIST;
21+
22+
/**
23+
* Defines the filtering mode.
24+
*/
25+
public enum FilterMode {
26+
ALLOWLIST,
27+
BLOCKLIST
28+
}
29+
30+
@Override
31+
public void init() {
32+
// Intentionally empty - no initialization needed
33+
}
34+
35+
/**
36+
* Filters incoming HTTP requests based on the client's IP address.
37+
*
38+
* @param request the incoming HTTP request
39+
* @param response the HTTP response builder used when blocking requests
40+
* @param chain the filter chain to continue if the request is allowed
41+
*/
42+
@Override
43+
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {
44+
String clientIp = normalizeIp((String) request.getAttribute("clientIp"));
45+
46+
if (clientIp == null || clientIp.trim().isEmpty()) {
47+
response.setStatusCode(HttpResponseBuilder.SC_BAD_REQUEST);
48+
response.setBody("Bad Request: Missing client IP address");
49+
return;
50+
}
51+
52+
boolean allowed = isIpAllowed(clientIp);
53+
54+
if (allowed) {
55+
chain.doFilter(request, response);
56+
} else {
57+
response.setStatusCode(HttpResponseBuilder.SC_FORBIDDEN);
58+
response.setBody("Forbidden: IP address " + clientIp + " is not allowed");
59+
}
60+
}
61+
62+
@Override
63+
public void destroy() {
64+
// Intentionally empty - no cleanup needed
65+
}
66+
67+
/**
68+
* Determines whether an IP address is allowed based on the current filter mode.
69+
*
70+
* @param ip the IP address to check
71+
* @return true if the IP address is allowed, otherwise false
72+
*/
73+
private boolean isIpAllowed(String ip) {
74+
if (mode == FilterMode.ALLOWLIST) {
75+
return allowedIps.contains(ip);
76+
} else {
77+
return !blockedIps.contains(ip);
78+
}
79+
}
80+
81+
/**
82+
* Trims leading and trailing whitespace from an IP address.
83+
*
84+
* @param ip the IP address
85+
* @return the trimmed IP address, or {@code null} if the input is {@code null}
86+
*/
87+
private String normalizeIp(String ip) {
88+
return ip == null ? null : ip.trim();
89+
}
90+
91+
public void setMode(FilterMode mode) {
92+
this.mode = mode;
93+
}
94+
95+
public void addBlockedIp(String ip) {
96+
if (ip == null) {
97+
throw new IllegalArgumentException("IP address cannot be null");
98+
}
99+
blockedIps.add(normalizeIp(ip));
100+
}
101+
102+
public void addAllowedIp(String ip) {
103+
if (ip == null) {
104+
throw new IllegalArgumentException("IP address cannot be null");
105+
}
106+
allowedIps.add(normalizeIp(ip));
107+
}
108+
}

0 commit comments

Comments
 (0)