├── 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 |
--------------------------------------------------------------------------------