Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 30 additions & 21 deletions src/main/java/de/igslandstuhl/database/server/WebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.*;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
Expand Down Expand Up @@ -36,10 +37,11 @@
public class WebServer implements Runnable {
public static final int SESSION_DURATION = 21600; // six hours
public static final int MAXIMUM_INACTIVITY_DURATION = 3600; // An hour
public static final int RATELIMIT = 60;

private volatile boolean running;
private final SSLServerSocket serverSocket;
private final SessionManager userManager = new SessionManager(SESSION_DURATION, MAXIMUM_INACTIVITY_DURATION);
private final SessionManager userManager = new SessionManager(SESSION_DURATION, MAXIMUM_INACTIVITY_DURATION, RATELIMIT);
private final ExecutorService clientPool = Executors.newCachedThreadPool();
private final boolean secure = true;

Expand Down Expand Up @@ -85,48 +87,55 @@ class ClientHandler implements Runnable {

@Override
public void run() {
try (BufferedOutputStream rawOut = new BufferedOutputStream(clientSocket.getOutputStream())) {
PrintStream out = new PrintStream(rawOut, true, StandardCharsets.UTF_8);
try {
BufferedOutputStream rawOut = new BufferedOutputStream(clientSocket.getOutputStream());
PrintStream out = new PrintStream(rawOut, false, StandardCharsets.UTF_8);
try (BufferedInputStream bis = new BufferedInputStream(clientSocket.getInputStream())) {
String headerString = readHeadersAsString(bis);
if (headerString == null) {
GetResponse.internalServerError().respond(out);
return;
rawOut.close();
}
if (headerString.startsWith("GET")) {
handleGet(headerString, out);
} else if (headerString.startsWith("POST")) {
handlePost(headerString, bis, out);
} else {
GetResponse.internalServerError().respond(out);
try {
if (headerString.startsWith("GET")) {
handleGet(headerString, out);
} else if (headerString.startsWith("POST")) {
handlePost(headerString, bis, out);
} else {
rawOut.close();
// TODO: response with "Unsupported Method"
}
out.flush();
} catch (SocketException e) {
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
e.printStackTrace();
try {
PrintStream out = new PrintStream(clientSocket.getOutputStream(), true);
GetResponse.internalServerError().respond(out);
} catch (IOException ignored) {}
} finally {
try { clientSocket.close(); } catch (IOException ignored) {}
}
}

String readHeadersAsString(InputStream in) throws IOException {
byte[] headerBytes = readUntilDoubleCRLF(in);
if (headerBytes == null || headerBytes.length == 0) return null;
return new String(headerBytes, StandardCharsets.ISO_8859_1);
try {
byte[] headerBytes = readUntilDoubleCRLF(in);
if (headerBytes == null || headerBytes.length == 0) return null;
return new String(headerBytes, StandardCharsets.ISO_8859_1);
} catch (SocketException|SSLHandshakeException e) {
Thread.currentThread().interrupt();
return ""; // not accessible
}
}

void handleGet(String headerString, PrintStream out) {
SessionManager sessionManager = Server.getInstance().getWebServer().getSessionManager();
GetRequest get = new GetRequest(headerString, clientIp, secure);
GetResponse response;
if (!sessionManager.validateSession(get)) {
response = GetResponse.internalServerError();
response = GetResponse.forbidden(get);
} else {
String user = sessionManager.getSessionUser(get).getUsername();
response = GetResponse.getResource(get.toResourceLocation(user), user);
response = GetResponse.getResource(get, get.toResourceLocation(user), user);
}
response.respond(out);
}
Expand All @@ -143,7 +152,7 @@ void handlePost(String headerString, InputStream in, PrintStream out) throws IOE
body = URLDecoder.decode(raw, bodyCharset.name());
}
PostRequest parsedRequest = new PostRequest(postHeader, body, clientIp, secure);
PostResponse response = Server.getInstance().getWebServer().getSessionManager().validateSession(parsedRequest) ? PostRequestHandler.getInstance().handlePostRequest(parsedRequest) : PostResponse.badRequest("Bad request: session manipulation", parsedRequest);
PostResponse response = Server.getInstance().getWebServer().getSessionManager().validateSession(parsedRequest) ? PostRequestHandler.getInstance().handlePostRequest(parsedRequest) : PostResponse.forbidden("Forbidden: session manipulation or ratelimit", parsedRequest);
response.respond(out);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,41 @@
import java.io.InputStream;
import java.io.PrintStream;

import de.igslandstuhl.database.server.Server;
import de.igslandstuhl.database.server.resources.ResourceHelper;
import de.igslandstuhl.database.server.resources.ResourceLocation;

/**
* Represents a response to a GET request in the web server.
*/
public class GetResponse {
/**
* Represents a response for a GET request that was not found.
*/
private static final GetResponse NOT_FOUND = new GetResponse(Status.NOT_FOUND, new ResourceLocation("html", "errors", "404.html"), ContentType.HTML, "");
/**
* Returns a response for a GET request that was not found.
* @return the GetResponse object
*/
public static GetResponse notFound() {
return NOT_FOUND;
public static GetResponse notFound(HttpRequest request) {
return new GetResponse(request, Status.NOT_FOUND, new ResourceLocation("html", "errors", "404.html"), ContentType.HTML, "");
}
/**
* Represents a response for a GET request that resulted in an internal error.
*/
private static final GetResponse INTERNAL_ERROR = new GetResponse(Status.INTERNAL_SERVER_ERROR, new ResourceLocation("html", "errors", "500.html"), ContentType.HTML, "");
/**
* Returns a response for a GET request that resulted in an internal error.
* @return the GetResponse object
*/
public static GetResponse internalServerError() {
return INTERNAL_ERROR;
public static GetResponse internalServerError(HttpRequest request) {
return new GetResponse(request, Status.INTERNAL_SERVER_ERROR, new ResourceLocation("html", "errors", "500.html"), ContentType.HTML, "");
}
/**
* Represents a response for a GET request the user has no access to.
*/
private static final GetResponse FORBIDDEN = new GetResponse(Status.FORBIDDEN, new ResourceLocation("html", "errors", "403.html"), ContentType.HTML, "");
/**
* Returns a response for a GET request the user has no access to.
* @return the GetRequest object
* @return the HttpRequest object
*/
public static GetResponse forbidden() {
return FORBIDDEN;
public static GetResponse forbidden(HttpRequest request) {
return new GetResponse(request, Status.FORBIDDEN, new ResourceLocation("html", "errors", "403.html"), ContentType.HTML, "");
}
/**
* Represents a response for a GET request the user must be logged in for.
*/
private static final GetResponse UNAUTHORIZED = new GetResponse(Status.UNAUTHORIZED, new ResourceLocation("html", "errors", "401.html"), ContentType.HTML, "");
/**
* Returns a response for a GET request the user must be logged in for.
* @return the GetResponse object
*/
public static GetResponse unauthorized() {
return UNAUTHORIZED;
public static GetResponse unauthorized(HttpRequest request) {
return new GetResponse(request, Status.UNAUTHORIZED, new ResourceLocation("html", "errors", "401.html"), ContentType.HTML, "");
}
/**
* The HTTP status of this response
Expand All @@ -79,6 +64,7 @@ public static GetResponse unauthorized() {
* This is used to check access permissions.
*/
private final String user;
private final HttpRequest request;

/**
* Creates a new GetResponse with the given parameters.
Expand All @@ -88,27 +74,28 @@ public static GetResponse unauthorized() {
* @param contentType the HTTP content type of the resource
* @param user the user who made the request
*/
public GetResponse(Status status, ResourceLocation resourceLocation, ContentType contentType, String user) {
public GetResponse(HttpRequest request, Status status, ResourceLocation resourceLocation, ContentType contentType, String user) {
this.status = status;
this.resourceLocation = resourceLocation;
this.contentType = contentType;
this.user = user;
this.request = request;
}
/**
* Returns a response for a GET request for the given resource.
* @param resourceLocation the location of the resource to be returned
* @param user the user who made the request
* @return the GetResponse object
*/
public static GetResponse getResource(ResourceLocation resourceLocation, String user) {
public static GetResponse getResource(HttpRequest request, ResourceLocation resourceLocation, String user) {
try {
if (AccessManager.hasAccess(user, resourceLocation)) {
return new GetResponse(Status.OK, resourceLocation, ContentType.ofResourceLocation(resourceLocation), user);
return new GetResponse(request, Status.OK, resourceLocation, ContentType.ofResourceLocation(resourceLocation), user);
} else {
return unauthorized();
return unauthorized(request);
}
} catch (NoWebResourceException e) {
return forbidden();
return forbidden(request);
}
}

Expand All @@ -126,6 +113,7 @@ public void respond(PrintStream out) {
out.print("; charset=");out.print(charset);
}
out.println();
out.println("Set-Cookie: " + Server.getInstance().getWebServer().getSessionManager().getSession(request).createSessionCookie());
}
out.println(); // <--- This line is important: seperates Header and Body!
if (contentType.isText()) {
Expand All @@ -145,11 +133,11 @@ public void respond(PrintStream out) {
}
}
} catch (FileNotFoundException e) {
notFound().respond(out);
notFound(request).respond(out);
} catch (Exception e) {
e.printStackTrace();
if (status != Status.INTERNAL_SERVER_ERROR) {
internalServerError().respond(out);
internalServerError(request).respond(out);
} else {
throw new IllegalStateException("Uncaught exception", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public static PostResponse ok(String body, ContentType contentType, PostRequest
public static PostResponse getResource(ResourceLocation resourceLocation, String user, PostRequest request) {
try {
if (AccessManager.hasAccess(user, resourceLocation)) {
return new PostResponse(Status.OK, GetResponse.getResource(resourceLocation, user).getResponseBody(), ContentType.ofResourceLocation(resourceLocation), request);
return new PostResponse(Status.OK, GetResponse.getResource(request, resourceLocation, user).getResponseBody(), ContentType.ofResourceLocation(resourceLocation), request);
} else {
return unauthorized("You have to be logged in to access this resource.", request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ public class SessionManager {
*/
private Map<Session, String> sessionUsers = new HashMap<>();
private Map<Session, Instant> lastActivity = new HashMap<>();
private Map<Session, Integer> requestCount = new HashMap<>();

/**
* After this duration, sessions expire (are removed from the session store). It is measured in seconds.
*/
private final int sessionExpireDuration;
private final int maximumInactivityDuration;
private final int maxRequests;

public SessionManager(int sessionExpireDuration, int maximumInactivityDuration) {
public SessionManager(int sessionExpireDuration, int maximumInactivityDuration, int maxRequests) {
this.sessionExpireDuration = sessionExpireDuration;
this.maximumInactivityDuration = maximumInactivityDuration;
this.maxRequests = maxRequests;
new Thread(this::cleanSecondsJob, "Session Expiring").start();
}

Expand All @@ -51,8 +54,9 @@ private void cleanSecondsJob() {
)
.toList().stream() // Convert to list to avoid ConcurrentModificationException
.forEach(this::removeSession);
requestCount.clear();
try {
Thread.sleep(10000);
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Expand All @@ -62,6 +66,16 @@ private void cleanSecondsJob() {
public boolean validateSession(HttpRequest request) {
Session session = getSession(request);
lastActivity.put(session, Instant.now());

Integer requests = requestCount.get(session);
int count = requests == null ? 0 : requests;
count++;
requestCount.put(session, count);
if (count > maxRequests) {
System.out.println("Ratelimit!");
return false;
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,42 @@
import de.igslandstuhl.database.server.resources.ResourceLocation;

public class GetResponseTest {
GetRequest request = new GetRequest("GET / HTTP/1.1", "127.0.0.1", true);
@Test
void testForbidden() throws FileNotFoundException {
assertTrue(GetResponse.forbidden().getResponseBody().contains("403"));
assertTrue(GetResponse.forbidden(request).getResponseBody().contains("403"));
}

@Test
void testInternalServerError() throws FileNotFoundException {
assertTrue(GetResponse.internalServerError().getResponseBody().contains("500"));
assertTrue(GetResponse.internalServerError(request).getResponseBody().contains("500"));
}

@Test
void testNotFound() throws FileNotFoundException {
assertTrue(GetResponse.notFound().getResponseBody().contains("404"));
assertTrue(GetResponse.notFound(request).getResponseBody().contains("404"));
}

@Test
void testUnauthorized() throws FileNotFoundException {
assertTrue(GetResponse.unauthorized().getResponseBody().contains("401"));
assertTrue(GetResponse.unauthorized(request).getResponseBody().contains("401"));
}

@Test
void testGetResource() throws FileNotFoundException {
assertTrue(GetResponse.getResource(ResourceLocation.get("html", "site:login.html"), null).getResponseBody().contains("login"));
assertTrue(GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null).getResponseBody().contains("login"));
}

@Test
void testGetResponseBody() {
assertNotNull(GetResponse.getResource(ResourceLocation.get("html", "site:login.html"), null));
assertNotNull(GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null));
}

@Test
void testRespond() throws FileNotFoundException {
ByteArrayOutputStream testStream = new ByteArrayOutputStream();
PrintStream printWriter = new PrintStream(testStream);
GetResponse response = GetResponse.getResource(ResourceLocation.get("html", "site:login.html"), null);
GetResponse response = GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null);
response.respond(printWriter);
String responseString = testStream.toString();
String responseBody = response.getResponseBody();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class SessionManagerTest {
PostRequest requestWithoutSession;
@BeforeEach
void setup() {
sessionManager = new SessionManager(Integer.MAX_VALUE, Integer.MAX_VALUE);
sessionManager = new SessionManager(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
sessionRequest = new PostRequest("POST /student-data HTTP/1.1\r\n" + //
"Cookie: test=test;session=" + UUID.randomUUID().toString() + ";other=value", null, LOCALHOST, true);
requestWithoutSession = new PostRequest("POST /login HTTP/1.1", null, LOCALHOST, true);
Expand Down
Loading