diff --git a/src/main/java/de/igslandstuhl/database/server/WebServer.java b/src/main/java/de/igslandstuhl/database/server/WebServer.java index e701ec5..0dec0ae 100644 --- a/src/main/java/de/igslandstuhl/database/server/WebServer.java +++ b/src/main/java/de/igslandstuhl/database/server/WebServer.java @@ -1,6 +1,8 @@ package de.igslandstuhl.database.server; import java.io.*; +import java.net.URLDecoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import javax.net.ssl.*; @@ -10,7 +12,13 @@ import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; -import de.igslandstuhl.database.server.resources.ResourceHelper; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import de.igslandstuhl.database.server.webserver.GetRequest; import de.igslandstuhl.database.server.webserver.GetResponse; import de.igslandstuhl.database.server.webserver.PostHeader; @@ -24,148 +32,163 @@ * It supports GET and POST requests for login, subject requests, current topics, tasks, and room updates. */ public class WebServer implements Runnable { - /** - * Indicates whether the server is currently running. - */ - private boolean running; - /** - * The SSL server socket that listens for incoming HTTPS connections. - */ - private SSLServerSocket serverSocket; - /** - * The UserManager instance that handles user-related operations such as login and session management. - */ + private volatile boolean running; + private final SSLServerSocket serverSocket; private final UserManager userManager = new UserManager(); - /** - * Returns the session store used by the server. - * This method provides access to the session store, allowing for session management operations. - * - * @return The session store map containing session IDs and associated usernames. - */ + private final ExecutorService clientPool = Executors.newCachedThreadPool(); + public UserManager getUserManager() { return userManager; } - - /** - * Constructs a new WebServer instance with the specified port and keystore. - * - * @param port The port on which the server will listen for HTTPS requests. - * @param keystorePath The path to the keystore file containing the server's SSL certificate. - * @param keystorePassword The password for the keystore. - * @throws KeyStoreException If the keystore cannot be initialized. - * @throws FileNotFoundException If the keystore file is not found. - * @throws IOException If an I/O error occurs while reading the keystore. - * @throws NoSuchAlgorithmException If the specified algorithm is not available. - * @throws CertificateException If there is an error with the certificate in the keystore. - * @throws UnrecoverableKeyException If the key cannot be recovered from the keystore. - * @throws KeyManagementException If there is an error initializing the SSL context. - */ - public WebServer(int port, String keystorePath, String keystorePassword) throws KeyStoreException, FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException { + + public WebServer(int port, String keystorePath, String keystorePassword) + throws KeyStoreException, FileNotFoundException, IOException, + NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException { KeyStore ks = KeyStore.getInstance("JKS"); try (FileInputStream fis = new FileInputStream(keystorePath)) { ks.load(fis, keystorePassword.toCharArray()); } - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, keystorePassword.toCharArray()); - SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), null, null); - SSLServerSocketFactory factory = sslContext.getServerSocketFactory(); serverSocket = (SSLServerSocket) factory.createServerSocket(port); } - /** - * Handles incoming client connections in a separate thread. - * This class implements Runnable to allow concurrent handling of multiple clients. + * Constructs a test webserver without server socket */ + protected WebServer() { + this.serverSocket = null; + } + class ClientHandler implements Runnable { - /** - * The SSLSocket representing the client connection. - */ - private SSLSocket clientSocket; - - /** - * Constructs a new ClientHandler for the given SSLSocket. - * - * @param socket The SSLSocket representing the client connection. - */ - public ClientHandler(SSLSocket socket) { - this.clientSocket = socket; - } + private final SSLSocket clientSocket; - /** - * Responds to a GET request by retrieving the requested resource. - * - * @param request The GetRequest object containing the request details. - * @param user The username associated with the session, or null if not logged in. - * @return A GetResponse containing the requested resource or an error response. - */ - private GetResponse respond(GetRequest request, String user) { - return GetResponse.getResource(request.toResourceLocation(user), user); + ClientHandler(SSLSocket socket) { + this.clientSocket = socket; } - /** - * Handles the client request by reading the input, processing it, and sending a response. - * This method supports GET and POST requests for various functionalities such as login, - * subject requests, current topics, tasks, and room updates. - */ @Override public void run() { - PrintWriter out; - try { - out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8), true); - } catch (IOException e) { - e.printStackTrace(); - return; - } - try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) { - - String request = ResourceHelper.readResourceTillEmptyLine(in); - - if (request.startsWith("GET")) { - String user = Server.getInstance().getWebServer().getUserManager().getSessionUser(request); - GetRequest get = new GetRequest(request); - GetResponse response = respond(get, user); - response.respond(out); - } else if (request.startsWith("POST")) { - PostHeader header = new PostHeader(request); - int contentLength = header.getContentLength(); - String body = null; - if (contentLength > 0) { - char[] bodyChars = new char[contentLength]; - in.read(bodyChars, 0, contentLength); - body = new String(bodyChars); + try (BufferedOutputStream rawOut = new BufferedOutputStream(clientSocket.getOutputStream())) { + PrintWriter out = new PrintWriter(new OutputStreamWriter(rawOut, StandardCharsets.UTF_8), true); + try (BufferedInputStream bis = new BufferedInputStream(clientSocket.getInputStream())) { + String headerString = readHeadersAsString(bis); + if (headerString == null) { + GetResponse.internalServerError().respond(out); + return; + } + if (headerString.startsWith("GET")) { + handleGet(headerString, out); + } else if (headerString.startsWith("POST")) { + handlePost(headerString, bis, out); + } else { + GetResponse.internalServerError().respond(out); } - PostRequest parsedRequest = new PostRequest(header, body); - PostResponse response = PostRequestHandler.getInstance().handlePostRequest(parsedRequest); - response.respond(out); } - } catch (IOException e) { - e.printStackTrace(); - GetResponse.internalServerError().respond(out); - e.printStackTrace(out); } catch (Exception e) { e.printStackTrace(); - GetResponse.internalServerError().respond(out); - e.printStackTrace(out); - } finally { try { - out.flush(); - out.close(); - clientSocket.close(); - } catch (IOException e) { - e.printStackTrace(); + PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8), 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); + } + + void handleGet(String headerString, PrintWriter out) { + String user = Server.getInstance().getWebServer().getUserManager().getSessionUser(headerString); + GetRequest get = new GetRequest(headerString); + GetResponse response = GetResponse.getResource(get.toResourceLocation(user), user); + response.respond(out); + } + + void handlePost(String headerString, InputStream in, PrintWriter out) throws IOException { + Map headerMap = parseHeaders(headerString); + PostHeader postHeader = new PostHeader(headerString); + int contentLength = headerMap.containsKey("content-length") ? Integer.parseInt(headerMap.get("content-length")) : 0; + Charset bodyCharset = determineCharset(headerMap.get("content-type")); + String body = null; + if (contentLength > 0) { + byte[] bodyBytes = readNBytes(in, contentLength); + String raw = new String(bodyBytes, bodyCharset); + body = URLDecoder.decode(raw, bodyCharset.name()); + } + PostRequest parsedRequest = new PostRequest(postHeader, body); + PostResponse response = PostRequestHandler.getInstance().handlePostRequest(parsedRequest); + response.respond(out); + } + + byte[] readUntilDoubleCRLF(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + LinkedList window = new LinkedList<>(); + int current; + while ((current = in.read()) != -1) { + baos.write(current); + window.addLast(current); + if (window.size() > 4) window.removeFirst(); + if (window.size() == 4) { + Integer[] w = window.toArray(new Integer[0]); + if (w[0] == 13 && w[1] == 10 && w[2] == 13 && w[3] == 10) { + break; + } + } + if (baos.size() > 64 * 1024) break; + } + return baos.toByteArray(); + } + + byte[] readNBytes(InputStream in, int n) throws IOException { + byte[] buffer = new byte[n]; + int read = 0; + while (read < n) { + int r = in.read(buffer, read, n - read); + if (r == -1) return null; + read += r; + } + return buffer; + } + + Map parseHeaders(String headerString) { + Map map = new HashMap<>(); + String[] lines = headerString.split("\\r?\\n"); + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + int idx = line.indexOf(':'); + if (idx > 0) { + String name = line.substring(0, idx).trim().toLowerCase(Locale.ROOT); + String value = line.substring(idx + 1).trim(); + map.put(name, value); + } + } + return map; + } + + Charset determineCharset(String contentType) { + if (contentType != null) { + for (String p : contentType.split(";")) { + p = p.trim(); + if (p.toLowerCase(Locale.ROOT).startsWith("charset=")) { + try { + return Charset.forName(p.substring(8).replace("\"", "")); + } catch (Exception ignored) { + return StandardCharsets.UTF_8; + } + } } } + return StandardCharsets.UTF_8; } } - /** - * Starts the web server, allowing it to accept incoming HTTPS requests. - * If the server is already running, it throws an IllegalStateException. - */ public void start() { if (!running) { running = true; @@ -174,28 +197,24 @@ public void start() { throw new IllegalStateException("Server already started"); } } - /** - * Stops the web server, preventing it from accepting new requests. - * It closes the server socket and releases any resources held by the server. - */ + public void stop() { running = false; - try { - serverSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } + try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } + clientPool.shutdownNow(); } @Override public void run() { while (running) { try { - SSLSocket clientSocket = (SSLSocket) serverSocket.accept(); - new Thread(new ClientHandler(clientSocket)).start(); + final SSLSocket clientSocket = (SSLSocket) serverSocket.accept(); + clientPool.submit(new ClientHandler(clientSocket)); } catch (IOException e) { - System.err.println("Error while accepting client"); - e.printStackTrace(); + if (running) { + System.err.println("Error while accepting client"); + e.printStackTrace(); + } } } } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java b/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java index c2829e8..3cb2a86 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java @@ -46,15 +46,18 @@ public class AccessManager { * @return true if the user has access to the resource, false otherwise */ public static boolean hasAccess(String user, ResourceLocation resource) { + return hasAccess(User.getUser(user), resource); + } + public static boolean hasAccess(User user, ResourceLocation resource) { if (Arrays.asList(PUBLIC_SPACES).contains(resource.namespace()) || Arrays.asList(PUBLIC_LOCATIONS).contains(resource.resource())) { return true; } else if (user != null) { - if (resource.namespace().equals(USER_SPACE) || resource.resource().startsWith("my")) { + if (resource.namespace().equals(USER_SPACE) || resource.resource().startsWith("my") && !(user == User.ANONYMOUS)) { return true; } else if (resource.namespace().equals(TEACHER_SPACE)) { - return User.getUser(user).isTeacher() || User.getUser(user).isAdmin(); + return user.isTeacher() || user.isAdmin(); } else if (resource.namespace().equals(ADMIN_SPACE) || Arrays.asList(ADMIN_LOCATIONS).contains(resource.resource())) { - return User.getUser(user).isAdmin(); + return user.isAdmin(); } else { return false; } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/Cookie.java b/src/main/java/de/igslandstuhl/database/server/webserver/Cookie.java index 2fdae85..e2d8158 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/Cookie.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/Cookie.java @@ -48,4 +48,36 @@ public String getValue() { public String toString() { return name + "=" + value; } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Cookie other = (Cookie) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/GetRequest.java b/src/main/java/de/igslandstuhl/database/server/webserver/GetRequest.java index a3b0b7f..52ec247 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/GetRequest.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/GetRequest.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.Map; +import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.resources.ResourceLocation; /** @@ -38,7 +39,7 @@ public class GetRequest { * @param request the request string to parse */ public GetRequest(String request) { - if (!request.startsWith("GET")){ + if (!request.startsWith("GET") || !request.endsWith("HTTP/1.1")){ throw new IllegalArgumentException(); } String [] lines = request.split("\n"); @@ -77,6 +78,6 @@ public boolean isValid() { * @see de.igslandstuhl.database.server.resources.ResourceLocation */ public ResourceLocation toResourceLocation(String user) { - return WebResourceHandler.locationFromPath(path, user); + return WebResourceHandler.locationFromPath(path, User.getUser(user)); } } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/PostHeader.java b/src/main/java/de/igslandstuhl/database/server/webserver/PostHeader.java index 30916b4..7ec70e1 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/PostHeader.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/PostHeader.java @@ -36,10 +36,10 @@ public PostHeader(String header) { if (line.startsWith("Content-Length:")) { contentLength = Integer.parseInt(line.split(":")[1].trim()); } else if (line.startsWith("Cookie:")) { - String[] cookieData = line.substring(7).split("; "); + String[] cookieData = line.substring(7).split(";"); List cookieList = new ArrayList<>(); for (String cookie : cookieData) { - String[] keyValue = cookie.split("="); + String[] keyValue = cookie.trim().split("="); if (keyValue.length == 2) { cookieList.add(new Cookie(keyValue[0].trim(), keyValue[1].trim())); } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/PostRequest.java b/src/main/java/de/igslandstuhl/database/server/webserver/PostRequest.java index 201a7f2..65f1da6 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/PostRequest.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/PostRequest.java @@ -8,6 +8,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.resources.ResourceLocation; /** @@ -59,10 +60,10 @@ public PostRequest(String header, String body) { if (line.startsWith("Content-Length:")) { contentLength = Integer.parseInt(line.split(":")[1].trim()); } else if (line.startsWith("Cookie:")) { - String[] cookieData = line.substring(7).split("; "); + String[] cookieData = line.substring(7).split(";"); List cookieList = new ArrayList<>(); for (String cookie : cookieData) { - String[] keyValue = cookie.split("="); + String[] keyValue = cookie.trim().split("="); if (keyValue.length == 2) { cookieList.add(new Cookie(keyValue[0].trim(), keyValue[1].trim())); } @@ -125,15 +126,17 @@ public int getContentLength() { } public Map getFormData() { + if (body == null || body.isEmpty()) return Map.of(); + else if (!Character.isLetter(body.charAt(0))) throw new IllegalArgumentException("No Form Data"); Map params = new HashMap<>(); // Parse body as form data: key1=value1&key2=value2 - if (body != null && !body.isEmpty()) { - String[] pairs = body.split("&"); - for (String pair : pairs) { - String[] kv = pair.split("="); - if (kv.length == 2) { - params.put(kv[0], kv[1]); - } + String[] pairs = body.split("&"); + for (String pair : pairs) { + String[] kv = pair.split("="); + if (kv.length == 2) { + params.put(kv[0], kv[1]); + } else { + throw new IllegalArgumentException("No form data"); } } return params; @@ -160,6 +163,6 @@ public Cookie[] getCookies() { * @return a ResourceLocation object representing the path of the POST request */ public ResourceLocation toResourceLocation(String username) { - return de.igslandstuhl.database.server.webserver.WebResourceHandler.locationFromPath(path, username); + return de.igslandstuhl.database.server.webserver.WebResourceHandler.locationFromPath(path, User.getUser(username)); } } \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/PostRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/PostRequestHandler.java index 499f5dd..1951d74 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/PostRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/PostRequestHandler.java @@ -1,6 +1,9 @@ package de.igslandstuhl.database.server.webserver; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.Map; import java.util.Random; @@ -155,42 +158,11 @@ private String prepare(String webInput) { return prepare(webInput, true); } private String prepare(String webInput, boolean sanitize) { - webInput = webInput.replaceAll("%20", " ") - .replaceAll("\\+", " ") - .replaceAll("%0A", "\n") - .replaceAll("%0D", "\r") - .replaceAll("%21", "!") - .replaceAll("%23", "#") - .replaceAll("%26", "&") - .replaceAll("%28", "(") - .replaceAll("%29", ")") - .replaceAll("%2A", "*") - .replaceAll("%2B", "+") - .replaceAll("%2C", ",") - .replaceAll("%2F", "/") - .replaceAll("%3A", ":") - .replaceAll("%3B", ";") - .replaceAll("%3C", "<") - .replaceAll("%3D", "=") - .replaceAll("%3E", ">") - .replaceAll("%3F", "?") - .replaceAll("%40", "@") - .replaceAll("%5B", "[") - .replaceAll("%5D", "]") - .replaceAll("%7B", "{") - .replaceAll("%7D", "}") - .replaceAll("ß", "ß") - .replaceAll("ä", "ä") - .replaceAll("ö", "ö") - .replaceAll("ü", "ü") - .replaceAll("Ä", "Ä") - .replaceAll("Ö", "Ö") - .replaceAll("Ü", "Ü") - .replaceAll("%C3%A4", "ä") - .replaceAll("%C3%BC", "ü") - .replaceAll("%C3%B6", "ö") - .replaceAll("%C3%9F", "ß") - .replaceAll("%C2%A0", " ") + try { + webInput = URLDecoder.decode(webInput, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } ; if (sanitize) { // Sanitize the input to prevent XSS attacks @@ -458,7 +430,7 @@ private PostResponse handleStudentGetData(PostRequest request) { } String email = student.getEmail(); // Email is the username for the student - return PostResponse.getResource(WebResourceHandler.locationFromPath(path, email), email); + return PostResponse.getResource(WebResourceHandler.locationFromPath(path, User.getUser(email)), email); } private PostResponse handleTeacherGetData(PostRequest request) { String path = request.getPath(); @@ -475,7 +447,7 @@ private PostResponse handleTeacherGetData(PostRequest request) { Teacher teacher = Teacher.get(id); String email = teacher.getEmail(); // Email is the username for the teacher - return PostResponse.getResource(WebResourceHandler.locationFromPath(path, email), email); + return PostResponse.getResource(WebResourceHandler.locationFromPath(path, User.getUser(email)), email); } private PostResponse handleStudentList(PostRequest request) { String path = request.getPath(); diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/WebResourceHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/WebResourceHandler.java index b26141a..88549ab 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/WebResourceHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/WebResourceHandler.java @@ -47,7 +47,7 @@ private static boolean inAdminOnlySpace(String path) { return false; } - public static ResourceLocation locationFromPath(String path, String username) { + public static ResourceLocation locationFromPath(String path, User user) { if (isSQLWebResource(path)) { return new ResourceLocation("virtual", "sql", path.replaceFirst("/", "")); } @@ -68,7 +68,7 @@ public static ResourceLocation locationFromPath(String path, String username) { path += ".html"; } - User user = username == null ? null : User.getUser(username); + if (user == null) user = User.ANONYMOUS; parts = path.split("/", 3); String namespace; @@ -77,12 +77,16 @@ public static ResourceLocation locationFromPath(String path, String username) { namespace = parts[1]; resource = parts[2]; } else { - if (inAdminOnlySpace(parts[1]) && (user == null || user.isAdmin())) { + if (inAdminOnlySpace(parts[1]) && (user == User.ANONYMOUS || user.isAdmin())) { namespace = "admin"; - } else if (inTeacherOnlySpace(parts[1]) && (user == null || user.isTeacher() || user.isAdmin())) { + } else if (inTeacherOnlySpace(parts[1]) && (user == User.ANONYMOUS || user.isTeacher() || user.isAdmin())) { namespace = "teacher"; } else if (inUserOnlySpace(parts[1])) { namespace = "user"; + } else if (inAdminOnlySpace(parts[1])) { + namespace = "admin"; + } else if (inTeacherOnlySpace(parts[1])) { + namespace = "teacher"; } else { namespace = "site"; } diff --git a/src/test/java/de/igslandstuhl/database/server/WebServerTest.java b/src/test/java/de/igslandstuhl/database/server/WebServerTest.java new file mode 100644 index 0000000..93c234c --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/WebServerTest.java @@ -0,0 +1,28 @@ +package de.igslandstuhl.database.server; + +import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +public class WebServerTest { + @Test + public void testUtf8DecodingSimpleText() throws Exception { + String original = "äöüßÄÖÜ"; + String encoded = java.net.URLEncoder.encode(original, StandardCharsets.UTF_8.name()); + + String decoded = java.net.URLDecoder.decode(encoded, StandardCharsets.UTF_8.name()); + assertEquals(original, decoded); + } + + @Test + public void testReadHeadersAsString() throws Exception { + String headers = "POST /test HTTP/1.1\r\nHost: localhost\r\nContent-Length: 11\r\n\r\n"; + InputStream in = new ByteArrayInputStream(headers.getBytes(StandardCharsets.ISO_8859_1)); + WebServer.ClientHandler handler = new WebServer().new ClientHandler(null); + String result = handler.readHeadersAsString(in); + assertTrue(result.contains("Content-Length: 11")); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/AccessManagerTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/AccessManagerTest.java new file mode 100644 index 0000000..9908305 --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/AccessManagerTest.java @@ -0,0 +1,156 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.SQLException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.igslandstuhl.database.api.User; +import de.igslandstuhl.database.server.resources.ResourceLocation; + +public class AccessManagerTest { + private User teacher; + private User student; + private User admin; + private User anonymous; + + @BeforeEach + public void setupUsers() { + admin = new User() { + @Override + public boolean isTeacher() { + return false; + } + @Override + public boolean isStudent() { + return false; + } + @Override + public boolean isAdmin() { + return true; + } + @Override + public String getPasswordHash() { + throw new IllegalStateException("Access manager should not query admin password"); + } + @Override + public String toJSON() { + throw new IllegalStateException("Access manager should not query admin json"); + } + @Override + public User setPassword(String password) throws SQLException { + throw new IllegalStateException("Access manager should not change admin password"); + } + @Override + public String getUsername() { + return "example@admin.de"; + } + }; + teacher = new User() { + @Override + public boolean isTeacher() { + return true; + } + @Override + public boolean isStudent() { + return false; + } + @Override + public boolean isAdmin() { + return false; + } + @Override + public String getPasswordHash() { + throw new IllegalStateException("Access manager should not query teacher password"); + } + @Override + public String toJSON() { + throw new IllegalStateException("Access manager should not query teacher json"); + } + @Override + public User setPassword(String password) throws SQLException { + throw new IllegalStateException("Access manager should not change teacher password"); + } + @Override + public String getUsername() { + return "example@teacher.de"; + } + }; + student = new User() { + @Override + public boolean isTeacher() { + return false; + } + @Override + public boolean isStudent() { + return true; + } + @Override + public boolean isAdmin() { + return false; + } + @Override + public String getPasswordHash() { + throw new IllegalStateException("Access manager should not query student password"); + } + @Override + public String toJSON() { + throw new IllegalStateException("Access manager should not query student json"); + } + @Override + public User setPassword(String password) throws SQLException { + throw new IllegalStateException("Access manager should not change student password"); + } + @Override + public String getUsername() { + return "example@student.de"; + } + }; + anonymous = User.ANONYMOUS; + } + @Test + public void testErrorMessageAccess() { + assertTrue(AccessManager.hasAccess(anonymous, ResourceLocation.get("html", "error:404"))); + assertTrue(AccessManager.hasAccess(student, ResourceLocation.get("html", "error:404"))); + assertTrue(AccessManager.hasAccess(teacher, ResourceLocation.get("html", "error:404"))); + assertTrue(AccessManager.hasAccess(admin, ResourceLocation.get("html", "error:404"))); + } + @Test + public void testIconAccess() { + assertTrue(AccessManager.hasAccess(anonymous, ResourceLocation.get("imgs", "icons:favicon.ico"))); + assertTrue(AccessManager.hasAccess(student, ResourceLocation.get("imgs", "icons:favicon.ico"))); + assertTrue(AccessManager.hasAccess(teacher, ResourceLocation.get("imgs", "icons:favicon.ico"))); + assertTrue(AccessManager.hasAccess(admin, ResourceLocation.get("imgs", "icons:favicon.ico"))); + } + @Test + public void testLoginAccess() { + assertTrue(AccessManager.hasAccess(anonymous, ResourceLocation.get("html", "site:login"))); + assertTrue(AccessManager.hasAccess(student, ResourceLocation.get("html", "site:login"))); + assertTrue(AccessManager.hasAccess(teacher, ResourceLocation.get("html", "site:login"))); + assertTrue(AccessManager.hasAccess(admin, ResourceLocation.get("html", "site:login"))); + } + @Test + public void testDashboardAccess() { + assertFalse(AccessManager.hasAccess(anonymous, WebResourceHandler.locationFromPath("/dashboard", anonymous))); + assertTrue(AccessManager.hasAccess(student, WebResourceHandler.locationFromPath("/dashboard", student))); + assertTrue(AccessManager.hasAccess(teacher, WebResourceHandler.locationFromPath("/dashboard", teacher))); + assertTrue(AccessManager.hasAccess(admin, WebResourceHandler.locationFromPath("/dashboard", admin))); + } + @Test + public void testStudentManagementAccess() { + assertFalse(AccessManager.hasAccess(anonymous, WebResourceHandler.locationFromPath("/student", anonymous))); + assertFalse(AccessManager.hasAccess(student, WebResourceHandler.locationFromPath("/student", student))); + assertTrue(AccessManager.hasAccess(teacher, WebResourceHandler.locationFromPath("/student", teacher))); + assertTrue(AccessManager.hasAccess(admin, WebResourceHandler.locationFromPath("/student", admin))); + } + @Test + public void testTeacherManagementAccess() { + assertFalse(AccessManager.hasAccess(anonymous, WebResourceHandler.locationFromPath("/teacher", anonymous))); + assertFalse(AccessManager.hasAccess(student, WebResourceHandler.locationFromPath("/teacher", student))); + assertFalse(AccessManager.hasAccess(teacher, WebResourceHandler.locationFromPath("/teacher", teacher))); + assertTrue(AccessManager.hasAccess(admin, WebResourceHandler.locationFromPath("/teacher", admin))); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/ContentTypeTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/ContentTypeTest.java new file mode 100644 index 0000000..e9a4458 --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/ContentTypeTest.java @@ -0,0 +1,70 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.sql.SQLException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.igslandstuhl.database.api.User; +import de.igslandstuhl.database.server.resources.ResourceLocation; + +public class ContentTypeTest { + private User student; + + @BeforeEach + public void setupUsers() { + student = new User() { + @Override + public boolean isTeacher() { + return false; + } + @Override + public boolean isStudent() { + return true; + } + @Override + public boolean isAdmin() { + return false; + } + @Override + public String getPasswordHash() { + throw new IllegalStateException("Access manager should not query student password"); + } + @Override + public String toJSON() { + throw new IllegalStateException("Access manager should not query student json"); + } + @Override + public User setPassword(String password) throws SQLException { + throw new IllegalStateException("Access manager should not change student password"); + } + @Override + public String getUsername() { + return "example@student.de"; + } + }; + } + @Test + void testGetName() { + assertEquals(ContentType.TEXT_PLAIN.getName(), "text/plain"); + assertEquals(ContentType.HTML.getName(), "text/html"); + assertEquals(ContentType.JAVASCRIPT.getName(), "text/javascript"); + assertEquals(ContentType.CSS.getName(), "text/css"); + assertEquals(ContentType.PNG.getName(), "image/png"); + assertEquals(ContentType.JSON.getName(), "text/json"); + } + + @Test + void testOfResourceLocation() throws NoWebResourceException { + ResourceLocation err404 = ResourceLocation.get("html", "error:404"); + assertEquals(ContentType.ofResourceLocation(err404), ContentType.HTML); + ResourceLocation icon = ResourceLocation.get("imgs", "icons:favicon.ico"); + assertEquals(ContentType.ofResourceLocation(icon), ContentType.PNG); + ResourceLocation dashboardJs = WebResourceHandler.locationFromPath("/build_dashboard.js", student); + assertEquals(ContentType.ofResourceLocation(dashboardJs), ContentType.JAVASCRIPT); + ResourceLocation css = WebResourceHandler.locationFromPath("/style.css", student); + assertEquals(ContentType.ofResourceLocation(css), ContentType.CSS); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/CookieTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/CookieTest.java new file mode 100644 index 0000000..ccdc980 --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/CookieTest.java @@ -0,0 +1,33 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CookieTest { + Cookie cookie; + @BeforeEach + public void initCookie() { + cookie = new Cookie("test-key", "test-value"); + } + @Test + void testGetName() { + assertEquals(cookie.getName(), "test-key"); + } + + @Test + void testGetValue() { + assertEquals(cookie.getValue(), "test-value"); + } + + @Test + void testToString() { + assertEquals(cookie.toString(), "test-key=test-value"); + } + + @Test + void testEquals() { + assertEquals(cookie, new Cookie("test-key", "test-value")); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/GetRequestTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/GetRequestTest.java new file mode 100644 index 0000000..86e5b14 --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/GetRequestTest.java @@ -0,0 +1,38 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.igslandstuhl.database.server.resources.ResourceLocation; + +public class GetRequestTest { + GetRequest validRequest; + @BeforeEach + void initRequest() { + validRequest = new GetRequest("GET /login HTTP/1.1"); + } + @Test + void testConstructorThrows() { + String[] invalidRequests = { + "GET /login HTP/1", + "GET", + "POST", + "GET /login" + }; + for (String rqString : invalidRequests) { + assertThrows(IllegalArgumentException.class, () -> new GetRequest(rqString)); + } + } + @Test + void testIsValid() { + assertTrue(validRequest::isValid); + assertFalse(new GetRequest("GET /test.sql HTTP/1.1")::isValid); + } + + @Test + void testToResourceLocation() { + assertEquals(ResourceLocation.get("html", "site:login.html"), validRequest.toResourceLocation(null)); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/GetResponseTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/GetResponseTest.java new file mode 100644 index 0000000..8b399f3 --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/GetResponseTest.java @@ -0,0 +1,55 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.jupiter.api.Test; + +import de.igslandstuhl.database.server.resources.ResourceLocation; + +public class GetResponseTest { + @Test + void testForbidden() throws FileNotFoundException { + assertTrue(GetResponse.forbidden().getResponseBody().contains("403")); + } + + @Test + void testInternalServerError() throws FileNotFoundException { + assertTrue(GetResponse.internalServerError().getResponseBody().contains("500")); + } + + @Test + void testNotFound() throws FileNotFoundException { + assertTrue(GetResponse.notFound().getResponseBody().contains("404")); + } + + @Test + void testUnauthorized() throws FileNotFoundException { + assertTrue(GetResponse.unauthorized().getResponseBody().contains("401")); + } + + @Test + void testGetResource() throws FileNotFoundException { + assertTrue(GetResponse.getResource(ResourceLocation.get("html", "site:login.html"), null).getResponseBody().contains("login")); + } + + @Test + void testGetResponseBody() { + assertNotNull(GetResponse.getResource(ResourceLocation.get("html", "site:login.html"), null)); + } + + @Test + void testRespond() throws FileNotFoundException { + StringWriter testWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(testWriter); + GetResponse response = GetResponse.getResource(ResourceLocation.get("html", "site:login.html"), null); + response.respond(printWriter); + String responseString = testWriter.toString(); + String responseBody = response.getResponseBody(); + assertTrue(responseString.contains(responseBody)); + assertTrue(responseString.contains("HTTP/1.1 200 OK")); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/PostHeaderTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/PostHeaderTest.java new file mode 100644 index 0000000..72ce00d --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/PostHeaderTest.java @@ -0,0 +1,30 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PostHeaderTest { + PostHeader postHeader; + @BeforeEach + void initPostHeader() { + postHeader = new PostHeader("POST /login HTTP/1.1\r\nContent-Length: 45\r\nCookie:test-key=test-value"); + } + + @Test + void testGetContentLength() { + assertEquals(postHeader.getContentLength(), 45); + } + + @Test + void testGetCookies() { + assertArrayEquals(postHeader.getCookies(), new Cookie [] {new Cookie("test-key", "test-value")}); + } + + @Test + void testGetPath() { + assertEquals(postHeader.getPath(), "/login"); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/PostRequestTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/PostRequestTest.java new file mode 100644 index 0000000..6c6aa6f --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/PostRequestTest.java @@ -0,0 +1,99 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.google.gson.JsonSyntaxException; + +public class PostRequestTest { + PostRequest postRequest1; + PostRequest postRequest2; + PostRequest postRequestJson; + @BeforeEach + void initPostRequest() { + postRequest1 = new PostRequest( + "POST /login HTTP/1.1\r\n" + // + "Content-Length: 37\r\n" + // + "Cookie:test-key=test-value", + "username=adminUser&password=adminPass"); + postRequest2 = new PostRequest( + new PostHeader("POST /login HTTP/1.1\r\n" + // + "Content-Length: 37\r\n" + // + "Cookie:test-key=test-value;other=value"), + "username=adminUser&password=adminPass"); + postRequestJson = new PostRequest( + new PostHeader("POST /student-data HTTP/1.1\r\n" + // + "Content-Length: 9\r\n" + // + "Cookie:test-key=test-value"), + "{" + // + "\"id\": 0" + // + "}"); + } + @Test + void testGetBodyAsString() { + assertEquals("username=adminUser&password=adminPass", postRequest1.getBodyAsString()); + assertEquals("username=adminUser&password=adminPass", postRequest2.getBodyAsString()); + assertEquals("{\"id\": 0}", postRequestJson.getBodyAsString()); + } + + @Test + void testGetContentLength() { + assertEquals(37, postRequest1.getContentLength()); + assertEquals(37, postRequest2.getContentLength()); + assertEquals(9, postRequestJson.getContentLength()); + } + + @Test + void testGetContext() { + assertEquals(postRequest1.getContext(), "html"); + assertEquals(postRequest2.getContext(), "html"); + assertEquals(postRequestJson.getContext(), "html"); + } + + @Test + void testGetCookies() { + assertArrayEquals(new Cookie[] {new Cookie("test-key", "test-value")}, postRequest1.getCookies()); + assertArrayEquals(new Cookie[] {new Cookie("test-key", "test-value"), new Cookie("other", "value")}, postRequest2.getCookies()); + assertArrayEquals(postRequest1.getCookies(), postRequestJson.getCookies()); + } + + @Test + void testGetFormData() { + Map data = postRequest1.getFormData(); + assertEquals(data, postRequest2.getFormData()); + + assertTrue(data.containsKey("username")); + assertTrue(data.containsKey("password")); + + assertEquals("adminUser", data.get("username")); + assertEquals("adminPass", data.get("password")); + + assertThrows(IllegalArgumentException.class, postRequestJson::getFormData); + } + + @Test + void testGetJson() { + Map json = postRequestJson.getJson(); + assertTrue(json.containsKey("id")); + assertInstanceOf(Number.class, json.get("id")); + assertEquals(0,((Number)json.get("id")).intValue()); + + assertThrows(JsonSyntaxException.class, postRequest1::getJson); + assertThrows(JsonSyntaxException.class, postRequest2::getJson); + } + + @Test + void testGetPath() { + assertEquals("/login", postRequest1.getPath()); + assertEquals("/login", postRequest2.getPath()); + assertEquals("/student-data", postRequestJson.getPath()); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/PostResponseTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/PostResponseTest.java new file mode 100644 index 0000000..4cccbfa --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/PostResponseTest.java @@ -0,0 +1,49 @@ +package de.igslandstuhl.database.server.webserver; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.jupiter.api.Test; + +public class PostResponseTest { + private String read(PostResponse r) { + StringWriter w = new StringWriter(); + r.respond(new PrintWriter(w)); + return w.toString(); + } + @Test + void testBadRequest() { + assert read(PostResponse.badRequest("Test")).contains("400"); + } + + @Test + void testForbidden() { + assert read(PostResponse.forbidden("Test")).contains("403"); + } + + @Test + void testInternalServerError() { + assert read(PostResponse.internalServerError("Test")).contains("500"); + } + + @Test + void testNotFound() { + assert read(PostResponse.notFound("Test")).contains("404"); + } + + @Test + void testOk() { + assert read(PostResponse.ok("Test", ContentType.TEXT_PLAIN)).contains("200"); + assert read(PostResponse.ok("Test", ContentType.TEXT_PLAIN, new Cookie("test-key", "test-value"))).contains("Set-Cookie: test-key=test-value"); + } + + @Test + void testRedirect() { + assert read(PostResponse.redirect("Test")).contains("302"); + } + + @Test + void testUnauthorized() { + assert read(PostResponse.unauthorized("Test")).contains("401"); + } +} diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/UserManagerTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/UserManagerTest.java new file mode 100644 index 0000000..790632b --- /dev/null +++ b/src/test/java/de/igslandstuhl/database/server/webserver/UserManagerTest.java @@ -0,0 +1,35 @@ +package de.igslandstuhl.database.server.webserver; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class UserManagerTest { + UserManager userManager; + PostRequest sessionRequest; + PostRequest requestWithoutSession; + @BeforeEach + void setup() { + userManager = new UserManager(); + sessionRequest = new PostRequest("POST /student-data HTTP/1.1\r\n" + // + "Cookie: test=test;session=sessionId;other=value", null); + requestWithoutSession = new PostRequest("POST /login HTTP/1.1", null); + } + @Test + void testNonExistingSessionUser() { + assertNull(userManager.getSessionUser("testUser")); + } + @Test + void testAddSessionUser() { + userManager.addSession("sessionId", "testUser"); + assertEquals("testUser", userManager.getSessionUser(sessionRequest)); + } + @Test + void testSessionRequest() { + userManager.addSession("sessionId", "testUser"); + assertEquals("testUser", userManager.getSessionUser(sessionRequest)); + assertNull(userManager.getSessionUser(requestWithoutSession)); + } +}