├── LICENSE ├── README.md ├── Request.txt ├── WebRoot ├── favicon.ico ├── index.html └── logo.png ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── coderfromscratch │ │ ├── http │ │ ├── BadHttpVersionException.java │ │ ├── HttpHeaderName.java │ │ ├── HttpMessage.java │ │ ├── HttpMethod.java │ │ ├── HttpParser.java │ │ ├── HttpParsingException.java │ │ ├── HttpRequest.java │ │ ├── HttpResponse.java │ │ ├── HttpStatusCode.java │ │ └── HttpVersion.java │ │ └── httpserver │ │ ├── HttpServer.java │ │ ├── config │ │ ├── Configuration.java │ │ ├── ConfigurationManager.java │ │ └── HttpConfigurationException.java │ │ ├── core │ │ ├── HttpConnectionWorkerThread.java │ │ ├── ServerListenerThread.java │ │ └── io │ │ │ ├── ReadFileException.java │ │ │ ├── WebRootHandler.java │ │ │ └── WebRootNotFoundException.java │ │ └── util │ │ └── Json.java └── resources │ └── http.json └── test └── java └── com └── coderfromscratch ├── http ├── HttpHeadersParserTest.java ├── HttpParserTest.java └── HttpVersionTest.java └── httpserver └── core └── io └── WebRootHandlerTest.java /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CoderFromScratch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-java-http-server 2 | Create a Simple HTTP Server in Java Tutorial Series - https://www.youtube.com/playlist?list=PLAuGQNR28pW56GigraPdiI0oKwcs8gglW 3 | -------------------------------------------------------------------------------- /Request.txt: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host: localhost:8080 3 | Connection: keep-alive 4 | Upgrade-Insecure-Requests: 1 5 | User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36 6 | Sec-Fetch-User: ?1 7 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 8 | Sec-Fetch-Site: none 9 | Sec-Fetch-Mode: navigate 10 | Accept-Encoding: gzip, deflate, br 11 | 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 12 | -------------------------------------------------------------------------------- /WebRoot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoderFromScratch/simple-java-http-server/a458d94c314817b8c910d98d3cf303f04dc0e39d/WebRoot/favicon.ico -------------------------------------------------------------------------------- /WebRoot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Java HTTP Server 4 | 9 | 10 |
11 |

Welcome to our first rendered page!

12 |
13 |

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

14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /WebRoot/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoderFromScratch/simple-java-http-server/a458d94c314817b8c910d98d3cf303f04dc0e39d/WebRoot/logo.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.coderfromscratch 8 | simplehttpserver 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 1.8 13 | 1.8 14 | 15 | 16 | 17 | 18 | 19 | com.fasterxml.jackson.core 20 | jackson-core 21 | 2.9.9 22 | 23 | 24 | com.fasterxml.jackson.core 25 | jackson-databind 26 | 2.9.10.3 27 | 28 | 29 | 30 | org.slf4j 31 | slf4j-api 32 | 1.7.29 33 | 34 | 35 | ch.qos.logback 36 | logback-classic 37 | 1.2.3 38 | 39 | 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter 44 | RELEASE 45 | test 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-jar-plugin 55 | 3.3.0 56 | 57 | 58 | 59 | com.coderfromscratch.httpserver.HttpServer 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-shade-plugin 69 | 3.2.4 70 | 71 | 72 | package 73 | 74 | shade 75 | 76 | 77 | 78 | 80 | com.coderfromscratch.httpserver.HttpServer 81 | 82 | 83 | 84 | 85 | 86 | *:* 87 | 88 | META-INF/*.SF 89 | META-INF/*.DSA 90 | META-INF/*.RSA 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/BadHttpVersionException.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | public class BadHttpVersionException extends Exception{ 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpHeaderName.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | public enum HttpHeaderName { 4 | CONTENT_TYPE("Content-Type"), 5 | CONTENT_LENGTH("Content-Length"); 6 | 7 | public final String headerName; 8 | 9 | HttpHeaderName(String headerName) { 10 | this.headerName = headerName; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpMessage.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | import java.util.HashMap; 4 | import java.util.Set; 5 | 6 | public abstract class HttpMessage { 7 | 8 | private HashMap headers = new HashMap<>(); 9 | 10 | private byte[] messageBody = new byte[0]; 11 | 12 | public Set getHeaderNames() { 13 | return headers.keySet(); 14 | } 15 | 16 | public String getHeader(String headerName) { 17 | return headers.get(headerName.toLowerCase()); 18 | } 19 | 20 | void addHeader(String headerName, String headerField) { 21 | headers.put(headerName.toLowerCase(), headerField); 22 | } 23 | 24 | public byte[] getMessageBody() { 25 | return messageBody; 26 | } 27 | 28 | public void setMessageBody(byte[] messageBody) { 29 | this.messageBody = messageBody; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpMethod.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | public enum HttpMethod { 4 | GET, HEAD; 5 | 6 | public static final int MAX_LENGTH; 7 | 8 | static { 9 | int tempMaxLength = -1; 10 | for (HttpMethod method : values()) { 11 | if (method.name().length() > tempMaxLength) { 12 | tempMaxLength = method.name().length(); 13 | } 14 | } 15 | MAX_LENGTH = tempMaxLength; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpParser.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | public class HttpParser { 14 | 15 | private final static Logger LOGGER = LoggerFactory.getLogger(HttpParser.class); 16 | 17 | private static final int SP = 0x20; // 32 18 | private static final int CR = 0x0D; // 13 19 | private static final int LF = 0x0A; // 10 20 | 21 | public HttpRequest parseHttpRequest(InputStream inputStream) throws HttpParsingException { 22 | InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); 23 | 24 | HttpRequest request = new HttpRequest(); 25 | 26 | try { 27 | parseRequestLine(reader, request); 28 | } catch (IOException e) { 29 | e.printStackTrace(); 30 | } 31 | try { 32 | parseHeaders(reader, request); 33 | } catch (IOException e) { 34 | e.printStackTrace(); 35 | } 36 | parseBody(reader, request); 37 | 38 | return request; 39 | } 40 | 41 | private void parseRequestLine(InputStreamReader reader, HttpRequest request) throws IOException, HttpParsingException { 42 | StringBuilder processingDataBuffer = new StringBuilder(); 43 | 44 | boolean methodParsed = false; 45 | boolean requestTargetParsed = false; 46 | 47 | // TODO validate URI size! 48 | 49 | int _byte; 50 | while ((_byte = reader.read()) >=0) { 51 | if (_byte == CR) { 52 | _byte = reader.read(); 53 | if (_byte == LF) { 54 | LOGGER.debug("Request Line VERSION to Process : {}" , processingDataBuffer.toString()); 55 | if (!methodParsed || !requestTargetParsed) { 56 | throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 57 | } 58 | 59 | try { 60 | request.setHttpVersion(processingDataBuffer.toString()); 61 | } catch (BadHttpVersionException e) { 62 | throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 63 | } 64 | 65 | return; 66 | } else { 67 | throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 68 | } 69 | } 70 | 71 | if (_byte == SP) { 72 | if (!methodParsed) { 73 | LOGGER.debug("Request Line METHOD to Process : {}" , processingDataBuffer.toString()); 74 | request.setMethod(processingDataBuffer.toString()); 75 | methodParsed = true; 76 | } else if (!requestTargetParsed) { 77 | LOGGER.debug("Request Line REQ TARGET to Process : {}" , processingDataBuffer.toString()); 78 | request.setRequestTarget(processingDataBuffer.toString()); 79 | requestTargetParsed = true; 80 | } else { 81 | throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 82 | } 83 | processingDataBuffer.delete(0, processingDataBuffer.length()); 84 | } else { 85 | processingDataBuffer.append((char)_byte); 86 | if (!methodParsed) { 87 | if (processingDataBuffer.length() > HttpMethod.MAX_LENGTH) { 88 | throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); 89 | } 90 | } 91 | } 92 | } 93 | 94 | } 95 | 96 | private void parseHeaders(InputStreamReader reader, HttpRequest request) throws IOException, HttpParsingException { 97 | StringBuilder processingDataBuffer = new StringBuilder(); 98 | boolean crlfFound = false; 99 | 100 | int _byte; 101 | while ((_byte = reader.read()) >=0) { 102 | if (_byte == CR) { 103 | _byte = reader.read(); 104 | if (_byte == LF) { 105 | if (!crlfFound) { 106 | crlfFound = true; 107 | 108 | // Do Things like processing 109 | processSingleHeaderField(processingDataBuffer, request); 110 | // Clear the buffer 111 | processingDataBuffer.delete(0, processingDataBuffer.length()); 112 | } else { 113 | // Two CRLF received, end of Headers section 114 | return; 115 | } 116 | } else { 117 | throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 118 | } 119 | } else { 120 | crlfFound = false; 121 | // Append to Buffer 122 | processingDataBuffer.append((char)_byte); 123 | } 124 | } 125 | } 126 | 127 | private void processSingleHeaderField(StringBuilder processingDataBuffer, HttpRequest request) throws HttpParsingException { 128 | String rawHeaderField = processingDataBuffer.toString(); 129 | Pattern pattern = Pattern.compile("^(?[!#$%&’*+\\-./^_‘|˜\\dA-Za-z]+):\\s?(?[!#$%&’*+\\-./^_‘|˜(),:;<=>?@[\\\\]{}\" \\dA-Za-z]+)\\s?$"); 130 | 131 | Matcher matcher = pattern.matcher(rawHeaderField); 132 | if (matcher.matches()) { 133 | // We found a proper header 134 | String fieldName = matcher.group("fieldName"); 135 | String fieldValue = matcher.group("fieldValue"); 136 | request.addHeader(fieldName, fieldValue); 137 | } else{ 138 | throw new HttpParsingException(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 139 | } 140 | } 141 | 142 | private void parseBody(InputStreamReader reader, HttpRequest request) { 143 | 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpParsingException.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | public class HttpParsingException extends Exception { 4 | 5 | private final HttpStatusCode errorCode; 6 | 7 | public HttpParsingException(HttpStatusCode errorCode) { 8 | super(errorCode.MESSAGE); 9 | this.errorCode = errorCode; 10 | } 11 | 12 | public HttpStatusCode getErrorCode() { 13 | return errorCode; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpRequest.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | import java.util.Collection; 4 | import java.util.HashMap; 5 | import java.util.Hashtable; 6 | import java.util.Set; 7 | 8 | public class HttpRequest extends HttpMessage{ 9 | 10 | private HttpMethod method; 11 | private String requestTarget; 12 | private String originalHttpVersion; // literal from the request 13 | private HttpVersion bestCompatibleHttpVersion; 14 | 15 | HttpRequest() { 16 | } 17 | 18 | public HttpMethod getMethod() { 19 | return method; 20 | } 21 | 22 | public String getRequestTarget() { 23 | return requestTarget; 24 | } 25 | 26 | public HttpVersion getBestCompatibleHttpVersion() { 27 | return bestCompatibleHttpVersion; 28 | } 29 | 30 | public String getOriginalHttpVersion() { 31 | return originalHttpVersion; 32 | } 33 | 34 | void setMethod(String methodName) throws HttpParsingException { 35 | for (HttpMethod method : HttpMethod.values()) { 36 | if (methodName.equals(method.name())) { 37 | this.method = method; 38 | return; 39 | } 40 | } 41 | throw new HttpParsingException( 42 | HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED 43 | ); 44 | } 45 | 46 | void setRequestTarget(String requestTarget) throws HttpParsingException { 47 | if (requestTarget == null || requestTarget.length() == 0) { 48 | throw new HttpParsingException(HttpStatusCode.SERVER_ERROR_500_INTERNAL_SERVER_ERROR); 49 | } 50 | this.requestTarget = requestTarget; 51 | } 52 | 53 | void setHttpVersion(String originalHttpVersion) throws BadHttpVersionException, HttpParsingException { 54 | this.originalHttpVersion = originalHttpVersion; 55 | this.bestCompatibleHttpVersion = HttpVersion.getBestCompatibleVersion(originalHttpVersion); 56 | if (this.bestCompatibleHttpVersion == null) { 57 | throw new HttpParsingException( 58 | HttpStatusCode.SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED 59 | ); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | public class HttpResponse extends HttpMessage { 4 | 5 | private final String CRLF = "\r\n"; 6 | 7 | // status-line = HTTP-version SP status-code SP reason-phrase CRLF 8 | private String httpVersion; 9 | 10 | private HttpStatusCode statusCode; 11 | 12 | private String reasonPhrase = null; 13 | 14 | private HttpResponse() { 15 | } 16 | 17 | public String getHttpVersion() { 18 | return httpVersion; 19 | } 20 | 21 | public void setHttpVersion(String httpVersion) { 22 | this.httpVersion = httpVersion; 23 | } 24 | 25 | public HttpStatusCode getStatusCode() { 26 | return statusCode; 27 | } 28 | 29 | public void setStatusCode(HttpStatusCode statusCode) { 30 | this.statusCode = statusCode; 31 | } 32 | 33 | public String getReasonPhrase() { 34 | if (reasonPhrase == null && statusCode!=null) { 35 | return statusCode.MESSAGE; 36 | } 37 | return reasonPhrase; 38 | } 39 | 40 | public void setReasonPhrase(String reasonPhrase) { 41 | this.reasonPhrase = reasonPhrase; 42 | } 43 | 44 | public byte[] getResponseBytes() { 45 | StringBuilder responseBuilder = new StringBuilder(); 46 | responseBuilder.append(httpVersion) 47 | .append(" ") 48 | .append(statusCode.STATUS_CODE) 49 | .append(" ") 50 | .append(getReasonPhrase()) 51 | .append(CRLF); 52 | 53 | for (String headerName: getHeaderNames()) { 54 | responseBuilder.append(headerName) 55 | .append(": ") 56 | .append(getHeader(headerName)) 57 | .append(CRLF); 58 | } 59 | 60 | responseBuilder.append(CRLF); 61 | 62 | byte[] responseBytes = responseBuilder.toString().getBytes(); 63 | 64 | if (getMessageBody().length == 0) 65 | return responseBytes; 66 | 67 | byte[] responseWithBody = new byte[responseBytes.length + getMessageBody().length]; 68 | System.arraycopy(responseBytes, 0, responseWithBody, 0, responseBytes.length); 69 | System.arraycopy(getMessageBody(), 0, responseWithBody, responseBytes.length, getMessageBody().length); 70 | 71 | return responseWithBody; 72 | } 73 | 74 | public static class Builder { 75 | 76 | private HttpResponse response = new HttpResponse(); 77 | 78 | public Builder httpVersion ( String httpVersion) { 79 | response.setHttpVersion(httpVersion); 80 | return this; 81 | } 82 | 83 | public Builder statusCode(HttpStatusCode statusCode) { 84 | response.setStatusCode(statusCode); 85 | return this; 86 | } 87 | 88 | public Builder reasonPhrase(String reasonPhrase) { 89 | response.setReasonPhrase(reasonPhrase); 90 | return this; 91 | } 92 | 93 | public Builder addHeader(String headerName, String headerField) { 94 | response.addHeader(headerName, headerField); 95 | return this; 96 | } 97 | 98 | public Builder messageBody(byte[] messageBody) { 99 | response.setMessageBody(messageBody); 100 | return this; 101 | } 102 | 103 | public HttpResponse build() { 104 | return response; 105 | } 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpStatusCode.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | public enum HttpStatusCode { 4 | 5 | /* --- CLIENT ERRORS --- */ 6 | CLIENT_ERROR_400_BAD_REQUEST(400, "Bad Request"), 7 | CLIENT_ERROR_401_METHOD_NOT_ALLOWED(401, "Method Not Allowed"), 8 | CLIENT_ERROR_414_BAD_REQUEST(414, "URI Too Long"), 9 | CLIENT_ERROR_404_NOT_FOUND(404, "Not Found" ), 10 | 11 | /* --- SERVER ERRORS --- */ 12 | SERVER_ERROR_500_INTERNAL_SERVER_ERROR(500, "Internal Server Error"), 13 | SERVER_ERROR_501_NOT_IMPLEMENTED(501, "Not Implemented"), 14 | SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED(505, "Http Version Not Supported"), 15 | OK(200,"OK" ); 16 | 17 | 18 | public final int STATUS_CODE; 19 | public final String MESSAGE; 20 | 21 | HttpStatusCode(int STATUS_CODE, String MESSAGE) { 22 | this.STATUS_CODE = STATUS_CODE; 23 | this.MESSAGE = MESSAGE; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/http/HttpVersion.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public enum HttpVersion { 7 | HTTP_1_1("HTTP/1.1", 1 , 1); 8 | 9 | public final String LITERAL; 10 | public final int MAJOR; 11 | public final int MINOR; 12 | 13 | HttpVersion(String LITERAL, int MAJOR, int MINOR) { 14 | this.LITERAL = LITERAL; 15 | this.MAJOR = MAJOR; 16 | this.MINOR = MINOR; 17 | } 18 | 19 | private static final Pattern httpVersionRegexPattern = Pattern.compile("^HTTP/(?\\d+).(?\\d+)"); 20 | 21 | public static HttpVersion getBestCompatibleVersion(String literalVersion) throws BadHttpVersionException { 22 | Matcher matcher = httpVersionRegexPattern.matcher(literalVersion); 23 | if (!matcher.find() || matcher.groupCount() != 2) { 24 | throw new BadHttpVersionException(); 25 | } 26 | int major = Integer.parseInt(matcher.group("major")); 27 | int minor = Integer.parseInt(matcher.group("minor")); 28 | 29 | HttpVersion tempBestCompatible = null; 30 | for (HttpVersion version : HttpVersion.values()) { 31 | if (version.LITERAL.equals(literalVersion)) { 32 | return version; 33 | } else { 34 | if (version.MAJOR == major) { 35 | if (version.MINOR < minor) { 36 | tempBestCompatible = version; 37 | } 38 | } 39 | } 40 | } 41 | return tempBestCompatible; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/HttpServer.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver; 2 | 3 | import com.coderfromscratch.httpserver.config.Configuration; 4 | import com.coderfromscratch.httpserver.config.ConfigurationManager; 5 | import com.coderfromscratch.httpserver.core.ServerListenerThread; 6 | import com.coderfromscratch.httpserver.core.io.WebRootNotFoundException; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * 14 | * Driver Class for the Http Server 15 | * 16 | */ 17 | public class HttpServer { 18 | 19 | private final static Logger LOGGER = LoggerFactory.getLogger(HttpServer.class); 20 | 21 | public static void main(String[] args) { 22 | 23 | if (args.length != 1) { 24 | LOGGER.error("No configuration file provided."); 25 | LOGGER.error("Syntax: java -jar simplehttpserver-1.0-SNAPSHOT.jar "); 26 | return; 27 | } 28 | 29 | LOGGER.info("Server starting..."); 30 | 31 | ConfigurationManager.getInstance().loadConfigurationFile(args[0]); 32 | Configuration conf = ConfigurationManager.getInstance().getCurrentConfiguration(); 33 | 34 | LOGGER.info("Using Port: " + conf.getPort()); 35 | LOGGER.info("Using WebRoot: " + conf.getWebroot()); 36 | 37 | try { 38 | ServerListenerThread serverListenerThread = new ServerListenerThread(conf.getPort(), conf.getWebroot()); 39 | serverListenerThread.start(); 40 | } catch (IOException e) { 41 | e.printStackTrace(); 42 | // TODO handle later. 43 | } catch (WebRootNotFoundException e) { 44 | LOGGER.error("Webroot folder not found",e); 45 | } 46 | 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/config/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.config; 2 | 3 | public class Configuration { 4 | 5 | private int port; 6 | private String webroot; 7 | 8 | public int getPort() { 9 | return port; 10 | } 11 | 12 | public void setPort(int port) { 13 | this.port = port; 14 | } 15 | 16 | public String getWebroot() { 17 | return webroot; 18 | } 19 | 20 | public void setWebroot(String webroot) { 21 | this.webroot = webroot; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/config/ConfigurationManager.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.config; 2 | 3 | import com.coderfromscratch.httpserver.util.Json; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | import java.io.FileNotFoundException; 8 | import java.io.FileReader; 9 | import java.io.IOException; 10 | 11 | public class ConfigurationManager { 12 | 13 | private static ConfigurationManager myConfigurationManager; 14 | private static Configuration myCurrentConfiguration; 15 | 16 | private ConfigurationManager() { 17 | } 18 | 19 | public static ConfigurationManager getInstance() { 20 | if (myConfigurationManager==null) 21 | myConfigurationManager = new ConfigurationManager(); 22 | return myConfigurationManager; 23 | } 24 | 25 | /** 26 | * Used to load a configuration file by the path provided 27 | */ 28 | public void loadConfigurationFile(String filePath) { 29 | FileReader fileReader = null; 30 | try { 31 | fileReader = new FileReader(filePath); 32 | } catch (FileNotFoundException e) { 33 | throw new HttpConfigurationException(e); 34 | } 35 | StringBuffer sb = new StringBuffer(); 36 | int i ; 37 | try { 38 | while ( ( i = fileReader.read()) != -1) { 39 | sb.append((char)i); 40 | } 41 | } catch (IOException e) { 42 | throw new HttpConfigurationException(e); 43 | } 44 | JsonNode conf = null; 45 | try { 46 | conf = Json.parse(sb.toString()); 47 | } catch (IOException e) { 48 | throw new HttpConfigurationException("Error parsing the Configuration File", e); 49 | } 50 | try { 51 | myCurrentConfiguration = Json.fromJson(conf, Configuration.class); 52 | } catch (JsonProcessingException e) { 53 | throw new HttpConfigurationException("Error parsing the Configuration file, internal",e); 54 | } 55 | } 56 | 57 | /** 58 | * Returns the Current loaded Configuration 59 | */ 60 | public Configuration getCurrentConfiguration() { 61 | if ( myCurrentConfiguration == null) { 62 | throw new HttpConfigurationException("No Current Configuration Set."); 63 | } 64 | return myCurrentConfiguration; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/config/HttpConfigurationException.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.config; 2 | 3 | public class HttpConfigurationException extends RuntimeException { 4 | 5 | public HttpConfigurationException() { 6 | } 7 | 8 | public HttpConfigurationException(String message) { 9 | super(message); 10 | } 11 | 12 | public HttpConfigurationException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public HttpConfigurationException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/core/HttpConnectionWorkerThread.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.core; 2 | 3 | import com.coderfromscratch.http.*; 4 | import com.coderfromscratch.httpserver.core.io.ReadFileException; 5 | import com.coderfromscratch.httpserver.core.io.WebRootHandler; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.FileNotFoundException; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.net.Socket; 14 | 15 | public class HttpConnectionWorkerThread extends Thread { 16 | private final static Logger LOGGER = LoggerFactory.getLogger(HttpConnectionWorkerThread.class); 17 | private Socket socket; 18 | private WebRootHandler webRootHandler; 19 | private HttpParser httpParser = new HttpParser(); 20 | 21 | public HttpConnectionWorkerThread(Socket socket, WebRootHandler webRootHandler) { 22 | this.socket = socket; 23 | this.webRootHandler = webRootHandler; 24 | } 25 | 26 | @Override 27 | public void run() { 28 | InputStream inputStream = null; 29 | OutputStream outputStream = null; 30 | 31 | try { 32 | inputStream = socket.getInputStream(); 33 | outputStream = socket.getOutputStream(); 34 | 35 | HttpRequest request = httpParser.parseHttpRequest(inputStream); 36 | HttpResponse response = handleRequest(request); 37 | 38 | outputStream.write(response.getResponseBytes()); 39 | 40 | LOGGER.info(" * Connection Processing Finished."); 41 | } catch (IOException e) { 42 | LOGGER.error("Problem with communication", e); 43 | } catch (HttpParsingException e) { 44 | LOGGER.info("Bag Request", e); 45 | 46 | HttpResponse response = new HttpResponse.Builder() 47 | .httpVersion(HttpVersion.HTTP_1_1.LITERAL) 48 | .statusCode(e.getErrorCode()) 49 | .build(); 50 | try { 51 | outputStream.write(response.getResponseBytes()); 52 | } catch (IOException ex) { 53 | LOGGER.error("Problem with communication", e); 54 | } 55 | 56 | } finally { 57 | if (inputStream!= null) { 58 | try { 59 | inputStream.close(); 60 | } catch (IOException e) {} 61 | } 62 | if (outputStream!=null) { 63 | try { 64 | outputStream.close(); 65 | } catch (IOException e) {} 66 | } 67 | if (socket!= null) { 68 | try { 69 | socket.close(); 70 | } catch (IOException e) {} 71 | } 72 | } 73 | } 74 | 75 | private HttpResponse handleRequest(HttpRequest request) { 76 | 77 | switch (request.getMethod()) { 78 | case GET: 79 | LOGGER.info(" * GET Request"); 80 | return handleGetRequest(request, true); 81 | case HEAD: 82 | LOGGER.info(" * HEAD Request"); 83 | return handleGetRequest(request, false); 84 | default: 85 | return new HttpResponse.Builder() 86 | .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) 87 | .statusCode(HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED) 88 | .build(); 89 | } 90 | 91 | } 92 | 93 | private HttpResponse handleGetRequest(HttpRequest request, boolean setMessageBody) { 94 | try { 95 | 96 | HttpResponse.Builder builder = new HttpResponse.Builder() 97 | .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) 98 | .statusCode(HttpStatusCode.OK) 99 | .addHeader(HttpHeaderName.CONTENT_TYPE.headerName, webRootHandler.getFileMimeType(request.getRequestTarget())); 100 | 101 | if (setMessageBody) { 102 | byte[] messageBody = webRootHandler.getFileByteArrayData(request.getRequestTarget()); 103 | builder.addHeader(HttpHeaderName.CONTENT_LENGTH.headerName, String.valueOf(messageBody.length)) 104 | .messageBody(messageBody); 105 | } 106 | 107 | return builder.build(); 108 | 109 | } catch (FileNotFoundException e) { 110 | 111 | return new HttpResponse.Builder() 112 | .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) 113 | .statusCode(HttpStatusCode.CLIENT_ERROR_404_NOT_FOUND) 114 | .build(); 115 | 116 | } catch (ReadFileException e) { 117 | 118 | return new HttpResponse.Builder() 119 | .httpVersion(request.getBestCompatibleHttpVersion().LITERAL) 120 | .statusCode(HttpStatusCode.SERVER_ERROR_500_INTERNAL_SERVER_ERROR) 121 | .build(); 122 | } 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/core/ServerListenerThread.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.core; 2 | 3 | import com.coderfromscratch.httpserver.core.io.WebRootHandler; 4 | import com.coderfromscratch.httpserver.core.io.WebRootNotFoundException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.net.ServerSocket; 12 | import java.net.Socket; 13 | 14 | public class ServerListenerThread extends Thread { 15 | 16 | private final static Logger LOGGER = LoggerFactory.getLogger(ServerListenerThread.class); 17 | 18 | private int port; 19 | private String webroot; 20 | private ServerSocket serverSocket; 21 | 22 | private WebRootHandler webRootHandler; 23 | 24 | public ServerListenerThread(int port, String webroot) throws IOException, WebRootNotFoundException { 25 | this.port = port; 26 | this.webroot = webroot; 27 | this.webRootHandler = new WebRootHandler(webroot); 28 | this.serverSocket = new ServerSocket(this.port); 29 | } 30 | 31 | @Override 32 | public void run() { 33 | 34 | try { 35 | 36 | while ( serverSocket.isBound() && !serverSocket.isClosed()) { 37 | Socket socket = serverSocket.accept(); 38 | 39 | LOGGER.info(" * Connection accepted: " + socket.getInetAddress()); 40 | 41 | HttpConnectionWorkerThread workerThread = new HttpConnectionWorkerThread(socket, webRootHandler); 42 | workerThread.start(); 43 | 44 | } 45 | 46 | } catch (IOException e) { 47 | LOGGER.error("Problem with setting socket", e); 48 | } finally { 49 | if (serverSocket!=null) { 50 | try { 51 | serverSocket.close(); 52 | } catch (IOException e) {} 53 | } 54 | } 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/core/io/ReadFileException.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.core.io; 2 | 3 | public class ReadFileException extends Throwable { 4 | public ReadFileException() { 5 | } 6 | 7 | public ReadFileException(String message) { 8 | super(message); 9 | } 10 | 11 | public ReadFileException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public ReadFileException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public ReadFileException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/core/io/WebRootHandler.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.core.io; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileNotFoundException; 6 | import java.io.IOException; 7 | import java.net.URLConnection; 8 | 9 | public class WebRootHandler { 10 | 11 | private File webRoot; 12 | 13 | public WebRootHandler(String webRootPath) throws WebRootNotFoundException { 14 | webRoot = new File(webRootPath); 15 | if (!webRoot.exists() || !webRoot.isDirectory()) { 16 | throw new WebRootNotFoundException("Webroot provided does not exist or is not a folder"); 17 | } 18 | } 19 | 20 | private boolean checkIfEndsWithSlash(String relativePath) { 21 | return relativePath.endsWith("/"); 22 | } 23 | 24 | /** 25 | * This method checks to see if the relative path provided exists inside WebRoot 26 | * 27 | * @param relativePath 28 | * @return true if the path exists inside WebRoot, false if not. 29 | */ 30 | private boolean checkIfProvidedRelativePathExists(String relativePath) { 31 | File file = new File(webRoot, relativePath); 32 | 33 | if (!file.exists()) 34 | return false; 35 | 36 | try { 37 | if (file.getCanonicalPath().startsWith(webRoot.getCanonicalPath())) { 38 | return true; 39 | } 40 | } catch (IOException e) { 41 | return false; 42 | } 43 | return false; 44 | } 45 | 46 | public String getFileMimeType(String relativePath) throws FileNotFoundException { 47 | if (checkIfEndsWithSlash(relativePath)) { 48 | relativePath += "index.html"; // By default serve the index.html, if it exists. 49 | } 50 | 51 | if (!checkIfProvidedRelativePathExists(relativePath)) { 52 | throw new FileNotFoundException("File not found: " + relativePath); 53 | } 54 | 55 | File file = new File(webRoot, relativePath); 56 | 57 | String mimeType = URLConnection.getFileNameMap().getContentTypeFor(file.getName()); 58 | 59 | if (mimeType == null) { 60 | return "application/octet-stream"; 61 | } 62 | 63 | return mimeType; 64 | } 65 | 66 | /** 67 | * Returns a byte array of the content of a file. 68 | * 69 | * Todo - For large files a new strategy might be necessary. 70 | * 71 | * @param relativePath the path to the file inside the webroot folder. 72 | * @return a byte array of the data. 73 | * @throws FileNotFoundException if the file can not be found 74 | * @throws ReadFileException if there was a problem reading the file. 75 | */ 76 | public byte[] getFileByteArrayData(String relativePath) throws FileNotFoundException, ReadFileException { 77 | if (checkIfEndsWithSlash(relativePath)) { 78 | relativePath += "index.html"; // By default serve the index.html, if it exists. 79 | } 80 | 81 | if (!checkIfProvidedRelativePathExists(relativePath)) { 82 | throw new FileNotFoundException("File not found: " + relativePath); 83 | } 84 | 85 | File file = new File(webRoot, relativePath); 86 | FileInputStream fileInputStream = new FileInputStream(file); 87 | byte[] fileBytes = new byte[(int)file.length()]; 88 | try { 89 | fileInputStream.read(fileBytes); 90 | fileInputStream.close(); 91 | } catch (IOException e) { 92 | throw new ReadFileException(e); 93 | } 94 | return fileBytes; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/core/io/WebRootNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.core.io; 2 | 3 | public class WebRootNotFoundException extends Throwable { 4 | public WebRootNotFoundException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/coderfromscratch/httpserver/util/Json.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.util; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.*; 5 | 6 | import java.io.IOException; 7 | 8 | public class Json { 9 | 10 | private static ObjectMapper myObjectMapper = defaultObjectMapper(); 11 | 12 | private static ObjectMapper defaultObjectMapper() { 13 | ObjectMapper om = new ObjectMapper(); 14 | om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 15 | return om; 16 | } 17 | 18 | public static JsonNode parse(String jsonSrc) throws IOException { 19 | return myObjectMapper.readTree(jsonSrc); 20 | } 21 | 22 | public static A fromJson(JsonNode node , Class clazz) throws JsonProcessingException { 23 | return myObjectMapper.treeToValue(node, clazz); 24 | } 25 | 26 | public static JsonNode toJson(Object obj) { 27 | return myObjectMapper.valueToTree(obj); 28 | } 29 | 30 | public static String stringify(JsonNode node) throws JsonProcessingException { 31 | return generateJson(node, false); 32 | } 33 | 34 | public static String stringifyPretty(JsonNode node) throws JsonProcessingException { 35 | return generateJson(node, true); 36 | } 37 | 38 | private static String generateJson(Object o, boolean pretty) throws JsonProcessingException { 39 | ObjectWriter objectWriter = myObjectMapper.writer(); 40 | if (pretty) { 41 | objectWriter = objectWriter.with(SerializationFeature.INDENT_OUTPUT); 42 | } 43 | return objectWriter.writeValueAsString(o); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/http.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "webroot": "WebRoot" 4 | } -------------------------------------------------------------------------------- /src/test/java/com/coderfromscratch/http/HttpHeadersParserTest.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | import org.junit.jupiter.api.BeforeAll; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.TestInstance; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.InputStream; 9 | import java.io.InputStreamReader; 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.lang.reflect.Method; 12 | import java.nio.charset.StandardCharsets; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.fail; 16 | 17 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 18 | public class HttpHeadersParserTest { 19 | 20 | private HttpParser httpParser; 21 | private Method parseHeadersMethod; 22 | 23 | @BeforeAll 24 | public void beforeClass() throws NoSuchMethodException { 25 | httpParser = new HttpParser(); 26 | Class cls = HttpParser.class; 27 | parseHeadersMethod = cls.getDeclaredMethod("parseHeaders", InputStreamReader.class, HttpRequest.class); 28 | parseHeadersMethod.setAccessible(true); 29 | } 30 | 31 | @Test 32 | public void testSimpleSingleHeader() throws InvocationTargetException, IllegalAccessException { 33 | HttpRequest request = new HttpRequest(); 34 | parseHeadersMethod.invoke( 35 | httpParser, 36 | generateSimpleSingleHeaderMessage(), 37 | request); 38 | assertEquals(1, request.getHeaderNames().size()); 39 | assertEquals("localhost:8080", request.getHeader("host")); 40 | } 41 | 42 | @Test 43 | public void testMultipleHeaders() throws InvocationTargetException, IllegalAccessException { 44 | HttpRequest request = new HttpRequest(); 45 | parseHeadersMethod.invoke( 46 | httpParser, 47 | generateMultipleHeadersMessage(), 48 | request); 49 | assertEquals(10, request.getHeaderNames().size()); 50 | assertEquals("localhost:8080", request.getHeader("host")); 51 | } 52 | 53 | @Test 54 | public void testErrorSpaceBeforeColonHeader() throws InvocationTargetException, IllegalAccessException { 55 | HttpRequest request = new HttpRequest(); 56 | 57 | try { 58 | parseHeadersMethod.invoke( 59 | httpParser, 60 | generateSpaceBeforeColonErrorHeaderMessage(), 61 | request); 62 | } catch (InvocationTargetException e) { 63 | if (e.getCause() instanceof HttpParsingException) { 64 | assertEquals(HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST, ((HttpParsingException)e.getCause()).getErrorCode()); 65 | } 66 | } 67 | 68 | } 69 | 70 | private InputStreamReader generateSimpleSingleHeaderMessage() { 71 | String rawData = "Host: localhost:8080\r\n" ; 72 | // "Connection: keep-alive\r\n" + 73 | // "Upgrade-Insecure-Requests: 1\r\n" + 74 | // "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" + 75 | // "Sec-Fetch-User: ?1\r\n" + 76 | // "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" + 77 | // "Sec-Fetch-Site: none\r\n" + 78 | // "Sec-Fetch-Mode: navigate\r\n" + 79 | // "Accept-Encoding: gzip, deflate, br\r\n" + 80 | // "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" + 81 | // "\r\n"; 82 | 83 | InputStream inputStream = new ByteArrayInputStream( 84 | rawData.getBytes( 85 | StandardCharsets.US_ASCII 86 | ) 87 | ); 88 | 89 | InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); 90 | return reader; 91 | } 92 | 93 | private InputStreamReader generateMultipleHeadersMessage() { 94 | String rawData = "Host: localhost:8080\r\n" + 95 | "Connection: keep-alive\r\n" + 96 | "Upgrade-Insecure-Requests: 1\r\n" + 97 | "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" + 98 | "Sec-Fetch-User: ?1\r\n" + 99 | "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" + 100 | "Sec-Fetch-Site: none\r\n" + 101 | "Sec-Fetch-Mode: navigate\r\n" + 102 | "Accept-Encoding: gzip, deflate, br\r\n" + 103 | "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" + 104 | "\r\n"; 105 | 106 | InputStream inputStream = new ByteArrayInputStream( 107 | rawData.getBytes( 108 | StandardCharsets.US_ASCII 109 | ) 110 | ); 111 | 112 | InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); 113 | return reader; 114 | } 115 | 116 | private InputStreamReader generateSpaceBeforeColonErrorHeaderMessage() { 117 | String rawData = "Host : localhost:8080\r\n\r\n" ; 118 | 119 | InputStream inputStream = new ByteArrayInputStream( 120 | rawData.getBytes( 121 | StandardCharsets.US_ASCII 122 | ) 123 | ); 124 | 125 | InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); 126 | return reader; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/com/coderfromscratch/http/HttpParserTest.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | import org.junit.jupiter.api.BeforeAll; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.TestInstance; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.InputStream; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 14 | class HttpParserTest { 15 | 16 | private HttpParser httpParser; 17 | 18 | @BeforeAll 19 | public void beforeClass() { 20 | httpParser = new HttpParser(); 21 | } 22 | 23 | @Test 24 | void parseHttpRequest() { 25 | HttpRequest request = null; 26 | try { 27 | request = httpParser.parseHttpRequest( 28 | generateValidGETTestCase() 29 | ); 30 | } catch (HttpParsingException e) { 31 | fail(e); 32 | } 33 | 34 | assertNotNull(request); 35 | assertEquals(request.getMethod(), HttpMethod.GET); 36 | assertEquals(request.getRequestTarget(), "/"); 37 | assertEquals(request.getOriginalHttpVersion(), "HTTP/1.1"); 38 | assertEquals(request.getBestCompatibleHttpVersion(), HttpVersion.HTTP_1_1); 39 | } 40 | 41 | @Test 42 | void parseHttpRequestBadMethod1() { 43 | try { 44 | HttpRequest request = httpParser.parseHttpRequest( 45 | generateBadTestCaseMethodName1() 46 | ); 47 | fail(); 48 | } catch (HttpParsingException e) { 49 | assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); 50 | } 51 | } 52 | 53 | @Test 54 | void parseHttpRequestBadMethod2() { 55 | try { 56 | HttpRequest request = httpParser.parseHttpRequest( 57 | generateBadTestCaseMethodName2() 58 | ); 59 | fail(); 60 | } catch (HttpParsingException e) { 61 | assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_501_NOT_IMPLEMENTED); 62 | } 63 | } 64 | 65 | @Test 66 | void parseHttpRequestInvNumItems1() { 67 | try { 68 | HttpRequest request = httpParser.parseHttpRequest( 69 | generateBadTestCaseRequestLineInvNumItems1() 70 | ); 71 | fail(); 72 | } catch (HttpParsingException e) { 73 | assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 74 | } 75 | } 76 | 77 | @Test 78 | void parseHttpEmptyRequestLine() { 79 | try { 80 | HttpRequest request = httpParser.parseHttpRequest( 81 | generateBadTestCaseEmptyRequestLine() 82 | ); 83 | fail(); 84 | } catch (HttpParsingException e) { 85 | assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 86 | } 87 | } 88 | 89 | @Test 90 | void parseHttpRequestLineCRnoLF() { 91 | try { 92 | HttpRequest request = httpParser.parseHttpRequest( 93 | generateBadTestCaseRequestLineOnlyCRnoLF() 94 | ); 95 | fail(); 96 | } catch (HttpParsingException e) { 97 | assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 98 | } 99 | } 100 | 101 | @Test 102 | void parseHttpRequestBadHttpVersion() { 103 | try { 104 | HttpRequest request = httpParser.parseHttpRequest( 105 | generateBadHttpVersionTestCase() 106 | ); 107 | fail(); 108 | } catch (HttpParsingException e) { 109 | assertEquals(e.getErrorCode(), HttpStatusCode.CLIENT_ERROR_400_BAD_REQUEST); 110 | } 111 | } 112 | 113 | @Test 114 | void parseHttpRequestUnsupportedHttpVersion() { 115 | try { 116 | HttpRequest request = httpParser.parseHttpRequest( 117 | generateUnsuportedHttpVersionTestCase() 118 | ); 119 | fail(); 120 | } catch (HttpParsingException e) { 121 | assertEquals(e.getErrorCode(), HttpStatusCode.SERVER_ERROR_505_HTTP_VERSION_NOT_SUPPORTED); 122 | } 123 | } 124 | 125 | @Test 126 | void parseHttpRequestSupportedHttpVersion1() { 127 | try { 128 | HttpRequest request = httpParser.parseHttpRequest( 129 | generateSupportedHttpVersion1() 130 | ); 131 | assertNotNull(request); 132 | assertEquals(request.getBestCompatibleHttpVersion(), HttpVersion.HTTP_1_1); 133 | assertEquals(request.getOriginalHttpVersion(), "HTTP/1.2"); 134 | } catch (HttpParsingException e) { 135 | fail(); 136 | } 137 | } 138 | 139 | private InputStream generateValidGETTestCase() { 140 | String rawData = "GET / HTTP/1.1\r\n" + 141 | "Host: localhost:8080\r\n" + 142 | "Connection: keep-alive\r\n" + 143 | "Upgrade-Insecure-Requests: 1\r\n" + 144 | "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" + 145 | "Sec-Fetch-User: ?1\r\n" + 146 | "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" + 147 | "Sec-Fetch-Site: none\r\n" + 148 | "Sec-Fetch-Mode: navigate\r\n" + 149 | "Accept-Encoding: gzip, deflate, br\r\n" + 150 | "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" + 151 | "\r\n"; 152 | 153 | InputStream inputStream = new ByteArrayInputStream( 154 | rawData.getBytes( 155 | StandardCharsets.US_ASCII 156 | ) 157 | ); 158 | 159 | return inputStream; 160 | } 161 | 162 | private InputStream generateBadTestCaseMethodName1() { 163 | String rawData = "GeT / HTTP/1.1\r\n" + 164 | "Host: localhost:8080\r\n" + 165 | "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" + 166 | "\r\n"; 167 | 168 | InputStream inputStream = new ByteArrayInputStream( 169 | rawData.getBytes( 170 | StandardCharsets.US_ASCII 171 | ) 172 | ); 173 | 174 | return inputStream; 175 | } 176 | 177 | private InputStream generateBadTestCaseMethodName2() { 178 | String rawData = "GETTTT / HTTP/1.1\r\n" + 179 | "Host: localhost:8080\r\n" + 180 | "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" + 181 | "\r\n"; 182 | 183 | InputStream inputStream = new ByteArrayInputStream( 184 | rawData.getBytes( 185 | StandardCharsets.US_ASCII 186 | ) 187 | ); 188 | 189 | return inputStream; 190 | } 191 | 192 | private InputStream generateBadTestCaseRequestLineInvNumItems1() { 193 | String rawData = "GET / AAAAAA HTTP/1.1\r\n" + 194 | "Host: localhost:8080\r\n" + 195 | "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" + 196 | "\r\n"; 197 | 198 | InputStream inputStream = new ByteArrayInputStream( 199 | rawData.getBytes( 200 | StandardCharsets.US_ASCII 201 | ) 202 | ); 203 | 204 | return inputStream; 205 | } 206 | 207 | private InputStream generateBadTestCaseEmptyRequestLine() { 208 | String rawData = "\r\n" + 209 | "Host: localhost:8080\r\n" + 210 | "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" + 211 | "\r\n"; 212 | 213 | InputStream inputStream = new ByteArrayInputStream( 214 | rawData.getBytes( 215 | StandardCharsets.US_ASCII 216 | ) 217 | ); 218 | 219 | return inputStream; 220 | } 221 | 222 | private InputStream generateBadTestCaseRequestLineOnlyCRnoLF() { 223 | String rawData = "GET / HTTP/1.1\r" + // <----- no LF 224 | "Host: localhost:8080\r\n" + 225 | "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" + 226 | "\r\n"; 227 | 228 | InputStream inputStream = new ByteArrayInputStream( 229 | rawData.getBytes( 230 | StandardCharsets.US_ASCII 231 | ) 232 | ); 233 | 234 | return inputStream; 235 | } 236 | 237 | private InputStream generateBadHttpVersionTestCase() { 238 | String rawData = "GET / HTP/1.1\r\n" + 239 | "Host: localhost:8080\r\n" + 240 | "Connection: keep-alive\r\n" + 241 | "Upgrade-Insecure-Requests: 1\r\n" + 242 | "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" + 243 | "Sec-Fetch-User: ?1\r\n" + 244 | "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" + 245 | "Sec-Fetch-Site: none\r\n" + 246 | "Sec-Fetch-Mode: navigate\r\n" + 247 | "Accept-Encoding: gzip, deflate, br\r\n" + 248 | "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" + 249 | "\r\n"; 250 | 251 | InputStream inputStream = new ByteArrayInputStream( 252 | rawData.getBytes( 253 | StandardCharsets.US_ASCII 254 | ) 255 | ); 256 | 257 | return inputStream; 258 | } 259 | 260 | private InputStream generateUnsuportedHttpVersionTestCase() { 261 | String rawData = "GET / HTTP/2.1\r\n" + 262 | "Host: localhost:8080\r\n" + 263 | "Connection: keep-alive\r\n" + 264 | "Upgrade-Insecure-Requests: 1\r\n" + 265 | "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" + 266 | "Sec-Fetch-User: ?1\r\n" + 267 | "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" + 268 | "Sec-Fetch-Site: none\r\n" + 269 | "Sec-Fetch-Mode: navigate\r\n" + 270 | "Accept-Encoding: gzip, deflate, br\r\n" + 271 | "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" + 272 | "\r\n"; 273 | 274 | InputStream inputStream = new ByteArrayInputStream( 275 | rawData.getBytes( 276 | StandardCharsets.US_ASCII 277 | ) 278 | ); 279 | 280 | return inputStream; 281 | } 282 | 283 | private InputStream generateSupportedHttpVersion1() { 284 | String rawData = "GET / HTTP/1.2\r\n" + 285 | "Host: localhost:8080\r\n" + 286 | "Connection: keep-alive\r\n" + 287 | "Upgrade-Insecure-Requests: 1\r\n" + 288 | "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" + 289 | "Sec-Fetch-User: ?1\r\n" + 290 | "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" + 291 | "Sec-Fetch-Site: none\r\n" + 292 | "Sec-Fetch-Mode: navigate\r\n" + 293 | "Accept-Encoding: gzip, deflate, br\r\n" + 294 | "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" + 295 | "\r\n"; 296 | 297 | InputStream inputStream = new ByteArrayInputStream( 298 | rawData.getBytes( 299 | StandardCharsets.US_ASCII 300 | ) 301 | ); 302 | 303 | return inputStream; 304 | } 305 | } -------------------------------------------------------------------------------- /src/test/java/com/coderfromscratch/http/HttpVersionTest.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.http; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | public class HttpVersionTest { 8 | 9 | @Test 10 | void getBestCompatibleVersionExactMatch() { 11 | HttpVersion version = null; 12 | try { 13 | version = HttpVersion.getBestCompatibleVersion("HTTP/1.1"); 14 | } catch (BadHttpVersionException e) { 15 | fail(); 16 | } 17 | assertNotNull(version); 18 | assertEquals(version, HttpVersion.HTTP_1_1); 19 | } 20 | 21 | @Test 22 | void getBestCompatibleVersionBadFormat() { 23 | HttpVersion version = null; 24 | try { 25 | version = HttpVersion.getBestCompatibleVersion("http/1.1"); 26 | fail(); 27 | } catch (BadHttpVersionException e) { 28 | 29 | } 30 | } 31 | 32 | @Test 33 | void getBestCompatibleVersionHigherVersion() { 34 | HttpVersion version = null; 35 | try { 36 | version = HttpVersion.getBestCompatibleVersion("HTTP/1.2"); 37 | assertNotNull(version); 38 | assertEquals(version, HttpVersion.HTTP_1_1); 39 | } catch (BadHttpVersionException e) { 40 | fail(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/coderfromscratch/httpserver/core/io/WebRootHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.coderfromscratch.httpserver.core.io; 2 | 3 | import com.coderfromscratch.http.HttpParser; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestInstance; 7 | 8 | import java.io.FileNotFoundException; 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 15 | public class WebRootHandlerTest { 16 | 17 | private WebRootHandler webRootHandler; 18 | 19 | private Method checkIfEndsWithSlashMethod; 20 | 21 | private Method checkIfProvidedRelativePathExistsMethod; 22 | @BeforeAll 23 | public void beforeClass() throws WebRootNotFoundException, NoSuchMethodException { 24 | webRootHandler = new WebRootHandler("WebRoot"); 25 | Class cls = WebRootHandler.class; 26 | checkIfEndsWithSlashMethod = cls.getDeclaredMethod("checkIfEndsWithSlash", String.class); 27 | checkIfEndsWithSlashMethod.setAccessible(true); 28 | 29 | checkIfProvidedRelativePathExistsMethod = cls.getDeclaredMethod("checkIfProvidedRelativePathExists", String.class); 30 | checkIfProvidedRelativePathExistsMethod.setAccessible(true); 31 | } 32 | 33 | @Test 34 | void constructorGoodPath() { 35 | try { 36 | WebRootHandler webRootHandler = new WebRootHandler("E:\\Projects\\CoderFromScratch\\simple-java-http-server\\WebRoot"); 37 | } catch (WebRootNotFoundException e) { 38 | fail(e); 39 | } 40 | } 41 | 42 | @Test 43 | void constructorBadPath() { 44 | try { 45 | WebRootHandler webRootHandler = new WebRootHandler("E:\\Projects\\CoderFromScratch\\simple-java-http-server\\WebRoot2"); 46 | fail(); 47 | } catch (WebRootNotFoundException e) { 48 | } 49 | } 50 | 51 | @Test 52 | void constructorGoodPath2() { 53 | try { 54 | WebRootHandler webRootHandler = new WebRootHandler("WebRoot"); 55 | } catch (WebRootNotFoundException e) { 56 | fail(e); 57 | } 58 | } 59 | 60 | @Test 61 | void constructorBadPath2() { 62 | try { 63 | WebRootHandler webRootHandler = new WebRootHandler("WebRoot2"); 64 | fail(); 65 | } catch (WebRootNotFoundException e) { 66 | } 67 | } 68 | 69 | @Test 70 | void checkIfEndsWithSlashMethodFalse() { 71 | try { 72 | boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"index.html"); 73 | assertFalse(result); 74 | } catch (IllegalAccessException e) { 75 | fail(e); 76 | } catch (InvocationTargetException e) { 77 | fail(e); 78 | } 79 | } 80 | 81 | @Test 82 | void checkIfEndsWithSlashMethodFalse2() { 83 | try { 84 | boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/index.html"); 85 | assertFalse(result); 86 | } catch (IllegalAccessException e) { 87 | fail(e); 88 | } catch (InvocationTargetException e) { 89 | fail(e); 90 | } 91 | } 92 | 93 | @Test 94 | void checkIfEndsWithSlashMethodFalse3() { 95 | try { 96 | boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/private/index.html"); 97 | assertFalse(result); 98 | } catch (IllegalAccessException e) { 99 | fail(e); 100 | } catch (InvocationTargetException e) { 101 | fail(e); 102 | } 103 | } 104 | 105 | @Test 106 | void checkIfEndsWithSlashMethodTrue() { 107 | try { 108 | boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/"); 109 | assertTrue(result); 110 | } catch (IllegalAccessException e) { 111 | fail(e); 112 | } catch (InvocationTargetException e) { 113 | fail(e); 114 | } 115 | } 116 | 117 | @Test 118 | void checkIfEndsWithSlashMethodTrue2() { 119 | try { 120 | boolean result = (Boolean) checkIfEndsWithSlashMethod.invoke(webRootHandler,"/private/"); 121 | assertTrue(result); 122 | } catch (IllegalAccessException e) { 123 | fail(e); 124 | } catch (InvocationTargetException e) { 125 | fail(e); 126 | } 127 | } 128 | 129 | @Test 130 | void testWebRootFilePathExists() { 131 | try { 132 | boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/index.html"); 133 | assertTrue(result); 134 | } catch (IllegalAccessException e) { 135 | fail(e); 136 | } catch (InvocationTargetException e) { 137 | fail(e); 138 | } 139 | } 140 | 141 | @Test 142 | void testWebRootFilePathExistsGoodRelative() { 143 | try { 144 | boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/./././index.html"); 145 | assertTrue(result); 146 | } catch (IllegalAccessException e) { 147 | fail(e); 148 | } catch (InvocationTargetException e) { 149 | fail(e); 150 | } 151 | } 152 | 153 | @Test 154 | void testWebRootFilePathExistsDoesNotExist() { 155 | try { 156 | boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/indexNotHere.html"); 157 | assertFalse(result); 158 | } catch (IllegalAccessException e) { 159 | fail(e); 160 | } catch (InvocationTargetException e) { 161 | fail(e); 162 | } 163 | } 164 | 165 | @Test 166 | void testWebRootFilePathExistsInvalid() { 167 | try { 168 | boolean result = (boolean) checkIfProvidedRelativePathExistsMethod.invoke(webRootHandler, "/../LICENSE"); 169 | assertFalse(result); 170 | } catch (IllegalAccessException e) { 171 | fail(e); 172 | } catch (InvocationTargetException e) { 173 | fail(e); 174 | } 175 | } 176 | 177 | @Test 178 | void testGetFileMimeTypeText() { 179 | try { 180 | String mimeType = webRootHandler.getFileMimeType("/"); 181 | assertEquals("text/html", mimeType); 182 | } catch (FileNotFoundException e) { 183 | fail(e); 184 | } 185 | } 186 | 187 | @Test 188 | void testGetFileMimeTypePng() { 189 | try { 190 | String mimeType = webRootHandler.getFileMimeType("/logo.png"); 191 | assertEquals("image/png", mimeType); 192 | } catch (FileNotFoundException e) { 193 | fail(e); 194 | } 195 | } 196 | 197 | @Test 198 | void testGetFileMimeTypeDefault() { 199 | try { 200 | String mimeType = webRootHandler.getFileMimeType("/favicon.ico"); 201 | assertEquals("application/octet-stream", mimeType); 202 | } catch (FileNotFoundException e) { 203 | fail(e); 204 | } 205 | } 206 | 207 | @Test 208 | void testGetFileByteArrayData() { 209 | try { 210 | assertTrue(webRootHandler.getFileByteArrayData("/").length > 0); 211 | } catch (FileNotFoundException e) { 212 | fail(e); 213 | } catch (ReadFileException e) { 214 | fail(e); 215 | } 216 | } 217 | 218 | @Test 219 | void testGetFileByteArrayDataFileNotThere() { 220 | try { 221 | webRootHandler.getFileByteArrayData("/test.html"); 222 | fail(); 223 | } catch (FileNotFoundException e) { 224 | // pass 225 | } catch (ReadFileException e) { 226 | fail(e); 227 | } 228 | } 229 | } 230 | --------------------------------------------------------------------------------