diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..59afce5646 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -1,12 +1,11 @@ package thread.stage0; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; /** * 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. @@ -41,7 +40,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..c3ecd5c80c 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -1,13 +1,12 @@ package thread.stage0; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 스레드 풀은 무엇이고 어떻게 동작할까? @@ -31,8 +30,8 @@ void testNewFixedThreadPool() { executor.submit(logWithSleep("hello fixed thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; - final int expectedQueueSize = 0; + final int expectedPoolSize = 2; + final int expectedQueueSize = 1; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); @@ -46,7 +45,7 @@ void testNewCachedThreadPool() { executor.submit(logWithSleep("hello cached thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; + final int expectedPoolSize = 3; final int expectedQueueSize = 0; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); 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..d91f52d861 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java @@ -0,0 +1,24 @@ +package com.techcourse.controller; + +import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; + +public abstract class AbstractController implements Controller { + + @Override + public void service(HttpRequest httpRequest, HttpResponse httpResponse) { + if (httpRequest.isMethod(HttpMethod.GET)) { + doGet(httpRequest, httpResponse); + } + if (httpRequest.isMethod(HttpMethod.POST)) { + doPost(httpRequest, httpResponse); + } + + throw new IllegalArgumentException("유효하지 않은 메소드입니다."); + } + + abstract protected void doPost(HttpRequest httpRequest, HttpResponse httpResponse); + + abstract protected void doGet(HttpRequest httpRequest, HttpResponse httpResponse); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java b/tomcat/src/main/java/com/techcourse/controller/Controller.java similarity index 56% rename from tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java rename to tomcat/src/main/java/com/techcourse/controller/Controller.java index 3f2f18697a..7a1cffb57a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java +++ b/tomcat/src/main/java/com/techcourse/controller/Controller.java @@ -1,9 +1,10 @@ -package org.apache.coyote.http11.controller; +package com.techcourse.controller; import org.apache.coyote.http11.httprequest.HttpRequest; import org.apache.coyote.http11.httpresponse.HttpResponse; public interface Controller { - HttpResponse service(HttpRequest httpRequest); + void service(HttpRequest request, HttpResponse response) throws Exception; } + 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..1efa021996 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -0,0 +1,74 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.coyote.http11.exception.UnauthorizedException; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; +import org.apache.coyote.http11.session.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoginController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(LoginController.class); + private static final String LOGIN_PATH = "/login"; + private static final String ACCOUNT = "account"; + private static final String PASSWORD = "password"; + private static final String INDEX_PATH = "/index.html"; + + @Override + protected void doPost(HttpRequest httpRequest, HttpResponse httpResponse) { + if (validateUserInput(httpRequest)) { + log.error("입력하지 않은 항목이 있습니다."); + redirectLoginPage(httpRequest, httpResponse); + return; + } + acceptLogin(httpRequest, httpResponse); + } + + private void redirectLoginPage(HttpRequest httpRequest, HttpResponse httpResponse) { + httpResponse.location(httpRequest, LOGIN_PATH); + } + + private boolean validateUserInput(HttpRequest httpRequest) { + return !httpRequest.containsBody(ACCOUNT) || !httpRequest.containsBody(PASSWORD); + } + + private void acceptLogin(HttpRequest httpRequest, HttpResponse httpResponse) { + String account = httpRequest.getBodyValue(ACCOUNT); + String password = httpRequest.getBodyValue(PASSWORD); + + InMemoryUserRepository.findByAccount(account) + .filter(user -> user.checkPassword(password)) + .ifPresentOrElse( + user -> redirectWithCookie(httpRequest, httpResponse, user), + () -> { + log.error("존재하지 않는 계정이거나 비밀번호 불일치"); + throw new UnauthorizedException("존재하지 않는 계정입니다"); + } + ); + } + + private void redirectWithCookie(HttpRequest httpRequest, HttpResponse httpResponse, User user) { + Session session = httpRequest.getSession(); + session.setUser(user); + log.info(user.toString()); + httpResponse.location(httpRequest, INDEX_PATH); + httpResponse.setSession(session); + } + + @Override + protected void doGet(HttpRequest httpRequest, HttpResponse httpResponse) { + Session session = httpRequest.getSession(); + if (!session.hasUser()) { + httpResponse.ok(httpRequest); + httpResponse.staticResource(LOGIN_PATH); + return; + } + User user = (User) session.getUser(); + log.info(user.toString()); + httpResponse.location(httpRequest, INDEX_PATH); + httpResponse.setSession(session); + } +} 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..acb6eda770 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -0,0 +1,57 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RegisterController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(RegisterController.class); + private static final String REGISTER_PATH = "/register"; + private static final String ACCOUNT = "account"; + private static final String PASSWORD = "password"; + private static final String EMAIL = "email"; + private static final String INDEX_PATH = "/index.html"; + + @Override + protected void doPost(HttpRequest httpRequest, HttpResponse httpResponse) { + if (validateUserInput(httpRequest)) { + log.error("입력하지 않은 항목이 있습니다."); + redirectPage(httpRequest, httpResponse, REGISTER_PATH); + return; + } + acceptRegister(httpRequest, httpResponse); + } + + private void acceptRegister(HttpRequest httpRequest, HttpResponse httpResponse) { + String account = httpRequest.getBodyValue(ACCOUNT); + String password = httpRequest.getBodyValue(PASSWORD); + String email = httpRequest.getBodyValue(EMAIL); + if (InMemoryUserRepository.containsByAccount(account)) { + log.error("이미 존재하는 account입니다"); + redirectPage(httpRequest, httpResponse, REGISTER_PATH); + return; + } + InMemoryUserRepository.save(new User(account, password, email)); + redirectPage(httpRequest, httpResponse, INDEX_PATH); + } + + private boolean validateUserInput(HttpRequest httpRequest) { + return !httpRequest.containsBody(ACCOUNT) + || !httpRequest.containsBody(PASSWORD) + || !httpRequest.containsBody(EMAIL); + } + + @Override + protected void doGet(HttpRequest httpRequest, HttpResponse httpResponse) { + httpResponse.ok(httpRequest); + httpResponse.staticResource(httpRequest.getPath()); + } + + private void redirectPage(HttpRequest httpRequest, HttpResponse httpResponse, String path) { + httpResponse.location(httpRequest, path); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java similarity index 50% rename from tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java rename to tomcat/src/main/java/com/techcourse/controller/RequestMapping.java index fa012d5ca8..4f21da64cb 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java +++ b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java @@ -1,11 +1,9 @@ -package org.apache.coyote.http11; +package com.techcourse.controller; import java.util.HashMap; import java.util.Map; -import org.apache.coyote.http11.controller.Controller; -import org.apache.coyote.http11.controller.LoginController; -import org.apache.coyote.http11.controller.PageController; -import org.apache.coyote.http11.controller.RegisterController; +import org.apache.coyote.http11.exception.NotFoundException; +import org.apache.coyote.http11.httprequest.HttpRequest; public class RequestMapping { @@ -14,14 +12,14 @@ public class RequestMapping { public RequestMapping() { controllers.put("/login", new LoginController()); controllers.put("/register", new RegisterController()); - controllers.put("page", new PageController()); } - public Controller getController(String path) { + public Controller getController(HttpRequest httpRequest) { + String path = httpRequest.getPath(); if (controllers.containsKey(path)) { return controllers.get(path); } - return new PageController(); + throw new NotFoundException("존재하지 않는 경로입니다."); } } diff --git a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java index b041a264ea..67f2be3c77 100644 --- a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java @@ -23,5 +23,9 @@ public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } + public static boolean containsByAccount(String account) { + return database.containsKey(account); + } + private InMemoryUserRepository() {} } diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..c07f2c5405 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,8 +1,7 @@ package org.apache.catalina; -import jakarta.servlet.http.HttpSession; - import java.io.IOException; +import org.apache.coyote.http11.session.Session; /** * 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; + Session 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/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..d171bb84a8 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,13 +1,12 @@ package org.apache.catalina.connector; -import org.apache.coyote.http11.Http11Processor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import org.apache.coyote.http11.Http11Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Connector implements Runnable { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/CharSet.java b/tomcat/src/main/java/org/apache/coyote/http11/CharSet.java new file mode 100644 index 0000000000..c68ff941ae --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/CharSet.java @@ -0,0 +1,17 @@ +package org.apache.coyote.http11; + +public enum CharSet { + + UTF_8(";charset=utf-8") + ; + + private final String charset; + + CharSet(String charset) { + this.charset = charset; + } + + public String getCharset() { + return charset; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java deleted file mode 100644 index cd36e4a10c..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.Arrays; - -public enum ContentType { - - CSS("text/css;charset=utf-8", "css"), - JS("application/javascript;charset=utf-8", "js"), - HTML("text/html;charset=utf-8", "html"), - PNG("image/png", "png"), - JPG("image/jpeg", "jpeg") - ; - - private final String contentType; - private final String extention; - - ContentType(String contentType, String extention) { - this.contentType = contentType; - this.extention = extention; - } - - public String getContentType() { - return contentType; - } - - public static ContentType getContentType(String extention) { - return Arrays.stream(values()) - .filter(contentType1 -> contentType1.extention.equals(extention)) - .findAny() - .orElseThrow(); - } -} 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 d1486f2416..e623c76679 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,17 @@ package org.apache.coyote.http11; +import com.techcourse.controller.Controller; +import com.techcourse.controller.RequestMapping; import com.techcourse.exception.UncheckedServletException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import org.apache.coyote.Processor; -import org.apache.coyote.http11.controller.Controller; +import org.apache.coyote.http11.exception.NotFoundException; +import org.apache.coyote.http11.exception.UnauthorizedException; import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httprequest.HttpRequestConvertor; import org.apache.coyote.http11.httpresponse.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +19,9 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final String NOT_FOUND_PATH = "/404.html"; + private static final String UNAUTHORIZED_PATH = "/401.html"; + private static final String CHECKED_STATIC_RESOURCE = "."; private final Socket connection; @@ -30,23 +37,53 @@ public void run() { @Override public void process(final Socket connection) { - try ( - final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream() - ) { + try (final var inputStream = connection.getInputStream(); + final var outputStream = connection.getOutputStream()) { var bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); HttpRequest httpRequest = HttpRequestConvertor.convertHttpRequest(bufferedReader); - RequestMapping requestMapping = new RequestMapping(); + HttpResponse httpResponse = createResponse(httpRequest); - Controller controller = requestMapping.getController(httpRequest.getPath()); - - HttpResponse httpResponse = controller.service(httpRequest); - - outputStream.write(httpResponse.getBytes()); + outputStream.write(httpResponse.toResponse()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private HttpResponse createResponse(HttpRequest httpRequest) { + HttpResponse httpResponse = new HttpResponse(); + try { + handle(httpRequest, httpResponse); + return httpResponse; + } catch (NotFoundException e) { + httpResponse.location(httpRequest, NOT_FOUND_PATH); + } catch (UnauthorizedException e) { + httpResponse.location(httpRequest, UNAUTHORIZED_PATH); + } + return httpResponse; + } + + private void handle(HttpRequest httpRequest, HttpResponse httpResponse) { + try { + if (isStaticResource(httpRequest)) { + httpResponse.ok(httpRequest); + httpResponse.staticResource(httpRequest.getPath()); + return; + } + setHttpResponse(httpRequest, httpResponse); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + private static void setHttpResponse(HttpRequest httpRequest, HttpResponse httpResponse) throws Exception { + RequestMapping requestMapping = new RequestMapping(); + Controller controller = requestMapping.getController(httpRequest); + controller.service(httpRequest, httpResponse); + } + + private boolean isStaticResource(HttpRequest httpRequest) { + return httpRequest.getMethod() == HttpMethod.GET && httpRequest.getPath().contains(CHECKED_STATIC_RESOURCE); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderName.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderName.java new file mode 100644 index 0000000000..e5f505ac08 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderName.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11; + +public enum HttpHeaderName { + + CONTENT_TYPE("Content-Type"), + SET_COOKIE("Set-Cookie"), + CONTENT_LENGTH("Content-Length"), + LOCATION("Location"), + COOKIE("Cookie") + ; + + private final String name; + + HttpHeaderName(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java index 4cb9976c42..5535bfdcb1 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java @@ -1,27 +1,22 @@ package org.apache.coyote.http11; import java.util.Arrays; +import org.apache.coyote.http11.exception.NotFoundException; public enum HttpMethod { - GET("GET"), - POST("POST") + GET, + POST ; - private final String name; - - HttpMethod(String name) { - this.name = name; - } - public static HttpMethod getHttpMethod(String name) { return Arrays.stream(values()) - .filter(httpMethod -> httpMethod.name.equals(name)) + .filter(httpMethod -> httpMethod.name().equals(name)) .findAny() - .orElseThrow(); + .orElseThrow(() -> new NotFoundException("유효하지 않은 메소드 입니다.")); } - public boolean isMethod(String name) { - return this.name.equals(name); + public boolean isMethod(HttpMethod method) { + return this.equals(method); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestConvertor.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestConvertor.java deleted file mode 100644 index cd992e7d10..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestConvertor.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.apache.coyote.http11; - -import java.io.BufferedReader; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import org.apache.coyote.http11.httprequest.HttpRequest; -import org.apache.coyote.http11.httprequest.HttpRequestBody; -import org.apache.coyote.http11.httprequest.HttpRequestHeader; -import org.apache.coyote.http11.httprequest.HttpRequestLine; - -public class HttpRequestConvertor { - - public static HttpRequest convertHttpRequest(BufferedReader bufferedReader) { - try { - String requestLine = bufferedReader.readLine(); - if (requestLine == null) { - throw new RuntimeException("요청이 비어 있습니다."); - } - - HttpRequestLine httpRequestLine = new HttpRequestLine(requestLine); - - Map headers = getHeaders(bufferedReader); - - HttpRequestHeader httpRequestHeader = new HttpRequestHeader(headers); - - if (httpRequestHeader.containsKey("Content-Length")) { - HttpRequestBody httpRequestBody = getHttpRequestBody(bufferedReader, httpRequestHeader); - - return new HttpRequest(httpRequestLine, httpRequestHeader, httpRequestBody); - } - - return new HttpRequest(httpRequestLine, httpRequestHeader); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static HttpRequestBody getHttpRequestBody( - BufferedReader bufferedReader, - HttpRequestHeader httpRequestHeader - ) throws IOException { - int contentLength = Integer.parseInt(httpRequestHeader.getValue("Content-Length")); - char[] buffer = new char[contentLength]; - bufferedReader.read(buffer, 0, contentLength); - String body = new String(buffer); - return new HttpRequestBody(body); - } - - private static Map getHeaders(BufferedReader bufferedReader) throws IOException { - String line; - Map headers = new HashMap<>(); - while ((line = bufferedReader.readLine()) != null && !line.isEmpty()) { - String[] requestLine = line.split(":"); - headers.put(requestLine[0], parseHeaderValue(requestLine)); - } - - return headers; - } - - private static String parseHeaderValue(String[] requestLine) { - StringBuilder sb = new StringBuilder(); - for (int i = 1; i < requestLine.length; i++) { - sb.append(requestLine[i].strip()); - } - - return sb.toString(); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java index a51eac9edf..1d41586ef4 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java @@ -3,7 +3,9 @@ public enum HttpStatusCode { OK(200, "OK"), - FOUND(302, "Found") + FOUND(302, "Found"), + UNAUTHORIZED(401, "Unauthorized"), + NOT_FOUND(404, "Not Found") ; private final int code; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/Session.java deleted file mode 100644 index 424b9cf6f9..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/Session.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.apache.coyote.http11; - -import com.techcourse.model.User; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class Session { - - private static final Session INSTANCE = new Session(); - private final Map userMap; - - private Session() { - this.userMap = new HashMap<>(); - } - - public static Session getInstance() { - return INSTANCE; - } - - public void save(String uuid, User user) { - userMap.put(uuid, user); - } - - public boolean containsUser(String uuid) { - return userMap.containsKey(uuid); - } - - public User getUser(String uuid) { - return userMap.get(uuid); - } - - public Set getKeySet() { - return userMap.keySet(); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java deleted file mode 100644 index e52a2508da..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.apache.coyote.http11.controller; - -import org.apache.coyote.http11.httprequest.HttpRequest; -import org.apache.coyote.http11.httpresponse.HttpResponse; - -public abstract class AbstractController implements Controller { - - @Override - public HttpResponse service(HttpRequest httpRequest) { - if (httpRequest.isMethod("GET")) { - return doGet(httpRequest); - } else if (httpRequest.isMethod("POST")) { - return doPost(httpRequest); - } - - throw new RuntimeException(); - } - - abstract protected HttpResponse doPost(HttpRequest httpRequest); - - abstract protected HttpResponse doGet(HttpRequest httpRequest); -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java deleted file mode 100644 index 62d0b29b6f..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.apache.coyote.http11.controller; - -import com.techcourse.db.InMemoryUserRepository; -import com.techcourse.model.User; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.UUID; -import org.apache.coyote.http11.ContentType; -import org.apache.coyote.http11.HttpStatusCode; -import org.apache.coyote.http11.Session; -import org.apache.coyote.http11.httprequest.HttpRequest; -import org.apache.coyote.http11.httpresponse.HttpResponse; -import org.apache.coyote.http11.httpresponse.HttpResponseBody; -import org.apache.coyote.http11.httpresponse.HttpResponseHeader; -import org.apache.coyote.http11.httpresponse.HttpStatusLine; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class LoginController extends AbstractController { - - private static final Logger log = LoggerFactory.getLogger(LoginController.class); - - private final Session session = Session.getInstance(); - - @Override - protected HttpResponse doPost(HttpRequest httpRequest) { - String requestBody = httpRequest.getBody(); - String[] token = requestBody.split("&"); - String account = token[0].split("=")[1]; - String password = token[1].split("=")[1]; - - User user = InMemoryUserRepository.findByAccount(account) - .orElseThrow(); - UUID uuid = UUID.randomUUID(); - - HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.FOUND); - HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); - if (user.checkPassword(password)) { - session.save(uuid.toString(), user); - httpResponseHeader.addHeaders("Set-Cookie", "JSESSIONID=" + uuid); - httpResponseHeader.addHeaders("Location", "/index.html"); - log.info(user.toString()); - } else { - httpResponseHeader.addHeaders("Location", "/401.html"); - log.error("비밀번호 불일치"); - } - - return new HttpResponse(httpStatusLine, httpResponseHeader); - } - - @Override - protected HttpResponse doGet(HttpRequest httpRequest) { - try { - HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); - - if (httpRequest.containsKey("Cookie")) { - String[] cookies = httpRequest.getValue("Cookie").split("; "); - String cookie = ""; - for (String c : cookies) { - if (c.contains("JSESSIONID")) { - cookie = c.split("=")[1]; - } - } - if (session.containsUser(cookie)) { - User user = session.getUser(cookie); - log.info(user.toString()); - httpStatusLine = new HttpStatusLine(httpStatusLine.getVersion(), HttpStatusCode.FOUND); - HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); - httpResponseHeader.addHeaders("Set-Cookie", "JSESSIONID=" + cookie); - httpResponseHeader.addHeaders("Location", "/index.html"); - return new HttpResponse(httpStatusLine, httpResponseHeader); - } - } - - String fileName = "static/login.html"; - var resourceUrl = getClass().getClassLoader().getResource(fileName); - Path filePath = Path.of(resourceUrl.toURI()); - String responseBody = new String(Files.readAllBytes(filePath)); - HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); - httpResponseHeader.addHeaders("Content-Type", ContentType.HTML.getContentType()); - httpResponseHeader.addHeaders("Content-Length", String.valueOf(responseBody.getBytes().length)); - HttpResponseBody httpResponseBody = new HttpResponseBody(responseBody); - - return new HttpResponse(httpStatusLine, httpResponseHeader, httpResponseBody); - } catch (URISyntaxException | IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/PageController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/PageController.java deleted file mode 100644 index cda0b2f59a..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/controller/PageController.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.apache.coyote.http11.controller; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.apache.coyote.http11.ContentType; -import org.apache.coyote.http11.HttpStatusCode; -import org.apache.coyote.http11.httprequest.HttpRequest; -import org.apache.coyote.http11.httpresponse.HttpResponse; -import org.apache.coyote.http11.httpresponse.HttpResponseBody; -import org.apache.coyote.http11.httpresponse.HttpResponseHeader; -import org.apache.coyote.http11.httpresponse.HttpStatusLine; - -public class PageController extends AbstractController { - - @Override - protected HttpResponse doPost(HttpRequest httpRequest) { - return null; - } - - @Override - protected HttpResponse doGet(HttpRequest httpRequest) { - try { - HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); - - String path = httpRequest.getPath(); - if (!httpRequest.getPath().contains(".")) { - path += ".html"; - } - String fileName = "static" + path; - var resourceUrl = getClass().getClassLoader().getResource(fileName); - Path filePath = Path.of(resourceUrl.toURI()); - String responseBody = new String(Files.readAllBytes(filePath)); - HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); - httpResponseHeader.addHeaders("Content-Type", ContentType.getContentType(path.split("\\.")[1]).getContentType()); - httpResponseHeader.addHeaders("Content-Length", String.valueOf(responseBody.getBytes().length)); - HttpResponseBody httpResponseBody = new HttpResponseBody(responseBody); - - return new HttpResponse(httpStatusLine, httpResponseHeader, httpResponseBody); - } catch (URISyntaxException | IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java deleted file mode 100644 index 736aa4d852..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.apache.coyote.http11.controller; - -import com.techcourse.db.InMemoryUserRepository; -import com.techcourse.model.User; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.apache.coyote.http11.ContentType; -import org.apache.coyote.http11.HttpStatusCode; -import org.apache.coyote.http11.httprequest.HttpRequest; -import org.apache.coyote.http11.httpresponse.HttpResponse; -import org.apache.coyote.http11.httpresponse.HttpResponseBody; -import org.apache.coyote.http11.httpresponse.HttpResponseHeader; -import org.apache.coyote.http11.httpresponse.HttpStatusLine; - -public class RegisterController extends AbstractController { - - @Override - protected HttpResponse doPost(HttpRequest httpRequest) { - String requestBody = httpRequest.getBody(); - String[] token = requestBody.split("&"); - String account = token[0].split("=")[1]; - String email = token[1].split("=")[1]; - String password = token[2].split("=")[1]; - User user = new User(account, password, email); - InMemoryUserRepository.save(user); - - HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.FOUND); - HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); - httpResponseHeader.addHeaders("Location", "/index.html"); - - return new HttpResponse(httpStatusLine, httpResponseHeader); - } - - @Override - protected HttpResponse doGet(HttpRequest httpRequest) { - try { - HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); - - String fileName = "static/register.html"; - var resourceUrl = getClass().getClassLoader().getResource(fileName); - Path filePath = Path.of(resourceUrl.toURI()); - String responseBody = new String(Files.readAllBytes(filePath)); - HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); - httpResponseHeader.addHeaders("Content-Type", ContentType.HTML.getContentType()); - httpResponseHeader.addHeaders("Content-Length", String.valueOf(responseBody.getBytes().length)); - HttpResponseBody httpResponseBody = new HttpResponseBody(responseBody); - - return new HttpResponse(httpStatusLine, httpResponseHeader, httpResponseBody); - } catch (URISyntaxException | IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/NotFoundException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/NotFoundException.java new file mode 100644 index 0000000000..441c69b4a6 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/UnauthorizedException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/UnauthorizedException.java new file mode 100644 index 0000000000..96de0c8a45 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/UnauthorizedException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.exception; + +public class UnauthorizedException extends RuntimeException { + + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpCookie.java new file mode 100644 index 0000000000..00a3253ff0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpCookie.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11.httprequest; + +import java.util.HashMap; +import java.util.Map; + +public class HttpCookie { + + public static final String JSESSIONID = "JSESSIONID"; + + private final Map cookies; + + public HttpCookie() { + this.cookies = new HashMap<>(); + } + + public void addCookie(String key, String value) { + cookies.put(key, value); + } + + public boolean containsCookie(String key) { + return cookies.containsKey(key); + } + + public boolean containsSession() { + return containsCookie(JSESSIONID); + } + + public String getCookieValue(String key) { + return cookies.get(key); + } + + public String getSessionId() { + return getCookieValue(JSESSIONID); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpCookieConvertor.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpCookieConvertor.java new file mode 100644 index 0000000000..a2fcc1aee6 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpCookieConvertor.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.httprequest; + +import java.util.Arrays; + +public class HttpCookieConvertor { + + private static final String COOKIE_DELIMITER = "; "; + private static final String COOKIE_TOKEN_DELIMITER = "="; + private static final int COOKIE_TOKEN_MIN_LENGTH = 2; + private static final int COOKIE_KEY_INDEX = 0; + private static final int COOKIE_VALUE_INDEX = 1; + + public static HttpCookie convertHttpCookie(String rowCookie) { + HttpCookie httpCookie = new HttpCookie(); + String[] cookieTokens = rowCookie.split(COOKIE_DELIMITER); + Arrays.stream(cookieTokens) + .filter(HttpCookieConvertor::filterCookie) + .map(cookieToken -> cookieToken.split(COOKIE_TOKEN_DELIMITER)) + .forEach(cookie -> httpCookie.addCookie(cookie[COOKIE_KEY_INDEX], cookie[COOKIE_VALUE_INDEX])); + + return httpCookie; + } + + private static boolean filterCookie(String cookie) { + return cookie.split(COOKIE_TOKEN_DELIMITER).length >= COOKIE_TOKEN_MIN_LENGTH; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java index 1e614078b8..d85024cf0b 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java @@ -1,37 +1,59 @@ package org.apache.coyote.http11.httprequest; +import java.util.Map; +import org.apache.coyote.http11.HttpHeaderName; import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.http11.session.Session; public class HttpRequest { private final HttpRequestLine httpRequestLine; private final HttpRequestHeader httpRequestHeader; private final HttpRequestBody httpRequestBody; - - public HttpRequest(HttpRequestLine httpRequestLine, HttpRequestHeader httpRequestHeader, HttpRequestBody httpRequestBody) { + private final Session session; + + public HttpRequest( + HttpRequestLine httpRequestLine, + HttpRequestHeader httpRequestHeader, + HttpRequestBody httpRequestBody, + Session session + ) { this.httpRequestLine = httpRequestLine; this.httpRequestHeader = httpRequestHeader; this.httpRequestBody = httpRequestBody; + this.session = session; + } + + public HttpRequest(HttpRequestLine httpRequestLine, HttpRequestHeader httpRequestHeader, Session session) { + this(httpRequestLine, httpRequestHeader, null, session); + } + + public boolean isMethod(HttpMethod method) { + return httpRequestLine.isMethod(method); + } + + public boolean containsHeader(String key) { + return httpRequestHeader.containsHeader(key); } - public HttpRequest(HttpRequestLine httpRequestLine, HttpRequestHeader httpRequestHeader) { - this(httpRequestLine, httpRequestHeader, null); + public boolean containsHeader(HttpHeaderName httpHeaderName) { + return httpRequestHeader.containsHeader(httpHeaderName); } - public boolean isMethod(String name) { - return httpRequestLine.isMethod(name); + public String getHeaderValue(String key) { + return httpRequestHeader.getHeaderValue(key); } - public boolean isPath(String path) { - return httpRequestLine.isPath(path); + public String getHeaderValue(HttpHeaderName httpHeaderName) { + return httpRequestHeader.getHeaderValue(httpHeaderName); } - public boolean containsKey(String key) { - return httpRequestHeader.containsKey(key); + public boolean containsBody(String key) { + return httpRequestBody.containsBody(key); } - public String getValue(String key) { - return httpRequestHeader.getValue(key); + public String getBodyValue(String key) { + return httpRequestBody.getBodyValue(key); } public HttpMethod getMethod() { @@ -46,7 +68,7 @@ public String getVersion() { return httpRequestLine.getVersion(); } - public String getBody() { + public Map getBody() { return httpRequestBody.getBody(); } @@ -58,11 +80,7 @@ public HttpRequestBody getHttpRequestBody() { return httpRequestBody; } - @Override - public String toString() { - return "HttpRequest{\n" + - "httpRequestHeader=" + httpRequestHeader + - ",\n httpRequestBody=" + httpRequestBody + - "\n}"; + public Session getSession() { + return session; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestBody.java index 6c303051c2..6b7d6cb94e 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestBody.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestBody.java @@ -1,21 +1,24 @@ package org.apache.coyote.http11.httprequest; +import java.util.Map; + public class HttpRequestBody { - private final String body; + private final Map body; - public HttpRequestBody(String body) { + public HttpRequestBody(Map body) { this.body = body; } - public String getBody() { - return body; + public boolean containsBody(String key) { + return body.containsKey(key); } - @Override - public String toString() { - return "HttpRequestBody{\n" + - "body='" + body + '\'' + - "\n}"; + public String getBodyValue(String key) { + return body.get(key); + } + + public Map getBody() { + return body; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestConvertor.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestConvertor.java new file mode 100644 index 0000000000..0d65cb918d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestConvertor.java @@ -0,0 +1,108 @@ +package org.apache.coyote.http11.httprequest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import org.apache.coyote.http11.HttpHeaderName; +import org.apache.coyote.http11.session.Session; +import org.apache.coyote.http11.session.SessionManager; + +public class HttpRequestConvertor { + + private static final String HEADER_DELIMITER = ":"; + private static final int HEADER_KEY_INDEX = 0; + private static final String BODY_DELIMITER = "&"; + private static final int BODY_TUPLE_MIN_LENGTH = 2; + private static final String TUPLE_DELIMITER = "="; + private static final int TUPLE_KEY_INDEX = 0; + private static final int TUPLE_VALUE_INDEX = 1; + private static final SessionManager SESSION_MANAGER = SessionManager.getInstance(); + + public static HttpRequest convertHttpRequest(BufferedReader bufferedReader) throws IOException { + String requestLine = bufferedReader.readLine(); + HttpRequestLine httpRequestLine = new HttpRequestLine(requestLine); + + HttpRequestHeader httpRequestHeader = new HttpRequestHeader(getHeaders(bufferedReader)); + Session session = getOrCreateSession(httpRequestHeader); + + if (isExistRequestBody(httpRequestHeader)) { + HttpRequestBody httpRequestBody = getHttpRequestBody(bufferedReader, httpRequestHeader); + return new HttpRequest(httpRequestLine, httpRequestHeader, httpRequestBody, session); + } + return new HttpRequest(httpRequestLine, httpRequestHeader, session); + } + + private static Map getHeaders(BufferedReader bufferedReader) throws IOException { + String line; + Map headers = new HashMap<>(); + while ((line = bufferedReader.readLine()) != null && !line.isEmpty()) { + String[] requestLine = line.split(HEADER_DELIMITER); + headers.put(requestLine[HEADER_KEY_INDEX], parseHeaderValue(requestLine)); + } + + return headers; + } + + private static String parseHeaderValue(String[] requestLine) { + return String.join(HEADER_DELIMITER, Arrays.copyOfRange(requestLine, 1, requestLine.length)).strip(); + } + + private static Session getOrCreateSession(HttpRequestHeader httpRequestHeader) { + if (!httpRequestHeader.containsHeader(HttpHeaderName.COOKIE)) { + return createSession(); + } + + HttpCookie httpCookie = HttpCookieConvertor.convertHttpCookie( + httpRequestHeader.getHeaderValue(HttpHeaderName.COOKIE)); + if (!httpCookie.containsSession()) { + return createSession(); + } + + return getOrCreateSession(httpCookie); + } + + private static Session getOrCreateSession(HttpCookie httpCookie) { + String sessionId = httpCookie.getSessionId(); + if (!SESSION_MANAGER.containsSession(sessionId)) { + return createSession(); + } + return SESSION_MANAGER.findSession(sessionId); + } + + private static Session createSession() { + Session session = new Session(UUID.randomUUID().toString()); + SESSION_MANAGER.add(session); + return session; + } + + private static HttpRequestBody getHttpRequestBody( + BufferedReader bufferedReader, + HttpRequestHeader httpRequestHeader + ) throws IOException { + int contentLength = Integer.parseInt(httpRequestHeader.getHeaderValue(HttpHeaderName.CONTENT_LENGTH)); + char[] buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + String requestBody = new String(buffer); + Map body = extractBody(requestBody); + return new HttpRequestBody(body); + } + + private static Map extractBody(String requestBody) { + String[] tokens = requestBody.split(BODY_DELIMITER); + return Arrays.stream(tokens) + .filter(token -> token.split(TUPLE_DELIMITER).length >= BODY_TUPLE_MIN_LENGTH) + .map(token -> token.split(TUPLE_DELIMITER)) + .collect(Collectors.toMap( + token -> token[TUPLE_KEY_INDEX], + token -> token[TUPLE_VALUE_INDEX] + )); + } + + private static boolean isExistRequestBody(HttpRequestHeader httpRequestHeader) { + return httpRequestHeader.containsHeader(HttpHeaderName.CONTENT_LENGTH) && httpRequestHeader.existRequestBody(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestHeader.java index bf29b0cdc4..766592af59 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestHeader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestHeader.java @@ -1,27 +1,35 @@ package org.apache.coyote.http11.httprequest; import java.util.Map; +import org.apache.coyote.http11.HttpHeaderName; public class HttpRequestHeader { + private static final String BODY_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"; + private final Map headers; public HttpRequestHeader(Map headers) { this.headers = headers; } - public boolean containsKey(String key) { + public boolean containsHeader(String key) { return headers.containsKey(key); } - public String getValue(String key) { + public boolean containsHeader(HttpHeaderName httpHeaderName) { + return headers.containsKey(httpHeaderName.getName()); + } + + public boolean existRequestBody() { + return getHeaderValue(HttpHeaderName.CONTENT_TYPE).equals(BODY_FORM_CONTENT_TYPE); + } + + public String getHeaderValue(String key) { return headers.get(key); } - @Override - public String toString() { - return "HttpRequestHeader{" + - ", \nheaders=" + headers + - '}'; + public String getHeaderValue(HttpHeaderName httpHeaderName) { + return headers.get(httpHeaderName.getName()); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestLine.java index 6befc619d4..495755a154 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestLine.java @@ -4,19 +4,35 @@ public class HttpRequestLine { + private static final String REQUEST_LINE_DELIMITER = " "; + private static final int REQUEST_LINE_MIN_LENGTH = 3; + private static final int METHOD_INDEX = 0; + private static final int PATH_INDEX = 1; + private static final int VERSION_INDEX = 2; + private final HttpMethod method; private final String path; private final String version; public HttpRequestLine(String requestLine) { - String[] headerFirstLine = requestLine.split(" "); - this.method = HttpMethod.getHttpMethod(headerFirstLine[0]); - this.path = headerFirstLine[1]; - this.version = headerFirstLine[2]; + validateRequestLine(requestLine); + String[] headerFirstLine = requestLine.split(REQUEST_LINE_DELIMITER); + this.method = HttpMethod.getHttpMethod(headerFirstLine[METHOD_INDEX]); + this.path = headerFirstLine[PATH_INDEX]; + this.version = headerFirstLine[VERSION_INDEX]; + } + + private void validateRequestLine(String requestLine) { + if (requestLine == null) { + throw new IllegalArgumentException("요청이 비어 있습니다"); + } + if (requestLine.split(REQUEST_LINE_DELIMITER).length < REQUEST_LINE_MIN_LENGTH) { + throw new IllegalArgumentException("RequestLine이 잘못된 요청입니다"); + } } - public boolean isMethod(String name) { - return method.isMethod(name); + public boolean isMethod(HttpMethod method) { + return this.method.isMethod(method); } public boolean isPath(String path) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java index 2960b502c7..f57c27375f 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java @@ -1,14 +1,31 @@ package org.apache.coyote.http11.httpresponse; -import java.util.Map; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.coyote.http11.CharSet; +import org.apache.coyote.http11.HttpHeaderName; +import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.exception.NotFoundException; +import org.apache.coyote.http11.httprequest.HttpCookie; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.session.Session; public class HttpResponse { - private final HttpStatusLine httpStatusLine; + private static final String COOKIE_TOKEN_DELIMITER = "="; + private static final String RESPONSE_LINE_DELIMITER = "\r\n"; + private static final String EXTENSION_DELIMITER = "."; + private static final String HTML_EXTENSION = ".html"; + private static final String STATIC_PATH = "static"; + + private HttpStatusLine httpStatusLine; private final HttpResponseHeader httpResponseHeader; - private final HttpResponseBody httpResponseBody; + private HttpResponseBody httpResponseBody; - public HttpResponse( + private HttpResponse( HttpStatusLine httpStatusLine, HttpResponseHeader httpResponseHeader, HttpResponseBody httpResponseBody @@ -18,37 +35,99 @@ public HttpResponse( this.httpResponseBody = httpResponseBody; } - public HttpResponse(HttpStatusLine httpStatusLine, HttpResponseHeader httpResponseHeader) { - this(httpStatusLine, httpResponseHeader, null); - } - - public byte[] getBytes() { - String statusLine = httpStatusLine.getVersion() + " " + httpStatusLine.getHttpStatusCode().getCode() + " " - + httpStatusLine.getHttpStatusCode().getMessage(); - Map headers = httpResponseHeader.getHeaders(); - StringBuilder sb = new StringBuilder(); - int size = headers.keySet().size(); - int i = 1; - for (String key : headers.keySet()) { - if (i < size) { - sb.append(key).append(": ").append(headers.get(key)).append(" \r\n"); - size++; - } else { - sb.append(key).append(": ").append(headers.get(key)); - } + public HttpResponse() { + this(null, new HttpResponseHeader(), null); + } + + public void ok(HttpRequest httpRequest) { + this.httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); + } + + public void found(HttpRequest httpRequest) { + this.httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.FOUND); + } + + public void unauthorized(HttpRequest httpRequest) { + this.httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.UNAUTHORIZED); + } + + public void addHeader(HttpHeaderName headerName, String value) { + httpResponseHeader.addHeader(headerName, value); + } + + public void location(HttpRequest httpRequest, String path) { + found(httpRequest); + addHeader(HttpHeaderName.LOCATION, path); + } + + public void setCookie(String cookie) { + addHeader(HttpHeaderName.SET_COOKIE, cookie); + } + + public void setSession(Session session) { + setCookie(HttpCookie.JSESSIONID + COOKIE_TOKEN_DELIMITER + session.getId()); + } + + public void contentLength(String contentLength) { + addHeader(HttpHeaderName.CONTENT_LENGTH, contentLength); + } + + public void contentType(String contentType) { + addHeader(HttpHeaderName.CONTENT_TYPE, contentType); + } + + public void responseBody(String responseBody) { + this.httpResponseBody = new HttpResponseBody(responseBody); + } + + public void staticResource(String path) { + try { + URL resourceUrl = getResourceUrl(path); + validateResourceUrl(resourceUrl); + Path filePath = Path.of(resourceUrl.toURI()); + String responseBody = new String(Files.readAllBytes(filePath)); + + setHttpHeader(filePath, responseBody); + this.httpResponseBody = new HttpResponseBody(responseBody); + } catch (URISyntaxException | IOException e) { + throw new IllegalArgumentException(e.getMessage() + e); + } + } + + private URL getResourceUrl(String path) { + path = settingExtension(path); + String fileName = STATIC_PATH + settingExtension(path); + return getClass().getClassLoader().getResource(fileName); + } + + private String settingExtension(String path) { + if (!path.contains(EXTENSION_DELIMITER)) { + path += HTML_EXTENSION; + } + return path; + } + + private void validateResourceUrl(URL resourceUrl) { + if (resourceUrl == null) { + throw new NotFoundException("존재하지 않는 경로입니다."); } + } + + private void setHttpHeader(Path filePath, String responseBody) throws IOException { + contentType(Files.probeContentType(filePath) + CharSet.UTF_8.getCharset()); + contentLength(String.valueOf(responseBody.getBytes().length)); + } + + public byte[] toResponse() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(httpStatusLine.createStatusLineResponse()) + .append(RESPONSE_LINE_DELIMITER) + .append(httpResponseHeader.createHeadersResponse()); if (httpResponseBody != null) { - String responseBody = httpResponseBody.getBody(); - String join = String.join("\r\n", - statusLine, - sb.toString(), - responseBody); - return join.getBytes(); + stringBuilder.append(RESPONSE_LINE_DELIMITER) + .append(httpResponseBody.getBody()); } - String join = String.join("\r\n", - statusLine, - sb.toString()); - return join.getBytes(); + return stringBuilder.toString().getBytes(); } public HttpStatusLine getHttpStatusLine() { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseHeader.java index e350a1b84a..e97862d0c3 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseHeader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseHeader.java @@ -2,20 +2,31 @@ import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; +import org.apache.coyote.http11.HttpHeaderName; public class HttpResponseHeader { - private final Map headers; + private static final String HEADER_DELIMITER = ": "; + private static final String RESPONSE_LINE_DELIMITER = " \r\n"; + + private final Map headers; public HttpResponseHeader() { this.headers = new HashMap<>(); } - public void addHeaders(String key, String value) { - headers.put(key, value); + public void addHeader(HttpHeaderName headerName, String value) { + headers.put(headerName, value); + } + + public String createHeadersResponse() { + return headers.keySet().stream() + .map(key -> key.getName() + HEADER_DELIMITER + headers.get(key)) + .collect(Collectors.joining(RESPONSE_LINE_DELIMITER)); } - public Map getHeaders() { + public Map getHeaders() { return headers; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpStatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpStatusLine.java index 4eaf9eba69..377be4dd53 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpStatusLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpStatusLine.java @@ -4,6 +4,8 @@ public class HttpStatusLine { + private static final String STATUS_LINE_FORMAT = "%s %s %s "; + private final String version; private final HttpStatusCode httpStatusCode; @@ -12,6 +14,10 @@ public HttpStatusLine(String version, HttpStatusCode httpStatusCode) { this.httpStatusCode = httpStatusCode; } + public String createStatusLineResponse() { + return STATUS_LINE_FORMAT.formatted(version, httpStatusCode.getCode(), httpStatusCode.getMessage()); + } + public String getVersion() { return version; } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java new file mode 100644 index 0000000000..9054689244 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.session; + +import com.techcourse.model.User; +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private static final String SESSION_USER_NAME = "user"; + + private final String id; + private final Map attributes; + + public Session(String id) { + this.id = id; + this.attributes = new HashMap<>(); + } + + public boolean hasAttribute(String name) { + return attributes.containsKey(name); + } + + public boolean hasUser() { + return hasAttribute(SESSION_USER_NAME); + } + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public Object getUser() { + return getAttribute(SESSION_USER_NAME); + } + + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + public void setUser(User user) { + setAttribute(SESSION_USER_NAME, user); + } + + public String getId() { + return id; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java new file mode 100644 index 0000000000..d196714211 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11.session; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.catalina.Manager; + +public class SessionManager implements Manager { + + private static final SessionManager INSTANCE = new SessionManager(); + private static final Map SESSIONS = new ConcurrentHashMap<>(); + + private SessionManager() {} + + public static SessionManager getInstance() { + return INSTANCE; + } + + @Override + public void add(Session session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public Session findSession(String id) { + return SESSIONS.get(id); + } + + @Override + public void remove(Session session) { + SESSIONS.remove(session.getId()); + } + + public boolean containsSession(String id) { + return SESSIONS.containsKey(id); + } +} diff --git a/tomcat/src/main/resources/static/favicon.ico b/tomcat/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000..815a3ab175 Binary files /dev/null and b/tomcat/src/main/resources/static/favicon.ico differ 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..dbcde0bf20 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/LoginControllerTest.java @@ -0,0 +1,156 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import org.apache.coyote.http11.exception.UnauthorizedException; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; +import org.apache.coyote.http11.session.Session; +import org.apache.coyote.http11.session.SessionManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import support.HttpRequestMaker; + +class LoginControllerTest { + + @DisplayName("회원가입 되어있는 id와 password로 로그인을 하면 쿠키를 세팅하고 index.html로 이동한다") + @Test + void login() { + String body = "account=gugu&password=1"; + final String login = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(login); + HttpResponse httpResponse = new HttpResponse(); + String id = httpRequest.getSession().getId(); + + LoginController loginController = new LoginController(); + loginController.doPost(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 302 Found ".getBytes()) + .contains("Location: /index.html ".getBytes()) + .contains(("Set-Cookie: JSESSIONID=" + id + " ").getBytes()); + } + + @DisplayName("id와 비밀번호가 일치하지 않으면 예외를 발생시킨다") + @Test + void notMatchAccountAndPassword() { + String body = "account=gugu&password=2"; + final String login = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(login); + HttpResponse httpResponse = new HttpResponse(); + + LoginController loginController = new LoginController(); + + assertThatThrownBy(() -> loginController.doPost(httpRequest, httpResponse)) + .isInstanceOf(UnauthorizedException.class); + } + + @DisplayName("회원가입 되어있지 않은 account로 로그인을 하면 예외를 발생시킨다") + @Test + void notExistByAccount() { + String body = "account=gugu2&password=1"; + final String login = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(login); + HttpResponse httpResponse = new HttpResponse(); + + LoginController loginController = new LoginController(); + + assertThatThrownBy(() -> loginController.doPost(httpRequest, httpResponse)) + .isInstanceOf(UnauthorizedException.class); + } + + @DisplayName("유저가 일부 값을 입력하지 않을 시 로그인 페이지로 이동한다") + @Test + void notExistUserInput() { + String body = "account=&password=1"; + final String login = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(login); + + LoginController loginController = new LoginController(); + HttpResponse httpResponse = new HttpResponse(); + loginController.doPost(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 302 Found ".getBytes()) + .contains("Location: /login".getBytes()); + } + + @DisplayName("쿠키에 유저 정보를 의미하는 세션이 없으면 로그인 페이지로 이동한다") + @Test + void notExistSessionLogin() throws IOException { + final String login = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(login); + + final URL resource = getClass().getClassLoader().getResource("static/login.html"); + String body = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + LoginController loginController = new LoginController(); + HttpResponse httpResponse = new HttpResponse(); + loginController.doGet(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 200 OK ".getBytes()) + .contains("Content-Type: text/html;charset=utf-8 ".getBytes()) + .contains(("Content-Length: " + body.getBytes().length + " ").getBytes()) + .contains(body.getBytes()); + } + + @DisplayName("쿠키에 유저 정보가 저장된 세션이 있으면 index.html 페이지로 이동한다") + @Test + void existSessionLogin() { + SessionManager sessionManager = SessionManager.getInstance(); + Session session = new Session("abcdefg"); + sessionManager.add(session); + final String login = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Cookie: JSESSIONID=abcdefg", + "Connection: keep-alive ", + ""); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(login); + + LoginController loginController = new LoginController(); + HttpResponse httpResponse = new HttpResponse(); + loginController.doGet(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 200 OK ".getBytes()) + .contains("Location: /login".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..78f703d005 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/RegisterControllerTest.java @@ -0,0 +1,108 @@ +package com.techcourse.controller; + +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 org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import support.HttpRequestMaker; + +class RegisterControllerTest { + + @DisplayName("회원가입이 성공하면 /index.html 페이지로 이동한다") + @Test + void register() { + String body = "account=f&password=12&email=bito@wooea.net"; + final String register = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(register); + HttpResponse httpResponse = new HttpResponse(); + + RegisterController registerController = new RegisterController(); + registerController.doPost(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 302 Found ".getBytes()) + .contains("Location: /index".getBytes()); + } + + @DisplayName("일부 항목을 입력하지 않고 회원가입을 시도하면 /register 페이지로 이동한다") + @Test + void notExistUserInput() { + String body = "account=f&password=&email=bito@wooea.net"; + final String register = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(register); + HttpResponse httpResponse = new HttpResponse(); + + RegisterController registerController = new RegisterController(); + registerController.doPost(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 302 Found ".getBytes()) + .contains("Location: /register".getBytes()); + } + + @DisplayName("이미 회원가입이 되어있는 account로 회원가입을 시도할 경우 /register 페이지로 이동한다") + @Test + void existUserRegister() { + String body = "account=gugu&password=1&email=bito@wooea.net"; + final String register = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(register); + HttpResponse httpResponse = new HttpResponse(); + + RegisterController registerController = new RegisterController(); + registerController.doPost(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 302 Found ".getBytes()) + .contains("Location: /register".getBytes()); + } + + @DisplayName("GET으로 /register를 접근하면 static/register.html을 띄운다") + @Test + void doGet() throws IOException { + final String register = String.join("\r\n", + "GET /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(register); + HttpResponse httpResponse = new HttpResponse(); + + final URL resource = getClass().getClassLoader().getResource("static/register.html"); + String body = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + RegisterController registerController = new RegisterController(); + registerController.doGet(httpRequest, httpResponse); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 200 OK ".getBytes()) + .contains("Content-Type: text/html;charset=utf-8 ".getBytes()) + .contains(("Content-Length: " + body.getBytes().length + " ").getBytes()) + .contains(body.getBytes()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/controller/RequestMappingTest.java b/tomcat/src/test/java/com/techcourse/controller/RequestMappingTest.java new file mode 100644 index 0000000000..bc6cdaba73 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/controller/RequestMappingTest.java @@ -0,0 +1,46 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.coyote.http11.exception.NotFoundException; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import support.HttpRequestMaker; + +class RequestMappingTest { + + @DisplayName("해당 path에 알맞은 Controller를 반환한다") + @Test + void getController() { + final String login = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(login); + + RequestMapping requestMapping = new RequestMapping(); + Controller controller = requestMapping.getController(httpRequest); + + assertThat(controller) + .isInstanceOf(LoginController.class); + } + + @DisplayName("등록되지 않은 path일 경우 예외를 발생시킨다") + @Test + void notExistPath() { + final String request = String.join("\r\n", + "GET /lo HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(request); + + RequestMapping requestMapping = new RequestMapping(); + + assertThatThrownBy(() -> requestMapping.getController(httpRequest)) + .isInstanceOf(NotFoundException.class); + } +} 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 9d9071b6f9..48bbb028ee 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -6,34 +6,15 @@ 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("정적 파일을 요청하면 해당 파일을 반환한다") @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); - } - - @Test - void index() throws IOException { - // given + void staticResource() throws IOException { final String httpRequest= String.join("\r\n", "GET /index.html HTTP/1.1 ", "Host: localhost:8080 ", @@ -44,17 +25,15 @@ void index() throws IOException { final var socket = new StubSocket(httpRequest); final Http11Processor processor = new Http11Processor(socket); - // when processor.process(socket); - // then final URL resource = getClass().getClassLoader().getResource("static/index.html"); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: 5670 \r\n" + - "\r\n"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + String body = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - assertThat(socket.output()).isEqualTo(expected); + assertThat(socket.output().getBytes()) + .contains("HTTP/1.1 200 OK".getBytes()) + .contains("Content-Length: 5670".getBytes()) + .contains("Content-Type: text/html;charset=utf-8".getBytes()) + .contains(body.getBytes()); } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HttpMethodTest.java b/tomcat/src/test/java/org/apache/coyote/http11/HttpMethodTest.java new file mode 100644 index 0000000000..ddb8a3fee8 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/HttpMethodTest.java @@ -0,0 +1,40 @@ +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.apache.coyote.http11.exception.NotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpMethodTest { + + @DisplayName("이름과 일치하는 HttpMethod를 반환한다") + @Test + void getHttpMethod() { + HttpMethod method = HttpMethod.getHttpMethod("GET"); + + HttpMethod expected = HttpMethod.GET; + + assertThat(method).isEqualTo(expected); + } + + @DisplayName("해당하는 HttpMethod가 없을 경우 예외를 발생시킨다") + @Test + void notExistHttpMethod() { + assertThatThrownBy(() -> HttpMethod.getHttpMethod("G")) + .isInstanceOf(NotFoundException.class); + } + + @DisplayName("해당 HttpMethod와 일치 여부를 반환한다.") + @Test + void isMethod() { + HttpMethod method = HttpMethod.GET; + + assertAll( + () -> assertThat(method.isMethod(HttpMethod.GET)).isTrue(), + () -> assertThat(method.isMethod(HttpMethod.POST)).isFalse() + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/httprequest/HttpCookieConvertorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/httprequest/HttpCookieConvertorTest.java new file mode 100644 index 0000000000..5ae55b4c5f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/httprequest/HttpCookieConvertorTest.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.httprequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpCookieConvertorTest { + + @DisplayName("HttpCookie를 정확히 추출한다") + @Test + void convertHttpCookie() { + String rowCookie = "JSESSIONID=abcdefg; account=gugu; password=; uuid=mk"; + HttpCookie httpCookie = HttpCookieConvertor.convertHttpCookie(rowCookie); + + assertAll( + () -> assertThat(httpCookie.getCookieValue("JSESSIONID")).isEqualTo("abcdefg"), + () -> assertThat(httpCookie.getCookieValue("account")).isEqualTo("gugu"), + () -> assertThat(httpCookie.getCookieValue("password")).isNull(), + () -> assertThat(httpCookie.getCookieValue("uuid")).isEqualTo("mk") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/httprequest/HttpRequestConvertorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/httprequest/HttpRequestConvertorTest.java new file mode 100644 index 0000000000..10671335d4 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/httprequest/HttpRequestConvertorTest.java @@ -0,0 +1,156 @@ +package org.apache.coyote.http11.httprequest; + +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 java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.apache.coyote.http11.HttpHeaderName; +import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.http11.session.Session; +import org.apache.coyote.http11.session.SessionManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import support.HttpRequestMaker; + +class HttpRequestConvertorTest { + + @DisplayName("빈 값이 들어오면 예외를 발생시킨다") + @Test + void throwExceptionWhenNull() { + final String register = ""; + InputStream inputStream = new ByteArrayInputStream(register.getBytes()); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + + assertThatThrownBy(() -> HttpRequestConvertor.convertHttpRequest(bufferedReader)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("요청이 비어 있습니다"); + } + + @DisplayName("RequestLine이 잘못 전달되면 예외를 발생시킨다") + @Test + void invalidRequestLine() { + final String login = String.join("\r\n", + "GET HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + InputStream inputStream = new ByteArrayInputStream(login.getBytes()); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + + assertThatThrownBy(() -> HttpRequestConvertor.convertHttpRequest(bufferedReader)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("RequestLine이 잘못된 요청입니다"); + } + + @DisplayName("들어온 요청을 HttpRequest로 변환한다") + @Test + void convertHttpRequest() { + String body = "account=gugu&password=1"; + final String request = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(request); + + assertAll( + () -> assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.GET), + () -> assertThat(httpRequest.getPath()).isEqualTo("/login"), + () -> assertThat(httpRequest.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(httpRequest.getHeaderValue("Host")).isEqualTo("localhost:8080"), + () -> assertThat(httpRequest.getHeaderValue("Connection")).isEqualTo("keep-alive"), + () -> assertThat(httpRequest.getHeaderValue(HttpHeaderName.CONTENT_LENGTH)).isEqualTo(String.valueOf(body.getBytes().length)), + () -> assertThat(httpRequest.getHeaderValue(HttpHeaderName.CONTENT_TYPE)).isEqualTo("application/x-www-form-urlencoded"), + () -> assertThat(httpRequest.getBodyValue("account")).isEqualTo("gugu"), + () -> assertThat(httpRequest.getBodyValue("password")).isEqualTo("1") + ); + } + + @DisplayName("들어온 요청에 RequestBody가 없을 경우 RequestBody를 생성하지 않는다") + @Test + void notExistRequestBody() { + final String request = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(request); + + assertAll( + () -> assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.GET), + () -> assertThat(httpRequest.getPath()).isEqualTo("/login"), + () -> assertThat(httpRequest.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(httpRequest.getHeaderValue("Host")).isEqualTo("localhost:8080"), + () -> assertThat(httpRequest.getHeaderValue("Connection")).isEqualTo("keep-alive"), + () -> assertThat(httpRequest.getHttpRequestBody()).isNull() + ); + } + + @DisplayName("SessionManager에 저장된 세션이 쿠키로 들어오면 해당 세션을 불러온다") + @Test + void loadSession() { + SessionManager sessionManager = SessionManager.getInstance(); + Session session = new Session("abcdefg"); + sessionManager.add(session); + String body = "account=gugu&password=1"; + final String request = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Cookie: JSESSIONID=abcdefg", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(request); + + assertAll( + () -> assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.GET), + () -> assertThat(httpRequest.getPath()).isEqualTo("/login"), + () -> assertThat(httpRequest.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(httpRequest.getHeaderValue("Host")).isEqualTo("localhost:8080"), + () -> assertThat(httpRequest.getHeaderValue("Connection")).isEqualTo("keep-alive"), + () -> assertThat(httpRequest.getHeaderValue(HttpHeaderName.CONTENT_LENGTH)).isEqualTo(String.valueOf(body.getBytes().length)), + () -> assertThat(httpRequest.getHeaderValue(HttpHeaderName.CONTENT_TYPE)).isEqualTo("application/x-www-form-urlencoded"), + () -> assertThat(httpRequest.getBodyValue("account")).isEqualTo("gugu"), + () -> assertThat(httpRequest.getBodyValue("password")).isEqualTo("1"), + () -> assertThat(httpRequest.getSession().getId()).isEqualTo("abcdefg") + ); + } + + @DisplayName("쿠키에 저장된 세션이 저장되지 않은 세션이면 새로 세션을 생성한다") + @Test + void createSession() { + String body = "account=gugu&password=1"; + final String request = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Cookie: JSESSIONID=abcd", + "Content-Length: " + body.getBytes().length + " ", + "Content-Type: application/x-www-form-urlencoded ", + "", + body); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(request); + + assertAll( + () -> assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.GET), + () -> assertThat(httpRequest.getPath()).isEqualTo("/login"), + () -> assertThat(httpRequest.getVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(httpRequest.getHeaderValue("Host")).isEqualTo("localhost:8080"), + () -> assertThat(httpRequest.getHeaderValue("Connection")).isEqualTo("keep-alive"), + () -> assertThat(httpRequest.getHeaderValue(HttpHeaderName.CONTENT_LENGTH)).isEqualTo(String.valueOf(body.getBytes().length)), + () -> assertThat(httpRequest.getHeaderValue(HttpHeaderName.CONTENT_TYPE)).isEqualTo("application/x-www-form-urlencoded"), + () -> assertThat(httpRequest.getBodyValue("account")).isEqualTo("gugu"), + () -> assertThat(httpRequest.getBodyValue("password")).isEqualTo("1"), + () -> assertThat(httpRequest.getSession().getId()).isNotEqualTo("abcdefg") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/httpresponse/HttpResponseTest.java b/tomcat/src/test/java/org/apache/coyote/http11/httpresponse/HttpResponseTest.java new file mode 100644 index 0000000000..ee538c0bcd --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/httpresponse/HttpResponseTest.java @@ -0,0 +1,53 @@ +package org.apache.coyote.http11.httpresponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.coyote.http11.exception.NotFoundException; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import support.HttpRequestMaker; + +class HttpResponseTest { + + @DisplayName("잘못된 파일 경로를 입력할 경우 예외를 발생시킨다") + @Test + void invalidPath() { + final String request = String.join("\r\n", + "GET /ln HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + HttpResponse httpResponse = new HttpResponse(); + + assertThatThrownBy(() -> httpResponse.staticResource("/ln")) + .isInstanceOf(NotFoundException.class); + } + + @DisplayName("입력한 값을 정확하게 HttpResponse로 변환한다") + @Test + void httpResponseBuilder() { + final String request = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + ""); + HttpRequest httpRequest = HttpRequestMaker.makeHttpRequest(request); + HttpResponse httpResponse = new HttpResponse(); + + httpResponse.location(httpRequest, "/index"); + httpResponse.setCookie("JSESSIONID=abcde"); + httpResponse.contentLength("89"); + httpResponse.contentType("test"); + httpResponse.responseBody("testResponseBody"); + + assertThat(httpResponse.toResponse()) + .contains("HTTP/1.1 302 Found".getBytes()) + .contains("Location: /index".getBytes()) + .contains("Set-Cookie: JSESSIONID=abcde".getBytes()) + .contains("Content-Length: 89".getBytes()) + .contains("Content-Type: test".getBytes()) + .contains("testResponseBody".getBytes()); + } +} diff --git a/tomcat/src/test/java/support/HttpRequestMaker.java b/tomcat/src/test/java/support/HttpRequestMaker.java new file mode 100644 index 0000000000..2f9bb756a1 --- /dev/null +++ b/tomcat/src/test/java/support/HttpRequestMaker.java @@ -0,0 +1,22 @@ +package support; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httprequest.HttpRequestConvertor; + +public class HttpRequestMaker { + + public static HttpRequest makeHttpRequest(String httpRequest) { + try { + InputStream inputStream = new ByteArrayInputStream(httpRequest.getBytes()); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + return HttpRequestConvertor.convertHttpRequest(bufferedReader); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } +}