From b24344590026517ecd032a232df8e98600984cca Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Tue, 3 Sep 2024 19:06:47 +0900 Subject: [PATCH 01/44] feat: add convertor to read string easily --- .../coyote/http11/InputStreamConvertor.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java b/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java new file mode 100644 index 0000000000..c552e4384e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +public class InputStreamConvertor { + public static String convertToString(InputStream inputStream, Charset charset) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset))) { + StringBuilder stringBuilder = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + stringBuilder.append(line).append(System.lineSeparator()); + } + + return stringBuilder.toString(); + } + } + + public static List convertToLines(InputStream inputStream, Charset charset) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset))) { + List lines = new ArrayList<>(); + String line; + + while ((line = reader.readLine()) != null) { + lines.add(line); + } + + return lines; + } + } +} From a731539392d32ea7d8a05e2351261f2597218fd4 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Tue, 3 Sep 2024 19:07:05 +0900 Subject: [PATCH 02/44] feat: add static resource reader --- .../coyote/http11/StaticResourceReader.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java new file mode 100644 index 0000000000..b3e764b8eb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11; + + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class StaticResourceReader { + + private final ClassLoader classLoader = getClass().getClassLoader(); + + public String read(String path) throws IOException { + InputStream resourceAsStream = classLoader.getResourceAsStream(path); + if (resourceAsStream == null) { + throw new FileNotFoundException(path); + } + + return InputStreamConvertor.convertToString(resourceAsStream, StandardCharsets.UTF_8); + } +} From ee08db99f08457162550fb152da03e3be7020b26 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Tue, 3 Sep 2024 19:07:18 +0900 Subject: [PATCH 03/44] feat: use static resource reader on http processor --- .../java/org/apache/coyote/http11/Http11Processor.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index bb14184757..f2cbcf130b 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,21 +1,22 @@ package org.apache.coyote.http11; import com.techcourse.exception.UncheckedServletException; +import java.io.IOException; +import java.net.Socket; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; - public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; + private final StaticResourceReader staticResourceReader; public Http11Processor(final Socket connection) { this.connection = connection; + this.staticResourceReader = new StaticResourceReader(); } @Override From 01a069f87cb6c72dbda64c760eec91bf80f88a8e Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Tue, 3 Sep 2024 19:07:28 +0900 Subject: [PATCH 04/44] feat: add client request class --- .../coyote/http11/ClientServletRequest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java new file mode 100644 index 0000000000..9ab3590c6c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java @@ -0,0 +1,34 @@ +package org.apache.coyote.http11; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class ClientServletRequest { + + private final String method; + private final String path; + private final String protocolVersion; + + public ClientServletRequest(InputStream inputStream) throws IOException { + List lines = InputStreamConvertor.convertToLines(inputStream, StandardCharsets.UTF_8); + + String[] startLineParts = lines.getFirst().split(" "); + method = startLineParts[0]; + path = startLineParts[1]; + protocolVersion = startLineParts[2]; + } + + public String getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getProtocolVersion() { + return protocolVersion; + } +} From b44fef312d45f2422a418d481695ed5bff7516d4 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 00:12:28 +0900 Subject: [PATCH 05/44] feat: able to answer for root path with servlet --- .../coyote/http11/ClientServletRequest.java | 9 ++++++ .../apache/coyote/http11/Http11Processor.java | 28 ++++++++++++------ .../coyote/http11/InputStreamConvertor.java | 29 +++++++++---------- .../coyote/http11/StaticResourceReader.java | 3 +- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java index 9ab3590c6c..547c9765bd 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java @@ -31,4 +31,13 @@ public String getPath() { public String getProtocolVersion() { return protocolVersion; } + + @Override + public String toString() { + return "ClientServletRequest{" + + "method='" + method + '\'' + + ", path='" + path + '\'' + + ", protocolVersion='" + protocolVersion + '\'' + + '}'; + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index f2cbcf130b..bf80ce29b9 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -29,15 +29,25 @@ public void run() { public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - - final var responseBody = "Hello world!"; - - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); + if (inputStream.available() == 0) { + return; + } + + ClientServletRequest servletRequest = new ClientServletRequest(inputStream); + + final var responseBody = + servletRequest.getPath().equals("/") ? + "Hello world!" + : staticResourceReader.read(servletRequest.getPath()); + + final var response = responseBody == null ? + "HTTP/1.1 404 Not Found" : + String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: text/html;charset=utf-8 ", + "Content-Length: " + responseBody.getBytes().length + " ", + "", + responseBody); outputStream.write(response.getBytes()); outputStream.flush(); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java b/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java index c552e4384e..90ce8038ef 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java @@ -10,28 +10,25 @@ public class InputStreamConvertor { public static String convertToString(InputStream inputStream, Charset charset) throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset))) { - StringBuilder stringBuilder = new StringBuilder(); - String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset)); + StringBuilder stringBuilder = new StringBuilder(); - while ((line = reader.readLine()) != null) { - stringBuilder.append(line).append(System.lineSeparator()); - } - - return stringBuilder.toString(); + while (reader.ready()) { + stringBuilder.append(reader.readLine()) + .append(System.lineSeparator()); } + + return stringBuilder.toString(); } public static List convertToLines(InputStream inputStream, Charset charset) throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset))) { - List lines = new ArrayList<>(); - String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset)); + List lines = new ArrayList<>(); - while ((line = reader.readLine()) != null) { - lines.add(line); - } - - return lines; + while (reader.ready()) { + lines.add(reader.readLine()); } + + return lines; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java index b3e764b8eb..eed4436f2a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java @@ -1,7 +1,6 @@ package org.apache.coyote.http11; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -13,7 +12,7 @@ public class StaticResourceReader { public String read(String path) throws IOException { InputStream resourceAsStream = classLoader.getResourceAsStream(path); if (resourceAsStream == null) { - throw new FileNotFoundException(path); + return null; } return InputStreamConvertor.convertToString(resourceAsStream, StandardCharsets.UTF_8); From 07b0ec8a8e8ec9b5104649e1d82814611eda509a Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 00:34:06 +0900 Subject: [PATCH 06/44] feat: able to answer for index.html but occur connection closed sometimes --- .../java/org/apache/coyote/http11/Http11Processor.java | 7 +++---- .../org/apache/coyote/http11/StaticResourceReader.java | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index bf80ce29b9..9fad90cd57 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -3,6 +3,7 @@ import com.techcourse.exception.UncheckedServletException; import java.io.IOException; import java.net.Socket; +import java.net.URLConnection; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,9 +30,6 @@ public void run() { public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - if (inputStream.available() == 0) { - return; - } ClientServletRequest servletRequest = new ClientServletRequest(inputStream); @@ -40,11 +38,12 @@ public void process(final Socket connection) { "Hello world!" : staticResourceReader.read(servletRequest.getPath()); + String contentType = URLConnection.guessContentTypeFromName(servletRequest.getPath()); final var response = responseBody == null ? "HTTP/1.1 404 Not Found" : String.join("\r\n", "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", + "Content-Type: " + contentType + ";charset=utf-8 ", "Content-Length: " + responseBody.getBytes().length + " ", "", responseBody); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java index eed4436f2a..ba663cbe77 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java @@ -7,10 +7,12 @@ public class StaticResourceReader { + private final static String BASE_PATH = "static"; + private final ClassLoader classLoader = getClass().getClassLoader(); public String read(String path) throws IOException { - InputStream resourceAsStream = classLoader.getResourceAsStream(path); + InputStream resourceAsStream = classLoader.getResourceAsStream(BASE_PATH + path); if (resourceAsStream == null) { return null; } From b6be233deda290c8001791b9bf641df830a0ad78 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 01:19:05 +0900 Subject: [PATCH 07/44] fix: read stream correctly until fitting http message --- .../coyote/http11/ClientServletRequest.java | 38 +---------- .../apache/coyote/http11/Http11Processor.java | 17 ++--- .../coyote/http11/HttpInputStreamReader.java | 63 +++++++++++++++++++ .../coyote/http11/InputStreamConvertor.java | 34 ---------- .../coyote/http11/StaticResourceReader.java | 14 ++--- 5 files changed, 82 insertions(+), 84 deletions(-) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/HttpInputStreamReader.java delete mode 100644 tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java index 547c9765bd..8d54940368 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java @@ -1,43 +1,11 @@ package org.apache.coyote.http11; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.List; -public class ClientServletRequest { - - private final String method; - private final String path; - private final String protocolVersion; - - public ClientServletRequest(InputStream inputStream) throws IOException { - List lines = InputStreamConvertor.convertToLines(inputStream, StandardCharsets.UTF_8); +public record ClientServletRequest(String method, String path, String protocolVersion) { + public static ClientServletRequest parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); - method = startLineParts[0]; - path = startLineParts[1]; - protocolVersion = startLineParts[2]; - } - - public String getMethod() { - return method; - } - - public String getPath() { - return path; - } - - public String getProtocolVersion() { - return protocolVersion; - } - - @Override - public String toString() { - return "ClientServletRequest{" + - "method='" + method + '\'' + - ", path='" + path + '\'' + - ", protocolVersion='" + protocolVersion + '\'' + - '}'; + return new ClientServletRequest(startLineParts[0], startLineParts[1], startLineParts[2]); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 9fad90cd57..136bdbf13c 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.net.Socket; import java.net.URLConnection; +import java.util.List; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,23 +31,23 @@ public void run() { public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - - ClientServletRequest servletRequest = new ClientServletRequest(inputStream); + List requestLines = HttpInputStreamReader.read(inputStream); + ClientServletRequest servletRequest = ClientServletRequest.parse(requestLines); + String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); final var responseBody = - servletRequest.getPath().equals("/") ? - "Hello world!" - : staticResourceReader.read(servletRequest.getPath()); + servletRequest.path().equals("/") ? + "Hello world!".getBytes() + : staticResourceReader.read(servletRequest.path()); - String contentType = URLConnection.guessContentTypeFromName(servletRequest.getPath()); final var response = responseBody == null ? "HTTP/1.1 404 Not Found" : String.join("\r\n", "HTTP/1.1 200 OK ", "Content-Type: " + contentType + ";charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", + "Content-Length: " + responseBody.length + " ", "", - responseBody); + new String(responseBody)); outputStream.write(response.getBytes()); outputStream.flush(); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpInputStreamReader.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpInputStreamReader.java new file mode 100644 index 0000000000..730a31ab8d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpInputStreamReader.java @@ -0,0 +1,63 @@ +package org.apache.coyote.http11; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpInputStreamReader { + + private static final Logger log = LoggerFactory.getLogger(HttpInputStreamReader.class); + + public static List read(InputStream inputStream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + + List lines = new ArrayList<>(); + String line; + + boolean readingHeaders = true; + while ((line = reader.readLine()) != null) { + lines.add(line); + if (line.isEmpty()) { + readingHeaders = false; + break; + } + } + + if (readingHeaders) { + log.warn("Incomplete headers received"); + } + + StringBuilder bodyBuilder = new StringBuilder(); + String requestHeaders = String.join("\r\n", lines); + int contentLength = getContentLength(requestHeaders); + if (contentLength > 0) { + char[] body = new char[contentLength]; + int read = reader.read(body, 0, contentLength); + if (read != contentLength) { + log.warn("Not all body data was read"); + } + bodyBuilder.append(body, 0, read); + } + lines.add(bodyBuilder.toString()); + + return lines; + } + + private static int getContentLength(String headers) { + for (String headerLine : headers.split("\r\n")) { + if (headerLine.toLowerCase().startsWith("content-length:")) { + try { + return Integer.parseInt(headerLine.split(":")[1].trim()); + } catch (NumberFormatException e) { + log.warn("Invalid Content-Length header format"); + } + } + } + return 0; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java b/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java deleted file mode 100644 index 90ce8038ef..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/InputStreamConvertor.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.apache.coyote.http11; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; - -public class InputStreamConvertor { - public static String convertToString(InputStream inputStream, Charset charset) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset)); - StringBuilder stringBuilder = new StringBuilder(); - - while (reader.ready()) { - stringBuilder.append(reader.readLine()) - .append(System.lineSeparator()); - } - - return stringBuilder.toString(); - } - - public static List convertToLines(InputStream inputStream, Charset charset) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset)); - List lines = new ArrayList<>(); - - while (reader.ready()) { - lines.add(reader.readLine()); - } - - return lines; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java index ba663cbe77..322d030afd 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; public class StaticResourceReader { @@ -11,12 +10,13 @@ public class StaticResourceReader { private final ClassLoader classLoader = getClass().getClassLoader(); - public String read(String path) throws IOException { - InputStream resourceAsStream = classLoader.getResourceAsStream(BASE_PATH + path); - if (resourceAsStream == null) { - return null; - } + public byte[] read(String path) throws IOException { + try (InputStream resourceAsStream = classLoader.getResourceAsStream(BASE_PATH + path)) { + if (resourceAsStream == null) { + return null; + } - return InputStreamConvertor.convertToString(resourceAsStream, StandardCharsets.UTF_8); + return resourceAsStream.readAllBytes(); + } } } From cac052c0e696c3eb6fd254712f8a976eb1f45dbe Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 01:24:19 +0900 Subject: [PATCH 08/44] refactor: rename http 1.1 classes --- ...pInputStreamReader.java => Http11InputStreamReader.java} | 4 ++-- .../main/java/org/apache/coyote/http11/Http11Processor.java | 4 ++-- ...{ClientServletRequest.java => Http11ServletRequest.java} | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename tomcat/src/main/java/org/apache/coyote/http11/{HttpInputStreamReader.java => Http11InputStreamReader.java} (96%) rename tomcat/src/main/java/org/apache/coyote/http11/{ClientServletRequest.java => Http11ServletRequest.java} (50%) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpInputStreamReader.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java similarity index 96% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpInputStreamReader.java rename to tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java index 730a31ab8d..e1507773b6 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpInputStreamReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java @@ -9,9 +9,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class HttpInputStreamReader { +public class Http11InputStreamReader { - private static final Logger log = LoggerFactory.getLogger(HttpInputStreamReader.class); + private static final Logger log = LoggerFactory.getLogger(Http11InputStreamReader.class); public static List read(InputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 136bdbf13c..fc6db25bcb 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -31,8 +31,8 @@ public void run() { public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - List requestLines = HttpInputStreamReader.read(inputStream); - ClientServletRequest servletRequest = ClientServletRequest.parse(requestLines); + List requestLines = Http11InputStreamReader.read(inputStream); + Http11ServletRequest servletRequest = Http11ServletRequest.parse(requestLines); String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); final var responseBody = diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java similarity index 50% rename from tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java rename to tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java index 8d54940368..10f5ce4220 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/ClientServletRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java @@ -2,10 +2,10 @@ import java.util.List; -public record ClientServletRequest(String method, String path, String protocolVersion) { +public record Http11ServletRequest(String method, String path, String protocolVersion) { - public static ClientServletRequest parse(List lines) { + public static Http11ServletRequest parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); - return new ClientServletRequest(startLineParts[0], startLineParts[1], startLineParts[2]); + return new Http11ServletRequest(startLineParts[0], startLineParts[1], startLineParts[2]); } } From cf1baa0534cb1b236a9b60bbe22adf87547515c6 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 01:40:33 +0900 Subject: [PATCH 09/44] refactor: revoke index.html origin with crlf --- .../apache/coyote/http11/Http11Processor.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index fc6db25bcb..d8cd517b50 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -2,6 +2,7 @@ import com.techcourse.exception.UncheckedServletException; import java.io.IOException; +import java.io.OutputStream; import java.net.Socket; import java.net.URLConnection; import java.util.List; @@ -33,13 +34,14 @@ public void process(final Socket connection) { final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); Http11ServletRequest servletRequest = Http11ServletRequest.parse(requestLines); - String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); - final var responseBody = - servletRequest.path().equals("/") ? - "Hello world!".getBytes() - : staticResourceReader.read(servletRequest.path()); + if (servletRequest.path().equals("/")) { + processRootPath(outputStream); + return; + } + String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); + final var responseBody = staticResourceReader.read(servletRequest.path()); final var response = responseBody == null ? "HTTP/1.1 404 Not Found" : String.join("\r\n", @@ -55,4 +57,16 @@ public void process(final Socket connection) { log.error(e.getMessage(), e); } } + + private void processRootPath(OutputStream outputStream) throws IOException { + final var responseBody = "Hello world!"; + final var response = String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: text/html;charset=utf-8 ", + "Content-Length: " + responseBody.getBytes().length + " ", + "", + responseBody); + outputStream.write(response.getBytes()); + outputStream.flush(); + } } From 320f1b43455435fb2e90b6ef112b0b837c4829e1 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 02:01:56 +0900 Subject: [PATCH 10/44] feat: able to access login page and split parameters with decoding url --- .../apache/coyote/http11/Http11Processor.java | 52 ++++++++++++++----- .../coyote/http11/Http11ServletRequest.java | 35 ++++++++++++- .../coyote/http11/StaticResourceReader.java | 3 +- 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index d8cd517b50..1197c9a9f1 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -34,25 +34,19 @@ public void process(final Socket connection) { final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); Http11ServletRequest servletRequest = Http11ServletRequest.parse(requestLines); + log.debug(servletRequest.toString()); if (servletRequest.path().equals("/")) { processRootPath(outputStream); return; } - String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); - final var responseBody = staticResourceReader.read(servletRequest.path()); - final var response = responseBody == null ? - "HTTP/1.1 404 Not Found" : - String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: " + contentType + ";charset=utf-8 ", - "Content-Length: " + responseBody.length + " ", - "", - new String(responseBody)); + if (servletRequest.path().equals("/login")) { + processLoginPage(outputStream); + return; + } - outputStream.write(response.getBytes()); - outputStream.flush(); + processStaticResource(servletRequest, outputStream); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } @@ -69,4 +63,38 @@ private void processRootPath(OutputStream outputStream) throws IOException { outputStream.write(response.getBytes()); outputStream.flush(); } + + private void processLoginPage(OutputStream outputStream) throws IOException { + String loginResourcePath = "login.html"; + String contentType = URLConnection.guessContentTypeFromName(loginResourcePath); + final var responseBody = staticResourceReader.read(loginResourcePath); + final var response = responseBody == null ? + "HTTP/1.1 404 Not Found" : + String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: " + contentType + ";charset=utf-8 ", + "Content-Length: " + responseBody.length + " ", + "", + new String(responseBody)); + + outputStream.write(response.getBytes()); + outputStream.flush(); + } + + private void processStaticResource(Http11ServletRequest servletRequest, OutputStream outputStream) + throws IOException { + String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); + final var responseBody = staticResourceReader.read(servletRequest.path()); + final var response = responseBody == null ? + "HTTP/1.1 404 Not Found" : + String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: " + contentType + ";charset=utf-8 ", + "Content-Length: " + responseBody.length + " ", + "", + new String(responseBody)); + + outputStream.write(response.getBytes()); + outputStream.flush(); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java index 10f5ce4220..bd1a5f9875 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java @@ -1,11 +1,42 @@ package org.apache.coyote.http11; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -public record Http11ServletRequest(String method, String path, String protocolVersion) { +public record Http11ServletRequest(String method, String path, Map parameters, String protocolVersion) { public static Http11ServletRequest parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); - return new Http11ServletRequest(startLineParts[0], startLineParts[1], startLineParts[2]); + String method = startLineParts[0]; + String path = ""; + Map parameters = new HashMap<>(); + String protocolVersion = startLineParts[2]; + + Pattern pattern = Pattern.compile("([^?]+)(\\?(.*))?"); + Matcher matcher = pattern.matcher(startLineParts[1]); + + if (matcher.find()) { + path = matcher.group(1); + + String queryString = matcher.group(3); + if (queryString != null) { + String[] pairs = queryString.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("="); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = URLDecoder.decode(keyValue[1], Charset.defaultCharset()); + parameters.put(key, value); + } + } + } + } + + return new Http11ServletRequest(method, path, parameters, protocolVersion); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java index 322d030afd..fc8820e24a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Paths; public class StaticResourceReader { @@ -11,7 +12,7 @@ public class StaticResourceReader { private final ClassLoader classLoader = getClass().getClassLoader(); public byte[] read(String path) throws IOException { - try (InputStream resourceAsStream = classLoader.getResourceAsStream(BASE_PATH + path)) { + try (InputStream resourceAsStream = classLoader.getResourceAsStream(Paths.get(BASE_PATH, path).toString())) { if (resourceAsStream == null) { return null; } From 2d9045006cf3ee4d0d3a38386f842c78bf1b2d3a Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 09:26:37 +0900 Subject: [PATCH 11/44] feat: add http 1.1 servlet response with builder --- .../coyote/http11/Http11ServletResponse.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java new file mode 100644 index 0000000000..d3a3316106 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java @@ -0,0 +1,65 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public record Http11ServletResponse(String protocolVersion, int statusCode, String statusText, + Map headers, byte[] body) { + + public static Http11ServletResponse.Http11ServletResponseBuilder builder() { + return new Http11ServletResponse.Http11ServletResponseBuilder(); + } + + public static class Http11ServletResponseBuilder { + private String protocolVersion; + private int statusCode; + private String statusText; + private Map headers; + private byte[] body; + + Http11ServletResponseBuilder() { + headers = new HashMap<>(); + } + + public Http11ServletResponse.Http11ServletResponseBuilder protocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + + public Http11ServletResponse.Http11ServletResponseBuilder statusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public Http11ServletResponse.Http11ServletResponseBuilder statusText(String statusText) { + this.statusText = statusText; + return this; + } + + public Http11ServletResponse.Http11ServletResponseBuilder addHeader(String key, String value) { + headers.put(key, value); + return this; + } + + public Http11ServletResponse.Http11ServletResponseBuilder body(byte[] body) { + this.body = body; + return this; + } + + public Http11ServletResponse build() { + return new Http11ServletResponse(protocolVersion, statusCode, statusText, headers, body); + } + + @Override + public String toString() { + return "Http11ServletResponseBuilder{" + + "protocolVersion='" + protocolVersion + '\'' + + ", statusCode=" + statusCode + + ", statusText='" + statusText + '\'' + + ", headers=" + headers + + ", body=" + Arrays.toString(body) + + '}'; + } + } +} From dc668a10ead924f7606e91a4a4b245a20bd879c4 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 09:37:33 +0900 Subject: [PATCH 12/44] feat: add converting to byte array method --- .../coyote/http11/Http11ServletResponse.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java index d3a3316106..29db5a7d25 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java @@ -11,6 +11,29 @@ public static Http11ServletResponse.Http11ServletResponseBuilder builder() { return new Http11ServletResponse.Http11ServletResponseBuilder(); } + public static byte[] mergeByteArrays(byte[] array1, byte[] array2) { + byte[] mergedArray = new byte[array1.length + array2.length]; + + System.arraycopy(array1, 0, mergedArray, 0, array1.length); + System.arraycopy(array2, 0, mergedArray, array1.length, array2.length); + + return mergedArray; + } + + public byte[] toMessage() { + StringBuilder builder = new StringBuilder(); + builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append("\r\n") + .append(headers.entrySet().stream().map(entry -> entry.getKey() + ": " + entry.getValue())) + .append("\r\n"); + + if (body != null && body.length > 0) { + builder.append("\r\n"); + return mergeByteArrays(builder.toString().getBytes(), body); + } + + return builder.toString().getBytes(); + } + public static class Http11ServletResponseBuilder { private String protocolVersion; private int statusCode; From 2531c6a5da633fb03790ce1b93522f3b251a5401 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 09:43:07 +0900 Subject: [PATCH 13/44] refactor: rename http request and response --- .../apache/coyote/http11/Http11Processor.java | 4 ++-- ...ServletRequest.java => Http11Request.java} | 6 ++--- ...rvletResponse.java => Http11Response.java} | 22 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) rename tomcat/src/main/java/org/apache/coyote/http11/{Http11ServletRequest.java => Http11Request.java} (81%) rename tomcat/src/main/java/org/apache/coyote/http11/{Http11ServletResponse.java => Http11Response.java} (69%) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 1197c9a9f1..5f1bfe8653 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -33,7 +33,7 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); - Http11ServletRequest servletRequest = Http11ServletRequest.parse(requestLines); + Http11Request servletRequest = Http11Request.parse(requestLines); log.debug(servletRequest.toString()); if (servletRequest.path().equals("/")) { @@ -81,7 +81,7 @@ private void processLoginPage(OutputStream outputStream) throws IOException { outputStream.flush(); } - private void processStaticResource(Http11ServletRequest servletRequest, OutputStream outputStream) + private void processStaticResource(Http11Request servletRequest, OutputStream outputStream) throws IOException { String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); final var responseBody = staticResourceReader.read(servletRequest.path()); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java similarity index 81% rename from tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java rename to tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java index bd1a5f9875..ed5d0c3c1a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java @@ -8,9 +8,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public record Http11ServletRequest(String method, String path, Map parameters, String protocolVersion) { +public record Http11Request(String method, String path, Map parameters, String protocolVersion) { - public static Http11ServletRequest parse(List lines) { + public static Http11Request parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); String method = startLineParts[0]; String path = ""; @@ -37,6 +37,6 @@ public static Http11ServletRequest parse(List lines) { } } - return new Http11ServletRequest(method, path, parameters, protocolVersion); + return new Http11Request(method, path, parameters, protocolVersion); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java similarity index 69% rename from tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java rename to tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java index 29db5a7d25..5423255ccb 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11ServletResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java @@ -4,11 +4,11 @@ import java.util.HashMap; import java.util.Map; -public record Http11ServletResponse(String protocolVersion, int statusCode, String statusText, - Map headers, byte[] body) { +public record Http11Response(String protocolVersion, int statusCode, String statusText, + Map headers, byte[] body) { - public static Http11ServletResponse.Http11ServletResponseBuilder builder() { - return new Http11ServletResponse.Http11ServletResponseBuilder(); + public static Http11Response.Http11ServletResponseBuilder builder() { + return new Http11Response.Http11ServletResponseBuilder(); } public static byte[] mergeByteArrays(byte[] array1, byte[] array2) { @@ -45,33 +45,33 @@ public static class Http11ServletResponseBuilder { headers = new HashMap<>(); } - public Http11ServletResponse.Http11ServletResponseBuilder protocolVersion(String protocolVersion) { + public Http11Response.Http11ServletResponseBuilder protocolVersion(String protocolVersion) { this.protocolVersion = protocolVersion; return this; } - public Http11ServletResponse.Http11ServletResponseBuilder statusCode(int statusCode) { + public Http11Response.Http11ServletResponseBuilder statusCode(int statusCode) { this.statusCode = statusCode; return this; } - public Http11ServletResponse.Http11ServletResponseBuilder statusText(String statusText) { + public Http11Response.Http11ServletResponseBuilder statusText(String statusText) { this.statusText = statusText; return this; } - public Http11ServletResponse.Http11ServletResponseBuilder addHeader(String key, String value) { + public Http11Response.Http11ServletResponseBuilder addHeader(String key, String value) { headers.put(key, value); return this; } - public Http11ServletResponse.Http11ServletResponseBuilder body(byte[] body) { + public Http11Response.Http11ServletResponseBuilder body(byte[] body) { this.body = body; return this; } - public Http11ServletResponse build() { - return new Http11ServletResponse(protocolVersion, statusCode, statusText, headers, body); + public Http11Response build() { + return new Http11Response(protocolVersion, statusCode, statusText, headers, body); } @Override From 8ecae036448fe8442d4cac31b0ee5e144518b1bc Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 10:51:58 +0900 Subject: [PATCH 14/44] feat: apply http 1.1 response on processor --- .../apache/coyote/http11/Http11Processor.java | 97 +++++++++---------- .../apache/coyote/http11/Http11Response.java | 57 ++++++++--- 2 files changed, 90 insertions(+), 64 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 5f1bfe8653..973c1d81fe 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -2,10 +2,11 @@ import com.techcourse.exception.UncheckedServletException; import java.io.IOException; -import java.io.OutputStream; import java.net.Socket; import java.net.URLConnection; import java.util.List; +import java.util.Map; +import java.util.function.Function; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,10 +17,17 @@ public class Http11Processor implements Runnable, Processor { private final Socket connection; private final StaticResourceReader staticResourceReader; + private final Function defaultProcessor; + private final Map> processors; public Http11Processor(final Socket connection) { this.connection = connection; this.staticResourceReader = new StaticResourceReader(); + this.defaultProcessor = this::processStaticResource; + this.processors = Map.of( + "/", this::processRootPage, + "/login", this::processLoginPage + ); } @Override @@ -36,65 +44,56 @@ public void process(final Socket connection) { Http11Request servletRequest = Http11Request.parse(requestLines); log.debug(servletRequest.toString()); - if (servletRequest.path().equals("/")) { - processRootPath(outputStream); - return; + for (final var processor : processors.entrySet()) { + if (servletRequest.path().equals(processor.getKey())) { + Http11Response response = processor.getValue().apply(servletRequest); + outputStream.write(response.toMessage()); + outputStream.flush(); + return; + } } - if (servletRequest.path().equals("/login")) { - processLoginPage(outputStream); - return; - } - - processStaticResource(servletRequest, outputStream); + Http11Response response = defaultProcessor.apply(servletRequest); + outputStream.write(response.toMessage()); + outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } - private void processRootPath(OutputStream outputStream) throws IOException { - final var responseBody = "Hello world!"; - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); - outputStream.write(response.getBytes()); - outputStream.flush(); + private Http11Response processRootPage(Http11Request servletRequest) { + return Http11Response.builder(200) + .addHeader("Content-Type", "text/html;charset=utf-8") + .body("Hello world!".getBytes()) + .build(); } - private void processLoginPage(OutputStream outputStream) throws IOException { - String loginResourcePath = "login.html"; - String contentType = URLConnection.guessContentTypeFromName(loginResourcePath); - final var responseBody = staticResourceReader.read(loginResourcePath); - final var response = responseBody == null ? - "HTTP/1.1 404 Not Found" : - String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: " + contentType + ";charset=utf-8 ", - "Content-Length: " + responseBody.length + " ", - "", - new String(responseBody)); - - outputStream.write(response.getBytes()); - outputStream.flush(); + private Http11Response processLoginPage(Http11Request servletRequest) { + return processStaticResource(new Http11Request( + servletRequest.method(), + "login.html", + servletRequest.parameters(), + servletRequest.protocolVersion() + )); } - private void processStaticResource(Http11Request servletRequest, OutputStream outputStream) - throws IOException { + private Http11Response processStaticResource(Http11Request servletRequest) { String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); - final var responseBody = staticResourceReader.read(servletRequest.path()); - final var response = responseBody == null ? - "HTTP/1.1 404 Not Found" : - String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: " + contentType + ";charset=utf-8 ", - "Content-Length: " + responseBody.length + " ", - "", - new String(responseBody)); - - outputStream.write(response.getBytes()); - outputStream.flush(); + final byte[] responseBody; + try { + responseBody = staticResourceReader.read(servletRequest.path()); + if (responseBody == null) { + return Http11Response.builder(404) + .build(); + } + return Http11Response.builder(200) + .contentType(contentType) + .body(responseBody) + .build(); + } catch (IOException e) { + log.error(e.getMessage(), e); + return Http11Response.builder(500) + .build(); + } } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java index 5423255ccb..927d32a617 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java @@ -7,11 +7,33 @@ public record Http11Response(String protocolVersion, int statusCode, String statusText, Map headers, byte[] body) { - public static Http11Response.Http11ServletResponseBuilder builder() { - return new Http11Response.Http11ServletResponseBuilder(); + public static Builder builder() { + return new Builder(); } - public static byte[] mergeByteArrays(byte[] array1, byte[] array2) { + public static Builder builder(int statusCode) { + if (statusCode == 200) { + return new Builder() + .protocolVersion("HTTP/1.1") + .statusCode(statusCode) + .statusText("OK"); + } + if (statusCode == 404) { + return new Builder() + .protocolVersion("HTTP/1.1") + .statusCode(statusCode) + .statusText("Not Found"); + } + if (statusCode == 500) { + return new Builder() + .protocolVersion("HTTP/1.1") + .statusCode(statusCode) + .statusText("Internal Server Error"); + } + return builder(); + } + + private static byte[] mergeByteArrays(byte[] array1, byte[] array2) { byte[] mergedArray = new byte[array1.length + array2.length]; System.arraycopy(array1, 0, mergedArray, 0, array1.length); @@ -22,50 +44,55 @@ public static byte[] mergeByteArrays(byte[] array1, byte[] array2) { public byte[] toMessage() { StringBuilder builder = new StringBuilder(); - builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append("\r\n") - .append(headers.entrySet().stream().map(entry -> entry.getKey() + ": " + entry.getValue())) - .append("\r\n"); + builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append(" "); + headers.forEach((key, value) -> builder.append("\r\n").append(key).append(": ").append(value).append(" ")); if (body != null && body.length > 0) { - builder.append("\r\n"); + builder.append("\r\n").append("Content-Length: ").append(body.length).append(" "); + builder.append("\r\n\r\n"); return mergeByteArrays(builder.toString().getBytes(), body); } return builder.toString().getBytes(); } - public static class Http11ServletResponseBuilder { + public static class Builder { + private final Map headers; private String protocolVersion; private int statusCode; private String statusText; - private Map headers; private byte[] body; - Http11ServletResponseBuilder() { + private Builder() { headers = new HashMap<>(); } - public Http11Response.Http11ServletResponseBuilder protocolVersion(String protocolVersion) { + public Builder protocolVersion(String protocolVersion) { this.protocolVersion = protocolVersion; return this; } - public Http11Response.Http11ServletResponseBuilder statusCode(int statusCode) { + public Builder statusCode(int statusCode) { this.statusCode = statusCode; return this; } - public Http11Response.Http11ServletResponseBuilder statusText(String statusText) { + public Builder statusText(String statusText) { this.statusText = statusText; return this; } - public Http11Response.Http11ServletResponseBuilder addHeader(String key, String value) { + public Builder addHeader(String key, String value) { headers.put(key, value); return this; } - public Http11Response.Http11ServletResponseBuilder body(byte[] body) { + public Builder contentType(String value) { + headers.put("Content-Type", value + ";charset=utf-8"); + return this; + } + + public Builder body(byte[] body) { this.body = body; return this; } From 088008ca7063844496f0ea352bd7f8001ad968b3 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 10:58:19 +0900 Subject: [PATCH 15/44] feat: predicate request servlet on processing --- .../java/org/apache/coyote/http11/Http11Processor.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 973c1d81fe..7f8918ab08 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.function.Predicate; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,15 +19,15 @@ public class Http11Processor implements Runnable, Processor { private final Socket connection; private final StaticResourceReader staticResourceReader; private final Function defaultProcessor; - private final Map> processors; + private final Map, Function> processors; public Http11Processor(final Socket connection) { this.connection = connection; this.staticResourceReader = new StaticResourceReader(); this.defaultProcessor = this::processStaticResource; this.processors = Map.of( - "/", this::processRootPage, - "/login", this::processLoginPage + req -> req.method().equals("GET") && req.path().equals("/"), this::processRootPage, + req -> req.method().equals("GET") && req.path().equals("/login"), this::processLoginPage ); } @@ -45,7 +46,7 @@ public void process(final Socket connection) { log.debug(servletRequest.toString()); for (final var processor : processors.entrySet()) { - if (servletRequest.path().equals(processor.getKey())) { + if (processor.getKey().test(servletRequest)) { Http11Response response = processor.getValue().apply(servletRequest); outputStream.write(response.toMessage()); outputStream.flush(); From 2165a0037cebafb9613f60e10d477c3f8c13407b Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 11:15:07 +0900 Subject: [PATCH 16/44] feat: login with account and password --- .../apache/coyote/http11/Http11Processor.java | 47 ++++++++++++++----- .../apache/coyote/http11/Http11Response.java | 11 +++++ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 7f8918ab08..ad8a4f65c3 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,11 +1,14 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.model.User; import java.io.IOException; import java.net.Socket; import java.net.URLConnection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import org.apache.coyote.Processor; @@ -42,19 +45,21 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); - Http11Request servletRequest = Http11Request.parse(requestLines); - log.debug(servletRequest.toString()); + Http11Request request = Http11Request.parse(requestLines); + log.debug(request.toString()); for (final var processor : processors.entrySet()) { - if (processor.getKey().test(servletRequest)) { - Http11Response response = processor.getValue().apply(servletRequest); + if (processor.getKey().test(request)) { + Http11Response response = processor.getValue().apply(request); + log.debug(response.toString()); outputStream.write(response.toMessage()); outputStream.flush(); return; } } - Http11Response response = defaultProcessor.apply(servletRequest); + Http11Response response = defaultProcessor.apply(request); + log.debug(response.toString()); outputStream.write(response.toMessage()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { @@ -62,27 +67,43 @@ public void process(final Socket connection) { } } - private Http11Response processRootPage(Http11Request servletRequest) { + private Http11Response processRootPage(Http11Request request) { return Http11Response.builder(200) .addHeader("Content-Type", "text/html;charset=utf-8") .body("Hello world!".getBytes()) .build(); } - private Http11Response processLoginPage(Http11Request servletRequest) { + private Http11Response processLoginPage(Http11Request request) { + if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { + String account = request.parameters().get("account"); + String password = request.parameters().get("password"); + + Optional optionalUser = InMemoryUserRepository.findByAccount(account); + if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { + return Http11Response.builder(302) + .location("/index.html") + .build(); + } + + return Http11Response.builder(302) + .location("/401.html") + .build(); + } + return processStaticResource(new Http11Request( - servletRequest.method(), + request.method(), "login.html", - servletRequest.parameters(), - servletRequest.protocolVersion() + request.parameters(), + request.protocolVersion() )); } - private Http11Response processStaticResource(Http11Request servletRequest) { - String contentType = URLConnection.guessContentTypeFromName(servletRequest.path()); + private Http11Response processStaticResource(Http11Request request) { + String contentType = URLConnection.guessContentTypeFromName(request.path()); final byte[] responseBody; try { - responseBody = staticResourceReader.read(servletRequest.path()); + responseBody = staticResourceReader.read(request.path()); if (responseBody == null) { return Http11Response.builder(404) .build(); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java index 927d32a617..3b430b037c 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java @@ -18,6 +18,12 @@ public static Builder builder(int statusCode) { .statusCode(statusCode) .statusText("OK"); } + if (statusCode == 302) { + return new Builder() + .protocolVersion("HTTP/1.1") + .statusCode(statusCode) + .statusText("Found"); + } if (statusCode == 404) { return new Builder() .protocolVersion("HTTP/1.1") @@ -92,6 +98,11 @@ public Builder contentType(String value) { return this; } + public Builder location(String value) { + headers.put("Location", value); + return this; + } + public Builder body(byte[] body) { this.body = body; return this; From e2d95148c0b32882a9c7226275534bdc097b7353 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 11:17:21 +0900 Subject: [PATCH 17/44] feat: view register page --- .../org/apache/coyote/http11/Http11Processor.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index ad8a4f65c3..f84f9ea197 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -30,7 +30,8 @@ public Http11Processor(final Socket connection) { this.defaultProcessor = this::processStaticResource; this.processors = Map.of( req -> req.method().equals("GET") && req.path().equals("/"), this::processRootPage, - req -> req.method().equals("GET") && req.path().equals("/login"), this::processLoginPage + req -> req.method().equals("GET") && req.path().equals("/login"), this::processLoginPage, + req -> req.method().equals("GET") && req.path().equals("/register"), this::processRegisterPage ); } @@ -99,6 +100,15 @@ private Http11Response processLoginPage(Http11Request request) { )); } + private Http11Response processRegisterPage(Http11Request request) { + return processStaticResource(new Http11Request( + request.method(), + "register.html", + request.parameters(), + request.protocolVersion() + )); + } + private Http11Response processStaticResource(Http11Request request) { String contentType = URLConnection.guessContentTypeFromName(request.path()); final byte[] responseBody; From c6266e581fc962e1785746068924f127975433a3 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 13:22:45 +0900 Subject: [PATCH 18/44] feat: process register --- .../apache/coyote/http11/Http11Processor.java | 38 +++++++++++++---- .../apache/coyote/http11/Http11Request.java | 42 +++++++++++++------ .../apache/coyote/http11/Http11Response.java | 35 +++------------- .../java/org/apache/coyote/http11/Status.java | 27 ++++++++++++ 4 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/Status.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index f84f9ea197..3186c9f691 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -31,7 +31,8 @@ public Http11Processor(final Socket connection) { this.processors = Map.of( req -> req.method().equals("GET") && req.path().equals("/"), this::processRootPage, req -> req.method().equals("GET") && req.path().equals("/login"), this::processLoginPage, - req -> req.method().equals("GET") && req.path().equals("/register"), this::processRegisterPage + req -> req.method().equals("GET") && req.path().equals("/register"), this::processRegisterPage, + req -> req.method().equals("POST") && req.path().equals("/register"), this::processRegister ); } @@ -69,7 +70,7 @@ public void process(final Socket connection) { } private Http11Response processRootPage(Http11Request request) { - return Http11Response.builder(200) + return Http11Response.builder(Status.OK) .addHeader("Content-Type", "text/html;charset=utf-8") .body("Hello world!".getBytes()) .build(); @@ -82,12 +83,13 @@ private Http11Response processLoginPage(Http11Request request) { Optional optionalUser = InMemoryUserRepository.findByAccount(account); if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { - return Http11Response.builder(302) + log.info("로그인 성공! - 아이디 : " + optionalUser.get().getAccount()); + return Http11Response.builder(Status.FOUND) .location("/index.html") .build(); } - return Http11Response.builder(302) + return Http11Response.builder(Status.FOUND) .location("/401.html") .build(); } @@ -109,22 +111,44 @@ private Http11Response processRegisterPage(Http11Request request) { )); } + private Http11Response processRegister(Http11Request request) { + Map body = Http11Request.extractParameters(request.body()); + + if (!body.containsKey("account") || + !body.containsKey("password") || + !body.containsKey("email")) { + return Http11Response.builder(Status.BAD_REQUEST) + .build(); + } + + String account = body.get("account"); + if (InMemoryUserRepository.findByAccount(account).isPresent()) { + return Http11Response.builder(Status.CONFLICT) + .build(); + } + + InMemoryUserRepository.save(new User(account, body.get("password"), body.get("email"))); + return Http11Response.builder(Status.FOUND) + .location("/index.html") + .build(); + } + private Http11Response processStaticResource(Http11Request request) { String contentType = URLConnection.guessContentTypeFromName(request.path()); final byte[] responseBody; try { responseBody = staticResourceReader.read(request.path()); if (responseBody == null) { - return Http11Response.builder(404) + return Http11Response.builder(Status.NOT_FOUND) .build(); } - return Http11Response.builder(200) + return Http11Response.builder(Status.OK) .contentType(contentType) .body(responseBody) .build(); } catch (IOException e) { log.error(e.getMessage(), e); - return Http11Response.builder(500) + return Http11Response.builder(Status.INTERNAL_SERVER_ERROR) .build(); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java index ed5d0c3c1a..303a1987ba 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java @@ -8,35 +8,51 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public record Http11Request(String method, String path, Map parameters, String protocolVersion) { +public record Http11Request(String method, String path, Map parameters, String protocolVersion, + String body) { + + public Http11Request(String method, String path, Map parameters, String protocolVersion) { + this(method, path, parameters, protocolVersion, null); + } public static Http11Request parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); String method = startLineParts[0]; String path = ""; - Map parameters = new HashMap<>(); + Map parameters = Map.of(); String protocolVersion = startLineParts[2]; + String body = null; Pattern pattern = Pattern.compile("([^?]+)(\\?(.*))?"); Matcher matcher = pattern.matcher(startLineParts[1]); if (matcher.find()) { path = matcher.group(1); + parameters = extractParameters(matcher.group(3)); + } + + if (lines.size() > 1 && lines.get(lines.size() - 2).isEmpty()) { + body = lines.getLast(); + } + + return new Http11Request(method, path, parameters, protocolVersion, body); + } + + public static Map extractParameters(String query) { + Map parameters = new HashMap<>(); - String queryString = matcher.group(3); - if (queryString != null) { - String[] pairs = queryString.split("&"); - for (String pair : pairs) { - String[] keyValue = pair.split("="); - if (keyValue.length == 2) { - String key = keyValue[0]; - String value = URLDecoder.decode(keyValue[1], Charset.defaultCharset()); - parameters.put(key, value); - } + if (query != null) { + String[] pairs = query.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("="); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = URLDecoder.decode(keyValue[1], Charset.defaultCharset()); + parameters.put(key, value); } } } - return new Http11Request(method, path, parameters, protocolVersion); + return parameters; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java index 3b430b037c..e687fdcc17 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java @@ -7,36 +7,11 @@ public record Http11Response(String protocolVersion, int statusCode, String statusText, Map headers, byte[] body) { - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(int statusCode) { - if (statusCode == 200) { - return new Builder() - .protocolVersion("HTTP/1.1") - .statusCode(statusCode) - .statusText("OK"); - } - if (statusCode == 302) { - return new Builder() - .protocolVersion("HTTP/1.1") - .statusCode(statusCode) - .statusText("Found"); - } - if (statusCode == 404) { - return new Builder() - .protocolVersion("HTTP/1.1") - .statusCode(statusCode) - .statusText("Not Found"); - } - if (statusCode == 500) { - return new Builder() - .protocolVersion("HTTP/1.1") - .statusCode(statusCode) - .statusText("Internal Server Error"); - } - return builder(); + public static Builder builder(Status status) { + return new Builder() + .protocolVersion("HTTP/1.1") + .statusCode(status.getCode()) + .statusText(status.getMessage()); } private static byte[] mergeByteArrays(byte[] array1, byte[] array2) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Status.java b/tomcat/src/main/java/org/apache/coyote/http11/Status.java new file mode 100644 index 0000000000..86b7b16bb3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Status.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11; + +public enum Status { + OK(200, "OK"), + FOUND(302, "Found"), + BAD_REQUEST(400, "Bad Request"), + NOT_FOUND(404, "Not Found"), + CONFLICT(409, "Conflict"), + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + ; + + private final int code; + private final String message; + + Status(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} From 19b3bc8d9bf1e1775f9dce21ff1927124924c81f Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 14:13:49 +0900 Subject: [PATCH 19/44] feat: extract headers on request and response --- .../apache/coyote/http11/Http11Processor.java | 14 ++-------- .../apache/coyote/http11/Http11Request.java | 27 ++++++++++++++----- .../apache/coyote/http11/Http11Response.java | 25 +++++++++++++---- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 3186c9f691..b78831465c 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -94,21 +94,11 @@ private Http11Response processLoginPage(Http11Request request) { .build(); } - return processStaticResource(new Http11Request( - request.method(), - "login.html", - request.parameters(), - request.protocolVersion() - )); + return processStaticResource(request.updatePath("login.html")); } private Http11Response processRegisterPage(Http11Request request) { - return processStaticResource(new Http11Request( - request.method(), - "register.html", - request.parameters(), - request.protocolVersion() - )); + return processStaticResource(request.updatePath("register.html")); } private Http11Response processRegister(Http11Request request) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java index 303a1987ba..2296959079 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java @@ -8,18 +8,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public record Http11Request(String method, String path, Map parameters, String protocolVersion, +public record Http11Request(String method, String path, Map parameters, Map headers, + String protocolVersion, String body) { - public Http11Request(String method, String path, Map parameters, String protocolVersion) { - this(method, path, parameters, protocolVersion, null); - } - public static Http11Request parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); String method = startLineParts[0]; String path = ""; Map parameters = Map.of(); + Map headers = extractHeaders(lines); String protocolVersion = startLineParts[2]; String body = null; @@ -35,7 +33,20 @@ public static Http11Request parse(List lines) { body = lines.getLast(); } - return new Http11Request(method, path, parameters, protocolVersion, body); + return new Http11Request(method, path, parameters, headers, protocolVersion, body); + } + + private static Map extractHeaders(List lines) { + Map headers = new HashMap<>(); + + for (int i = 1; i < lines.size() - 2; i++) { + String[] lineParts = lines.get(i).trim().split(": "); + if (lineParts.length >= 2) { + headers.put(lineParts[0], lineParts[1]); + } + } + + return headers; } public static Map extractParameters(String query) { @@ -55,4 +66,8 @@ public static Map extractParameters(String query) { return parameters; } + + public Http11Request updatePath(String path) { + return new Http11Request(method, path, parameters, headers, protocolVersion, body); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java index e687fdcc17..c76843aaa4 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java @@ -3,9 +3,11 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; public record Http11Response(String protocolVersion, int statusCode, String statusText, - Map headers, byte[] body) { + Map headers, Map cookies, byte[] body) { public static Builder builder(Status status) { return new Builder() @@ -28,6 +30,11 @@ public byte[] toMessage() { builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append(" "); headers.forEach((key, value) -> builder.append("\r\n").append(key).append(": ").append(value).append(" ")); + String cookiesMessage = cookies.entrySet().stream() + .map(Entry::toString) + .collect(Collectors.joining("; ")); + builder.append("\r\n").append("Set-Cookie: ").append(cookiesMessage).append(" "); + if (body != null && body.length > 0) { builder.append("\r\n").append("Content-Length: ").append(body.length).append(" "); builder.append("\r\n\r\n"); @@ -39,6 +46,7 @@ public byte[] toMessage() { public static class Builder { private final Map headers; + private final Map cookies; private String protocolVersion; private int statusCode; private String statusText; @@ -46,6 +54,7 @@ public static class Builder { private Builder() { headers = new HashMap<>(); + cookies = new HashMap<>(); } public Builder protocolVersion(String protocolVersion) { @@ -68,6 +77,11 @@ public Builder addHeader(String key, String value) { return this; } + public Builder addCookie(String key, String value) { + cookies.put(key, value); + return this; + } + public Builder contentType(String value) { headers.put("Content-Type", value + ";charset=utf-8"); return this; @@ -84,16 +98,17 @@ public Builder body(byte[] body) { } public Http11Response build() { - return new Http11Response(protocolVersion, statusCode, statusText, headers, body); + return new Http11Response(protocolVersion, statusCode, statusText, headers, cookies, body); } @Override public String toString() { - return "Http11ServletResponseBuilder{" + - "protocolVersion='" + protocolVersion + '\'' + + return "Builder{" + + "headers=" + headers + + ", cookies=" + cookies + + ", protocolVersion='" + protocolVersion + '\'' + ", statusCode=" + statusCode + ", statusText='" + statusText + '\'' + - ", headers=" + headers + ", body=" + Arrays.toString(body) + '}'; } From 4041b65f01fbd3ed96853b05cfe69c9e5eff9d2c Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 15:39:20 +0900 Subject: [PATCH 20/44] feat: use session to login automatically --- .../db/InMemorySessionRepository.java | 21 +++++++++++ .../apache/coyote/http11/Http11Processor.java | 33 ++++++++++++++--- .../apache/coyote/http11/Http11Request.java | 37 ++++++++++++++++--- 3 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java diff --git a/tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java new file mode 100644 index 0000000000..1b2c9d8d37 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java @@ -0,0 +1,21 @@ +package com.techcourse.db; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class InMemorySessionRepository { + + private static final Map sessions = new ConcurrentHashMap<>(); + + private InMemorySessionRepository() { + } + + public static void save(String sessionId, String userAccount) { + sessions.put(sessionId, userAccount); + } + + public static Optional findBySessionId(String sessionId) { + return Optional.ofNullable(sessions.get(sessionId)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index b78831465c..9ab5ee347c 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,5 +1,6 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemorySessionRepository; import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; import com.techcourse.model.User; @@ -9,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.function.Function; import java.util.function.Predicate; import org.apache.coyote.Processor; @@ -38,7 +40,7 @@ public Http11Processor(final Socket connection) { @Override public void run() { - log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); +// log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); process(connection); } @@ -48,12 +50,12 @@ public void process(final Socket connection) { final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); Http11Request request = Http11Request.parse(requestLines); - log.debug(request.toString()); for (final var processor : processors.entrySet()) { if (processor.getKey().test(request)) { +// log.debug(request.toString()); Http11Response response = processor.getValue().apply(request); - log.debug(response.toString()); +// log.debug(response.toString()); outputStream.write(response.toMessage()); outputStream.flush(); return; @@ -61,7 +63,6 @@ public void process(final Socket connection) { } Http11Response response = defaultProcessor.apply(request); - log.debug(response.toString()); outputStream.write(response.toMessage()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { @@ -77,15 +78,37 @@ private Http11Response processRootPage(Http11Request request) { } private Http11Response processLoginPage(Http11Request request) { + final String sessionCookieName = "JSESSIONID"; + + if (request.cookies().containsKey(sessionCookieName)) { + String sessionId = request.cookies().get(sessionCookieName); + Optional optionalUserAccount = InMemorySessionRepository.findBySessionId(sessionId); + if (optionalUserAccount.isPresent()) { + Optional optionalUser = InMemoryUserRepository.findByAccount(optionalUserAccount.get()); + if (optionalUser.isPresent()) { + log.info("세션 로그인 성공! - 아이디 : {}, 세션 ID : {}", optionalUser.get().getAccount(), sessionId); + return Http11Response.builder(Status.FOUND) + .location("/index.html") + .build(); + } + } + } + if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { String account = request.parameters().get("account"); String password = request.parameters().get("password"); Optional optionalUser = InMemoryUserRepository.findByAccount(account); if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { - log.info("로그인 성공! - 아이디 : " + optionalUser.get().getAccount()); + User user = optionalUser.get(); + String sessionId = UUID.randomUUID().toString(); + InMemorySessionRepository.save(sessionId, user.getAccount()); + + log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); + return Http11Response.builder(Status.FOUND) .location("/index.html") + .addCookie(sessionCookieName, sessionId) .build(); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java index 2296959079..d3bd773f16 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java @@ -8,9 +8,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public record Http11Request(String method, String path, Map parameters, Map headers, - String protocolVersion, - String body) { +public record Http11Request( + String method, + String path, + Map parameters, + Map headers, + Map cookies, + String protocolVersion, + String body) { public static Http11Request parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); @@ -18,6 +23,7 @@ public static Http11Request parse(List lines) { String path = ""; Map parameters = Map.of(); Map headers = extractHeaders(lines); + Map cookies = extractCookies(headers.get("Cookie")); String protocolVersion = startLineParts[2]; String body = null; @@ -33,7 +39,28 @@ public static Http11Request parse(List lines) { body = lines.getLast(); } - return new Http11Request(method, path, parameters, headers, protocolVersion, body); + return new Http11Request(method, path, parameters, headers, cookies, protocolVersion, body); + } + + private static Map extractCookies(String cookieMessage) { + if (cookieMessage == null) { + return Map.of(); + } + + Map cookies = new HashMap<>(); + + for (String entry : cookieMessage.split("; ")) { + int delimiterIndex = entry.indexOf("="); + if (delimiterIndex == -1) { + continue; + } + + String key = entry.substring(0, delimiterIndex).trim(); + String value = entry.substring(delimiterIndex + 1).trim(); + cookies.put(key, value); + } + + return cookies; } private static Map extractHeaders(List lines) { @@ -68,6 +95,6 @@ public static Map extractParameters(String query) { } public Http11Request updatePath(String path) { - return new Http11Request(method, path, parameters, headers, protocolVersion, body); + return new Http11Request(method, path, parameters, headers, cookies, protocolVersion, body); } } From 6e797020606fbc21b98d1e1563f565d68476fcc7 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 16:43:39 +0900 Subject: [PATCH 21/44] refactor: change session repository to session manager --- .../db/InMemorySessionRepository.java | 21 ---- .../org/apache/catalina/session/JSession.java | 102 ++++++++++++++++++ .../catalina/session/SessionManager.java | 26 +++++ .../apache/coyote/http11/Http11Processor.java | 36 ++++--- .../apache/coyote/http11/Http11Request.java | 17 +++ 5 files changed, 165 insertions(+), 37 deletions(-) delete mode 100644 tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java create mode 100644 tomcat/src/main/java/org/apache/catalina/session/JSession.java create mode 100644 tomcat/src/main/java/org/apache/catalina/session/SessionManager.java diff --git a/tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java deleted file mode 100644 index 1b2c9d8d37..0000000000 --- a/tomcat/src/main/java/com/techcourse/db/InMemorySessionRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.techcourse.db; - -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -public class InMemorySessionRepository { - - private static final Map sessions = new ConcurrentHashMap<>(); - - private InMemorySessionRepository() { - } - - public static void save(String sessionId, String userAccount) { - sessions.put(sessionId, userAccount); - } - - public static Optional findBySessionId(String sessionId) { - return Optional.ofNullable(sessions.get(sessionId)); - } -} diff --git a/tomcat/src/main/java/org/apache/catalina/session/JSession.java b/tomcat/src/main/java/org/apache/catalina/session/JSession.java new file mode 100644 index 0000000000..cabce50005 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/JSession.java @@ -0,0 +1,102 @@ +package org.apache.catalina.session; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionContext; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class JSession implements HttpSession { + + public static final String COOKIE_NAME = "JSESSIONID"; + + private final String id; + private final Map attributes = new HashMap<>(); + private final long creationTime = System.currentTimeMillis(); + + public JSession(String id) { + this.id = id; + } + + @Override + public long getCreationTime() { + return creationTime; + } + + @Override + public String getId() { + return id; + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public int getMaxInactiveInterval() { + return 0; + } + + @Override + public void setMaxInactiveInterval(int i) { + } + + @Override + public HttpSessionContext getSessionContext() { + return null; + } + + @Override + public Object getAttribute(String s) { + return attributes.get(s); + } + + @Override + public Object getValue(String s) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public String[] getValueNames() { + return new String[0]; + } + + @Override + public void setAttribute(String s, Object o) { + attributes.put(s, o); + } + + @Override + public void putValue(String s, Object o) { + } + + @Override + public void removeAttribute(String s) { + attributes.remove(s); + } + + @Override + public void removeValue(String s) { + } + + @Override + public void invalidate() { + } + + @Override + public boolean isNew() { + return false; + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java new file mode 100644 index 0000000000..473e0478f4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,26 @@ +package org.apache.catalina.session; + +import jakarta.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; +import org.apache.catalina.Manager; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new HashMap<>(); + + @Override + public void add(HttpSession session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public HttpSession findSession(String id) { + return SESSIONS.get(id); + } + + @Override + public void remove(HttpSession session) { + SESSIONS.remove(session.getId()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 9ab5ee347c..fa86d0fc3f 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,18 +1,21 @@ package org.apache.coyote.http11; -import com.techcourse.db.InMemorySessionRepository; import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; import com.techcourse.model.User; +import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.net.Socket; import java.net.URLConnection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.function.Function; import java.util.function.Predicate; +import org.apache.catalina.session.JSession; +import org.apache.catalina.session.SessionManager; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,12 +25,14 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; + private final SessionManager sessionManager; private final StaticResourceReader staticResourceReader; private final Function defaultProcessor; private final Map, Function> processors; public Http11Processor(final Socket connection) { this.connection = connection; + this.sessionManager = new SessionManager(); this.staticResourceReader = new StaticResourceReader(); this.defaultProcessor = this::processStaticResource; this.processors = Map.of( @@ -78,19 +83,16 @@ private Http11Response processRootPage(Http11Request request) { } private Http11Response processLoginPage(Http11Request request) { - final String sessionCookieName = "JSESSIONID"; - - if (request.cookies().containsKey(sessionCookieName)) { - String sessionId = request.cookies().get(sessionCookieName); - Optional optionalUserAccount = InMemorySessionRepository.findBySessionId(sessionId); - if (optionalUserAccount.isPresent()) { - Optional optionalUser = InMemoryUserRepository.findByAccount(optionalUserAccount.get()); - if (optionalUser.isPresent()) { - log.info("세션 로그인 성공! - 아이디 : {}, 세션 ID : {}", optionalUser.get().getAccount(), sessionId); - return Http11Response.builder(Status.FOUND) - .location("/index.html") - .build(); - } + if (request.cookies().containsKey(JSession.COOKIE_NAME)) { + HttpSession session = request.getSession(sessionManager); + if (session != null) { + User user = (User) Objects.requireNonNull(session).getAttribute("user"); + + log.info("세션 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); + + return Http11Response.builder(Status.FOUND) + .location("/index.html") + .build(); } } @@ -102,13 +104,15 @@ private Http11Response processLoginPage(Http11Request request) { if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { User user = optionalUser.get(); String sessionId = UUID.randomUUID().toString(); - InMemorySessionRepository.save(sessionId, user.getAccount()); + JSession session = new JSession(sessionId); + session.setAttribute("user", user); + sessionManager.add(session); log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); return Http11Response.builder(Status.FOUND) .location("/index.html") - .addCookie(sessionCookieName, sessionId) + .addCookie(JSession.COOKIE_NAME, sessionId) .build(); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java index d3bd773f16..953d6149ea 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java @@ -1,5 +1,6 @@ package org.apache.coyote.http11; +import jakarta.servlet.http.HttpSession; import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.HashMap; @@ -7,6 +8,8 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.catalina.session.JSession; +import org.apache.catalina.session.SessionManager; public record Http11Request( String method, @@ -94,6 +97,20 @@ public static Map extractParameters(String query) { return parameters; } + public HttpSession getSession(SessionManager sessionManager) { + String sessionId = cookies.get(JSession.COOKIE_NAME); + if (sessionId == null) { + return null; + } + + HttpSession session = sessionManager.findSession(sessionId); + if (session == null || session.getAttribute("user") == null) { + return null; + } + + return sessionManager.findSession(sessionId); + } + public Http11Request updatePath(String path) { return new Http11Request(method, path, parameters, headers, cookies, protocolVersion, body); } From 3c7190627a6347482d2e768beafebcd8966f0434 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 16:47:11 +0900 Subject: [PATCH 22/44] refactor: rename request and response to general --- .../apache/coyote/http11/Http11Processor.java | 42 +++++++++---------- .../{Http11Request.java => HttpRequest.java} | 10 ++--- ...{Http11Response.java => HttpResponse.java} | 8 ++-- 3 files changed, 30 insertions(+), 30 deletions(-) rename tomcat/src/main/java/org/apache/coyote/http11/{Http11Request.java => HttpRequest.java} (90%) rename tomcat/src/main/java/org/apache/coyote/http11/{Http11Response.java => HttpResponse.java} (91%) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index fa86d0fc3f..64249673f9 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -27,8 +27,8 @@ public class Http11Processor implements Runnable, Processor { private final Socket connection; private final SessionManager sessionManager; private final StaticResourceReader staticResourceReader; - private final Function defaultProcessor; - private final Map, Function> processors; + private final Function defaultProcessor; + private final Map, Function> processors; public Http11Processor(final Socket connection) { this.connection = connection; @@ -54,12 +54,12 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); - Http11Request request = Http11Request.parse(requestLines); + HttpRequest request = HttpRequest.parse(requestLines); for (final var processor : processors.entrySet()) { if (processor.getKey().test(request)) { // log.debug(request.toString()); - Http11Response response = processor.getValue().apply(request); + HttpResponse response = processor.getValue().apply(request); // log.debug(response.toString()); outputStream.write(response.toMessage()); outputStream.flush(); @@ -67,7 +67,7 @@ public void process(final Socket connection) { } } - Http11Response response = defaultProcessor.apply(request); + HttpResponse response = defaultProcessor.apply(request); outputStream.write(response.toMessage()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { @@ -75,14 +75,14 @@ public void process(final Socket connection) { } } - private Http11Response processRootPage(Http11Request request) { - return Http11Response.builder(Status.OK) + private HttpResponse processRootPage(HttpRequest request) { + return HttpResponse.builder(Status.OK) .addHeader("Content-Type", "text/html;charset=utf-8") .body("Hello world!".getBytes()) .build(); } - private Http11Response processLoginPage(Http11Request request) { + private HttpResponse processLoginPage(HttpRequest request) { if (request.cookies().containsKey(JSession.COOKIE_NAME)) { HttpSession session = request.getSession(sessionManager); if (session != null) { @@ -90,7 +90,7 @@ private Http11Response processLoginPage(Http11Request request) { log.info("세션 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); - return Http11Response.builder(Status.FOUND) + return HttpResponse.builder(Status.FOUND) .location("/index.html") .build(); } @@ -110,13 +110,13 @@ private Http11Response processLoginPage(Http11Request request) { log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); - return Http11Response.builder(Status.FOUND) + return HttpResponse.builder(Status.FOUND) .location("/index.html") .addCookie(JSession.COOKIE_NAME, sessionId) .build(); } - return Http11Response.builder(Status.FOUND) + return HttpResponse.builder(Status.FOUND) .location("/401.html") .build(); } @@ -124,48 +124,48 @@ private Http11Response processLoginPage(Http11Request request) { return processStaticResource(request.updatePath("login.html")); } - private Http11Response processRegisterPage(Http11Request request) { + private HttpResponse processRegisterPage(HttpRequest request) { return processStaticResource(request.updatePath("register.html")); } - private Http11Response processRegister(Http11Request request) { - Map body = Http11Request.extractParameters(request.body()); + private HttpResponse processRegister(HttpRequest request) { + Map body = HttpRequest.extractParameters(request.body()); if (!body.containsKey("account") || !body.containsKey("password") || !body.containsKey("email")) { - return Http11Response.builder(Status.BAD_REQUEST) + return HttpResponse.builder(Status.BAD_REQUEST) .build(); } String account = body.get("account"); if (InMemoryUserRepository.findByAccount(account).isPresent()) { - return Http11Response.builder(Status.CONFLICT) + return HttpResponse.builder(Status.CONFLICT) .build(); } InMemoryUserRepository.save(new User(account, body.get("password"), body.get("email"))); - return Http11Response.builder(Status.FOUND) + return HttpResponse.builder(Status.FOUND) .location("/index.html") .build(); } - private Http11Response processStaticResource(Http11Request request) { + private HttpResponse processStaticResource(HttpRequest request) { String contentType = URLConnection.guessContentTypeFromName(request.path()); final byte[] responseBody; try { responseBody = staticResourceReader.read(request.path()); if (responseBody == null) { - return Http11Response.builder(Status.NOT_FOUND) + return HttpResponse.builder(Status.NOT_FOUND) .build(); } - return Http11Response.builder(Status.OK) + return HttpResponse.builder(Status.OK) .contentType(contentType) .body(responseBody) .build(); } catch (IOException e) { log.error(e.getMessage(), e); - return Http11Response.builder(Status.INTERNAL_SERVER_ERROR) + return HttpResponse.builder(Status.INTERNAL_SERVER_ERROR) .build(); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java similarity index 90% rename from tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java rename to tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java index 953d6149ea..1d7410ea3d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -11,7 +11,7 @@ import org.apache.catalina.session.JSession; import org.apache.catalina.session.SessionManager; -public record Http11Request( +public record HttpRequest( String method, String path, Map parameters, @@ -20,7 +20,7 @@ public record Http11Request( String protocolVersion, String body) { - public static Http11Request parse(List lines) { + public static HttpRequest parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); String method = startLineParts[0]; String path = ""; @@ -42,7 +42,7 @@ public static Http11Request parse(List lines) { body = lines.getLast(); } - return new Http11Request(method, path, parameters, headers, cookies, protocolVersion, body); + return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); } private static Map extractCookies(String cookieMessage) { @@ -111,7 +111,7 @@ public HttpSession getSession(SessionManager sessionManager) { return sessionManager.findSession(sessionId); } - public Http11Request updatePath(String path) { - return new Http11Request(method, path, parameters, headers, cookies, protocolVersion, body); + public HttpRequest updatePath(String path) { + return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java similarity index 91% rename from tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java rename to tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java index c76843aaa4..eb3a77f39d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Response.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -6,8 +6,8 @@ import java.util.Map.Entry; import java.util.stream.Collectors; -public record Http11Response(String protocolVersion, int statusCode, String statusText, - Map headers, Map cookies, byte[] body) { +public record HttpResponse(String protocolVersion, int statusCode, String statusText, + Map headers, Map cookies, byte[] body) { public static Builder builder(Status status) { return new Builder() @@ -97,8 +97,8 @@ public Builder body(byte[] body) { return this; } - public Http11Response build() { - return new Http11Response(protocolVersion, statusCode, statusText, headers, cookies, body); + public HttpResponse build() { + return new HttpResponse(protocolVersion, statusCode, statusText, headers, cookies, body); } @Override From fa763c6314b15eb1a9512d7937825677f19328a0 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Wed, 4 Sep 2024 17:45:08 +0900 Subject: [PATCH 23/44] refactor: add controller interface and map all request with it --- .../techcourse}/StaticResourceReader.java | 2 +- .../controller/AbstractController.java | 21 +++ .../com/techcourse/controller/Controller.java | 8 + .../techcourse/controller/HomeController.java | 15 ++ .../controller/LoginController.java | 69 +++++++++ .../controller/RegisterController.java | 44 ++++++ .../techcourse/controller/RequestMapping.java | 29 ++++ .../controller/ResourceController.java | 42 ++++++ .../apache/coyote/http11/Http11Processor.java | 140 ++---------------- .../apache/coyote/http11/HttpResponse.java | 17 +-- 10 files changed, 243 insertions(+), 144 deletions(-) rename tomcat/src/main/java/{org/apache/coyote/http11 => com/techcourse}/StaticResourceReader.java (94%) create mode 100644 tomcat/src/main/java/com/techcourse/controller/AbstractController.java create mode 100644 tomcat/src/main/java/com/techcourse/controller/Controller.java create mode 100644 tomcat/src/main/java/com/techcourse/controller/HomeController.java create mode 100644 tomcat/src/main/java/com/techcourse/controller/LoginController.java create mode 100644 tomcat/src/main/java/com/techcourse/controller/RegisterController.java create mode 100644 tomcat/src/main/java/com/techcourse/controller/RequestMapping.java create mode 100644 tomcat/src/main/java/com/techcourse/controller/ResourceController.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java b/tomcat/src/main/java/com/techcourse/StaticResourceReader.java similarity index 94% rename from tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java rename to tomcat/src/main/java/com/techcourse/StaticResourceReader.java index fc8820e24a..aed229a387 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/StaticResourceReader.java +++ b/tomcat/src/main/java/com/techcourse/StaticResourceReader.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package com.techcourse; import java.io.IOException; diff --git a/tomcat/src/main/java/com/techcourse/controller/AbstractController.java b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java new file mode 100644 index 0000000000..cada57aae4 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java @@ -0,0 +1,21 @@ +package com.techcourse.controller; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public abstract class AbstractController implements Controller { + + @Override + public void service(HttpRequest request, HttpResponse.Builder responseBuilder) { + if (request.method().equals("GET")) { + doGet(request, responseBuilder); + } + if (request.method().equals("POST")) { + doPost(request, responseBuilder); + } + } + + protected void doPost(HttpRequest request, HttpResponse.Builder responseBuilder) { /* NOOP */ } + + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { /* NOOP */ } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/Controller.java b/tomcat/src/main/java/com/techcourse/controller/Controller.java new file mode 100644 index 0000000000..ef38bd1ec4 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/Controller.java @@ -0,0 +1,8 @@ +package com.techcourse.controller; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public interface Controller { + void service(HttpRequest request, HttpResponse.Builder responseBuilder); +} diff --git a/tomcat/src/main/java/com/techcourse/controller/HomeController.java b/tomcat/src/main/java/com/techcourse/controller/HomeController.java new file mode 100644 index 0000000000..2c66b69733 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/HomeController.java @@ -0,0 +1,15 @@ +package com.techcourse.controller; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.Status; + +public class HomeController extends AbstractController { + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + responseBuilder.status(Status.OK) + .addHeader("Content-Type", "text/html;charset=utf-8") + .body("Hello world!".getBytes()); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java new file mode 100644 index 0000000000..9d8f5c3f26 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -0,0 +1,69 @@ +package com.techcourse.controller; + +import static org.reflections.Reflections.log; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import jakarta.servlet.http.HttpSession; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import org.apache.catalina.session.JSession; +import org.apache.catalina.session.SessionManager; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.Status; + +public class LoginController extends AbstractController { + + private final SessionManager sessionManager; + private final ResourceController resourceController; + + public LoginController() { + this.sessionManager = new SessionManager(); + this.resourceController = new ResourceController(); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + if (request.cookies().containsKey(JSession.COOKIE_NAME)) { + HttpSession session = request.getSession(sessionManager); + if (session != null) { + User user = (User) Objects.requireNonNull(session).getAttribute("user"); + + log.info("세션 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); + + responseBuilder.status(Status.FOUND) + .location("/index.html"); + return; + } + } + + if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { + String account = request.parameters().get("account"); + String password = request.parameters().get("password"); + + Optional optionalUser = InMemoryUserRepository.findByAccount(account); + if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { + User user = optionalUser.get(); + String sessionId = UUID.randomUUID().toString(); + JSession session = new JSession(sessionId); + session.setAttribute("user", user); + sessionManager.add(session); + + log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); + + responseBuilder.status(Status.FOUND) + .location("/index.html") + .addCookie(JSession.COOKIE_NAME, sessionId); + return; + } + + responseBuilder.status(Status.FOUND) + .location("/401.html"); + return; + } + + resourceController.doGet(request.updatePath("login.html"), responseBuilder); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/RegisterController.java b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java new file mode 100644 index 0000000000..5ba88730f2 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -0,0 +1,44 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import java.util.Map; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.Status; + +public class RegisterController extends AbstractController { + + private final ResourceController resourceController; + + public RegisterController() { + this.resourceController = new ResourceController(); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + resourceController.doGet(request.updatePath("register.html"), responseBuilder); + } + + @Override + protected void doPost(HttpRequest request, HttpResponse.Builder responseBuilder) { + Map body = HttpRequest.extractParameters(request.body()); + + if (!body.containsKey("account") || + !body.containsKey("password") || + !body.containsKey("email")) { + responseBuilder.status(Status.BAD_REQUEST); + return; + } + + String account = body.get("account"); + if (InMemoryUserRepository.findByAccount(account).isPresent()) { + responseBuilder.status(Status.CONFLICT); + return; + } + + InMemoryUserRepository.save(new User(account, body.get("password"), body.get("email"))); + responseBuilder.status(Status.FOUND) + .location("/index.html"); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java new file mode 100644 index 0000000000..c07c246e05 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java @@ -0,0 +1,29 @@ +package com.techcourse.controller; + +import java.util.Map; +import java.util.function.Predicate; +import org.apache.coyote.http11.HttpRequest; + +public class RequestMapping { + + private final Controller defaultController; + private final Map, Controller> controllers; + + public RequestMapping() { + this.defaultController = new ResourceController(); + this.controllers = Map.of( + req -> req.path().equals("/"), new HomeController(), + req -> req.path().equals("/login"), new LoginController(), + req -> req.path().equals("/register"), new RegisterController() + ); + } + + public Controller getController(HttpRequest request) { + for (final var entry : controllers.entrySet()) { + if (entry.getKey().test(request)) { + return entry.getValue(); + } + } + return defaultController; + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/ResourceController.java b/tomcat/src/main/java/com/techcourse/controller/ResourceController.java new file mode 100644 index 0000000000..fb596f3721 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/ResourceController.java @@ -0,0 +1,42 @@ +package com.techcourse.controller; + +import com.techcourse.StaticResourceReader; +import java.io.IOException; +import java.net.URLConnection; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ResourceController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(ResourceController.class); + + private final StaticResourceReader staticResourceReader; + + public ResourceController() { + this.staticResourceReader = new StaticResourceReader(); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { + String contentType = URLConnection.guessContentTypeFromName(request.path()); + final byte[] responseBody; + try { + responseBody = staticResourceReader.read(request.path()); + if (responseBody == null) { + responseBuilder.status(Status.NOT_FOUND) + .build(); + } + responseBuilder.status(Status.OK) + .contentType(contentType) + .body(responseBody) + .build(); + } catch (IOException e) { + log.error(e.getMessage(), e); + responseBuilder.status(Status.INTERNAL_SERVER_ERROR) + .build(); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 64249673f9..1585d077e6 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,21 +1,10 @@ package org.apache.coyote.http11; -import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.controller.RequestMapping; import com.techcourse.exception.UncheckedServletException; -import com.techcourse.model.User; -import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.net.Socket; -import java.net.URLConnection; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Function; -import java.util.function.Predicate; -import org.apache.catalina.session.JSession; -import org.apache.catalina.session.SessionManager; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,22 +14,11 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; - private final SessionManager sessionManager; - private final StaticResourceReader staticResourceReader; - private final Function defaultProcessor; - private final Map, Function> processors; + private final RequestMapping requestMapping; public Http11Processor(final Socket connection) { this.connection = connection; - this.sessionManager = new SessionManager(); - this.staticResourceReader = new StaticResourceReader(); - this.defaultProcessor = this::processStaticResource; - this.processors = Map.of( - req -> req.method().equals("GET") && req.path().equals("/"), this::processRootPage, - req -> req.method().equals("GET") && req.path().equals("/login"), this::processLoginPage, - req -> req.method().equals("GET") && req.path().equals("/register"), this::processRegisterPage, - req -> req.method().equals("POST") && req.path().equals("/register"), this::processRegister - ); + this.requestMapping = new RequestMapping(); } @Override @@ -55,118 +33,18 @@ public void process(final Socket connection) { final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); HttpRequest request = HttpRequest.parse(requestLines); +// log.debug(request.toString()); - for (final var processor : processors.entrySet()) { - if (processor.getKey().test(request)) { -// log.debug(request.toString()); - HttpResponse response = processor.getValue().apply(request); -// log.debug(response.toString()); - outputStream.write(response.toMessage()); - outputStream.flush(); - return; - } - } + HttpResponse.Builder responseBuilder = HttpResponse.builder(); + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); +// log.debug(response.toString()); - HttpResponse response = defaultProcessor.apply(request); outputStream.write(response.toMessage()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } - - private HttpResponse processRootPage(HttpRequest request) { - return HttpResponse.builder(Status.OK) - .addHeader("Content-Type", "text/html;charset=utf-8") - .body("Hello world!".getBytes()) - .build(); - } - - private HttpResponse processLoginPage(HttpRequest request) { - if (request.cookies().containsKey(JSession.COOKIE_NAME)) { - HttpSession session = request.getSession(sessionManager); - if (session != null) { - User user = (User) Objects.requireNonNull(session).getAttribute("user"); - - log.info("세션 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); - - return HttpResponse.builder(Status.FOUND) - .location("/index.html") - .build(); - } - } - - if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { - String account = request.parameters().get("account"); - String password = request.parameters().get("password"); - - Optional optionalUser = InMemoryUserRepository.findByAccount(account); - if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { - User user = optionalUser.get(); - String sessionId = UUID.randomUUID().toString(); - JSession session = new JSession(sessionId); - session.setAttribute("user", user); - sessionManager.add(session); - - log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); - - return HttpResponse.builder(Status.FOUND) - .location("/index.html") - .addCookie(JSession.COOKIE_NAME, sessionId) - .build(); - } - - return HttpResponse.builder(Status.FOUND) - .location("/401.html") - .build(); - } - - return processStaticResource(request.updatePath("login.html")); - } - - private HttpResponse processRegisterPage(HttpRequest request) { - return processStaticResource(request.updatePath("register.html")); - } - - private HttpResponse processRegister(HttpRequest request) { - Map body = HttpRequest.extractParameters(request.body()); - - if (!body.containsKey("account") || - !body.containsKey("password") || - !body.containsKey("email")) { - return HttpResponse.builder(Status.BAD_REQUEST) - .build(); - } - - String account = body.get("account"); - if (InMemoryUserRepository.findByAccount(account).isPresent()) { - return HttpResponse.builder(Status.CONFLICT) - .build(); - } - - InMemoryUserRepository.save(new User(account, body.get("password"), body.get("email"))); - return HttpResponse.builder(Status.FOUND) - .location("/index.html") - .build(); - } - - private HttpResponse processStaticResource(HttpRequest request) { - String contentType = URLConnection.guessContentTypeFromName(request.path()); - final byte[] responseBody; - try { - responseBody = staticResourceReader.read(request.path()); - if (responseBody == null) { - return HttpResponse.builder(Status.NOT_FOUND) - .build(); - } - return HttpResponse.builder(Status.OK) - .contentType(contentType) - .body(responseBody) - .build(); - } catch (IOException e) { - log.error(e.getMessage(), e); - return HttpResponse.builder(Status.INTERNAL_SERVER_ERROR) - .build(); - } - } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java index eb3a77f39d..f3cc0001d0 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -9,11 +9,8 @@ public record HttpResponse(String protocolVersion, int statusCode, String statusText, Map headers, Map cookies, byte[] body) { - public static Builder builder(Status status) { - return new Builder() - .protocolVersion("HTTP/1.1") - .statusCode(status.getCode()) - .statusText(status.getMessage()); + public static Builder builder() { + return new Builder().protocolVersion("HTTP/1.1"); } private static byte[] mergeByteArrays(byte[] array1, byte[] array2) { @@ -62,13 +59,9 @@ public Builder protocolVersion(String protocolVersion) { return this; } - public Builder statusCode(int statusCode) { - this.statusCode = statusCode; - return this; - } - - public Builder statusText(String statusText) { - this.statusText = statusText; + public Builder status(Status status) { + this.statusCode = status.getCode(); + this.statusText = status.getMessage(); return this; } From b104eb1a885be71dbd745253ba2810d0cd7c7284 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 17:05:21 +0900 Subject: [PATCH 24/44] fix: remove setCookie on response when empty --- .../java/org/apache/coyote/http11/HttpResponse.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java index f3cc0001d0..d5334a5887 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -27,10 +27,12 @@ public byte[] toMessage() { builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append(" "); headers.forEach((key, value) -> builder.append("\r\n").append(key).append(": ").append(value).append(" ")); - String cookiesMessage = cookies.entrySet().stream() - .map(Entry::toString) - .collect(Collectors.joining("; ")); - builder.append("\r\n").append("Set-Cookie: ").append(cookiesMessage).append(" "); + if (!cookies.isEmpty()) { + String cookiesMessage = cookies.entrySet().stream() + .map(Entry::toString) + .collect(Collectors.joining("; ")); + builder.append("\r\n").append("Set-Cookie: ").append(cookiesMessage).append(" "); + } if (body != null && body.length > 0) { builder.append("\r\n").append("Content-Length: ").append(body.length).append(" "); From d2f9c1c6987eb4530ccbfc7a013417bfafa71889 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 17:34:12 +0900 Subject: [PATCH 25/44] test: add reading page tests --- .../controller/HomeControllerTest.java | 36 +++++++++++++++++++ .../controller/LoginControllerTest.java | 35 ++++++++++++++++++ .../controller/RegisterControllerTest.java | 35 ++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 tomcat/src/test/java/com/techcourse/controller/HomeControllerTest.java create mode 100644 tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java create mode 100644 tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java diff --git a/tomcat/src/test/java/com/techcourse/controller/HomeControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/HomeControllerTest.java new file mode 100644 index 0000000000..613ed030dc --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/HomeControllerTest.java @@ -0,0 +1,36 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HomeControllerTest { + + @Test + @DisplayName("안녕세상 페이지를 조회한다.") + void viewHomePage() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "GET / HTTP/1.1" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 200 OK".getBytes()) + .contains("Content-Type: text/html".getBytes()) + .contains("Content-Length: 12".getBytes()) + .contains("Hello world!".getBytes()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java new file mode 100644 index 0000000000..e56bc24bf4 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java @@ -0,0 +1,35 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class LoginControllerTest { + + @Test + @DisplayName("로그인 페이지를 조회한다.") + void viewLoginPage() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 200 OK".getBytes()) + .contains("Content-Type: text/html".getBytes()) + .contains("로그인".getBytes()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java new file mode 100644 index 0000000000..12582b725b --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java @@ -0,0 +1,35 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RegisterControllerTest { + + @Test + @DisplayName("회원가입 페이지를 조회한다.") + void viewRegisterPage() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /register HTTP/1.1" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 200 OK".getBytes()) + .contains("Content-Type: text/html".getBytes()) + .contains("회원가입".getBytes()); + } +} From df95aa9e1a75f2ef7ea38f626b1754e226638550 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 18:40:46 +0900 Subject: [PATCH 26/44] test: add login and register tests --- .../controller/LoginControllerTest.java | 64 +++++++++++++++++++ .../controller/RegisterControllerTest.java | 47 ++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java index e56bc24bf4..c2f8cf8feb 100644 --- a/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java +++ b/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java @@ -32,4 +32,68 @@ void viewLoginPage() { .contains("Content-Type: text/html".getBytes()) .contains("로그인".getBytes()); } + + @Test + @DisplayName("로그인을 하면 홈 화면으로 이동한다.") + void loginAndRedirect() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login?account=gugu&password=password HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /index.html".getBytes()) + .contains("Set-Cookie: ".getBytes()); + } + + @Test + @DisplayName("존재하지 않는 사용자로 로그인을 하면 401 페이지로 이동한다.") + void loginWithNonAccountAndRedirect() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login?account=seyang&password=password HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /401.html".getBytes()); + } + + @Test + @DisplayName("잘못된 비밀번호로 로그인을 하면 401 페이지로 이동한다.") + void loginWithDifferentPasswordAndRedirect() { + // given + RequestMapping requestMapping = new RequestMapping(); + Builder responseBuilder = HttpResponse.builder(); + HttpRequest request = HttpRequest.parse(List.of( + "GET /login?account=gugu&password=pw HTTP/1.1" + )); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /401.html".getBytes()); + } } diff --git a/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java b/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java index 12582b725b..9f87107516 100644 --- a/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java +++ b/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java @@ -32,4 +32,51 @@ void viewRegisterPage() { .contains("Content-Type: text/html".getBytes()) .contains("회원가입".getBytes()); } + + @Test + @DisplayName("회원가입을 한다.") + void register() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "POST /register HTTP/1.1", + "Content-Type: application/x-www-form-urlencoded", + "", + "account=seyang&email=seyang%40woowa.com&password=pw" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /index.html".getBytes()); + } + + @Test + @DisplayName("일부 정보 없이 회원가입 할 경우 400을 반환한다.") + void registerWithoutInformation() { + // given + RequestMapping requestMapping = new RequestMapping(); + HttpRequest request = HttpRequest.parse(List.of( + "POST /register HTTP/1.1", + "Content-Type: application/x-www-form-urlencoded", + "", + "account=seyang&password=pw" + )); + Builder responseBuilder = HttpResponse.builder(); + + // when + requestMapping.getController(request) + .service(request, responseBuilder); + HttpResponse response = responseBuilder.build(); + + // then + assertThat(response.toMessage()) + .contains("HTTP/1.1 400 Bad Request".getBytes()); + } } From d16797b48e756514121c93ffd3cf8ab878674f45 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Fri, 6 Sep 2024 15:10:59 +0900 Subject: [PATCH 27/44] test: solve study of file class --- study/src/test/java/study/FileTest.java | 43 +++++++++++++------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..642f00cdbd 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,56 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; +import java.nio.file.Paths; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; /** - * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. - * File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. + * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. */ @DisplayName("File 클래스 학습 테스트") class FileTest { /** * resource 디렉터리 경로 찾기 - * - * File 객체를 생성하려면 파일의 경로를 알아야 한다. - * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. - * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

+ * File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수 + * 있을까? */ @Test - void resource_디렉터리에_있는_파일의_경로를_찾는다() { + void resource_디렉터리에_있는_파일의_경로를_찾는다() throws IOException { final String fileName = "nextstep.txt"; - // todo - final String actual = ""; + URL url = getClass().getClassLoader().getResource(fileName); + assertThat(url).isNotNull(); + final String actual = url.toString(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws URISyntaxException, IOException { final String fileName = "nextstep.txt"; // todo - final Path path = null; + URL url = getClass().getClassLoader().getResource(fileName); + assertThat(url).isNotNull(); + final Path path = Paths.get(url.toURI()); // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } From c45c34a8f3b05c2975afa9f5c61bd91e7d615ba6 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Fri, 6 Sep 2024 15:23:59 +0900 Subject: [PATCH 28/44] test: solve study of io stream class --- study/src/test/java/study/IOStreamTest.java | 134 ++++++++++---------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..aa98d51bfa 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,50 @@ package study; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.io.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - /** - * 자바는 스트림(Stream)으로부터 I/O를 사용한다. - * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. - * - * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. - * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. - * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) - * - * Stream은 데이터를 바이트로 읽고 쓴다. - * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. - * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다. + * 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. + *

+ * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. FilterStream은 읽거나 쓰는 + * 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) + *

+ * Stream은 데이터를 바이트로 읽고 쓴다. 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 + * 처리할 수 있다. */ @DisplayName("Java I/O Stream 클래스 학습 테스트") class IOStreamTest { /** * OutputStream 학습하기 - * - * 자바의 기본 출력 클래스는 java.io.OutputStream이다. - * OutputStream의 write(int b) 메서드는 기반 메서드이다. + *

+ * 자바의 기본 출력 클래스는 java.io.OutputStream이다. OutputStream의 write(int b) 메서드는 기반 메서드이다. * public abstract void write(int b) throws IOException; */ @Nested class OutputStream_학습_테스트 { /** - * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. - * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. - * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, - * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 + * 사용한다. 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 + * 사용한다. + *

* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -50,10 +55,10 @@ class OutputStream_학습_테스트 { final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); /** - * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -61,62 +66,57 @@ class OutputStream_학습_테스트 { } /** - * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. - * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * - * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. - * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. - * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 - * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. + * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. + *

+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은 + * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. */ @Test void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { final OutputStream outputStream = mock(BufferedOutputStream.class); /** - * todo * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { final OutputStream outputStream = mock(OutputStream.class); /** - * todo * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + } verify(outputStream, atLeastOnce()).close(); } } /** * InputStream 학습하기 - * - * 자바의 기본 입력 클래스는 java.io.InputStream이다. - * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. - * InputStream의 read() 메서드는 기반 메서드이다. + *

+ * 자바의 기본 입력 클래스는 java.io.InputStream이다. InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. InputStream의 read() 메서드는 기반 + * 메서드이다. * public abstract int read() throws IOException; - * + *

* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested class InputStream_학습_테스트 { /** - * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. - * int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. + * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. * 그리고 Stream 끝에 도달하면 -1을 반환한다. */ @Test @@ -125,10 +125,15 @@ class InputStream_학습_테스트 { final InputStream inputStream = new ByteArrayInputStream(bytes); /** - * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + + byte[] data = new byte[bytes.length]; + int buf; + for (int i = 0; i < bytes.length && (buf = inputStream.read()) != -1; i++) { + data[i] = (byte) buf; + } + final String actual = new String(data); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -136,8 +141,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -149,32 +153,32 @@ class InputStream_학습_테스트 { * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } } /** * FilterStream 학습하기 - * - * 필터는 필터 스트림, reader, writer로 나뉜다. - * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. - * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. + *

+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 + * 텍스트를 처리하는 데 사용된다. */ @Nested class FilterStream_학습_테스트 { /** - * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. - * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. - * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? + * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -182,30 +186,30 @@ class FilterStream_학습_테스트 { } /** - * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. - * 문자열이 아닌 바이트 단위로 처리하려니 불편하다. - * 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. - * reader, writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. - * 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. + * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. 문자열이 아닌 바이트 단위로 처리하려니 불편하다. 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. reader, + * writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. */ @Nested class InputStreamReader_학습_테스트 { /** - * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. - * 읽어온 문자(char)를 문자열(String)로 처리하자. - * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. + * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. 읽어온 문자(char)를 문자열(String)로 처리하자. 필터인 BufferedReader를 사용하면 + * readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); final StringBuilder actual = new StringBuilder(); + while (bufferedReader.ready()) { + actual.append(bufferedReader.readLine()).append(System.lineSeparator()); + } assertThat(actual).hasToString(emoji); } From a8f6c7a3e6c6b7563b88b9011930ce661f60ef9e Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Fri, 6 Sep 2024 15:31:17 +0900 Subject: [PATCH 29/44] feat: set SessionManager as singleton --- .../java/org/apache/catalina/session/SessionManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index 473e0478f4..d133fe7dc2 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -8,6 +8,11 @@ public class SessionManager implements Manager { private static final Map SESSIONS = new HashMap<>(); + private static final SessionManager SESSION_MANAGER = new SessionManager(); + + public static SessionManager getInstance() { + return SESSION_MANAGER; + } @Override public void add(HttpSession session) { From 1b532f3bd9b9d91aa46797031cbab68d7c29536a Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Fri, 6 Sep 2024 15:31:42 +0900 Subject: [PATCH 30/44] feat: add thymeleaf dependencies --- study/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/study/build.gradle b/study/build.gradle index 87a1f0313c..7bdce4f751 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,6 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' From e15dc7b0d308062b352b69779d08f4b0b5007ad7 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Fri, 6 Sep 2024 16:37:54 +0900 Subject: [PATCH 31/44] test: solve study of http cache --- .../cache/com/example/GreetingController.java | 10 ++++++++-- .../cachecontrol/CacheHandlerInterceptor.java | 18 ++++++++++++++++++ .../example/cachecontrol/CacheWebConfig.java | 15 +++++++++++++++ .../example/etag/EtagFilterConfiguration.java | 11 +++++++---- study/src/main/resources/application.yml | 3 +++ .../com/example/GreetingControllerTest.java | 13 +++++-------- 6 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..2f29d87468 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,15 +1,21 @@ package cache.com.example; +import cache.com.example.version.ResourceVersion; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.http.HttpServletResponse; - @Controller public class GreetingController { + private final ResourceVersion version; + + public GreetingController(ResourceVersion version) { + this.version = version; + } + @GetMapping("/") public String index() { return "index"; diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java b/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java new file mode 100644 index 0000000000..ed47d0541a --- /dev/null +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java @@ -0,0 +1,18 @@ +package cache.com.example.cachecontrol; + +import com.google.common.net.HttpHeaders; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.HandlerInterceptor; + +public class CacheHandlerInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + CacheControl cacheControl = CacheControl.noCache().cachePrivate(); + response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue()); + + return true; + } +} diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index 305b1f1e1e..f3e04d3a3d 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,6 +1,12 @@ package cache.com.example.cachecontrol; +import com.google.common.net.HttpHeaders; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.time.Duration; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -9,5 +15,14 @@ public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new CacheHandlerInterceptor()).addPathPatterns("/"); + registry.addInterceptor(new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)).cachePublic(); + response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue()); + return true; + } + }).addPathPatterns("/resources/**"); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..c51500216c 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -1,12 +1,15 @@ package cache.com.example.etag; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + return new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index e3503a5fb9..8b74bdfd88 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -8,3 +8,6 @@ server: threads: min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 10 diff --git a/study/src/test/java/cache/com/example/GreetingControllerTest.java b/study/src/test/java/cache/com/example/GreetingControllerTest.java index 9ce2a394f7..2fea3e6dfb 100644 --- a/study/src/test/java/cache/com/example/GreetingControllerTest.java +++ b/study/src/test/java/cache/com/example/GreetingControllerTest.java @@ -1,6 +1,9 @@ package cache.com.example; +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; + import cache.com.example.version.ResourceVersion; +import java.time.Duration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,10 +13,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.test.web.reactive.server.WebTestClient; -import java.time.Duration; - -import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class GreetingControllerTest { @@ -68,10 +67,8 @@ void testETag() { } /** - * http://localhost:8080/resource-versioning - * 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. - * 보통 정적 파일을 캐싱 무효화하기 위해 캐싱과 함께 버전을 적용시킨다. - * 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다. + * http://localhost:8080/resource-versioning 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. 보통 정적 파일을 캐싱 무효화하기 + * 위해 캐싱과 함께 버전을 적용시킨다. 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다. */ @Test void testCacheBustingOfStaticResources() { From 064b479a2246280eee707f828c483b52047cad01 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 17:24:38 +0900 Subject: [PATCH 32/44] refactor: simplify checking incomplete headers received MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 최가희 <60508828+cutehumanS2@users.noreply.github.com> --- .../org/apache/coyote/http11/Http11InputStreamReader.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java index e1507773b6..96ef2d9b74 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java @@ -19,16 +19,13 @@ public static List read(InputStream inputStream) throws IOException { List lines = new ArrayList<>(); String line; - boolean readingHeaders = true; while ((line = reader.readLine()) != null) { lines.add(line); if (line.isEmpty()) { - readingHeaders = false; break; } } - - if (readingHeaders) { + if (line == null) { log.warn("Incomplete headers received"); } From f2aa8d08800e2df8bf62139e9dc89a6ddf006c1d Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 17:28:17 +0900 Subject: [PATCH 33/44] refactor: change log message for login with session --- .../main/java/com/techcourse/controller/LoginController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java index 9d8f5c3f26..6d6bda2c39 100644 --- a/tomcat/src/main/java/com/techcourse/controller/LoginController.java +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -31,7 +31,7 @@ protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) if (session != null) { User user = (User) Objects.requireNonNull(session).getAttribute("user"); - log.info("세션 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); + log.info("이미 로그인한 사용자 입니다. - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); responseBuilder.status(Status.FOUND) .location("/index.html"); From 68c4be7505964228e1c9ab46f81bafb601d7a22d Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 17:37:49 +0900 Subject: [PATCH 34/44] refactor: extract CRLF to constant value --- .../apache/coyote/http11/Http11InputStreamReader.java | 5 +++-- .../java/org/apache/coyote/http11/HttpResponse.java | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java index 96ef2d9b74..f979f7518a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java @@ -11,6 +11,7 @@ public class Http11InputStreamReader { + private static final String CRLF = "\r\n"; private static final Logger log = LoggerFactory.getLogger(Http11InputStreamReader.class); public static List read(InputStream inputStream) throws IOException { @@ -30,7 +31,7 @@ public static List read(InputStream inputStream) throws IOException { } StringBuilder bodyBuilder = new StringBuilder(); - String requestHeaders = String.join("\r\n", lines); + String requestHeaders = String.join(CRLF, lines); int contentLength = getContentLength(requestHeaders); if (contentLength > 0) { char[] body = new char[contentLength]; @@ -46,7 +47,7 @@ public static List read(InputStream inputStream) throws IOException { } private static int getContentLength(String headers) { - for (String headerLine : headers.split("\r\n")) { + for (String headerLine : headers.split(CRLF)) { if (headerLine.toLowerCase().startsWith("content-length:")) { try { return Integer.parseInt(headerLine.split(":")[1].trim()); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java index d5334a5887..24f32edce7 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -9,6 +9,8 @@ public record HttpResponse(String protocolVersion, int statusCode, String statusText, Map headers, Map cookies, byte[] body) { + private static final String CRLF = "\r\n"; + public static Builder builder() { return new Builder().protocolVersion("HTTP/1.1"); } @@ -25,18 +27,18 @@ private static byte[] mergeByteArrays(byte[] array1, byte[] array2) { public byte[] toMessage() { StringBuilder builder = new StringBuilder(); builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append(" "); - headers.forEach((key, value) -> builder.append("\r\n").append(key).append(": ").append(value).append(" ")); + headers.forEach((key, value) -> builder.append(CRLF).append(key).append(": ").append(value).append(" ")); if (!cookies.isEmpty()) { String cookiesMessage = cookies.entrySet().stream() .map(Entry::toString) .collect(Collectors.joining("; ")); - builder.append("\r\n").append("Set-Cookie: ").append(cookiesMessage).append(" "); + builder.append(CRLF).append("Set-Cookie: ").append(cookiesMessage).append(" "); } if (body != null && body.length > 0) { - builder.append("\r\n").append("Content-Length: ").append(body.length).append(" "); - builder.append("\r\n\r\n"); + builder.append(CRLF).append("Content-Length: ").append(body.length).append(" "); + builder.append(CRLF.repeat(2)); return mergeByteArrays(builder.toString().getBytes(), body); } From c06389540895900273b97d33442b8328fab71950 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 18:16:08 +0900 Subject: [PATCH 35/44] refactor: extract reading functions on http reader --- .../http11/Http11InputStreamReader.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java index f979f7518a..290fb164df 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java @@ -11,12 +11,32 @@ public class Http11InputStreamReader { - private static final String CRLF = "\r\n"; private static final Logger log = LoggerFactory.getLogger(Http11InputStreamReader.class); public static List read(InputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + List lines = new ArrayList<>(readHeaders(reader)); + int contentLength = getContentLength(lines); + lines.add(readBody(contentLength, reader)); + + return lines; + } + + private static int getContentLength(List headers) { + for (String headerLine : headers) { + if (headerLine.toLowerCase().startsWith("content-length:")) { + try { + return Integer.parseInt(headerLine.split(":")[1].trim()); + } catch (NumberFormatException e) { + log.warn("Invalid Content-Length header format"); + } + } + } + return 0; + } + + private static List readHeaders(BufferedReader reader) throws IOException { List lines = new ArrayList<>(); String line; @@ -30,9 +50,12 @@ public static List read(InputStream inputStream) throws IOException { log.warn("Incomplete headers received"); } + return lines; + } + + private static String readBody(int contentLength, BufferedReader reader) throws IOException { StringBuilder bodyBuilder = new StringBuilder(); - String requestHeaders = String.join(CRLF, lines); - int contentLength = getContentLength(requestHeaders); + if (contentLength > 0) { char[] body = new char[contentLength]; int read = reader.read(body, 0, contentLength); @@ -41,21 +64,7 @@ public static List read(InputStream inputStream) throws IOException { } bodyBuilder.append(body, 0, read); } - lines.add(bodyBuilder.toString()); - - return lines; - } - private static int getContentLength(String headers) { - for (String headerLine : headers.split(CRLF)) { - if (headerLine.toLowerCase().startsWith("content-length:")) { - try { - return Integer.parseInt(headerLine.split(":")[1].trim()); - } catch (NumberFormatException e) { - log.warn("Invalid Content-Length header format"); - } - } - } - return 0; + return bodyBuilder.toString(); } } From 3971630fa4e4795021935d99f76bb0e447d6d305 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 18:27:17 +0900 Subject: [PATCH 36/44] refactor: rearrange parsing order --- .../org/apache/coyote/http11/HttpRequest.java | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java index 1d7410ea3d..71c4907221 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -23,26 +23,43 @@ public record HttpRequest( public static HttpRequest parse(List lines) { String[] startLineParts = lines.getFirst().split(" "); String method = startLineParts[0]; + String path = ""; Map parameters = Map.of(); - Map headers = extractHeaders(lines); - Map cookies = extractCookies(headers.get("Cookie")); - String protocolVersion = startLineParts[2]; - String body = null; - Pattern pattern = Pattern.compile("([^?]+)(\\?(.*))?"); Matcher matcher = pattern.matcher(startLineParts[1]); - if (matcher.find()) { path = matcher.group(1); parameters = extractParameters(matcher.group(3)); } + String protocolVersion = startLineParts[2]; + Map headers = extractHeaders(lines); + Map cookies = extractCookies(headers.get("Cookie")); + + String body = extractBody(lines); + + return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); + } + + private static String extractBody(List lines) { if (lines.size() > 1 && lines.get(lines.size() - 2).isEmpty()) { - body = lines.getLast(); + return lines.getLast(); } + return null; + } - return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); + private static Map extractHeaders(List lines) { + Map headers = new HashMap<>(); + + for (int i = 1; i < lines.size() - 2; i++) { + String[] lineParts = lines.get(i).trim().split(": "); + if (lineParts.length >= 2) { + headers.put(lineParts[0], lineParts[1]); + } + } + + return headers; } private static Map extractCookies(String cookieMessage) { @@ -66,19 +83,6 @@ private static Map extractCookies(String cookieMessage) { return cookies; } - private static Map extractHeaders(List lines) { - Map headers = new HashMap<>(); - - for (int i = 1; i < lines.size() - 2; i++) { - String[] lineParts = lines.get(i).trim().split(": "); - if (lineParts.length >= 2) { - headers.put(lineParts[0], lineParts[1]); - } - } - - return headers; - } - public static Map extractParameters(String query) { Map parameters = new HashMap<>(); From accfb4103dd53c6377ef65e3d57fa97eba1d9a07 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 18:40:27 +0900 Subject: [PATCH 37/44] fix: use session manager as singleton --- .../java/com/techcourse/controller/LoginController.java | 6 ++---- .../java/org/apache/catalina/session/SessionManager.java | 3 +++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java index 6d6bda2c39..8d5211c817 100644 --- a/tomcat/src/main/java/com/techcourse/controller/LoginController.java +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -16,18 +16,16 @@ public class LoginController extends AbstractController { - private final SessionManager sessionManager; private final ResourceController resourceController; public LoginController() { - this.sessionManager = new SessionManager(); this.resourceController = new ResourceController(); } @Override protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { if (request.cookies().containsKey(JSession.COOKIE_NAME)) { - HttpSession session = request.getSession(sessionManager); + HttpSession session = request.getSession(SessionManager.getInstance()); if (session != null) { User user = (User) Objects.requireNonNull(session).getAttribute("user"); @@ -49,7 +47,7 @@ protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) String sessionId = UUID.randomUUID().toString(); JSession session = new JSession(sessionId); session.setAttribute("user", user); - sessionManager.add(session); + SessionManager.getInstance().add(session); log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index d133fe7dc2..057c630b74 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -10,6 +10,9 @@ public class SessionManager implements Manager { private static final Map SESSIONS = new HashMap<>(); private static final SessionManager SESSION_MANAGER = new SessionManager(); + private SessionManager() { + } + public static SessionManager getInstance() { return SESSION_MANAGER; } From 79ba4ca98468104d2cd90583ce60ada11caeec7a Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 18:44:23 +0900 Subject: [PATCH 38/44] refactor: remove duplication finding session --- tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java index 71c4907221..2826b0cce2 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -112,7 +112,7 @@ public HttpSession getSession(SessionManager sessionManager) { return null; } - return sessionManager.findSession(sessionId); + return session; } public HttpRequest updatePath(String path) { From e1170256c81b52be80c2586324a94b302660c01c Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 18:47:52 +0900 Subject: [PATCH 39/44] refactor: delegate checking attribute to http request --- .../java/org/apache/catalina/session/SessionManager.java | 8 +++++++- .../main/java/org/apache/coyote/http11/HttpRequest.java | 7 +------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index 057c630b74..684e6d1fed 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -24,7 +24,13 @@ public void add(HttpSession session) { @Override public HttpSession findSession(String id) { - return SESSIONS.get(id); + HttpSession session = SESSIONS.get(id); + + if (session == null || session.getAttribute("user") == null) { + return null; + } + + return session; } @Override diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java index 2826b0cce2..9172f70da4 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -107,12 +107,7 @@ public HttpSession getSession(SessionManager sessionManager) { return null; } - HttpSession session = sessionManager.findSession(sessionId); - if (session == null || session.getAttribute("user") == null) { - return null; - } - - return session; + return sessionManager.findSession(sessionId); } public HttpRequest updatePath(String path) { From 26f150514e7178aa216181878868f23302717f86 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 19:00:15 +0900 Subject: [PATCH 40/44] refactor: use defined method to set content type --- .../src/main/java/com/techcourse/controller/HomeController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomcat/src/main/java/com/techcourse/controller/HomeController.java b/tomcat/src/main/java/com/techcourse/controller/HomeController.java index 2c66b69733..47141dcc32 100644 --- a/tomcat/src/main/java/com/techcourse/controller/HomeController.java +++ b/tomcat/src/main/java/com/techcourse/controller/HomeController.java @@ -9,7 +9,7 @@ public class HomeController extends AbstractController { @Override protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { responseBuilder.status(Status.OK) - .addHeader("Content-Type", "text/html;charset=utf-8") + .contentType("text/html") .body("Hello world!".getBytes()); } } From d3281a10980b86c7f7b17e3d3e093adf5ce7a005 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 19:07:17 +0900 Subject: [PATCH 41/44] refactor: use stream when get controller --- .../com/techcourse/controller/RequestMapping.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java index c07c246e05..a7587aac2d 100644 --- a/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java +++ b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java @@ -19,11 +19,10 @@ public RequestMapping() { } public Controller getController(HttpRequest request) { - for (final var entry : controllers.entrySet()) { - if (entry.getKey().test(request)) { - return entry.getValue(); - } - } - return defaultController; + return controllers.entrySet().stream() + .filter((entry) -> entry.getKey().test(request)) + .findAny() + .map(Map.Entry::getValue) + .orElse(defaultController); } } From f91dcee9d789baaedf4bfa387a6bbad4a71de655 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 19:16:22 +0900 Subject: [PATCH 42/44] refactor: use request mapping as static --- .../main/java/org/apache/coyote/http11/Http11Processor.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 1585d077e6..874a906045 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -12,13 +12,12 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final RequestMapping REQUEST_MAPPING = new RequestMapping(); private final Socket connection; - private final RequestMapping requestMapping; public Http11Processor(final Socket connection) { this.connection = connection; - this.requestMapping = new RequestMapping(); } @Override @@ -36,7 +35,7 @@ public void process(final Socket connection) { // log.debug(request.toString()); HttpResponse.Builder responseBuilder = HttpResponse.builder(); - requestMapping.getController(request) + REQUEST_MAPPING.getController(request) .service(request, responseBuilder); HttpResponse response = responseBuilder.build(); // log.debug(response.toString()); From d222e3c971dbf59ec05afcf1aa8dc14796617bbb Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 19:30:53 +0900 Subject: [PATCH 43/44] refactor: delegate getting session to session manager --- .../controller/LoginController.java | 22 +++++++++---------- .../catalina/session/SessionManager.java | 10 +++++++++ .../org/apache/coyote/http11/HttpRequest.java | 12 ---------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java index 8d5211c817..a4cdfb8abb 100644 --- a/tomcat/src/main/java/com/techcourse/controller/LoginController.java +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -24,17 +24,15 @@ public LoginController() { @Override protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { - if (request.cookies().containsKey(JSession.COOKIE_NAME)) { - HttpSession session = request.getSession(SessionManager.getInstance()); - if (session != null) { - User user = (User) Objects.requireNonNull(session).getAttribute("user"); + HttpSession session = SessionManager.getInstance().getSession(request); + if (session != null) { + User user = (User) Objects.requireNonNull(session).getAttribute("user"); - log.info("이미 로그인한 사용자 입니다. - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); + log.info("이미 로그인한 사용자 입니다. - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); - responseBuilder.status(Status.FOUND) - .location("/index.html"); - return; - } + responseBuilder.status(Status.FOUND) + .location("/index.html"); + return; } if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { @@ -45,9 +43,9 @@ protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { User user = optionalUser.get(); String sessionId = UUID.randomUUID().toString(); - JSession session = new JSession(sessionId); - session.setAttribute("user", user); - SessionManager.getInstance().add(session); + JSession jSession = new JSession(sessionId); + jSession.setAttribute("user", user); + SessionManager.getInstance().add(jSession); log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index 684e6d1fed..efa41aaeb8 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.Map; import org.apache.catalina.Manager; +import org.apache.coyote.http11.HttpRequest; public class SessionManager implements Manager { @@ -37,4 +38,13 @@ public HttpSession findSession(String id) { public void remove(HttpSession session) { SESSIONS.remove(session.getId()); } + + public HttpSession getSession(HttpRequest request) { + String sessionId = request.cookies().get(JSession.COOKIE_NAME); + if (sessionId == null) { + return null; + } + + return SESSIONS.get(sessionId); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java index 9172f70da4..6a06e4c756 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -1,6 +1,5 @@ package org.apache.coyote.http11; -import jakarta.servlet.http.HttpSession; import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.HashMap; @@ -8,8 +7,6 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.catalina.session.JSession; -import org.apache.catalina.session.SessionManager; public record HttpRequest( String method, @@ -101,15 +98,6 @@ public static Map extractParameters(String query) { return parameters; } - public HttpSession getSession(SessionManager sessionManager) { - String sessionId = cookies.get(JSession.COOKIE_NAME); - if (sessionId == null) { - return null; - } - - return sessionManager.findSession(sessionId); - } - public HttpRequest updatePath(String path) { return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); } From cb34c02fe53dbee1ac735a6c626139582d28af55 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sun, 8 Sep 2024 19:49:16 +0900 Subject: [PATCH 44/44] refactor: extract methods and simplify doGet on login --- .../controller/LoginController.java | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java index a4cdfb8abb..2b8f24a9fb 100644 --- a/tomcat/src/main/java/com/techcourse/controller/LoginController.java +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -12,6 +12,7 @@ import org.apache.catalina.session.SessionManager; import org.apache.coyote.http11.HttpRequest; import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpResponse.Builder; import org.apache.coyote.http11.Status; public class LoginController extends AbstractController { @@ -22,44 +23,52 @@ public LoginController() { this.resourceController = new ResourceController(); } + private static void processSessionLogin(Builder responseBuilder, HttpSession session) { + User user = (User) Objects.requireNonNull(session).getAttribute("user"); + + log.info("이미 로그인한 사용자 입니다. - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); + + responseBuilder.status(Status.FOUND) + .location("/index.html"); + } + + private static void processAccountLogin(Builder responseBuilder, User user) { + String sessionId = UUID.randomUUID().toString(); + JSession jSession = new JSession(sessionId); + jSession.setAttribute("user", user); + SessionManager.getInstance().add(jSession); + + log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); + + responseBuilder.status(Status.FOUND) + .location("/index.html") + .addCookie(JSession.COOKIE_NAME, sessionId); + } + @Override protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { HttpSession session = SessionManager.getInstance().getSession(request); if (session != null) { - User user = (User) Objects.requireNonNull(session).getAttribute("user"); - - log.info("이미 로그인한 사용자 입니다. - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); - - responseBuilder.status(Status.FOUND) - .location("/index.html"); + processSessionLogin(responseBuilder, session); return; } if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { - String account = request.parameters().get("account"); - String password = request.parameters().get("password"); - - Optional optionalUser = InMemoryUserRepository.findByAccount(account); - if (optionalUser.isPresent() && optionalUser.get().checkPassword(password)) { - User user = optionalUser.get(); - String sessionId = UUID.randomUUID().toString(); - JSession jSession = new JSession(sessionId); - jSession.setAttribute("user", user); - SessionManager.getInstance().add(jSession); - - log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); - - responseBuilder.status(Status.FOUND) - .location("/index.html") - .addCookie(JSession.COOKIE_NAME, sessionId); - return; - } - - responseBuilder.status(Status.FOUND) - .location("/401.html"); + findValidUser(request).ifPresentOrElse( + user -> processAccountLogin(responseBuilder, user), + () -> responseBuilder.status(Status.FOUND).location("/401.html") + ); return; } resourceController.doGet(request.updatePath("login.html"), responseBuilder); } + + private Optional findValidUser(HttpRequest request) { + String account = request.parameters().get("account"); + String password = request.parameters().get("password"); + + return InMemoryUserRepository.findByAccount(account) + .filter(user -> user.checkPassword(password)); + } }