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' 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/cachecontrol/CacheControlInterceptor.java b/study/src/main/java/cache/com/example/cachecontrol/CacheControlInterceptor.java new file mode 100644 index 0000000000..0d82209124 --- /dev/null +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheControlInterceptor.java @@ -0,0 +1,22 @@ +package cache.com.example.cachecontrol; + +import static com.google.common.net.HttpHeaders.CACHE_CONTROL; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.HandlerInterceptor; + +public class CacheControlInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String cacheControl = CacheControl + .noCache() + .cachePrivate() + .getHeaderValue(); + + response.setHeader(CACHE_CONTROL, cacheControl); + 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..1c8197d1e2 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -9,5 +9,6 @@ public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new CacheControlInterceptor()); } } diff --git a/study/src/main/java/cache/com/example/cachecontrol/WebMvcConfig.java b/study/src/main/java/cache/com/example/cachecontrol/WebMvcConfig.java new file mode 100644 index 0000000000..c09903919f --- /dev/null +++ b/study/src/main/java/cache/com/example/cachecontrol/WebMvcConfig.java @@ -0,0 +1,15 @@ +package cache.com.example.cachecontrol; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**") + .addResourceLocations("classpath:/templates/", "classpath:/static/"); + } +} 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..52f1ec46b0 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,29 @@ package cache.com.example.etag; +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; + +import cache.com.example.version.ResourceVersion; +import org.springframework.beans.factory.annotation.Autowired; +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; -// } + private final ResourceVersion version; + + @Autowired + public EtagFilterConfiguration(ResourceVersion version) { + this.version = version; + } + + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/*"); + return filterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..57504d8dbd 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -1,7 +1,9 @@ package cache.com.example.version; +import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -20,6 +22,7 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic()); } } 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/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..d596ede8f9 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,12 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 10 +spring: + mvc: + view: + suffix: .html 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"); } 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); } diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/manager/Manager.java similarity index 89% rename from tomcat/src/main/java/org/apache/catalina/Manager.java rename to tomcat/src/main/java/org/apache/catalina/manager/Manager.java index e69410f6a9..16944560be 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/manager/Manager.java @@ -1,8 +1,7 @@ -package org.apache.catalina; - -import jakarta.servlet.http.HttpSession; +package org.apache.catalina.manager; import java.io.IOException; +import java.util.Optional; /** * A Manager manages the pool of Sessions that are associated with a @@ -29,7 +28,7 @@ public interface Manager { * * @param session Session to be added */ - void add(HttpSession session); + void add(Session session); /** * Return the active Session, associated with this Manager, with the @@ -45,12 +44,12 @@ public interface Manager { * @return the request session or {@code null} if a session with the * requested ID could not be found */ - HttpSession findSession(String id) throws IOException; + Optional findSession(String id) throws IOException; /** * Remove this Session from the active Sessions for this Manager. * * @param session Session to be removed */ - void remove(HttpSession session); + void remove(Session session); } diff --git a/tomcat/src/main/java/org/apache/catalina/manager/Session.java b/tomcat/src/main/java/org/apache/catalina/manager/Session.java new file mode 100644 index 0000000000..f19a4fbcce --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/manager/Session.java @@ -0,0 +1,27 @@ +package org.apache.catalina.manager; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map values; + + public Session(String id) { + this.id = id; + values = new HashMap<>(); + } + + public String getId() { + return id; + } + + public void setAttribute(String name, Object value) { + values.put(name, value); + } + + public void removeAttribute(String name) { + values.remove(name); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/manager/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/manager/SessionManager.java new file mode 100644 index 0000000000..7c23e30ad1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/manager/SessionManager.java @@ -0,0 +1,26 @@ +package org.apache.catalina.manager; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionManager implements Manager { + + private static final Map STORE = new ConcurrentHashMap<>(); + + @Override + public void add(Session session) { + STORE.put(session.getId(), session); + } + + @Override + public Optional findSession(String id) throws IOException { + return Optional.ofNullable(STORE.get(id)); + } + + @Override + public void remove(Session session) { + STORE.remove(session.getId()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Cookie.java b/tomcat/src/main/java/org/apache/coyote/http11/Cookie.java new file mode 100644 index 0000000000..9c57023313 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Cookie.java @@ -0,0 +1,47 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class Cookie { + + public static final String JSESSIONID = "JSESSIONID"; + private static final String COOKIE_DELIMITER = "; "; + private static final String COOKIE_SEPARATOR = "="; + + private static final int VALID_COOKIE_PAIR_LENGTH = 2; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map cookies; + + public Cookie(String cookieHeader) { + this.cookies = Arrays.stream(cookieHeader.split(COOKIE_DELIMITER)) + .map(cookies -> cookies.split(COOKIE_SEPARATOR)) + .filter(cookie -> cookie.length == VALID_COOKIE_PAIR_LENGTH) + .collect(Collectors.toMap( + cookie -> cookie[KEY_INDEX], + cookie -> cookie[VALUE_INDEX]) + ); + } + + public void addCookie(String key, String value) { + cookies.put(key, value); + } + + public String toCookieHeader() { + return cookies.entrySet().stream() + .map(entry -> entry.getKey() + COOKIE_SEPARATOR + entry.getValue()) + .collect(Collectors.joining(COOKIE_DELIMITER)); + } + + public Optional getJSessionId() { + return Optional.ofNullable(cookies.get(JSESSIONID)); + } + + public Map getCookies() { + return cookies; + } +} 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..5c8571476c 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,31 @@ 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; +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 java.util.Optional; +import java.util.UUID; +import org.apache.catalina.manager.Session; +import org.apache.catalina.manager.SessionManager; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.body.RequestBody; +import org.apache.coyote.http11.request.startLine.HttpMethod; +import org.apache.coyote.http11.response.header.ContentType; +import org.apache.coyote.http11.response.startLine.HttpStatus; 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,22 +44,222 @@ 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())) + ) { + SessionManager sessionManager = new SessionManager(); - final var responseBody = "Hello world!"; + HttpRequest httpRequest = HttpRequest.parse(bufferedReader); + HttpMethod method = httpRequest.getHttpMethod(); + String uri = httpRequest.getUri(); + String key = method + " " + uri; - 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 ("GET /".equals(key)) { + handleRootGetRequest(bufferedWriter); + return; + } + + String resourcePath = "static" + uri; + URL resource = getClass().getClassLoader().getResource(resourcePath); + if (resource != null) { + handleStaticResourceRequest(resource, uri, bufferedWriter); + return; + } + + switch (key) { + case "GET /login" -> handleLoginGetRequest(httpRequest, sessionManager, bufferedWriter); + case "POST /login" -> handleLoginPostRequest(httpRequest, sessionManager, bufferedWriter); + case "GET /register" -> handleRegisterGetRequest(bufferedWriter); + case "POST /register" -> handleRegisterPostRequest(httpRequest, bufferedWriter); + default -> handleNotFound(bufferedWriter); + } - outputStream.write(response.getBytes()); - outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private void handleStaticResourceRequest( + URL resource, String uri, BufferedWriter bufferedWriter + ) throws IOException { + File file = new File(resource.getFile()); + String responseBody = Files.readString(file.toPath(), StandardCharsets.UTF_8); + String contentType = ContentType.findWithCharset(uri); + int contentLength = responseBody.getBytes(StandardCharsets.UTF_8).length; + + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.OK.compose() + " ", + "Content-Type: " + contentType + " ", + "Content-Length: " + contentLength + " ", + "", + responseBody + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + } + + private void handleRootGetRequest(BufferedWriter bufferedWriter) throws IOException { + String responseBody = "Hello world!"; + String contentType = ContentType.findWithCharset("/"); + int contentLength = responseBody.getBytes(StandardCharsets.UTF_8).length; + + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.OK.compose() + " ", + "Content-Type: " + contentType + " ", + "Content-Length: " + contentLength + " ", + "", + responseBody + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + } + + private void handleLoginGetRequest( + HttpRequest httpRequest, SessionManager sessionManager, BufferedWriter bufferedWriter + ) throws IOException { + String resourcePath = "static/login.html"; + Optional resource = Optional.ofNullable(getClass().getClassLoader().getResource(resourcePath)); + + if (resource.isPresent()) { + Optional cookieHeader = httpRequest.getHeader("Cookie"); + Cookie cookie = new Cookie(cookieHeader.orElse("")); + + Optional sessionId = cookie.getJSessionId(); + if (sessionId.isPresent()) { + Optional session = sessionManager.findSession(sessionId.get()); + + if (session.isPresent()) { + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.FOUND.compose() + " ", + "Location: /index.html ", + "Content-Length: 0 ", + ""); + + bufferedWriter.write(response); + bufferedWriter.flush(); + return; + } + } + + File file = new File(resource.get().getFile()); + String responseBody = Files.readString(file.toPath(), StandardCharsets.UTF_8); + String contentType = ContentType.findWithCharset(resourcePath); + int contentLength = responseBody.getBytes(StandardCharsets.UTF_8).length; + + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.OK.compose() + " ", + "Content-Type: " + contentType + " ", + "Content-Length: " + contentLength + " ", + "", + responseBody + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + } + } + + private void handleLoginPostRequest( + HttpRequest httpRequest, SessionManager sessionManager, BufferedWriter bufferedWriter + ) throws IOException { + RequestBody requestBody = httpRequest.getRequestBody(); + Optional optionalUser = InMemoryUserRepository.findByAccount(requestBody.get("account")); + + if (optionalUser.isPresent() && optionalUser.get().checkPassword(requestBody.get("password"))) { + User user = optionalUser.get(); + Optional cookieHeader = httpRequest.getHeader("Cookie"); + Cookie cookie = new Cookie(cookieHeader.orElse("")); + + Optional sessionId = cookie.getJSessionId(); + if (sessionId.isEmpty()) { + UUID uuid = UUID.randomUUID(); + Session session = new Session(uuid.toString()); + session.setAttribute("user", user); + sessionManager.add(session); + cookie.addCookie(Cookie.JSESSIONID, uuid.toString()); + } + + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.FOUND.compose() + " ", + "Set-Cookie: " + cookie.toCookieHeader() + " ", + "Location: /index.html ", + "Content-Length: 0 ", + "" + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + + log.info("로그인 성공! 아이디 : {}", user.getAccount()); + return; + } + + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.FOUND.compose() + " ", + "Location: /401.html ", + "Content-Length: 0 ", + "" + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + } + + private void handleRegisterGetRequest(BufferedWriter bufferedWriter) throws IOException { + String resourcePath = "static/register.html"; + Optional resource = Optional.ofNullable(getClass().getClassLoader().getResource(resourcePath)); + + if (resource.isPresent()) { + File file = new File(resource.get().getFile()); + String responseBody = Files.readString(file.toPath(), StandardCharsets.UTF_8); + String contentType = ContentType.findWithCharset(resourcePath); + int contentLength = responseBody.getBytes(StandardCharsets.UTF_8).length; + + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.FOUND.compose() + " ", + "Content-Type: " + contentType + " ", + "Content-Length: " + contentLength + " ", + "", + responseBody + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + } + } + + private void handleRegisterPostRequest(HttpRequest httpRequest, BufferedWriter bufferedWriter) throws IOException { + RequestBody requestBody = httpRequest.getRequestBody(); + + String account = requestBody.get("account"); + String password = requestBody.get("password"); + String email = requestBody.get("email"); + User newUser = new User(account, password, email); + + InMemoryUserRepository.save(newUser); + + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.FOUND.compose() + " ", + "Location: /index.html ", + "Content-Length: 0 ", + "" + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + } + + private void handleNotFound(BufferedWriter bufferedWriter) throws IOException { + String response = String.join("\r\n", + "HTTP/1.1 " + HttpStatus.FOUND.compose() + " ", + "Location: /404.html ", + "Content-Length: 0 ", + "" + ); + + bufferedWriter.write(response); + bufferedWriter.flush(); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..7d018b02e1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,104 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.apache.coyote.http11.request.body.RequestBody; +import org.apache.coyote.http11.request.header.RequestHeaders; +import org.apache.coyote.http11.request.startLine.HttpMethod; +import org.apache.coyote.http11.request.startLine.RequestLine; + +public class HttpRequest { + + private final RequestLine requestLine; + private final RequestHeaders requestHeaders; + private final RequestBody requestBody; + + private HttpRequest(RequestLine requestLine, RequestHeaders requestHeaders, RequestBody requestBody) { + this.requestLine = requestLine; + this.requestHeaders = requestHeaders; + this.requestBody = requestBody; + } + + public static HttpRequest parse(BufferedReader bufferedReader) throws IOException { + RequestLine requestLine = new RequestLine(bufferedReader.readLine()); + RequestHeaders requestHeaders = createRequestHeaders(bufferedReader); + RequestBody requestBody = createRequestBody(bufferedReader, requestHeaders); + + return new HttpRequest(requestLine, requestHeaders, requestBody); + } + + private static RequestHeaders createRequestHeaders(BufferedReader bufferedReader) throws IOException { + Map headers = new HashMap<>(); + + String headerLine = bufferedReader.readLine(); + while (!headerLine.isBlank()) { + String[] header = headerLine.split(": "); + headers.put(header[0], header[1]); + headerLine = bufferedReader.readLine(); + } + + return new RequestHeaders(headers); + } + + private static RequestBody createRequestBody(BufferedReader bufferedReader, RequestHeaders requestHeaders) + throws IOException { + StringBuilder body = new StringBuilder(); + int contentLength = requestHeaders.get("Content-Length") + .map(Integer::parseInt) + .orElse(0); + + if (contentLength > 0) { + char[] bodyChars = new char[contentLength]; + bufferedReader.read(bodyChars, 0, contentLength); + body.append(bodyChars); + } + + Map params = new HashMap<>(); + + 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) + ); + } + } + + return new RequestBody(params); + } + + public boolean matchesMethod(HttpMethod method) { + return requestLine.matchesMethod(method); + } + + public Optional getHeader(String header) { + return requestHeaders.get(header); + } + + public RequestLine getRequestLine() { + return requestLine; + } + + public RequestHeaders getRequestHeaders() { + return requestHeaders; + } + + public RequestBody getRequestBody() { + return requestBody; + } + + public String getUri() { + return requestLine.getUri(); + } + + public HttpMethod getHttpMethod() { + return requestLine.getHttpMethod(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/body/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/body/RequestBody.java new file mode 100644 index 0000000000..0bbb099392 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/body/RequestBody.java @@ -0,0 +1,16 @@ +package org.apache.coyote.http11.request.body; + +import java.util.Map; + +public class RequestBody { + + private final Map params; + + public RequestBody(Map params) { + this.params = params; + } + + public String get(String key) { + return params.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/header/RequestHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/header/RequestHeaders.java new file mode 100644 index 0000000000..58ff94be8f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/header/RequestHeaders.java @@ -0,0 +1,17 @@ +package org.apache.coyote.http11.request.header; + +import java.util.Map; +import java.util.Optional; + +public class RequestHeaders { + + private final Map headers; + + public RequestHeaders(Map headers) { + this.headers = headers; + } + + public Optional get(String header) { + return Optional.ofNullable(headers.get(header)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/HttpMethod.java new file mode 100644 index 0000000000..59bc69599b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/HttpMethod.java @@ -0,0 +1,26 @@ +package org.apache.coyote.http11.request.startLine; + +import java.util.Arrays; + +public enum HttpMethod { + + GET, + HEAD, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, + TRACE; + + public static HttpMethod find(String name) { + return Arrays.stream(values()) + .filter(httpMethod -> httpMethod.name().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 HTTP METHOD 입니다.")); + } + + public String getName() { + return name(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/HttpVersion.java b/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/HttpVersion.java new file mode 100644 index 0000000000..f4cecfa550 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/HttpVersion.java @@ -0,0 +1,33 @@ +package org.apache.coyote.http11.request.startLine; + +import java.util.Objects; + +public class HttpVersion { + + private final String httpVersion; + + public HttpVersion(String httpVersion) { + this.httpVersion = httpVersion; + } + + public String getHttpVersion() { + return httpVersion; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HttpVersion that = (HttpVersion) o; + return Objects.equals(httpVersion, that.httpVersion); + } + + @Override + public int hashCode() { + return Objects.hashCode(httpVersion); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/RequestLine.java new file mode 100644 index 0000000000..931b2376a5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/startLine/RequestLine.java @@ -0,0 +1,54 @@ +package org.apache.coyote.http11.request.startLine; + +public class RequestLine { + + private static final String REQUEST_LINE_DELIMITER = " "; + private static final int HTTP_METHOD_INDEX = 0; + private static final int URI_INDEX = 1; + private static final int HTTP_VERSION_INDEX = 2; + private static final int VALID_PARTS_LENGTH = 3; + + private final HttpMethod httpMethod; + private final String uri; + private final HttpVersion httpVersion; + + public RequestLine(String requestLine) { + validateRequestLine(requestLine); + + String[] requestParts = splitRequestLine(requestLine); + + this.httpMethod = HttpMethod.find(requestParts[HTTP_METHOD_INDEX]); + this.uri = requestParts[URI_INDEX]; + this.httpVersion = new HttpVersion(requestParts[HTTP_VERSION_INDEX]); + } + + private void validateRequestLine(String requestLine) { + if (requestLine == null || requestLine.isBlank()) { + throw new IllegalArgumentException("요청 라인은 비어있거나 공백일 수 없습니다."); + } + } + + private String[] splitRequestLine(String requestLine) { + String[] requestParts = requestLine.split(REQUEST_LINE_DELIMITER); + if (requestParts.length != VALID_PARTS_LENGTH) { + throw new IllegalArgumentException("올바르지 않은 요청 라인 구조입니다."); + } + return requestParts; + } + + public boolean matchesMethod(HttpMethod method) { + return httpMethod == method; + } + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public String getUri() { + return uri; + } + + public HttpVersion getHttpVersion() { + return httpVersion; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/header/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/response/header/ContentType.java new file mode 100644 index 0000000000..0e1751e549 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/header/ContentType.java @@ -0,0 +1,40 @@ +package org.apache.coyote.http11.response.header; + +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() + .orElse(HTML); + } + + public String getMediaType() { + return mediaType; + } + + public String getExtension() { + return extension; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/startLine/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/response/startLine/HttpStatus.java new file mode 100644 index 0000000000..bbad71447a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/startLine/HttpStatus.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.response.startLine; + +public enum HttpStatus { + + OK(200, "OK"), + FOUND(302, "FOUND"); + + private final int code; + private final String message; + + HttpStatus(int code, String message) { + this.code = code; + this.message = message; + } + + public String compose() { + return getCode() + " " + getMessage(); + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/startLine/StatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/response/startLine/StatusLine.java new file mode 100644 index 0000000000..7c27942775 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/startLine/StatusLine.java @@ -0,0 +1,16 @@ +package org.apache.coyote.http11.response.startLine; + +public class StatusLine { + + private final String protocol; + private final HttpStatus httpStatus; + + public StatusLine(String protocol, HttpStatus httpStatus) { + this.protocol = protocol; + this.httpStatus = httpStatus; + } + + public String compose() { + return protocol + " " + httpStatus.compose(); + } +} 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 @@ - - - - - - - 로그인 - - - - -
-
-
-
-
-
-
-

로그인

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