From fed02f6f5f4308400e55c160d9495cad010f5bfb Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 11:11:09 +0900 Subject: [PATCH 01/31] fix: remove implementation logback-classic on gradle (#501) --- study/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..87a1f0313c 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,7 +19,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'ch.qos.logback:logback-classic:1.5.7' 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 7e9135698878932274ddc1f523ba817ed9c56c70 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 13:51:07 +0900 Subject: [PATCH 02/31] fix: add threads min-spare configuration on properties (#502) --- study/src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..e3503a5fb9 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,5 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 From b1871e804866427e2cc6ba6784f8200a12b921a3 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Wed, 4 Sep 2024 09:58:34 +0900 Subject: [PATCH 03/31] =?UTF-8?q?test:=20File=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cache/com/example/GreetingController.java | 3 +-- .../com/example/version/ResourceVersion.java | 3 +-- study/src/test/java/study/FileTest.java | 20 ++++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..2416d9ac6e 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,12 +1,11 @@ package cache.com.example; +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 { diff --git a/study/src/main/java/cache/com/example/version/ResourceVersion.java b/study/src/main/java/cache/com/example/version/ResourceVersion.java index 27a2f22813..215ba7a705 100644 --- a/study/src/main/java/cache/com/example/version/ResourceVersion.java +++ b/study/src/main/java/cache/com/example/version/ResourceVersion.java @@ -1,10 +1,9 @@ package cache.com.example.version; -import org.springframework.stereotype.Component; - import jakarta.annotation.PostConstruct; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import org.springframework.stereotype.Component; @Component public class ResourceVersion { diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..d5b2546bd9 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,13 +1,14 @@ 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.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; 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 파일을 제공 할 수 있어야 한다. @@ -28,7 +29,7 @@ class FileTest { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + final String actual = getClass().getClassLoader().getResource(fileName).getPath(); assertThat(actual).endsWith(fileName); } @@ -40,14 +41,15 @@ class FileTest { * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; // todo - final Path path = null; + String pathStr = getClass().getClassLoader().getResource(fileName).getPath(); + Path path = new File(pathStr).toPath(); // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } From 97af7b2d1371cfd247eb03f2f22df99a847a2ee1 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Wed, 4 Sep 2024 09:58:40 +0900 Subject: [PATCH 04/31] =?UTF-8?q?test:=20IOStream=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/test/java/study/IOStreamTest.java | 44 +++++++++++++++------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..1b43ae6915 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,14 +1,24 @@ 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)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. @@ -39,7 +49,7 @@ class OutputStream_학습_테스트 { * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + * * write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -53,6 +63,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); @@ -63,7 +74,7 @@ class OutputStream_학습_테스트 { /** * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * + * * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 @@ -78,6 +89,7 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); @@ -96,6 +108,8 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + } verify(outputStream, atLeastOnce()).close(); } @@ -108,7 +122,7 @@ class OutputStream_학습_테스트 { * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. * InputStream의 read() 메서드는 기반 메서드이다. * public abstract int read() throws IOException; - * + * * InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested @@ -128,7 +142,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -148,6 +162,8 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } @@ -169,12 +185,12 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @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()); @@ -204,8 +220,12 @@ class InputStreamReader_학습_테스트 { "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); final StringBuilder actual = new StringBuilder(); + bufferedReader.lines() + .forEach(str -> actual.append(str).append("\r\n")); assertThat(actual).hasToString(emoji); } From 50f9b6ac9010550e85eb552dd6c4951c5b7147bd Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Wed, 4 Sep 2024 16:58:16 +0900 Subject: [PATCH 05/31] =?UTF-8?q?feat:=20index.html=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 52 ++++++++++++++----- .../coyote/http11/Http11ProcessorTest.java | 11 ++-- 2 files changed, 43 insertions(+), 20 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..07026f0fa0 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,13 +1,20 @@ package org.apache.coyote.http11; import com.techcourse.exception.UncheckedServletException; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; 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); @@ -26,20 +33,37 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()))) { + + String requestLine = bufferedReader.readLine(); + + if (!requestLine.isBlank()) { + String method = requestLine.split(" ")[0]; + String url = requestLine.split(" ")[1]; + String version = requestLine.split(" ")[2]; + + String responseBody = "Hello world!"; + + if (method.equals("GET") && url.equals("/index.html")) { + URL resource = getClass().getClassLoader().getResource("static/index.html"); - final var responseBody = "Hello world!"; + if (resource != null) { + responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + responseBody = responseBody.replace("\r\n", "\n"); + } + } - 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); + final String response = String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: text/html;charset=utf-8 ", + "Content-Length: " + responseBody.getBytes(StandardCharsets.UTF_8).length + " ", + "", + responseBody); - outputStream.write(response.getBytes()); - outputStream.flush(); + bufferedWriter.write(response); + bufferedWriter.flush(); + } } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index 2aba8c56e0..8af591bcf0 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,14 +1,13 @@ package org.apache.coyote.http11; -import org.junit.jupiter.api.Test; -import support.StubSocket; +import static org.assertj.core.api.Assertions.assertThat; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import support.StubSocket; class Http11ProcessorTest { @@ -35,7 +34,7 @@ void process() { @Test void index() throws IOException { // given - final String httpRequest= String.join("\r\n", + final String httpRequest = String.join("\r\n", "GET /index.html HTTP/1.1 ", "Host: localhost:8080 ", "Connection: keep-alive ", @@ -53,7 +52,7 @@ void index() throws IOException { var expected = "HTTP/1.1 200 OK \r\n" + "Content-Type: text/html;charset=utf-8 \r\n" + "Content-Length: 5564 \r\n" + - "\r\n"+ + "\r\n" + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected); From b628eade3f2e09d6224c31803731549de82273ff Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Thu, 5 Sep 2024 10:44:57 +0900 Subject: [PATCH 06/31] =?UTF-8?q?feat:=20ContentType=20Enum=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/apache/coyote/http11/ContentType.java | 40 +++++++++++++++++++ .../apache/coyote/http11/ContentTypeTest.java | 27 +++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/ContentType.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java new file mode 100644 index 0000000000..46acce2957 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java @@ -0,0 +1,40 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; + +public enum ContentType { + + HTML("text/html", ".html"), + CSS("text/css", ".css"), + JS("text/javascript", ".js"), + ICO("image/x-icon", ".ico"); + + private static final String CHARSET = "charset=utf-8"; + + private final String mediaType; + private final String extension; + + ContentType(String mediaType, String extension) { + this.mediaType = mediaType; + this.extension = extension; + } + + public static String findWithCharset(String path) { + return find(path).mediaType + ";" + CHARSET; + } + + public static ContentType find(String path) { + return Arrays.stream(values()) + .filter(contentType -> path.toLowerCase().endsWith(contentType.getExtension())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 컨텐츠 타입")); + } + + public String getMediaType() { + return mediaType; + } + + public String getExtension() { + return extension; + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java b/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java new file mode 100644 index 0000000000..3988f84a9c --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ContentTypeTest { + + @DisplayName("extension과 일치하는 ContentType을 반환한다.") + @Test + void successFindTest() { + assertAll( + () -> assertThat(ContentType.find("/index.html")).isEqualTo(ContentType.HTML), + () -> assertThat(ContentType.find("/index.css")).isEqualTo(ContentType.CSS) + ); + } + + @DisplayName("extension과 일치하는 ContentType이 없으면 예외를 반환한다.") + @Test + void failureFindTest() { + assertThatThrownBy(() -> ContentType.findWithCharset("/index.jazz")) + .isInstanceOf(IllegalArgumentException.class); + } +} From 0a8101efbbbdb5be294c33788c732f45edbd0001 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Thu, 5 Sep 2024 13:43:26 +0900 Subject: [PATCH 07/31] =?UTF-8?q?feat:=20=EC=9A=94=EC=B2=AD=20URI=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=EC=9E=90=EC=97=90=20=EB=94=B0=EB=9D=BC=20Con?= =?UTF-8?q?tentType=EC=9D=84=20=EB=8F=99=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/apache/coyote/http11/ContentType.java | 2 +- .../apache/coyote/http11/Http11Processor.java | 34 ++++++++++++++----- .../apache/coyote/http11/ContentTypeTest.java | 6 ++-- .../coyote/http11/Http11ProcessorTest.java | 32 +++++++++++++++++ 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java index 46acce2957..67e9f8b02b 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java @@ -27,7 +27,7 @@ public static ContentType find(String path) { return Arrays.stream(values()) .filter(contentType -> path.toLowerCase().endsWith(contentType.getExtension())) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 컨텐츠 타입")); + .orElse(HTML); } public String getMediaType() { 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 07026f0fa0..c738128887 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -11,6 +11,9 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,26 +40,39 @@ public void process(final Socket connection) { BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()))) { String requestLine = bufferedReader.readLine(); + if (requestLine == null || requestLine.isBlank()) { + return; + } + + System.out.println(requestLine); + + List header = new ArrayList<>(); + String headerLine = bufferedReader.readLine(); + while (!headerLine.isBlank()) { + header.add(headerLine); + headerLine = bufferedReader.readLine(); + } if (!requestLine.isBlank()) { - String method = requestLine.split(" ")[0]; - String url = requestLine.split(" ")[1]; - String version = requestLine.split(" ")[2]; + String[] requestParts = requestLine.split(" "); + String method = requestParts[0]; + String uri = requestParts[1]; + String version = requestParts[2]; String responseBody = "Hello world!"; - if (method.equals("GET") && url.equals("/index.html")) { - URL resource = getClass().getClassLoader().getResource("static/index.html"); + if (!"/".equals(uri)) { + String resourcePath = "static" + uri; + Optional resource = Optional.ofNullable(getClass().getClassLoader().getResource(resourcePath)); - if (resource != null) { - responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - responseBody = responseBody.replace("\r\n", "\n"); + if (resource.isPresent()) { + responseBody = new String(Files.readAllBytes(new File(resource.get().getFile()).toPath())); } } final String response = String.join("\r\n", "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", + "Content-Type: " + ContentType.findWithCharset(uri) + " ", "Content-Length: " + responseBody.getBytes(StandardCharsets.UTF_8).length + " ", "", responseBody); diff --git a/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java b/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java index 3988f84a9c..e3af1bd700 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/ContentTypeTest.java @@ -1,7 +1,6 @@ package org.apache.coyote.http11; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import org.junit.jupiter.api.DisplayName; @@ -18,10 +17,9 @@ void successFindTest() { ); } - @DisplayName("extension과 일치하는 ContentType이 없으면 예외를 반환한다.") + @DisplayName("extension과 일치하는 ContentType이 없으면 text/html을 반환한다.") @Test void failureFindTest() { - assertThatThrownBy(() -> ContentType.findWithCharset("/index.jazz")) - .isInstanceOf(IllegalArgumentException.class); + assertThat(ContentType.find("/index.html")).isEqualTo(ContentType.HTML); } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index 8af591bcf0..bcf27f4da8 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -6,11 +6,13 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import support.StubSocket; class Http11ProcessorTest { + @DisplayName("Hello World!를 출력한다.") @Test void process() { // given @@ -31,6 +33,7 @@ void process() { assertThat(socket.output()).isEqualTo(expected); } + @DisplayName("index.html를 응답한다.") @Test void index() throws IOException { // given @@ -57,4 +60,33 @@ void index() throws IOException { assertThat(socket.output()).isEqualTo(expected); } + + @DisplayName("styles.css를 응답한다.") + @Test + void css() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /css/styles.css HTTP/1.1 ", + "Host: localhost:8080 ", + "Accept: text/css,*/*;q=0.1 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/css/styles.css"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/css;charset=utf-8 \r\n" + + "Content-Length: 223255 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } } From 024165dfbadcbde26d760004c4a2769483a0eefd Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Thu, 5 Sep 2024 15:26:00 +0900 Subject: [PATCH 08/31] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20HTM?= =?UTF-8?q?L=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 31 +++++++++++++++++-- 1 file changed, 28 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 c738128887..45a2897b68 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,6 +1,8 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.model.User; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -12,7 +14,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.coyote.Processor; import org.slf4j.Logger; @@ -44,8 +48,6 @@ public void process(final Socket connection) { return; } - System.out.println(requestLine); - List header = new ArrayList<>(); String headerLine = bufferedReader.readLine(); while (!headerLine.isBlank()) { @@ -62,11 +64,34 @@ public void process(final Socket connection) { String responseBody = "Hello world!"; if (!"/".equals(uri)) { - String resourcePath = "static" + uri; + String path = uri; + String queryString = null; + Map params = new HashMap<>(); + + if (path.contains("?")) { + int index = uri.indexOf("?"); + path = uri.substring(0, index); + queryString = uri.substring(index + 1); + + for (String param : queryString.split("&")) { + params.put(param.split("=")[0], param.split("=")[1]); + } + } + + String resourcePath = "static" + path; + if (!resourcePath.contains(".") || resourcePath.lastIndexOf("/") > resourcePath.lastIndexOf(".")) { + resourcePath = resourcePath + ".html"; + } + Optional resource = Optional.ofNullable(getClass().getClassLoader().getResource(resourcePath)); if (resource.isPresent()) { responseBody = new String(Files.readAllBytes(new File(resource.get().getFile()).toPath())); + + if ("/login".equals(path)) { + Optional user = InMemoryUserRepository.findByAccount(params.get("account")); + user.ifPresent(u -> log.info(u.toString())); + } } } From e5cd64380f2e8b60d823506b83141ae6d1d10a70 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Thu, 5 Sep 2024 16:13:16 +0900 Subject: [PATCH 09/31] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EC=84=B1=EA=B3=B5,=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 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 45a2897b68..c611609a86 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -11,6 +11,7 @@ import java.io.OutputStreamWriter; import java.net.Socket; import java.net.URL; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; @@ -65,17 +66,10 @@ public void process(final Socket connection) { if (!"/".equals(uri)) { String path = uri; - String queryString = null; - Map params = new HashMap<>(); if (path.contains("?")) { int index = uri.indexOf("?"); path = uri.substring(0, index); - queryString = uri.substring(index + 1); - - for (String param : queryString.split("&")) { - params.put(param.split("=")[0], param.split("=")[1]); - } } String resourcePath = "static" + path; @@ -89,8 +83,48 @@ public void process(final Socket connection) { responseBody = new String(Files.readAllBytes(new File(resource.get().getFile()).toPath())); if ("/login".equals(path)) { - Optional user = InMemoryUserRepository.findByAccount(params.get("account")); - user.ifPresent(u -> log.info(u.toString())); + if (uri.contains("?")) { + Map params = new HashMap<>(); + String queryString = uri.substring(uri.indexOf("?") + 1); + + for (String param : queryString.split("&")) { + String[] keyValue = param.split("="); + + String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8); + String value = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); + params.put(key, value); + } + + Optional optionalUser = InMemoryUserRepository.findByAccount( + params.get("account")); + + if (optionalUser.isPresent()) { + User user = optionalUser.get(); + if (user.checkPassword(params.get("password"))) { + log.info("user : {}", user); + + String response = String.join("\r\n", + "HTTP/1.1 302 FOUND ", + "Location: /index.html ", + "Content-Length: 0 ", + ""); + + bufferedWriter.write(response); + bufferedWriter.flush(); + return; + } + } + + String response = String.join("\r\n", + "HTTP/1.1 302 FOUND ", + "Location: /401.html ", + "Content-Length: 0 ", + ""); + + bufferedWriter.write(response); + bufferedWriter.flush(); + return; + } } } } From 23f90920878dc31f739fcbaee13de2940e653f21 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Thu, 5 Sep 2024 16:22:44 +0900 Subject: [PATCH 10/31] =?UTF-8?q?feat:=20HTTP=20GET=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=EA=B1=B4=EC=9D=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/org/apache/coyote/http11/Http11Processor.java | 2 +- 1 file changed, 1 insertion(+), 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 c611609a86..c1048d931b 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -64,7 +64,7 @@ public void process(final Socket connection) { String responseBody = "Hello world!"; - if (!"/".equals(uri)) { + if ("GET".equals(method) && !"/".equals(uri)) { String path = uri; if (path.contains("?")) { From 2c65d2dc446b9140fd5e72bcecc90f575fcbde83 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Thu, 5 Sep 2024 19:27:50 +0900 Subject: [PATCH 11/31] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=84=20GET=EC=97=90=EC=84=9C=20POST?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 104 +++++++++------ tomcat/src/main/resources/static/login.html | 125 +++++++++--------- 2 files changed, 128 insertions(+), 101 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 c1048d931b..c86e5e17f9 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -14,9 +14,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.coyote.Processor; @@ -49,13 +47,23 @@ public void process(final Socket connection) { return; } - List header = new ArrayList<>(); + Map headers = new HashMap<>(); String headerLine = bufferedReader.readLine(); while (!headerLine.isBlank()) { - header.add(headerLine); + String[] header = headerLine.split(": "); + headers.put(header[0], header[1]); headerLine = bufferedReader.readLine(); } + StringBuilder body = new StringBuilder(); + int contentLength = Integer.parseInt(headers.getOrDefault("Content-Length", "0")); + + if (contentLength > 0) { + char[] bodyChars = new char[contentLength]; + bufferedReader.read(bodyChars, 0, contentLength); + body.append(bodyChars); + } + if (!requestLine.isBlank()) { String[] requestParts = requestLine.split(" "); String method = requestParts[0]; @@ -64,7 +72,7 @@ public void process(final Socket connection) { String responseBody = "Hello world!"; - if ("GET".equals(method) && !"/".equals(uri)) { + if (!"/".equals(uri)) { String path = uri; if (path.contains("?")) { @@ -72,52 +80,51 @@ public void process(final Socket connection) { path = uri.substring(0, index); } - String resourcePath = "static" + path; - if (!resourcePath.contains(".") || resourcePath.lastIndexOf("/") > resourcePath.lastIndexOf(".")) { - resourcePath = resourcePath + ".html"; - } + if ("GET".equals(method) && "/login".equals(path)) { + String resourcePath = "static" + path + ".html"; - Optional resource = Optional.ofNullable(getClass().getClassLoader().getResource(resourcePath)); + Optional resource = Optional.ofNullable( + getClass().getClassLoader().getResource(resourcePath)); - if (resource.isPresent()) { - responseBody = new String(Files.readAllBytes(new File(resource.get().getFile()).toPath())); + if (resource.isPresent()) { + responseBody = new String(Files.readAllBytes(new File(resource.get().getFile()).toPath())); - if ("/login".equals(path)) { - if (uri.contains("?")) { - Map params = new HashMap<>(); - String queryString = uri.substring(uri.indexOf("?") + 1); + final String response = String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: " + ContentType.findWithCharset(uri) + " ", + "Content-Length: " + responseBody.getBytes(StandardCharsets.UTF_8).length + " ", + "", + responseBody); - for (String param : queryString.split("&")) { - String[] keyValue = param.split("="); - - String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8); - String value = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); - params.put(key, value); - } + bufferedWriter.write(response); + bufferedWriter.flush(); + return; + } + } - Optional optionalUser = InMemoryUserRepository.findByAccount( - params.get("account")); + if ("POST".equals(method) && "/login".equals(path)) { + Map params = new HashMap<>(); - if (optionalUser.isPresent()) { - User user = optionalUser.get(); - if (user.checkPassword(params.get("password"))) { - log.info("user : {}", user); + String[] paramPairs = body.toString().split("&"); + for (String pair : paramPairs) { + String[] keyValue = pair.split("="); + if (keyValue.length == 2) { + params.put(URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8), + URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8)); + } + } - String response = String.join("\r\n", - "HTTP/1.1 302 FOUND ", - "Location: /index.html ", - "Content-Length: 0 ", - ""); + Optional optionalUser = InMemoryUserRepository.findByAccount( + params.get("account")); - bufferedWriter.write(response); - bufferedWriter.flush(); - return; - } - } + if (optionalUser.isPresent()) { + User user = optionalUser.get(); + if (user.checkPassword(params.get("password"))) { + log.info("user : {}", user); String response = String.join("\r\n", "HTTP/1.1 302 FOUND ", - "Location: /401.html ", + "Location: /index.html ", "Content-Length: 0 ", ""); @@ -126,9 +133,26 @@ public void process(final Socket connection) { return; } } + + String response = String.join("\r\n", + "HTTP/1.1 302 FOUND ", + "Location: /401.html ", + "Content-Length: 0 ", + ""); + + bufferedWriter.write(response); + bufferedWriter.flush(); + return; } } + String resourcePath = "static" + uri; + Optional resource = Optional.ofNullable(getClass().getClassLoader().getResource(resourcePath)); + + if (resource.isPresent()) { + responseBody = new String(Files.readAllBytes(new File(resource.get().getFile()).toPath())); + } + final String response = String.join("\r\n", "HTTP/1.1 200 OK ", "Content-Type: " + ContentType.findWithCharset(uri) + " ", diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..d406e1fe8f 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -1,66 +1,69 @@ - - - - - - - 로그인 - - - - -
-
-
-
-
-
-
-

로그인

-
-
-
- - -
-
- - -
-
- -
-
-
- -
-
-
-
-
-
-