From c55f0d93190345ea9189e97bcd65160b1ae1556a Mon Sep 17 00:00:00 2001 From: Johan Paul Date: Sat, 21 Mar 2026 14:36:00 +0530 Subject: [PATCH] Basic Working Version of HTTP Server --- .gitignore | 2 + README.md | 2 - Request.txt | 11 - WebRoot/favicon.ico | Bin 1342 -> 0 bytes WebRoot/index.html | 17 - WebRoot/logo.png | Bin 9996 -> 0 bytes httpserver/WebRoot/Index.html | 17 + httpserver/WebRoot/favicon.ico | Bin 0 -> 33548 bytes httpserver/WebRoot/randomlogo.png | Bin 0 -> 23673 bytes httpserver/pom.xml | 64 ++++ .../johan/http/BadHttpVersionException.java | 11 + .../main/java/com/johan/http/HttpMethod.java | 17 + .../main/java/com/johan/http/HttpParser.java | 135 ++++++++ .../com/johan}/http/HttpParsingException.java | 2 +- .../main/java/com/johan/http/HttpRequest.java | 65 ++++ .../java/com/johan/http/HttpStatusCode.java | 21 ++ .../main/java/com/johan/http/HttpVersion.java | 29 ++ .../config/HttpConfigException.java | 18 ++ .../httpserver/config/configmanager.java | 61 ++++ .../httpserver/config/configuration.java | 10 +- .../core/HttpConnectionWorkerThread.java | 105 ++++++ .../httpserver/core/ServerListenerThread.java | 47 +++ .../httpserver/core/io/WebRootHandler.java | 71 ++++ .../core/io/WebRootNotFoundException.java | 5 + .../java/com/johan/httpserver/httpserver.java | 32 ++ .../java/com/johan/httpserver/util/json.java | 45 +++ httpserver/src/main/resources/http.json | 4 + .../java/com/johan/http/HttpParserTest.java | 131 ++++++++ .../java/com/johan}/http/HttpVersionTest.java | 12 +- pom.xml | 102 ------ .../http/BadHttpVersionException.java | 5 - .../coderfromscratch/http/HttpHeaderName.java | 12 - .../coderfromscratch/http/HttpMessage.java | 31 -- .../com/coderfromscratch/http/HttpMethod.java | 17 - .../com/coderfromscratch/http/HttpParser.java | 146 --------- .../coderfromscratch/http/HttpRequest.java | 63 ---- .../coderfromscratch/http/HttpResponse.java | 108 ------- .../coderfromscratch/http/HttpStatusCode.java | 26 -- .../coderfromscratch/http/HttpVersion.java | 43 --- .../httpserver/HttpServer.java | 50 --- .../config/ConfigurationManager.java | 66 ---- .../config/HttpConfigurationException.java | 20 -- .../core/HttpConnectionWorkerThread.java | 125 ------- .../httpserver/core/ServerListenerThread.java | 57 ---- .../httpserver/core/io/ReadFileException.java | 22 -- .../httpserver/core/io/WebRootHandler.java | 96 ------ .../core/io/WebRootNotFoundException.java | 7 - .../httpserver/util/Json.java | 45 --- src/main/resources/http.json | 4 - .../http/HttpHeadersParserTest.java | 129 -------- .../coderfromscratch/http/HttpParserTest.java | 305 ------------------ .../core/io/WebRootHandlerTest.java | 229 ------------- 52 files changed, 890 insertions(+), 1752 deletions(-) create mode 100644 .gitignore delete mode 100644 README.md delete mode 100644 Request.txt delete mode 100644 WebRoot/favicon.ico delete mode 100644 WebRoot/index.html delete mode 100644 WebRoot/logo.png create mode 100644 httpserver/WebRoot/Index.html create mode 100644 httpserver/WebRoot/favicon.ico create mode 100644 httpserver/WebRoot/randomlogo.png create mode 100644 httpserver/pom.xml create mode 100644 httpserver/src/main/java/com/johan/http/BadHttpVersionException.java create mode 100644 httpserver/src/main/java/com/johan/http/HttpMethod.java create mode 100644 httpserver/src/main/java/com/johan/http/HttpParser.java rename {src/main/java/com/coderfromscratch => httpserver/src/main/java/com/johan}/http/HttpParsingException.java (90%) create mode 100644 httpserver/src/main/java/com/johan/http/HttpRequest.java create mode 100644 httpserver/src/main/java/com/johan/http/HttpStatusCode.java create mode 100644 httpserver/src/main/java/com/johan/http/HttpVersion.java create mode 100644 httpserver/src/main/java/com/johan/httpserver/config/HttpConfigException.java create mode 100644 httpserver/src/main/java/com/johan/httpserver/config/configmanager.java rename src/main/java/com/coderfromscratch/httpserver/config/Configuration.java => httpserver/src/main/java/com/johan/httpserver/config/configuration.java (79%) create mode 100644 httpserver/src/main/java/com/johan/httpserver/core/HttpConnectionWorkerThread.java create mode 100644 httpserver/src/main/java/com/johan/httpserver/core/ServerListenerThread.java create mode 100644 httpserver/src/main/java/com/johan/httpserver/core/io/WebRootHandler.java create mode 100644 httpserver/src/main/java/com/johan/httpserver/core/io/WebRootNotFoundException.java create mode 100644 httpserver/src/main/java/com/johan/httpserver/httpserver.java create mode 100644 httpserver/src/main/java/com/johan/httpserver/util/json.java create mode 100644 httpserver/src/main/resources/http.json create mode 100644 httpserver/src/test/java/com/johan/http/HttpParserTest.java rename {src/test/java/com/coderfromscratch => httpserver/src/test/java/com/johan}/http/HttpVersionTest.java (76%) delete mode 100644 pom.xml delete mode 100644 src/main/java/com/coderfromscratch/http/BadHttpVersionException.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpHeaderName.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpMessage.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpMethod.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpParser.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpRequest.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpResponse.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpStatusCode.java delete mode 100644 src/main/java/com/coderfromscratch/http/HttpVersion.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/HttpServer.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/config/ConfigurationManager.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/config/HttpConfigurationException.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/core/HttpConnectionWorkerThread.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/core/ServerListenerThread.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/core/io/ReadFileException.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/core/io/WebRootHandler.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/core/io/WebRootNotFoundException.java delete mode 100644 src/main/java/com/coderfromscratch/httpserver/util/Json.java delete mode 100644 src/main/resources/http.json delete mode 100644 src/test/java/com/coderfromscratch/http/HttpHeadersParserTest.java delete mode 100644 src/test/java/com/coderfromscratch/http/HttpParserTest.java delete mode 100644 src/test/java/com/coderfromscratch/httpserver/core/io/WebRootHandlerTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efff418 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +httpserver/target \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 7530104..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# simple-java-http-server -Create a Simple HTTP Server in Java Tutorial Series - https://www.youtube.com/playlist?list=PLAuGQNR28pW56GigraPdiI0oKwcs8gglW diff --git a/Request.txt b/Request.txt deleted file mode 100644 index bdd8dbc..0000000 --- a/Request.txt +++ /dev/null @@ -1,11 +0,0 @@ -GET / HTTP/1.1 -Host: localhost:8080 -Connection: keep-alive -Upgrade-Insecure-Requests: 1 -User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36 -Sec-Fetch-User: ?1 -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 -Sec-Fetch-Site: none -Sec-Fetch-Mode: navigate -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4 diff --git a/WebRoot/favicon.ico b/WebRoot/favicon.ico deleted file mode 100644 index dad8daf2877d97e1fe8928f5ccc6de3babf80faf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1342 zcmd7S-z$S*6u|MbB|nmFl1$i-6r!a>C}pOWze6tEx^=DQ%75Wr6c@NyOGJ_+5{XEn znTd9x=6g0zJ)12qC~tjw&U4Orp0}OWE8-BRcs#-ykm(AM4iWZF_6}JhULe<{IRv16 zfAvJdV zz3^ZgkLa^|i(~3$K6k|3rcSK$O(2Cqlebs$O*n$ir&DX;t@#{{$?1GI@r;7`vTw!- zuF)`euz9bvmk&o~_pJ5>;Kb{KISVhRQI-`ZreBL0ksJ0%jk s5=kUPu3{o7cb>+Yn+u9m^Fi@?MKb&g-$zCCY~gpN{O`$X^FMe#02ofo^#A|> diff --git a/WebRoot/index.html b/WebRoot/index.html deleted file mode 100644 index 13265de..0000000 --- a/WebRoot/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - Simple Java HTTP Server - - -
-

Welcome to our first rendered page!

-
-

This page was delivered using our own made Https Server made in Java.

-
-
- - \ No newline at end of file diff --git a/WebRoot/logo.png b/WebRoot/logo.png deleted file mode 100644 index 430c02beb591559bf09ed31206365136708af10b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9996 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 + + Simple Java HTTP Server + + +
+

Welcome to my rendered page!

+
+

This page was delivered using my custom-built HTTP Server made in Java.

+
+
+broken + \ No newline at end of file diff --git a/httpserver/WebRoot/favicon.ico b/httpserver/WebRoot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a243e6270f63d6a54914341826ba5ffa90aaf77a GIT binary patch literal 33548 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 i;>Jd0F?4PW%~c@NF@ErL;e8y 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 literal 0 HcmV?d00001 diff --git a/httpserver/pom.xml b/httpserver/pom.xml new file mode 100644 index 0000000..8faf82f --- /dev/null +++ b/httpserver/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + com.johan.httpserver + httpserver + 1.0-SNAPSHOT + + + 17 + 17 + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + com.johan.httpserver.httpserver + + + + + + + + + com.fasterxml.jackson.core + jackson-core + 2.21.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.21.1 + + + org.slf4j + slf4j-api + 2.0.13 + + + ch.qos.logback + logback-classic + 1.5.13 + + + org.jetbrains + annotations + 24.0.1 + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + + \ No newline at end of file diff --git a/httpserver/src/main/java/com/johan/http/BadHttpVersionException.java b/httpserver/src/main/java/com/johan/http/BadHttpVersionException.java new file mode 100644 index 0000000..f127ce0 --- /dev/null +++ b/httpserver/src/main/java/com/johan/http/BadHttpVersionException.java @@ -0,0 +1,11 @@ +package com.johan.http; + +public class BadHttpVersionException extends Exception { + public BadHttpVersionException() { + super("Unsupported or malformed HTTP version"); + } + + public BadHttpVersionException(String message) { + super(message); + } +} diff --git a/httpserver/src/main/java/com/johan/http/HttpMethod.java b/httpserver/src/main/java/com/johan/http/HttpMethod.java new file mode 100644 index 0000000..1101c71 --- /dev/null +++ b/httpserver/src/main/java/com/johan/http/HttpMethod.java @@ -0,0 +1,17 @@ +package com.johan.http; + +public enum HttpMethod { + GET, HEAD; + public static final int MAX_LENGTH; + + //this checks all the valid METHODS declared in the server and sets max_length as the biggest header + static{ + int tempMaxLength = -1; + for (HttpMethod method : HttpMethod.values()){ + if(method.name().length()>tempMaxLength){ + tempMaxLength = method.name().length(); + } + } + MAX_LENGTH = tempMaxLength; + } +} diff --git a/httpserver/src/main/java/com/johan/http/HttpParser.java b/httpserver/src/main/java/com/johan/http/HttpParser.java new file mode 100644 index 0000000..f7b0616 --- /dev/null +++ b/httpserver/src/main/java/com/johan/http/HttpParser.java @@ -0,0 +1,135 @@ +package com.johan.http; + +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +public class HttpParser { + + private final static Logger LOGGER = LoggerFactory.getLogger(HttpParser.class); + + private static final int SP = 0x20; //32 + private static final int CR = 0x0D; //13 + private static final int LF = 0x0A; //10 + + public static HttpRequest parseHttpReq(InputStream iptStream) throws HttpParsingException { + InputStreamReader isr = new InputStreamReader(iptStream, StandardCharsets.US_ASCII); + + HttpRequest request = new HttpRequest(); + try { + parseRequestLine(isr, request); + parseHeader(isr, request); + }catch(IOException e){ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + parseBody(isr, request); + + return request; + } + private static void parseRequestLine(InputStreamReader isr, HttpRequest req) throws IOException, HttpParsingException { + boolean methodParsed = false; + boolean reqTargetParsed = false; + StringBuilder processDataBuffer = new StringBuilder(); + int _byte; + while ((_byte=isr.read()) >=0){ + if(_byte==CR){ + _byte=isr.read(); + if(_byte==LF){ + LOGGER.debug("Request Line VERSION to process : {}", processDataBuffer.toString()); + if (!methodParsed || !reqTargetParsed) { + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + try { + req.setHttpVersion(processDataBuffer.toString()); + }catch (BadHttpVersionException e) { + LOGGER.error("Bad HTTP Version received : {}", e.getMessage()); + throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED); + } + return; + }else{ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + } + if(_byte==SP){ + if (!methodParsed) { + LOGGER.debug("Request Line METHOD to process : {}", processDataBuffer.toString()); + try { + req.setMethod(processDataBuffer.toString()); + methodParsed = true; + }catch (HttpParsingException e){ + LOGGER.error("Invalid HTTP Method received : {}", e.getMessage()); + throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); + } + }else if (!reqTargetParsed) { + LOGGER.debug("Request Line REQ TARGET to process : {}", processDataBuffer.toString()); + req.setRequestTarget(processDataBuffer.toString()); + reqTargetParsed = true; + } + else{ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + processDataBuffer.delete(0, processDataBuffer.length()); + }else{ + processDataBuffer.append((char)_byte); + if(!methodParsed){ + if(processDataBuffer.length()>HttpMethod.MAX_LENGTH){ + LOGGER.error("Terminating connection, Bad Method received : {}", processDataBuffer.toString()); + throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); + } + } + } + } + } + + private static void parseHeader(InputStreamReader isr, HttpRequest req) throws HttpParsingException, IOException { + StringBuilder processDataBuffer = new StringBuilder(); + boolean headerParsed = false; + int _byte; + while ((_byte=isr.read()) >=0){ + if(_byte==CR){ + _byte=isr.read(); + if(_byte==LF) { + String line = processDataBuffer.toString().trim(); + if (line.isEmpty()) { + if(!headerParsed){ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + return; + } + processingHeaderField(processDataBuffer, req); + headerParsed = true; + processDataBuffer.delete(0, processDataBuffer.length()); + } else{ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + } else{ + processDataBuffer.append((char)_byte); + } + } + } + + private static void processingHeaderField(StringBuilder processDataBuffer, HttpRequest req) throws HttpParsingException { + String rawHeaderField = processDataBuffer.toString(); + if (rawHeaderField.length() > 8192) { + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_414_URI_TOO_LONG); + } + int colonIndex = rawHeaderField.indexOf(':'); + if(colonIndex == -1){ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + String fieldName = rawHeaderField.substring(0, colonIndex).trim(); + String fieldValue = rawHeaderField.substring(colonIndex+1).trim(); + if(fieldName.isEmpty()){ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + req.addHeader(fieldName, fieldValue); + } + + private static void parseBody(InputStreamReader isr, HttpRequest req) { + + } +} diff --git a/src/main/java/com/coderfromscratch/http/HttpParsingException.java b/httpserver/src/main/java/com/johan/http/HttpParsingException.java similarity index 90% rename from src/main/java/com/coderfromscratch/http/HttpParsingException.java rename to httpserver/src/main/java/com/johan/http/HttpParsingException.java index 1e372eb..d4474d5 100644 --- a/src/main/java/com/coderfromscratch/http/HttpParsingException.java +++ b/httpserver/src/main/java/com/johan/http/HttpParsingException.java @@ -1,4 +1,4 @@ -package com.coderfromscratch.http; +package com.johan.http; public class HttpParsingException extends Exception { diff --git a/httpserver/src/main/java/com/johan/http/HttpRequest.java b/httpserver/src/main/java/com/johan/http/HttpRequest.java new file mode 100644 index 0000000..32470c5 --- /dev/null +++ b/httpserver/src/main/java/com/johan/http/HttpRequest.java @@ -0,0 +1,65 @@ +package com.johan.http; + +import java.util.HashMap; +import java.util.Set; + +public class HttpRequest { + + private HttpMethod method; + private String requestTarget; + private String originalHttpVersion; + private HttpVersion getBestCompatibleVersion; + private HashMap headers = new HashMap<>(); + + HttpRequest() { + } + + public HttpMethod getMethod() { + return method; + } + + public String getRequestTarget() { + return requestTarget; + } + + public HttpVersion getBestCompatibleVersion() { return getBestCompatibleVersion; } + + public String getOriginalHttpVersion() { return originalHttpVersion; } + + public Set getHeaderNames() { + return headers.keySet(); + } + + public String getHeader(String headerName) { + return headers.get(headerName.toLowerCase()); + } + + void setMethod(String methodName) throws HttpParsingException { + for (HttpMethod method: HttpMethod.values()) { + if(methodName.equals(method.name())){ + this.method = method; + return; + } + } + throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); + } + + public void setRequestTarget(String requestTarget) throws HttpParsingException { + if (requestTarget == null || requestTarget.isEmpty()){ + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + this.requestTarget = requestTarget; + } + + public void setHttpVersion(String originalHttpVersion) throws BadHttpVersionException, HttpParsingException { + this.originalHttpVersion = originalHttpVersion; + this.getBestCompatibleVersion = HttpVersion.getBestCompatibleVersion(originalHttpVersion); + if(this.getBestCompatibleVersion == null){ + throw new BadHttpVersionException(); + } + } + + void addHeader(String headerName, String HeaderField){ + headers.put(headerName.toLowerCase(), HeaderField); + } +} diff --git a/httpserver/src/main/java/com/johan/http/HttpStatusCode.java b/httpserver/src/main/java/com/johan/http/HttpStatusCode.java new file mode 100644 index 0000000..e13d252 --- /dev/null +++ b/httpserver/src/main/java/com/johan/http/HttpStatusCode.java @@ -0,0 +1,21 @@ +package com.johan.http; + +public enum HttpStatusCode { + /* ---Client Errors --- */ + CLIENT_ERROR_400_BAD_REQ(400, "Bad Request"), + CLIENT_ERROR_401_METHOD_NOT_ALLOWED(401, "Method not allowed"), + CLIENT_ERROR_404_NOT_FOUND(404, "Request Target not found"), + CLIENT_ERROR_414_URI_TOO_LONG(414, "URI too long"), + /* ---Server Errors --- */ + SERVER_ERROR_500_INTERNAL_SERVER_ERROR(500, "Internal server error"), + SERVER_ERROR_501_NOT_IMPLEMENTED(501, "Method Not implemented"), + SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED(505, "Http Version Not Supported"); + + public final int STATUS_CODE; + public final String MESSAGE; + + HttpStatusCode(int STATUS_CODE, String MESSAGE) { + this.STATUS_CODE=STATUS_CODE; + this.MESSAGE = MESSAGE; + } +} diff --git a/httpserver/src/main/java/com/johan/http/HttpVersion.java b/httpserver/src/main/java/com/johan/http/HttpVersion.java new file mode 100644 index 0000000..0ab9e6f --- /dev/null +++ b/httpserver/src/main/java/com/johan/http/HttpVersion.java @@ -0,0 +1,29 @@ +package com.johan.http; + +public enum HttpVersion { + + HTTP_1_1("HTTP/1.1", 1, 1), + HTTP_1_0("HTTP/1.0",1,0); + + public final String literal; + public final int major; + public final int minor; + + HttpVersion(String literal, int major, int minor){ + this.literal = literal; + this.major = major; + this.minor = minor; + } + + public static HttpVersion getBestCompatibleVersion(String version) throws BadHttpVersionException { + if(version == null){ + throw new BadHttpVersionException("Version NULL"); + } + for(HttpVersion v : HttpVersion.values()){ + if(v.literal.equals(version)){ + return v; + } + } + return null; + } +} \ No newline at end of file diff --git a/httpserver/src/main/java/com/johan/httpserver/config/HttpConfigException.java b/httpserver/src/main/java/com/johan/httpserver/config/HttpConfigException.java new file mode 100644 index 0000000..47c8f52 --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/config/HttpConfigException.java @@ -0,0 +1,18 @@ +package com.johan.httpserver.config; + +public class HttpConfigException extends RuntimeException{ + public HttpConfigException() { + } + + public HttpConfigException(String message) { + super(message); + } + + public HttpConfigException(String message, Throwable cause) { + super(message, cause); + } + + public HttpConfigException(Throwable cause) { + super(cause); + } +} diff --git a/httpserver/src/main/java/com/johan/httpserver/config/configmanager.java b/httpserver/src/main/java/com/johan/httpserver/config/configmanager.java new file mode 100644 index 0000000..a703fe4 --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/config/configmanager.java @@ -0,0 +1,61 @@ +package com.johan.httpserver.config; + +import com.johan.httpserver.util.json; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; + +public class configmanager { + private static configmanager myconfigmanager; + private static configuration mycurrentconfig; + + private configmanager(){ + + } + + public static configmanager getInstance(){ + if(myconfigmanager==null) + myconfigmanager = new configmanager(); + return myconfigmanager; + } + + //method for loading a config file by the path provided + public void loadConfigFile(String filePath) { + FileReader fr = null; + try { + fr = new FileReader(filePath); + } catch (FileNotFoundException e) { + throw new HttpConfigException(e); + } + StringBuffer sb = new StringBuffer(); + int i; + try { + while ((i = fr.read())!=-1){ + sb.append((char)i); + } + } catch (IOException e) { + throw new HttpConfigException(e); + } + JsonNode conf = null; + try { + conf = json.parse(sb.toString()); + } catch (IOException e) { + throw new HttpConfigException("Error parsing configuration file", e); + } + try { + mycurrentconfig = json.fromJson(conf, configuration.class); + } catch (IOException e) { + throw new HttpConfigException("Error parsing configuration file, internal", e); + } + } + + //method for returning currently loaded config + public configuration getCurrentConfig(){ + if (mycurrentconfig == null){ + throw new HttpConfigException("No Current Configuration Set"); + } + return mycurrentconfig; + } +} diff --git a/src/main/java/com/coderfromscratch/httpserver/config/Configuration.java b/httpserver/src/main/java/com/johan/httpserver/config/configuration.java similarity index 79% rename from src/main/java/com/coderfromscratch/httpserver/config/Configuration.java rename to httpserver/src/main/java/com/johan/httpserver/config/configuration.java index 91447e7..020ab2e 100644 --- a/src/main/java/com/coderfromscratch/httpserver/config/Configuration.java +++ b/httpserver/src/main/java/com/johan/httpserver/config/configuration.java @@ -1,22 +1,18 @@ -package com.coderfromscratch.httpserver.config; - -public class Configuration { +package com.johan.httpserver.config; +public class configuration { private int port; private String webroot; - + public int getPort() { return port; } - public void setPort(int port) { this.port = port; } - public String getWebroot() { return webroot; } - public void setWebroot(String webroot) { this.webroot = webroot; } diff --git a/httpserver/src/main/java/com/johan/httpserver/core/HttpConnectionWorkerThread.java b/httpserver/src/main/java/com/johan/httpserver/core/HttpConnectionWorkerThread.java new file mode 100644 index 0000000..740ffd4 --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/core/HttpConnectionWorkerThread.java @@ -0,0 +1,105 @@ +package com.johan.httpserver.core; + +import com.johan.http.HttpParser; +import com.johan.http.HttpParsingException; +import com.johan.http.HttpRequest; +import com.johan.http.HttpStatusCode; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.file.Files; + +public class HttpConnectionWorkerThread extends Thread{ + + final String CRLF = "\r\n"; //13, 10 + private final static Logger LOGGER = LoggerFactory.getLogger(HttpConnectionWorkerThread.class); + private Socket socket; + public HttpConnectionWorkerThread(Socket socket){ + this.socket=socket; + } + + //For sending the Error code and Message to Client + private void sendErrorResponse(HttpStatusCode code, OutputStream op) throws IOException { + String body = code.MESSAGE; + + String response = + "HTTP/1.1 " + code.STATUS_CODE + " " + code.MESSAGE + CRLF + + "Content-Length: " + body.length() + CRLF + + CRLF + + body; + + op.write(response.getBytes()); + } + + @Override + public void run(){ + InputStream ipStream = null; + OutputStream opStream = null; + try { + ipStream = socket.getInputStream(); + opStream = socket.getOutputStream(); + HttpRequest req = null; + try { + req = HttpParser.parseHttpReq(ipStream); + }catch (HttpParsingException e){ + sendErrorResponse(e.getErrorCode(), opStream); + return; + } + + //Get the file that the user wants + //finish working on this later + String path = req.getRequestTarget(); + if ("/".equals(path)){ + path="/Index.html"; + }else{ + LOGGER.error("Invalid Request Target received : {}", path); + throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_404_NOT_FOUND); + } + + File file = new File(System.getProperty("user.dir") + "/httpserver/WebRoot" + path); + + //Response + byte[] fileBytes = Files.readAllBytes(file.toPath()); + String response = + "HTTP/1.1 200 OK" + CRLF + // Status Line : HTTP Version, Response_code, Response_msg + "Content-Length: " + fileBytes.length + CRLF + // Header + "Content-Type: text/html"+ CRLF + //Add MIME Files later + CRLF; + + opStream.write(response.getBytes()); + opStream.write(fileBytes); + + LOGGER.info("Connection Processing Finished"); + }catch(IOException e){ + LOGGER.error("Problem with communication", e); + }catch(HttpParsingException e){ + try { + sendErrorResponse(e.getErrorCode(), opStream); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + finally { + if(ipStream!=null){ + try { + ipStream.close(); + }catch (IOException ignored) {} + } + if(opStream!=null){ + try{ + opStream.close(); + }catch (IOException ignored){} + } + if(socket!=null){ + try{ + socket.close(); + }catch (IOException ignored){} + } + } + } +} diff --git a/httpserver/src/main/java/com/johan/httpserver/core/ServerListenerThread.java b/httpserver/src/main/java/com/johan/httpserver/core/ServerListenerThread.java new file mode 100644 index 0000000..be088fa --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/core/ServerListenerThread.java @@ -0,0 +1,47 @@ +package com.johan.httpserver.core; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +public class ServerListenerThread extends Thread{ + private final static Logger LOGGER = LoggerFactory.getLogger(ServerListenerThread.class); + + private int port; + private String webRoot; + private ServerSocket serverSocket; + + public ServerListenerThread(int port, String webRoot) throws IOException { + this.port=port; + this.webRoot=webRoot; + this.serverSocket = new ServerSocket(this.port); + } + + @Override + public void run(){ + + try { + while(serverSocket.isBound() && !serverSocket.isClosed()) { + Socket socket = serverSocket.accept(); + + LOGGER.info(" Connection accepted: " + socket.getInetAddress()); + + HttpConnectionWorkerThread workerThread = new HttpConnectionWorkerThread(socket); + workerThread.start(); + } + + } + catch (IOException e) { + LOGGER.error("Problem with setting socket", e); + } + finally { + if(serverSocket!=null){ + try{ + serverSocket.close(); + }catch (IOException ignore){} + } + } + } +} diff --git a/httpserver/src/main/java/com/johan/httpserver/core/io/WebRootHandler.java b/httpserver/src/main/java/com/johan/httpserver/core/io/WebRootHandler.java new file mode 100644 index 0000000..01fed10 --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/core/io/WebRootHandler.java @@ -0,0 +1,71 @@ +package com.johan.httpserver.core.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URLConnection; + +public class WebRootHandler { + private File webRoot; + + public WebRootHandler(String webRootPath) throws WebRootNotFoundException{ + webRoot = new File(webRootPath); + if(!webRoot.exists() || !webRoot.isDirectory()){ + throw new WebRootNotFoundException(); + } + } + + private boolean CheckIfEndsWithSlash(String relativePath){ + return relativePath.endsWith("/"); + } + + private boolean CheckIfRelativePathExists(String relativePath){ + File file = new File(webRoot, relativePath); + if(!file.exists()) { + return false; + } + try { + if (!file.getCanonicalPath().startsWith(webRoot.getCanonicalPath())) { + return true; + } + }catch (Exception e){ + return false; + } + return false; + } + + public String GetFileType(String relativePath) throws FileNotFoundException{ + if (CheckIfEndsWithSlash(relativePath)) { + relativePath += "Index.html"; + } + if(CheckIfRelativePathExists(relativePath)) { + throw new FileNotFoundException("File not found: "+relativePath); + } + File file = new File(webRoot, relativePath);; + String mimeType = URLConnection.getFileNameMap().getContentTypeFor(file.getName()); + if(mimeType == null){ + return "application/octet-stream"; + } + return mimeType; + } + + public byte[] GetFileByteArrayData(String relativePath) throws IOException { + if (CheckIfEndsWithSlash(relativePath)) { + relativePath += "Index.html"; + } + if(CheckIfRelativePathExists(relativePath)) { + throw new FileNotFoundException("File not found: "+relativePath); + } + File file = new File(webRoot, relativePath); + FileInputStream fis = new FileInputStream(file); + byte[] fileBytes = new byte[(int)file.length()]; + try{ + fis.read(fileBytes); + fis.close(); + }catch (IOException e){ + throw new IOException(); + } + return fileBytes; + } +} diff --git a/httpserver/src/main/java/com/johan/httpserver/core/io/WebRootNotFoundException.java b/httpserver/src/main/java/com/johan/httpserver/core/io/WebRootNotFoundException.java new file mode 100644 index 0000000..d1c726f --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/core/io/WebRootNotFoundException.java @@ -0,0 +1,5 @@ +package com.johan.httpserver.core.io; + +public class WebRootNotFoundException extends Exception{ + +} diff --git a/httpserver/src/main/java/com/johan/httpserver/httpserver.java b/httpserver/src/main/java/com/johan/httpserver/httpserver.java new file mode 100644 index 0000000..f009280 --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/httpserver.java @@ -0,0 +1,32 @@ +package com.johan.httpserver; + +import com.johan.httpserver.config.configuration; +import com.johan.httpserver.config.configmanager; +import com.johan.httpserver.core.ServerListenerThread; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; + +public class httpserver { + + private final static Logger LOGGER = LoggerFactory.getLogger(httpserver.class); + public static void main(String[] args) + { + LOGGER.info("Server Starting"); + + System.out.println("Server starting..."); + + configmanager.getInstance().loadConfigFile(("httpserver/src/main/resources/http.json")); + configuration conf = configmanager.getInstance().getCurrentConfig(); + + LOGGER.info("Using Port: "+conf.getPort()); + LOGGER.info("Using WebRoot: "+ conf.getWebroot()); + + try { + ServerListenerThread serverListenerThread = new ServerListenerThread(conf.getPort(), conf.getWebroot()); + serverListenerThread.start(); + }catch (IOException e){ + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/httpserver/src/main/java/com/johan/httpserver/util/json.java b/httpserver/src/main/java/com/johan/httpserver/util/json.java new file mode 100644 index 0000000..ea2e8ee --- /dev/null +++ b/httpserver/src/main/java/com/johan/httpserver/util/json.java @@ -0,0 +1,45 @@ +package com.johan.httpserver.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; + +import java.io.IOException; + +public class json { + + private static ObjectMapper myobjectmapper = new ObjectMapper(); + + private static ObjectMapper defaultObjectMapper(){ + ObjectMapper om = new ObjectMapper(); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return om; + } + + public static JsonNode parse(String jsonSrc) throws IOException { + return myobjectmapper.readTree(jsonSrc); + } + + public static A fromJson(JsonNode node, Class c) throws IOException{ + return myobjectmapper.treeToValue(node, c); + } + + public static JsonNode toJson(Object obj){ + return myobjectmapper.valueToTree(obj); + } + + public static String stringify (JsonNode node) throws JsonProcessingException{ + return generateJson(node, false); + } + + public static String stringifyPretty (JsonNode node) throws JsonProcessingException{ + return generateJson(node, true); + } + + private static String generateJson (Object o, boolean pretty) throws JsonProcessingException { + ObjectWriter objectwriter = myobjectmapper.writer(); + if(pretty){ + objectwriter = objectwriter.with(SerializationFeature.INDENT_OUTPUT); + } + return objectwriter.writeValueAsString(o); + } +} diff --git a/httpserver/src/main/resources/http.json b/httpserver/src/main/resources/http.json new file mode 100644 index 0000000..e530b6f --- /dev/null +++ b/httpserver/src/main/resources/http.json @@ -0,0 +1,4 @@ +{ + "port": 8080, + "webroot":"httpserver/WebRoot/Index.html" +} \ No newline at end of file diff --git a/httpserver/src/test/java/com/johan/http/HttpParserTest.java b/httpserver/src/test/java/com/johan/http/HttpParserTest.java new file mode 100644 index 0000000..fbe5185 --- /dev/null +++ b/httpserver/src/test/java/com/johan/http/HttpParserTest.java @@ -0,0 +1,131 @@ +package com.johan.http; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class HttpParserTest { + + private HttpParser httpParser; + + @BeforeAll + public void beforeClass(){ + httpParser = new HttpParser(); + } + + @Test + void parseHttpReq() { + HttpRequest request = null; + try { + request = HttpParser.parseHttpReq(generateValidTestCase()); + } catch (HttpParsingException e){ + fail(e); + } + assertNotNull(request); + } + + @Test + void parseHttpReqBadMethod() { + try { + HttpRequest request = HttpParser.parseHttpReq(generateValidTestCaseBadMethod()); + fail(); + } catch (HttpParsingException e) { + assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); + } + } + + @Test + void parseHttpReqBadMethod2() { + try { + HttpRequest request = HttpParser.parseHttpReq(generateValidTestCaseBadMethod2()); + fail(); + } catch (HttpParsingException e) { + assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); + } + } + + @Test + void parseHttpReqReqLineInvalidItems() { + try { + HttpRequest request = HttpParser.parseHttpReq(generateValidTestCaseReqLineInvalidItems()); + fail(); + } catch (HttpParsingException e) { + assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + } + + @Test + void parseHttpReqEmptyReq() { + try { + HttpRequest request = HttpParser.parseHttpReq(generateValidTestCaseEmptyReqLine()); + fail(); + } catch (HttpParsingException e) { + assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQ); + } + } + + + private InputStream generateValidTestCase(){ + String rawData="GET / HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Connection: keep-alive\r\n" + + "sec-ch-ua: \"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"145\", \"Chromium\";v=\"145\"\r\n" + + "sec-ch-ua-mobile: ?0\r\n" + + "sec-ch-ua-platform: \"Windows\"\r\n" + + "Upgrade-Insecure-Requests: 1\r\n" + + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n" + + "Sec-Fetch-Site: none\r\n" + + "Sec-Fetch-Mode: navigate\r\n" + + "Sec-Fetch-User: ?1\r\n" + + "Sec-Fetch-Dest: document\r\n" + + "Accept-Encoding: gzip, deflate, br, zstd\r\n" + + "Accept-Language: en-US,en;q=0.9,fr;q=0.8\r\n"+ + "\r\n"; + InputStream ipStream = new ByteArrayInputStream(rawData.getBytes(StandardCharsets.US_ASCII)); + return ipStream; + } + + private InputStream generateValidTestCaseBadMethod(){ + String rawData="TF / HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Accept-Language: en-US,en;q=0.9,fr;q=0.8\r\n"+ + "\r\n"; + InputStream ipStream = new ByteArrayInputStream(rawData.getBytes(StandardCharsets.US_ASCII)); + return ipStream; + } + + private InputStream generateValidTestCaseBadMethod2(){ + String rawData="GETTTT / HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Accept-Language: en-US,en;q=0.9,fr;q=0.8\r\n"+ + "\r\n"; + InputStream ipStream = new ByteArrayInputStream(rawData.getBytes(StandardCharsets.US_ASCII)); + return ipStream; + } + + private InputStream generateValidTestCaseReqLineInvalidItems(){ + String rawData="GET / AAAAA HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Accept-Language: en-US,en;q=0.9,fr;q=0.8\r\n"+ + "\r\n"; + InputStream ipStream = new ByteArrayInputStream(rawData.getBytes(StandardCharsets.US_ASCII)); + return ipStream; + } + + private InputStream generateValidTestCaseEmptyReqLine(){ + String rawData="\r\n" + + "Host: localhost:8080\r\n" + + "Accept-Language: en-US,en;q=0.9,fr;q=0.8\r\n"+ + "\r\n"; + InputStream ipStream = new ByteArrayInputStream(rawData.getBytes(StandardCharsets.US_ASCII)); + return ipStream; + } +} \ No newline at end of file diff --git a/src/test/java/com/coderfromscratch/http/HttpVersionTest.java b/httpserver/src/test/java/com/johan/http/HttpVersionTest.java similarity index 76% rename from src/test/java/com/coderfromscratch/http/HttpVersionTest.java rename to httpserver/src/test/java/com/johan/http/HttpVersionTest.java index eec0460..38ec7ea 100644 --- a/src/test/java/com/coderfromscratch/http/HttpVersionTest.java +++ b/httpserver/src/test/java/com/johan/http/HttpVersionTest.java @@ -1,4 +1,4 @@ -package com.coderfromscratch.http; +package com.johan.http; import org.junit.jupiter.api.Test; @@ -7,11 +7,11 @@ public class HttpVersionTest { @Test - void getBestCompatibleVersionExactMatch() { - HttpVersion version = null; + void getBestCompatibleVersionExactMatch(){ + HttpVersion version=null; try { version = HttpVersion.getBestCompatibleVersion("HTTP/1.1"); - } catch (BadHttpVersionException e) { + }catch(BadHttpVersionException e){ fail(); } assertNotNull(version); @@ -19,7 +19,7 @@ void getBestCompatibleVersionExactMatch() { } @Test - void getBestCompatibleVersionBadFormat() { + void getBestCompatibleVersionBadMatch(){ HttpVersion version = null; try { version = HttpVersion.getBestCompatibleVersion("http/1.1"); @@ -30,7 +30,7 @@ void getBestCompatibleVersionBadFormat() { } @Test - void getBestCompatibleVersionHigherVersion() { + void getBestCompatibleVersionHigherVersion(){ HttpVersion version = null; try { version = HttpVersion.getBestCompatibleVersion("HTTP/1.2"); diff --git a/pom.xml b/pom.xml deleted file mode 100644 index b589e83..0000000 --- a/pom.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - 4.0.0 - - com.coderfromscratch - simplehttpserver - 1.0-SNAPSHOT - - - 1.8 - 1.8 - - - - - - com.fasterxml.jackson.core - jackson-core - 2.9.9 - - - com.fasterxml.jackson.core - jackson-databind - 2.9.10.3 - - - - org.slf4j - slf4j-api - 1.7.29 - - - ch.qos.logback - logback-classic - 1.2.3 - - - - - org.junit.jupiter - junit-jupiter - RELEASE - test - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - com.coderfromscratch.httpserver.HttpServer - - - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.4 - - - package - - shade - - - - - com.coderfromscratch.httpserver.HttpServer - - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/java/com/coderfromscratch/http/BadHttpVersionException.java b/src/main/java/com/coderfromscratch/http/BadHttpVersionException.java deleted file mode 100644 index 6037c4c..0000000 --- a/src/main/java/com/coderfromscratch/http/BadHttpVersionException.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.coderfromscratch.http; - -public class BadHttpVersionException extends Exception{ - -} diff --git a/src/main/java/com/coderfromscratch/http/HttpHeaderName.java b/src/main/java/com/coderfromscratch/http/HttpHeaderName.java deleted file mode 100644 index 1ada9e1..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpHeaderName.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.coderfromscratch.http; - -public enum HttpHeaderName { - CONTENT_TYPE("Content-Type"), - CONTENT_LENGTH("Content-Length"); - - public final String headerName; - - HttpHeaderName(String headerName) { - this.headerName = headerName; - } -} diff --git a/src/main/java/com/coderfromscratch/http/HttpMessage.java b/src/main/java/com/coderfromscratch/http/HttpMessage.java deleted file mode 100644 index 1c2e665..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpMessage.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.coderfromscratch.http; - -import java.util.HashMap; -import java.util.Set; - -public abstract class HttpMessage { - - private HashMap headers = new HashMap<>(); - - private byte[] messageBody = new byte[0]; - - public Set getHeaderNames() { - return headers.keySet(); - } - - public String getHeader(String headerName) { - return headers.get(headerName.toLowerCase()); - } - - void addHeader(String headerName, String headerField) { - headers.put(headerName.toLowerCase(), headerField); - } - - public byte[] getMessageBody() { - return messageBody; - } - - public void setMessageBody(byte[] messageBody) { - this.messageBody = messageBody; - } -} diff --git a/src/main/java/com/coderfromscratch/http/HttpMethod.java b/src/main/java/com/coderfromscratch/http/HttpMethod.java deleted file mode 100644 index 0f91338..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpMethod.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.coderfromscratch.http; - -public enum HttpMethod { - GET, HEAD; - - public static final int MAX_LENGTH; - - static { - int tempMaxLength = -1; - for (HttpMethod method : values()) { - if (method.name().length() > tempMaxLength) { - tempMaxLength = method.name().length(); - } - } - MAX_LENGTH = tempMaxLength; - } -} diff --git a/src/main/java/com/coderfromscratch/http/HttpParser.java b/src/main/java/com/coderfromscratch/http/HttpParser.java deleted file mode 100644 index d7e0341..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpParser.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.coderfromscratch.http; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class HttpParser { - - private final static Logger LOGGER = LoggerFactory.getLogger(HttpParser.class); - - private static final int SP = 0x20; // 32 - private static final int CR = 0x0D; // 13 - private static final int LF = 0x0A; // 10 - - public HttpRequest parseHttpRequest(InputStream inputStream) throws HttpParsingException { - InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); - - HttpRequest request = new HttpRequest(); - - try { - parseRequestLine(reader, request); - } catch (IOException e) { - e.printStackTrace(); - } - try { - parseHeaders(reader, request); - } catch (IOException e) { - e.printStackTrace(); - } - parseBody(reader, request); - - return request; - } - - private void parseRequestLine(InputStreamReader reader, HttpRequest request) throws IOException, HttpParsingException { - StringBuilder processingDataBuffer = new StringBuilder(); - - boolean methodParsed = false; - boolean requestTargetParsed = false; - - // TODO validate URI size! - - int _byte; - while ((_byte = reader.read()) >=0) { - if (_byte == CR) { - _byte = reader.read(); - if (_byte == LF) { - LOGGER.debug("Request Line VERSION to Process : {}" , processingDataBuffer.toString()); - if (!methodParsed || !requestTargetParsed) { - throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - - try { - request.setHttpVersion(processingDataBuffer.toString()); - } catch (BadHttpVersionException e) { - throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - - return; - } else { - throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - } - - if (_byte == SP) { - if (!methodParsed) { - LOGGER.debug("Request Line METHOD to Process : {}" , processingDataBuffer.toString()); - request.setMethod(processingDataBuffer.toString()); - methodParsed = true; - } else if (!requestTargetParsed) { - LOGGER.debug("Request Line REQ TARGET to Process : {}" , processingDataBuffer.toString()); - request.setRequestTarget(processingDataBuffer.toString()); - requestTargetParsed = true; - } else { - throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - processingDataBuffer.delete(0, processingDataBuffer.length()); - } else { - processingDataBuffer.append((char)_byte); - if (!methodParsed) { - if (processingDataBuffer.length() > HttpMethod.MAX_LENGTH) { - throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); - } - } - } - } - - } - - private void parseHeaders(InputStreamReader reader, HttpRequest request) throws IOException, HttpParsingException { - StringBuilder processingDataBuffer = new StringBuilder(); - boolean crlfFound = false; - - int _byte; - while ((_byte = reader.read()) >=0) { - if (_byte == CR) { - _byte = reader.read(); - if (_byte == LF) { - if (!crlfFound) { - crlfFound = true; - - // Do Things like processing - processSingleHeaderField(processingDataBuffer, request); - // Clear the buffer - processingDataBuffer.delete(0, processingDataBuffer.length()); - } else { - // Two CRLF received, end of Headers section - return; - } - } else { - throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - } else { - crlfFound = false; - // Append to Buffer - processingDataBuffer.append((char)_byte); - } - } - } - - private void processSingleHeaderField(StringBuilder processingDataBuffer, HttpRequest request) throws HttpParsingException { - String rawHeaderField = processingDataBuffer.toString(); - Pattern pattern = Pattern.compile("^(?[!#$%&’*+\\-./^_‘|˜\\dA-Za-z]+):\\s?(?[!#$%&’*+\\-./^_‘|˜(),:;<=>?@[\\\\]{}\" \\dA-Za-z]+)\\s?$"); - - Matcher matcher = pattern.matcher(rawHeaderField); - if (matcher.matches()) { - // We found a proper header - String fieldName = matcher.group("fieldName"); - String fieldValue = matcher.group("fieldValue"); - request.addHeader(fieldName, fieldValue); - } else{ - throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - } - - private void parseBody(InputStreamReader reader, HttpRequest request) { - - } - -} diff --git a/src/main/java/com/coderfromscratch/http/HttpRequest.java b/src/main/java/com/coderfromscratch/http/HttpRequest.java deleted file mode 100644 index 3c99bb0..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpRequest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.coderfromscratch.http; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Set; - -public class HttpRequest extends HttpMessage{ - - private HttpMethod method; - private String requestTarget; - private String originalHttpVersion; // literal from the request - private HttpVersion bestCompatibleHttpVersion; - - HttpRequest() { - } - - public HttpMethod getMethod() { - return method; - } - - public String getRequestTarget() { - return requestTarget; - } - - public HttpVersion getBestCompatibleHttpVersion() { - return bestCompatibleHttpVersion; - } - - public String getOriginalHttpVersion() { - return originalHttpVersion; - } - - void setMethod(String methodName) throws HttpParsingException { - for (HttpMethod method : HttpMethod.values()) { - if (methodName.equals(method.name())) { - this.method = method; - return; - } - } - throw new HttpParsingException( - HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED - ); - } - - void setRequestTarget(String requestTarget) throws HttpParsingException { - if (requestTarget == null || requestTarget.length() == 0) { - throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_500_INTERNAL_SERVER_ERROR); - } - this.requestTarget = requestTarget; - } - - void setHttpVersion(String originalHttpVersion) throws BadHttpVersionException, HttpParsingException { - this.originalHttpVersion = originalHttpVersion; - this.bestCompatibleHttpVersion = HttpVersion.getBestCompatibleVersion(originalHttpVersion); - if (this.bestCompatibleHttpVersion == null) { - throw new HttpParsingException( - HttpStatusCode.SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED - ); - } - } - -} diff --git a/src/main/java/com/coderfromscratch/http/HttpResponse.java b/src/main/java/com/coderfromscratch/http/HttpResponse.java deleted file mode 100644 index 6336da0..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpResponse.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.coderfromscratch.http; - -public class HttpResponse extends HttpMessage { - - private final String CRLF = "\r\n"; - - // status-line = HTTP-version SP status-code SP reason-phrase CRLF - private String httpVersion; - - private HttpStatusCode statusCode; - - private String reasonPhrase = null; - - private HttpResponse() { - } - - public String getHttpVersion() { - return httpVersion; - } - - public void setHttpVersion(String httpVersion) { - this.httpVersion = httpVersion; - } - - public HttpStatusCode getStatusCode() { - return statusCode; - } - - public void setStatusCode(HttpStatusCode statusCode) { - this.statusCode = statusCode; - } - - public String getReasonPhrase() { - if (reasonPhrase == null && statusCode!=null) { - return statusCode.MESSAGE; - } - return reasonPhrase; - } - - public void setReasonPhrase(String reasonPhrase) { - this.reasonPhrase = reasonPhrase; - } - - public byte[] getResponseBytes() { - StringBuilder responseBuilder = new StringBuilder(); - responseBuilder.append(httpVersion) - .append(" ") - .append(statusCode.STATUS_CODE) - .append(" ") - .append(getReasonPhrase()) - .append(CRLF); - - for (String headerName: getHeaderNames()) { - responseBuilder.append(headerName) - .append(": ") - .append(getHeader(headerName)) - .append(CRLF); - } - - responseBuilder.append(CRLF); - - byte[] responseBytes = responseBuilder.toString().getBytes(); - - if (getMessageBody().length == 0) - return responseBytes; - - byte[] responseWithBody = new byte[responseBytes.length + getMessageBody().length]; - System.arraycopy(responseBytes, 0, responseWithBody, 0, responseBytes.length); - System.arraycopy(getMessageBody(), 0, responseWithBody, responseBytes.length, getMessageBody().length); - - return responseWithBody; - } - - public static class Builder { - - private HttpResponse response = new HttpResponse(); - - public Builder httpVersion ( String httpVersion) { - response.setHttpVersion(httpVersion); - return this; - } - - public Builder statusCode(HttpStatusCode statusCode) { - response.setStatusCode(statusCode); - return this; - } - - public Builder reasonPhrase(String reasonPhrase) { - response.setReasonPhrase(reasonPhrase); - return this; - } - - public Builder addHeader(String headerName, String headerField) { - response.addHeader(headerName, headerField); - return this; - } - - public Builder messageBody(byte[] messageBody) { - response.setMessageBody(messageBody); - return this; - } - - public HttpResponse build() { - return response; - } - - } -} diff --git a/src/main/java/com/coderfromscratch/http/HttpStatusCode.java b/src/main/java/com/coderfromscratch/http/HttpStatusCode.java deleted file mode 100644 index 4eaf380..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpStatusCode.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.coderfromscratch.http; - -public enum HttpStatusCode { - - /* --- CLIENT ERRORS --- */ - CLIENT_ERROR_400_BAD_REQUEST(400, "Bad Request"), - CLIENT_ERROR_401_METHOD_NOT_ALLOWED(401, "Method Not Allowed"), - CLIENT_ERROR_414_BAD_REQUEST(414, "URI Too Long"), - CLIENT_ERROR_404_NOT_FOUND(404, "Not Found" ), - - /* --- SERVER ERRORS --- */ - SERVER_ERROR_500_INTERNAL_SERVER_ERROR(500, "Internal Server Error"), - SERVER_ERROR_501_NOT_IMPLEMENTED(501, "Not Implemented"), - SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED(505, "Http Version Not Supported"), - OK(200,"OK" ); - - - public final int STATUS_CODE; - public final String MESSAGE; - - HttpStatusCode(int STATUS_CODE, String MESSAGE) { - this.STATUS_CODE = STATUS_CODE; - this.MESSAGE = MESSAGE; - } - -} diff --git a/src/main/java/com/coderfromscratch/http/HttpVersion.java b/src/main/java/com/coderfromscratch/http/HttpVersion.java deleted file mode 100644 index 74f6070..0000000 --- a/src/main/java/com/coderfromscratch/http/HttpVersion.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.coderfromscratch.http; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public enum HttpVersion { - HTTP_1_1("HTTP/1.1", 1 , 1); - - public final String LITERAL; - public final int MAJOR; - public final int MINOR; - - HttpVersion(String LITERAL, int MAJOR, int MINOR) { - this.LITERAL = LITERAL; - this.MAJOR = MAJOR; - this.MINOR = MINOR; - } - - private static final Pattern httpVersionRegexPattern = Pattern.compile("^HTTP/(?\\d+).(?\\d+)"); - - public static HttpVersion getBestCompatibleVersion(String literalVersion) throws BadHttpVersionException { - Matcher matcher = httpVersionRegexPattern.matcher(literalVersion); - if (!matcher.find() || matcher.groupCount() != 2) { - throw new BadHttpVersionException(); - } - int major = Integer.parseInt(matcher.group("major")); - int minor = Integer.parseInt(matcher.group("minor")); - - HttpVersion tempBestCompatible = null; - for (HttpVersion version : HttpVersion.values()) { - if (version.LITERAL.equals(literalVersion)) { - return version; - } else { - if (version.MAJOR == major) { - if (version.MINOR < minor) { - tempBestCompatible = version; - } - } - } - } - return tempBestCompatible; - } -} diff --git a/src/main/java/com/coderfromscratch/httpserver/HttpServer.java b/src/main/java/com/coderfromscratch/httpserver/HttpServer.java deleted file mode 100644 index 894c650..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/HttpServer.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.coderfromscratch.httpserver; - -import com.coderfromscratch.httpserver.config.Configuration; -import com.coderfromscratch.httpserver.config.ConfigurationManager; -import com.coderfromscratch.httpserver.core.ServerListenerThread; -import com.coderfromscratch.httpserver.core.io.WebRootNotFoundException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -/** - * - * Driver Class for the Http Server - * - */ -public class HttpServer { - - private final static Logger LOGGER = LoggerFactory.getLogger(HttpServer.class); - - public static void main(String[] args) { - - if (args.length != 1) { - LOGGER.error("No configuration file provided."); - LOGGER.error("Syntax: java -jar simplehttpserver-1.0-SNAPSHOT.jar "); - return; - } - - LOGGER.info("Server starting..."); - - ConfigurationManager.getInstance().loadConfigurationFile(args[0]); - Configuration conf = ConfigurationManager.getInstance().getCurrentConfiguration(); - - LOGGER.info("Using Port: " + conf.getPort()); - LOGGER.info("Using WebRoot: " + conf.getWebroot()); - - try { - ServerListenerThread serverListenerThread = new ServerListenerThread(conf.getPort(), conf.getWebroot()); - serverListenerThread.start(); - } catch (IOException e) { - e.printStackTrace(); - // TODO handle later. - } catch (WebRootNotFoundException e) { - LOGGER.error("Webroot folder not found",e); - } - - - } - -} diff --git a/src/main/java/com/coderfromscratch/httpserver/config/ConfigurationManager.java b/src/main/java/com/coderfromscratch/httpserver/config/ConfigurationManager.java deleted file mode 100644 index 8857d58..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/config/ConfigurationManager.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.coderfromscratch.httpserver.config; - -import com.coderfromscratch.httpserver.util.Json; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; - -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; - -public class ConfigurationManager { - - private static ConfigurationManager myConfigurationManager; - private static Configuration myCurrentConfiguration; - - private ConfigurationManager() { - } - - public static ConfigurationManager getInstance() { - if (myConfigurationManager==null) - myConfigurationManager = new ConfigurationManager(); - return myConfigurationManager; - } - - /** - * Used to load a configuration file by the path provided - */ - public void loadConfigurationFile(String filePath) { - FileReader fileReader = null; - try { - fileReader = new FileReader(filePath); - } catch (FileNotFoundException e) { - throw new HttpConfigurationException(e); - } - StringBuffer sb = new StringBuffer(); - int i ; - try { - while ( ( i = fileReader.read()) != -1) { - sb.append((char)i); - } - } catch (IOException e) { - throw new HttpConfigurationException(e); - } - JsonNode conf = null; - try { - conf = Json.parse(sb.toString()); - } catch (IOException e) { - throw new HttpConfigurationException("Error parsing the Configuration File", e); - } - try { - myCurrentConfiguration = Json.fromJson(conf, Configuration.class); - } catch (JsonProcessingException e) { - throw new HttpConfigurationException("Error parsing the Configuration file, internal",e); - } - } - - /** - * Returns the Current loaded Configuration - */ - public Configuration getCurrentConfiguration() { - if ( myCurrentConfiguration == null) { - throw new HttpConfigurationException("No Current Configuration Set."); - } - return myCurrentConfiguration; - } -} diff --git a/src/main/java/com/coderfromscratch/httpserver/config/HttpConfigurationException.java b/src/main/java/com/coderfromscratch/httpserver/config/HttpConfigurationException.java deleted file mode 100644 index 516ddef..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/config/HttpConfigurationException.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.coderfromscratch.httpserver.config; - -public class HttpConfigurationException extends RuntimeException { - - public HttpConfigurationException() { - } - - public HttpConfigurationException(String message) { - super(message); - } - - public HttpConfigurationException(String message, Throwable cause) { - super(message, cause); - } - - public HttpConfigurationException(Throwable cause) { - super(cause); - } - -} diff --git a/src/main/java/com/coderfromscratch/httpserver/core/HttpConnectionWorkerThread.java b/src/main/java/com/coderfromscratch/httpserver/core/HttpConnectionWorkerThread.java deleted file mode 100644 index d55311d..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/core/HttpConnectionWorkerThread.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.coderfromscratch.httpserver.core; - -import com.coderfromscratch.http.*; -import com.coderfromscratch.httpserver.core.io.ReadFileException; -import com.coderfromscratch.httpserver.core.io.WebRootHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; - -public class HttpConnectionWorkerThread extends Thread { - private final static Logger LOGGER = LoggerFactory.getLogger(HttpConnectionWorkerThread.class); - private Socket socket; - private WebRootHandler webRootHandler; - private HttpParser httpParser = new HttpParser(); - - public HttpConnectionWorkerThread(Socket socket, WebRootHandler webRootHandler) { - this.socket = socket; - this.webRootHandler = webRootHandler; - } - - @Override - public void run() { - InputStream inputStream = null; - OutputStream outputStream = null; - - try { - inputStream = socket.getInputStream(); - outputStream = socket.getOutputStream(); - - HttpRequest request = httpParser.parseHttpRequest(inputStream); - HttpResponse response = handleRequest(request); - - outputStream.write(response.getResponseBytes()); - - LOGGER.info(" * Connection Processing Finished."); - } catch (IOException e) { - LOGGER.error("Problem with communication", e); - } catch (HttpParsingException e) { - LOGGER.info("Bag Request", e); - - HttpResponse response = new HttpResponse.Builder() - .httpVersion(HttpVersion.HTTP_1_1.LITERAL) - .statusCode(e.getErrorCode()) - .build(); - try { - outputStream.write(response.getResponseBytes()); - } catch (IOException ex) { - LOGGER.error("Problem with communication", e); - } - - } finally { - if (inputStream!= null) { - try { - inputStream.close(); - } catch (IOException e) {} - } - if (outputStream!=null) { - try { - outputStream.close(); - } catch (IOException e) {} - } - if (socket!= null) { - try { - socket.close(); - } catch (IOException e) {} - } - } - } - - private HttpResponse handleRequest(HttpRequest request) { - - switch (request.getMethod()) { - case GET: - LOGGER.info(" * GET Request"); - return handleGetRequest(request, true); - case HEAD: - LOGGER.info(" * HEAD Request"); - return handleGetRequest(request, false); - default: - return new HttpResponse.Builder() - .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) - .statusCode(HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED) - .build(); - } - - } - - private HttpResponse handleGetRequest(HttpRequest request, boolean setMessageBody) { - try { - - HttpResponse.Builder builder = new HttpResponse.Builder() - .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) - .statusCode(HttpStatusCode.OK) - .addHeader(HttpHeaderName.CONTENT_TYPE.headerName, webRootHandler.getFileMimeType(request.getRequestTarget())); - - if (setMessageBody) { - byte[] messageBody = webRootHandler.getFileByteArrayData(request.getRequestTarget()); - builder.addHeader(HttpHeaderName.CONTENT_LENGTH.headerName, String.valueOf(messageBody.length)) - .messageBody(messageBody); - } - - return builder.build(); - - } catch (FileNotFoundException e) { - - return new HttpResponse.Builder() - .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) - .statusCode(HttpStatusCode.CLIENT_ERROR_404_NOT_FOUND) - .build(); - - } catch (ReadFileException e) { - - return new HttpResponse.Builder() - .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) - .statusCode(HttpStatusCode.SERVER_ERROR_500_INTERNAL_SERVER_ERROR) - .build(); - } - - } -} diff --git a/src/main/java/com/coderfromscratch/httpserver/core/ServerListenerThread.java b/src/main/java/com/coderfromscratch/httpserver/core/ServerListenerThread.java deleted file mode 100644 index c00766d..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/core/ServerListenerThread.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.coderfromscratch.httpserver.core; - -import com.coderfromscratch.httpserver.core.io.WebRootHandler; -import com.coderfromscratch.httpserver.core.io.WebRootNotFoundException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ServerSocket; -import java.net.Socket; - -public class ServerListenerThread extends Thread { - - private final static Logger LOGGER = LoggerFactory.getLogger(ServerListenerThread.class); - - private int port; - private String webroot; - private ServerSocket serverSocket; - - private WebRootHandler webRootHandler; - - public ServerListenerThread(int port, String webroot) throws IOException, WebRootNotFoundException { - this.port = port; - this.webroot = webroot; - this.webRootHandler = new WebRootHandler(webroot); - this.serverSocket = new ServerSocket(this.port); - } - - @Override - public void run() { - - try { - - while ( serverSocket.isBound() && !serverSocket.isClosed()) { - Socket socket = serverSocket.accept(); - - LOGGER.info(" * Connection accepted: " + socket.getInetAddress()); - - HttpConnectionWorkerThread workerThread = new HttpConnectionWorkerThread(socket, webRootHandler); - workerThread.start(); - - } - - } catch (IOException e) { - LOGGER.error("Problem with setting socket", e); - } finally { - if (serverSocket!=null) { - try { - serverSocket.close(); - } catch (IOException e) {} - } - } - - } -} diff --git a/src/main/java/com/coderfromscratch/httpserver/core/io/ReadFileException.java b/src/main/java/com/coderfromscratch/httpserver/core/io/ReadFileException.java deleted file mode 100644 index b27608f..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/core/io/ReadFileException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.coderfromscratch.httpserver.core.io; - -public class ReadFileException extends Throwable { - public ReadFileException() { - } - - public ReadFileException(String message) { - super(message); - } - - public ReadFileException(String message, Throwable cause) { - super(message, cause); - } - - public ReadFileException(Throwable cause) { - super(cause); - } - - public ReadFileException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/src/main/java/com/coderfromscratch/httpserver/core/io/WebRootHandler.java b/src/main/java/com/coderfromscratch/httpserver/core/io/WebRootHandler.java deleted file mode 100644 index efcfc88..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/core/io/WebRootHandler.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.coderfromscratch.httpserver.core.io; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URLConnection; - -public class WebRootHandler { - - private File webRoot; - - public WebRootHandler(String webRootPath) throws WebRootNotFoundException { - webRoot = new File(webRootPath); - if (!webRoot.exists() || !webRoot.isDirectory()) { - throw new WebRootNotFoundException("Webroot provided does not exist or is not a folder"); - } - } - - private boolean checkIfEndsWithSlash(String relativePath) { - return relativePath.endsWith("/"); - } - - /** - * This method checks to see if the relative path provided exists inside WebRoot - * - * @param relativePath - * @return true if the path exists inside WebRoot, false if not. - */ - private boolean checkIfProvidedRelativePathExists(String relativePath) { - File file = new File(webRoot, relativePath); - - if (!file.exists()) - return false; - - try { - if (file.getCanonicalPath().startsWith(webRoot.getCanonicalPath())) { - return true; - } - } catch (IOException e) { - return false; - } - return false; - } - - public String getFileMimeType(String relativePath) throws FileNotFoundException { - if (checkIfEndsWithSlash(relativePath)) { - relativePath += "index.html"; // By default serve the index.html, if it exists. - } - - if (!checkIfProvidedRelativePathExists(relativePath)) { - throw new FileNotFoundException("File not found: " + relativePath); - } - - File file = new File(webRoot, relativePath); - - String mimeType = URLConnection.getFileNameMap().getContentTypeFor(file.getName()); - - if (mimeType == null) { - return "application/octet-stream"; - } - - return mimeType; - } - - /** - * Returns a byte array of the content of a file. - * - * Todo - For large files a new strategy might be necessary. - * - * @param relativePath the path to the file inside the webroot folder. - * @return a byte array of the data. - * @throws FileNotFoundException if the file can not be found - * @throws ReadFileException if there was a problem reading the file. - */ - public byte[] getFileByteArrayData(String relativePath) throws FileNotFoundException, ReadFileException { - if (checkIfEndsWithSlash(relativePath)) { - relativePath += "index.html"; // By default serve the index.html, if it exists. - } - - if (!checkIfProvidedRelativePathExists(relativePath)) { - throw new FileNotFoundException("File not found: " + relativePath); - } - - File file = new File(webRoot, relativePath); - FileInputStream fileInputStream = new FileInputStream(file); - byte[] fileBytes = new byte[(int)file.length()]; - try { - fileInputStream.read(fileBytes); - fileInputStream.close(); - } catch (IOException e) { - throw new ReadFileException(e); - } - return fileBytes; - } -} diff --git a/src/main/java/com/coderfromscratch/httpserver/core/io/WebRootNotFoundException.java b/src/main/java/com/coderfromscratch/httpserver/core/io/WebRootNotFoundException.java deleted file mode 100644 index d597787..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/core/io/WebRootNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.coderfromscratch.httpserver.core.io; - -public class WebRootNotFoundException extends Throwable { - public WebRootNotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/com/coderfromscratch/httpserver/util/Json.java b/src/main/java/com/coderfromscratch/httpserver/util/Json.java deleted file mode 100644 index 04d3656..0000000 --- a/src/main/java/com/coderfromscratch/httpserver/util/Json.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.coderfromscratch.httpserver.util; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.*; - -import java.io.IOException; - -public class Json { - - private static ObjectMapper myObjectMapper = defaultObjectMapper(); - - private static ObjectMapper defaultObjectMapper() { - ObjectMapper om = new ObjectMapper(); - om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - return om; - } - - public static JsonNode parse(String jsonSrc) throws IOException { - return myObjectMapper.readTree(jsonSrc); - } - - public static A fromJson(JsonNode node , Class clazz) throws JsonProcessingException { - return myObjectMapper.treeToValue(node, clazz); - } - - public static JsonNode toJson(Object obj) { - return myObjectMapper.valueToTree(obj); - } - - public static String stringify(JsonNode node) throws JsonProcessingException { - return generateJson(node, false); - } - - public static String stringifyPretty(JsonNode node) throws JsonProcessingException { - return generateJson(node, true); - } - - private static String generateJson(Object o, boolean pretty) throws JsonProcessingException { - ObjectWriter objectWriter = myObjectMapper.writer(); - if (pretty) { - objectWriter = objectWriter.with(SerializationFeature.INDENT_OUTPUT); - } - return objectWriter.writeValueAsString(o); - } -} diff --git a/src/main/resources/http.json b/src/main/resources/http.json deleted file mode 100644 index e1e8221..0000000 --- a/src/main/resources/http.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "port": 8080, - "webroot": "WebRoot" -} \ No newline at end of file diff --git a/src/test/java/com/coderfromscratch/http/HttpHeadersParserTest.java b/src/test/java/com/coderfromscratch/http/HttpHeadersParserTest.java deleted file mode 100644 index 21a960d..0000000 --- a/src/test/java/com/coderfromscratch/http/HttpHeadersParserTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.coderfromscratch.http; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class HttpHeadersParserTest { - - private HttpParser httpParser; - private Method parseHeadersMethod; - - @BeforeAll - public void beforeClass() throws NoSuchMethodException { - httpParser = new HttpParser(); - Class cls = HttpParser.class; - parseHeadersMethod = cls.getDeclaredMethod("parseHeaders", InputStreamReader.class, HttpRequest.class); - parseHeadersMethod.setAccessible(true); - } - - @Test - public void testSimpleSingleHeader() throws InvocationTargetException, IllegalAccessException { - HttpRequest request = new HttpRequest(); - parseHeadersMethod.invoke( - httpParser, - generateSimpleSingleHeaderMessage(), - request); - assertEquals(1, request.getHeaderNames().size()); - assertEquals("localhost:8080", request.getHeader("host")); - } - - @Test - public void testMultipleHeaders() throws InvocationTargetException, IllegalAccessException { - HttpRequest request = new HttpRequest(); - parseHeadersMethod.invoke( - httpParser, - generateMultipleHeadersMessage(), - request); - assertEquals(10, request.getHeaderNames().size()); - assertEquals("localhost:8080", request.getHeader("host")); - } - - @Test - public void testErrorSpaceBeforeColonHeader() throws InvocationTargetException, IllegalAccessException { - HttpRequest request = new HttpRequest(); - - try { - parseHeadersMethod.invoke( - httpParser, - generateSpaceBeforeColonErrorHeaderMessage(), - request); - } catch (InvocationTargetException e) { - if (e.getCause() instanceof HttpParsingException) { - assertEquals(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST, ((HttpParsingException)e.getCause()).getErrorCode()); - } - } - - } - - private InputStreamReader generateSimpleSingleHeaderMessage() { - String rawData = "Host: localhost:8080\r\n" ; -// "Connection: keep-alive\r\n" + -// "Upgrade-Insecure-Requests: 1\r\n" + -// "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n" + -// "Sec-Fetch-User: ?1\r\n" + -// "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\n" + -// "Sec-Fetch-Site: none\r\n" + -// "Sec-Fetch-Mode: navigate\r\n" + -// "Accept-Encoding: gzip, deflate, br\r\n" + -// "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + -// "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); - return reader; - } - - private InputStreamReader generateMultipleHeadersMessage() { - String rawData = "Host: localhost:8080\r\n" + - "Connection: keep-alive\r\n" + - "Upgrade-Insecure-Requests: 1\r\n" + - "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n" + - "Sec-Fetch-User: ?1\r\n" + - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\n" + - "Sec-Fetch-Site: none\r\n" + - "Sec-Fetch-Mode: navigate\r\n" + - "Accept-Encoding: gzip, deflate, br\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); - return reader; - } - - private InputStreamReader generateSpaceBeforeColonErrorHeaderMessage() { - String rawData = "Host : localhost:8080\r\n\r\n" ; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); - return reader; - } - -} diff --git a/src/test/java/com/coderfromscratch/http/HttpParserTest.java b/src/test/java/com/coderfromscratch/http/HttpParserTest.java deleted file mode 100644 index 1a5a867..0000000 --- a/src/test/java/com/coderfromscratch/http/HttpParserTest.java +++ /dev/null @@ -1,305 +0,0 @@ -package com.coderfromscratch.http; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.*; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class HttpParserTest { - - private HttpParser httpParser; - - @BeforeAll - public void beforeClass() { - httpParser = new HttpParser(); - } - - @Test - void parseHttpRequest() { - HttpRequest request = null; - try { - request = httpParser.parseHttpRequest( - generateValidGETTestCase() - ); - } catch (HttpParsingException e) { - fail(e); - } - - assertNotNull(request); - assertEquals(request.getMethod(), HttpMethod.GET); - assertEquals(request.getRequestTarget(), "/"); - assertEquals(request.getOriginalHttpVersion(), "HTTP/1.1"); - assertEquals(request.getBestCompatibleHttpVersion(), HttpVersion.HTTP_1_1); - } - - @Test - void parseHttpRequestBadMethod1() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateBadTestCaseMethodName1() - ); - fail(); - } catch (HttpParsingException e) { - assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); - } - } - - @Test - void parseHttpRequestBadMethod2() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateBadTestCaseMethodName2() - ); - fail(); - } catch (HttpParsingException e) { - assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); - } - } - - @Test - void parseHttpRequestInvNumItems1() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateBadTestCaseRequestLineInvNumItems1() - ); - fail(); - } catch (HttpParsingException e) { - assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - } - - @Test - void parseHttpEmptyRequestLine() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateBadTestCaseEmptyRequestLine() - ); - fail(); - } catch (HttpParsingException e) { - assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - } - - @Test - void parseHttpRequestLineCRnoLF() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateBadTestCaseRequestLineOnlyCRnoLF() - ); - fail(); - } catch (HttpParsingException e) { - assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - } - - @Test - void parseHttpRequestBadHttpVersion() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateBadHttpVersionTestCase() - ); - fail(); - } catch (HttpParsingException e) { - assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); - } - } - - @Test - void parseHttpRequestUnsupportedHttpVersion() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateUnsuportedHttpVersionTestCase() - ); - fail(); - } catch (HttpParsingException e) { - assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED); - } - } - - @Test - void parseHttpRequestSupportedHttpVersion1() { - try { - HttpRequest request = httpParser.parseHttpRequest( - generateSupportedHttpVersion1() - ); - assertNotNull(request); - assertEquals(request.getBestCompatibleHttpVersion(), HttpVersion.HTTP_1_1); - assertEquals(request.getOriginalHttpVersion(), "HTTP/1.2"); - } catch (HttpParsingException e) { - fail(); - } - } - - private InputStream generateValidGETTestCase() { - String rawData = "GET / HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Connection: keep-alive\r\n" + - "Upgrade-Insecure-Requests: 1\r\n" + - "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n" + - "Sec-Fetch-User: ?1\r\n" + - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\n" + - "Sec-Fetch-Site: none\r\n" + - "Sec-Fetch-Mode: navigate\r\n" + - "Accept-Encoding: gzip, deflate, br\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateBadTestCaseMethodName1() { - String rawData = "GeT / HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateBadTestCaseMethodName2() { - String rawData = "GETTTT / HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateBadTestCaseRequestLineInvNumItems1() { - String rawData = "GET / AAAAAA HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateBadTestCaseEmptyRequestLine() { - String rawData = "\r\n" + - "Host: localhost:8080\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateBadTestCaseRequestLineOnlyCRnoLF() { - String rawData = "GET / HTTP/1.1\r" + // <----- no LF - "Host: localhost:8080\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateBadHttpVersionTestCase() { - String rawData = "GET / HTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Connection: keep-alive\r\n" + - "Upgrade-Insecure-Requests: 1\r\n" + - "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n" + - "Sec-Fetch-User: ?1\r\n" + - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\n" + - "Sec-Fetch-Site: none\r\n" + - "Sec-Fetch-Mode: navigate\r\n" + - "Accept-Encoding: gzip, deflate, br\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateUnsuportedHttpVersionTestCase() { - String rawData = "GET / HTTP/2.1\r\n" + - "Host: localhost:8080\r\n" + - "Connection: keep-alive\r\n" + - "Upgrade-Insecure-Requests: 1\r\n" + - "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n" + - "Sec-Fetch-User: ?1\r\n" + - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\n" + - "Sec-Fetch-Site: none\r\n" + - "Sec-Fetch-Mode: navigate\r\n" + - "Accept-Encoding: gzip, deflate, br\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } - - private InputStream generateSupportedHttpVersion1() { - String rawData = "GET / HTTP/1.2\r\n" + - "Host: localhost:8080\r\n" + - "Connection: keep-alive\r\n" + - "Upgrade-Insecure-Requests: 1\r\n" + - "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n" + - "Sec-Fetch-User: ?1\r\n" + - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\n" + - "Sec-Fetch-Site: none\r\n" + - "Sec-Fetch-Mode: navigate\r\n" + - "Accept-Encoding: gzip, deflate, br\r\n" + - "Accept-Language: en-US,en;q=0.9,es;q=0.8,pt;q=0.7,de-DE;q=0.6,de;q=0.5,la;q=0.4\r\n" + - "\r\n"; - - InputStream inputStream = new ByteArrayInputStream( - rawData.getBytes( - StandardCharsets.US_ASCII - ) - ); - - return inputStream; - } -} \ No newline at end of file diff --git a/src/test/java/com/coderfromscratch/httpserver/core/io/WebRootHandlerTest.java b/src/test/java/com/coderfromscratch/httpserver/core/io/WebRootHandlerTest.java deleted file mode 100644 index c8d4a58..0000000 --- a/src/test/java/com/coderfromscratch/httpserver/core/io/WebRootHandlerTest.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.coderfromscratch.httpserver.core.io; - -import com.coderfromscratch.http.HttpParser; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.io.FileNotFoundException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import static org.junit.jupiter.api.Assertions.*; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class WebRootHandlerTest { - - private WebRootHandler webRootHandler; - - private Method checkIfEndsWithSlashMethod; - - private Method checkIfProvidedRelativePathExistsMethod; - @BeforeAll - public void beforeClass() throws WebRootNotFoundException, NoSuchMethodException { - webRootHandler = new WebRootHandler("WebRoot"); - Class cls = WebRootHandler.class; - checkIfEndsWithSlashMethod = cls.getDeclaredMethod("checkIfEndsWithSlash", String.class); - checkIfEndsWithSlashMethod.setAccessible(true); - - checkIfProvidedRelativePathExistsMethod = cls.getDeclaredMethod("checkIfProvidedRelativePathExists", String.class); - checkIfProvidedRelativePathExistsMethod.setAccessible(true); - } - - @Test - void constructorGoodPath() { - try { - WebRootHandler webRootHandler = new WebRootHandler("E:\\Projects\\CoderFromScratch\\simple-java-http-server\\WebRoot"); - } catch (WebRootNotFoundException e) { - fail(e); - } - } - - @Test - void constructorBadPath() { - try { - WebRootHandler webRootHandler = new WebRootHandler("E:\\Projects\\CoderFromScratch\\simple-java-http-server\\WebRoot2"); - fail(); - } catch (WebRootNotFoundException e) { - } - } - - @Test - void constructorGoodPath2() { - try { - WebRootHandler webRootHandler = new WebRootHandler("WebRoot"); - } catch (WebRootNotFoundException e) { - fail(e); - } - } - - @Test - void constructorBadPath2() { - try { - WebRootHandler webRootHandler = new WebRootHandler("WebRoot2"); - fail(); - } catch (WebRootNotFoundException e) { - } - } - - @Test - void checkIfEndsWithSlashMethodFalse() { - try { - boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"index.html"); - assertFalse(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void checkIfEndsWithSlashMethodFalse2() { - try { - boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/index.html"); - assertFalse(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void checkIfEndsWithSlashMethodFalse3() { - try { - boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/private/index.html"); - assertFalse(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void checkIfEndsWithSlashMethodTrue() { - try { - boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/"); - assertTrue(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void checkIfEndsWithSlashMethodTrue2() { - try { - boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/private/"); - assertTrue(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void testWebRootFilePathExists() { - try { - boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/index.html"); - assertTrue(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void testWebRootFilePathExistsGoodRelative() { - try { - boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/./././index.html"); - assertTrue(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void testWebRootFilePathExistsDoesNotExist() { - try { - boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/indexNotHere.html"); - assertFalse(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void testWebRootFilePathExistsInvalid() { - try { - boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/../LICENSE"); - assertFalse(result); - } catch (IllegalAccessException e) { - fail(e); - } catch (InvocationTargetException e) { - fail(e); - } - } - - @Test - void testGetFileMimeTypeText() { - try { - String mimeType = webRootHandler.getFileMimeType("/"); - assertEquals("text/html", mimeType); - } catch (FileNotFoundException e) { - fail(e); - } - } - - @Test - void testGetFileMimeTypePng() { - try { - String mimeType = webRootHandler.getFileMimeType("/logo.png"); - assertEquals("image/png", mimeType); - } catch (FileNotFoundException e) { - fail(e); - } - } - - @Test - void testGetFileMimeTypeDefault() { - try { - String mimeType = webRootHandler.getFileMimeType("/favicon.ico"); - assertEquals("application/octet-stream", mimeType); - } catch (FileNotFoundException e) { - fail(e); - } - } - - @Test - void testGetFileByteArrayData() { - try { - assertTrue(webRootHandler.getFileByteArrayData("/").length > 0); - } catch (FileNotFoundException e) { - fail(e); - } catch (ReadFileException e) { - fail(e); - } - } - - @Test - void testGetFileByteArrayDataFileNotThere() { - try { - webRootHandler.getFileByteArrayData("/test.html"); - fail(); - } catch (FileNotFoundException e) { - // pass - } catch (ReadFileException e) { - fail(e); - } - } -}