diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index c7f2608670..e0e8df29e2 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -2,17 +2,11 @@ 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 java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index c87e0b3891..5de32e8997 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,7 +1,6 @@ package study; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; diff --git a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java index 40b5104a69..5c7393c5e3 100644 --- a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java @@ -4,6 +4,8 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.apache.coyote.http11.exception.NoSuchUserException; + import com.techcourse.model.User; public class InMemoryUserRepository { @@ -19,6 +21,11 @@ public static void save(User user) { database.put(user.getAccount(), user); } + public static User fetchByAccount(String account) { + return findByAccount(account) + .orElseThrow(() -> new NoSuchUserException(account + " 에 해당하는 유저를 찾을 수 없습니다.")); + } + public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index 82b207fb05..fd850c6558 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -2,7 +2,7 @@ import java.io.IOException; -import jakarta.servlet.http.HttpSession; +import org.apache.coyote.http11.session.Session; /** * A Manager manages the pool of Sessions that are associated with a @@ -29,7 +29,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 @@ -43,12 +43,12 @@ public interface Manager { * @throws IOException if an input/output error occurs while * processing this request */ - 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(String session); } 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 8424340b4e..e61d416570 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -4,15 +4,10 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import org.apache.coyote.Processor; -import org.apache.coyote.http11.exception.NoHandlerException; -import org.apache.coyote.http11.handler.MethodHandler; -import org.apache.coyote.http11.handler.StaticResourceHandler; -import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.handler.DefaultResourceHandler; +import org.apache.coyote.http11.httpmessage.request.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,15 +16,12 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - private final List requestHandlers; + private final RequestHandler requestHandler; private final Socket connection; public Http11Processor(Socket connection) { this.connection = connection; - this.requestHandlers = List.of( - new MethodHandler(), - new StaticResourceHandler() - ); + this.requestHandler = new DefaultResourceHandler(); } @Override @@ -45,7 +37,7 @@ public void process(Socket connection) { BufferedReader requestBufferedReader = new BufferedReader(inputStreamReader); var outputStream = connection.getOutputStream()) { - Request request = Request.parseFrom(getPlainRequest(requestBufferedReader)); + Request request = Request.readFrom(requestBufferedReader); log.info("request : {}", request); String response = getResponse(request); @@ -57,21 +49,6 @@ public void process(Socket connection) { } private String getResponse(Request request) throws IOException { - for (RequestHandler requestHandler : requestHandlers) { - String response = requestHandler.handle(request); - if (response != null) { - return response; - } - } - throw new NoHandlerException("핸들러가 존재하지 않습니다. request : " + request); - } - - private List getPlainRequest(BufferedReader requestBufferedReader) throws IOException { - List plainRequest = new ArrayList<>(); - while (requestBufferedReader.ready()) { - String line = requestBufferedReader.readLine(); - plainRequest.add(line); - } - return Collections.unmodifiableList(plainRequest); + return requestHandler.handle(request); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestHandler.java index 9492e6aee1..89ac542ac2 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/RequestHandler.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/RequestHandler.java @@ -2,7 +2,7 @@ import java.io.IOException; -import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.httpmessage.request.Request; @FunctionalInterface public interface RequestHandler { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/CanNotHandleRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/CanNotHandleRequest.java new file mode 100644 index 0000000000..922fbc0c82 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/CanNotHandleRequest.java @@ -0,0 +1,7 @@ +package org.apache.coyote.http11.exception; + +public class CanNotHandleRequest extends RuntimeException { + public CanNotHandleRequest(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/NoSuchUserException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/NoSuchUserException.java new file mode 100644 index 0000000000..4430131224 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/NoSuchUserException.java @@ -0,0 +1,7 @@ +package org.apache.coyote.http11.exception; + +public class NoSuchUserException extends RuntimeException { + public NoSuchUserException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/NotCompleteResponseException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/NotCompleteResponseException.java new file mode 100644 index 0000000000..04eb9c1536 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/NotCompleteResponseException.java @@ -0,0 +1,7 @@ +package org.apache.coyote.http11.exception; + +public class NotCompleteResponseException extends RuntimeException { + public NotCompleteResponseException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/DefaultResourceHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/DefaultResourceHandler.java new file mode 100644 index 0000000000..fcca082d83 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/DefaultResourceHandler.java @@ -0,0 +1,113 @@ +package org.apache.coyote.http11.handler; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.coyote.http11.RequestHandler; +import org.apache.coyote.http11.exception.CanNotHandleRequest; +import org.apache.coyote.http11.exception.NoSuchUserException; +import org.apache.coyote.http11.httpmessage.request.Request; +import org.apache.coyote.http11.httpmessage.response.Response; +import org.apache.coyote.http11.httpmessage.response.StaticResource; +import org.apache.coyote.http11.session.Session; +import org.apache.coyote.http11.session.SessionManager; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; + +public class DefaultResourceHandler implements RequestHandler { + + @Override + public String handle(Request request) throws IOException { + if (request.isStaticResourceRequest()) { + return Response.builder() + .versionOf(request.getHttpVersion()) + .ofStaticResource(new StaticResource(request.getTarget())) + .toHttpMessage(); + } + if (request.getTarget().equals("/")) { + return Response.builder() + .versionOf(request.getHttpVersion()) + .ofStaticResource(new StaticResource("/index.html")) + .toHttpMessage(); + } + if (request.getTarget().equals("/login")) { + return loginResponse(request); + } + if (request.getTarget().contains("register")) { + return registerResponse(request); + } + throw new CanNotHandleRequest("처리할 수 없는 요청입니다. : " + request.getTarget()); + } + + private String loginResponse(Request request) throws IOException { + if (request.isPost()) { + return login(request).toHttpMessage(); + } + if (isLoggedIn(request)) { + return Response.builder() + .versionOf(request.getHttpVersion()) + .found("/index.html") + .toHttpMessage(); + } + return Response.builder() + .versionOf(request.getHttpVersion()) + .ofStaticResource(new StaticResource("/login.html")) + .toHttpMessage(); + } + + private boolean isLoggedIn(Request request) { + return Objects.nonNull(request.getSession(false)); + } + + private Response login(Request request) throws NoSuchUserException { + RequestParameters requestParams = RequestParameters.parseFrom(request.getBody()); + String account = requestParams.getParam("account"); + String password = requestParams.getParam("password"); + User user = InMemoryUserRepository.fetchByAccount(account); + if (user.checkPassword(password)) { + Session session = request.getSession(true); + session.setAttribute("user", user); + SessionManager.getInstance().add(session); + return Response.builder() + .versionOf(request.getHttpVersion()) + .addCookie("JSESSIONID", session.getId()) + .found("/index.html"); + } + + return Response.builder() + .versionOf(request.getHttpVersion()) + .found("/401.html"); + } + + private String registerResponse(Request request) throws IOException { + if (request.isPost()) { + RequestParameters methodRequest = RequestParameters.parseFrom(request.getBody()); + User user = register(methodRequest); + Session session = request.getSession(true); + session.setAttribute("user", user); + SessionManager.getInstance().add(session); + return Response.builder() + .versionOf(request.getHttpVersion()) + .addCookie("JSESSIONID", session.getId()) + .found("/index.html") + .toHttpMessage(); + } + + return Response.builder() + .versionOf(request.getHttpVersion()) + .ofStaticResource(new StaticResource("/register.html")) + .toHttpMessage(); + } + + private User register(RequestParameters requestParams) { + String account = requestParams.getParam("account"); + User user = new User( + account, + requestParams.getParam("password"), + requestParams.getParam("email") + ); + InMemoryUserRepository.save(user); + return InMemoryUserRepository.fetchByAccount(account); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodHandler.java deleted file mode 100644 index 8cf18b8065..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.apache.coyote.http11.handler; - -import java.io.IOException; - -import org.apache.coyote.http11.RequestHandler; -import org.apache.coyote.http11.request.Request; -import org.apache.coyote.http11.response.Response; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.techcourse.db.InMemoryUserRepository; -import com.techcourse.model.User; - -public class MethodHandler implements RequestHandler { - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public String handle(Request request) throws IOException { - MethodRequest methodRequest = new MethodRequest(request.getTarget()); - return Response.writeResponse(request, "application/json", handleMethod(methodRequest)); - } - - private String handleMethod(MethodRequest methodRequest) throws JsonProcessingException { - if (!methodRequest.getEndPoint().equals("/login")) { - return null; - } - String account = methodRequest.getParam("account"); - User user = InMemoryUserRepository.findByAccount(account) - .orElseThrow(() -> new IllegalArgumentException(account + " 이름의 유저가 없습니다.")); - return objectMapper.writeValueAsString(user); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodQueryParameters.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodQueryParameters.java deleted file mode 100644 index 61bc14b1b9..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodQueryParameters.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.apache.coyote.http11.handler; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; - -public class MethodQueryParameters { - private final Map queryParams; - - private MethodQueryParameters(Map queryParams) { - this.queryParams = Map.copyOf(queryParams); - } - - public static MethodQueryParameters empty() { - return new MethodQueryParameters(Collections.emptyMap()); - } - - public static MethodQueryParameters parseFrom(String queryStrings) { - Map queryParams = new HashMap<>(); - String[] queryParamTokens = queryStrings.split("&"); - for (String queryParam : queryParamTokens) { - String[] split = queryParam.split("="); - queryParams.put(split[0], split[1]); - } - return new MethodQueryParameters(queryParams); - } - - public String getParam(String key) { - return Optional.ofNullable(queryParams.get(key)) - .orElseThrow(() -> new NoSuchElementException(key + " 에 해당하는 값이 존재하지 않습니다.")); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodRequest.java deleted file mode 100644 index e18caf0cc6..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/handler/MethodRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.apache.coyote.http11.handler; - -public class MethodRequest { - private final String endPoint; - private final MethodQueryParameters queryParams; - - public MethodRequest(String url) { - String[] targetToken = url.split("\\?"); - this.endPoint = targetToken[0]; - - if (targetToken.length == 1) { - this.queryParams = MethodQueryParameters.empty(); - } else { - this.queryParams = MethodQueryParameters.parseFrom(targetToken[1]); - } - } - - public String getParam(String key) { - return queryParams.getParam(key); - } - - public String getEndPoint() { - return this.endPoint; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestParameters.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestParameters.java new file mode 100644 index 0000000000..f75590253a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestParameters.java @@ -0,0 +1,29 @@ +package org.apache.coyote.http11.handler; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; + +public class RequestParameters { + private final Map requestParams; + + private RequestParameters(Map requestParams) { + this.requestParams = Map.copyOf(requestParams); + } + + public static RequestParameters parseFrom(String paramString) { + Map requestParams = new HashMap<>(); + String[] requestParamTokens = paramString.split("&"); + for (String requestParam : requestParamTokens) { + String[] split = requestParam.split("="); + requestParams.put(split[0], split[1]); + } + return new RequestParameters(requestParams); + } + + public String getParam(String key) { + return Optional.ofNullable(requestParams.get(key)) + .orElseThrow(() -> new NoSuchElementException(key + " 에 해당하는 값이 존재하지 않습니다.")); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/StaticResourceHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/StaticResourceHandler.java deleted file mode 100644 index 56fda64094..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/handler/StaticResourceHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.apache.coyote.http11.handler; - -import java.io.IOException; -import java.nio.file.NoSuchFileException; - -import org.apache.coyote.http11.RequestHandler; -import org.apache.coyote.http11.request.Method; -import org.apache.coyote.http11.request.Request; -import org.apache.coyote.http11.response.Content; -import org.apache.coyote.http11.response.Response; - -public class StaticResourceHandler implements RequestHandler { - - @Override - public String handle(Request request) throws IOException { - if (request.getMethod() != Method.GET) { - return null; - } - - try { - Content content = getContent(request); - return Response.writeResponse(request, content.getContentType(), content.getContent()); - } catch (NoSuchFileException e) { - return null; - } - } - - private Content getContent(Request request) throws IOException { - String target = request.getTarget().equals("/") ? "index.html" : request.getTarget(); - return new Content(target); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/HttpCookie.java new file mode 100644 index 0000000000..331f268366 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/HttpCookie.java @@ -0,0 +1,50 @@ +package org.apache.coyote.http11.httpmessage; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpCookie { + private static final String COOKIE_DELIMITER = "; "; + private final Map cookies; + + private HttpCookie(Map cookies) { + this.cookies = cookies; + } + + public HttpCookie() { + this.cookies = new HashMap<>(); + } + + public static HttpCookie parseFrom(String cookiesLine) { + HashMap cookies = new HashMap<>(); + for (String cookie : cookiesLine.split(COOKIE_DELIMITER)) { + System.out.println(cookie); + String[] token = cookie.split("="); + cookies.put(token[0], token[1]); + } + return new HttpCookie(cookies); + } + + public void addCookie(String key, String value) { + cookies.put(key, value); + } + + public boolean contains(String key) { + return cookies.containsKey(key); + } + + public String getCookie(String key) { + return cookies.get(key); + } + + public boolean isEmpty() { + return cookies.isEmpty(); + } + + public String toHttpMessage() { + return cookies.entrySet().stream() + .map(entry -> String.format("%s=%s", entry.getKey(), entry.getValue())) + .collect(Collectors.joining(COOKIE_DELIMITER)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/HttpHeaders.java new file mode 100644 index 0000000000..e69036c1d6 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/HttpHeaders.java @@ -0,0 +1,60 @@ +package org.apache.coyote.http11.httpmessage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class HttpHeaders { + + public static final String CONTENT_TYPE = "Content-Type"; + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String LOCATION = "Location"; + public static final String COOKIE = "Cookie"; + public static final String JSESSIONID = "JSESSIONID"; + public static final String SET_COOKIE = "Set-Cookie"; + + private final Map headers; + + public HttpHeaders(Map headers) { + this.headers = headers; + } + + public HttpHeaders() { + this(new HashMap<>()); + } + + public void addHeader(String key, String value) { + headers.put(key, value); + } + + public boolean contains(String key) { + return headers.containsKey(key); + } + + public String get(String key) { + return headers.get(key); + } + + public int getContentLength() { + return Integer.parseInt( + Optional.ofNullable(headers.get(CONTENT_LENGTH)) + .orElse("0") + ); + } + + public String toHttpMessage() { + return headers.entrySet().stream() + .map(entry -> String.format("%s: %s ", entry.getKey(), entry.getValue())) + .collect(Collectors.joining("\r\n")); + } + + @Override + public String toString() { + return "HttpHeaders{" + + headers.entrySet().stream() + .map(entry -> String.format("\n\t\t%s %s", entry.getKey(), entry.getValue())) + .collect(Collectors.joining()) + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Method.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Method.java similarity index 87% rename from tomcat/src/main/java/org/apache/coyote/http11/request/Method.java rename to tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Method.java index 04ca6a3c38..46e3b4ed9a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/Method.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Method.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11.request; +package org.apache.coyote.http11.httpmessage.request; import java.util.Arrays; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Request.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Request.java new file mode 100644 index 0000000000..83798f2c7d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Request.java @@ -0,0 +1,105 @@ +package org.apache.coyote.http11.httpmessage.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.coyote.http11.httpmessage.HttpCookie; +import org.apache.coyote.http11.httpmessage.HttpHeaders; +import org.apache.coyote.http11.session.Session; +import org.apache.coyote.http11.session.SessionManager; + +public class Request { + + private final RequestLine requestLine; + private final HttpHeaders headers; + private final HttpCookie cookies; + private final String body; + + private Session session; + + private Request(RequestLine requestLine, HttpHeaders headers, String body) { + this.requestLine = requestLine; + this.headers = headers; + if (headers.contains(HttpHeaders.COOKIE)) { + this.cookies = HttpCookie.parseFrom(headers.get(HttpHeaders.COOKIE)); + } else { + this.cookies = new HttpCookie(); + } + this.body = body; + + if (cookies.contains(HttpHeaders.JSESSIONID)) { + SessionManager sessionManager = SessionManager.getInstance(); + this.session = sessionManager.findSession(cookies.getCookie(HttpHeaders.JSESSIONID)); + } + } + + public static Request readFrom(BufferedReader reader) throws IOException { + RequestLine requestLine = RequestLine.parseFrom(reader.readLine()); + HttpHeaders header = new HttpHeaders(readHeader(reader)); + String requestBody = readBody(reader, header.getContentLength()); + return new Request(requestLine, header, requestBody); + } + + private static Map readHeader(BufferedReader reader) throws IOException { + String line; + Map headers = new HashMap<>(); + while (true) { + line = reader.readLine(); + if (line == null || line.isBlank()) { + break; + } + String[] headerLine = line.split(": "); + headers.put(headerLine[0], headerLine[1]); + } + return headers; + } + + private static String readBody(BufferedReader reader, int contentLength) throws IOException { + char[] buffer = new char[contentLength]; + reader.read(buffer, 0, contentLength); + return new String(buffer); + } + + public Session getSession(boolean created) { + if (created && this.session == null) { + this.session = new Session(UUID.randomUUID().toString()); + } + return this.session; + } + + public boolean isPost() { + return requestLine.isPost(); + } + + public boolean isStaticResourceRequest() { + return requestLine.isStaticResourceRequest(); + } + + public Method getMethod() { + return requestLine.method(); + } + + public String getTarget() { + return requestLine.target(); + } + + public String getHttpVersion() { + return requestLine.httpVersion(); + } + + public String getBody() { + return body; + } + + @Override + public String toString() { + return "Request{" + + "requestLine=" + requestLine + + ", headers=" + headers + + ", body='" + body + '\'' + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/RequestLine.java similarity index 64% rename from tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java rename to tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/RequestLine.java index 04fd330939..66cad8037e 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/RequestLine.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11.request; +package org.apache.coyote.http11.httpmessage.request; public record RequestLine ( Method method, @@ -11,6 +11,16 @@ public static RequestLine parseFrom(String requestLineText) { return new RequestLine(Method.findByName(token[0]), token[1], token[2]); } + public boolean isPost() { + return method == Method.POST; + } + + public boolean isStaticResourceRequest() { + return target.contains(".css") || + target.contains(".html") || + target.contains(".js"); + } + @Override public String toString() { return "RequestLine{" + diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/Response.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/Response.java new file mode 100644 index 0000000000..13e65f1303 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/Response.java @@ -0,0 +1,89 @@ +package org.apache.coyote.http11.httpmessage.response; + +import java.io.IOException; + +import org.apache.coyote.http11.exception.NotCompleteResponseException; +import org.apache.coyote.http11.httpmessage.HttpCookie; +import org.apache.coyote.http11.httpmessage.HttpHeaders; + +public class Response { + + private final StatusLine statusLine; + private final HttpHeaders headers; + private final String content; + + public static ResponseBuilder builder() { + return new ResponseBuilder(); + } + + public Response(StatusLine statusLine, HttpHeaders headers, String content) { + this.statusLine = statusLine; + this.headers = headers; + this.content = content; + } + + public static class ResponseBuilder { + private final HttpCookie httpCookie; + private final HttpHeaders headers; + private String ProtocolVersion; + + private ResponseBuilder() { + ProtocolVersion = "HTTP/1.1"; + httpCookie = new HttpCookie(); + headers = new HttpHeaders(); + } + + public ResponseBuilder versionOf(String protocolVersion) { + this.ProtocolVersion = protocolVersion; + return this; + } + + public ResponseBuilder addCookie(String key, String value) { + httpCookie.addCookie(key, value); + return this; + } + + public Response found(String target) { + this.headers.addHeader(HttpHeaders.LOCATION, target); + + return build( + new StatusLine(this.ProtocolVersion, 301, "FOUND"), + this.headers, + "" + ); + } + + public Response ofStaticResource(StaticResource resource) throws IOException { + headers.addHeader(HttpHeaders.CONTENT_TYPE, resource.getContentType() + ";charset=utf-8"); + headers.addHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(resource.getContentLength())); + + return build( + new StatusLine(this.ProtocolVersion, 200, "OK"), + this.headers, + resource.getContent() + ); + } + + public Response build(StatusLine statusLine, HttpHeaders headers, String content) { + setCookie(headers); + return new Response(statusLine, headers, content); + } + + private void setCookie(HttpHeaders headers) { + if(!httpCookie.isEmpty()) { + headers.addHeader(HttpHeaders.SET_COOKIE, httpCookie.toHttpMessage()); + } + } + } + + public String toHttpMessage() { + if (statusLine == null) { + throw new NotCompleteResponseException("응답이 완성되지 않았습니다."); + } + return String.join("\r\n", + statusLine.toHttpMessage(), + headers.toHttpMessage(), + "", + content); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StaticResource.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StaticResource.java new file mode 100644 index 0000000000..2a8188f747 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StaticResource.java @@ -0,0 +1,31 @@ +package org.apache.coyote.http11.httpmessage.response; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; + +public class StaticResource { + private final File file; + + public StaticResource(String resourcePath) throws IOException { + URL resource = getClass().getClassLoader().getResource("static" + resourcePath); + if (resource == null) { + throw new NoSuchFileException(resourcePath + " 경로의 파일이 존재하지 않습니다."); + } + this.file = new File(resource.getPath()); + } + + public String getContentType() throws IOException { + return Files.probeContentType(file.toPath()); + } + + public String getContent() throws IOException { + return new String(Files.readAllBytes(file.toPath())); + } + + public long getContentLength() throws IOException { + return getContent().getBytes().length; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StatusLine.java new file mode 100644 index 0000000000..21b1a87990 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StatusLine.java @@ -0,0 +1,18 @@ +package org.apache.coyote.http11.httpmessage.response; + +public class StatusLine { + + private final String protocolVersion; + private final int statusCode; + private final String statusText; + + public StatusLine(String protocolVersion, int statusCode, String statusText) { + this.protocolVersion = protocolVersion; + this.statusCode = statusCode; + this.statusText = statusText; + } + + public String toHttpMessage() { + return String.format("%s %s %s ", protocolVersion, statusCode, statusText); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Request.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Request.java deleted file mode 100644 index 7c7021e36f..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/Request.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.apache.coyote.http11.request; - -import java.util.List; - -public class Request { - - private final RequestLine requestLine; - - private Request(RequestLine requestLine) { - this.requestLine = requestLine; - } - - public static Request parseFrom(List request) { - return new Request(RequestLine.parseFrom(request.getFirst())); - } - - public Method getMethod() { - return requestLine.method(); - } - - public String getTarget() { - return requestLine.target(); - } - - public String getHttpVersion() { - return requestLine.httpVersion(); - } - - @Override - public String toString() { - return "Request{" + - "requestLine=" + requestLine + - '}'; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Content.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Content.java deleted file mode 100644 index f206725f05..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/Content.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.apache.coyote.http11.response; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; - -public class Content { - private final String contentType; - private final String content; - - public Content(String resourceName) throws IOException { - this.contentType = determineContentType(resourceName); - String path = "static" + resourceName; - URL resource = getClass().getClassLoader().getResource(path); - if (resource == null) { - throw new NoSuchFileException("파일 " + resourceName + "이 존재하지 않습니다."); - } - this.content = new String(Files.readAllBytes(new File(resource.getPath()).toPath())); - } - - private String determineContentType(String resourceName) { - String extension = resourceName.split("\\.")[1]; - if (extension.equals("html")) { - return "text/html"; - } - if (extension.equals("css")) { - return "text/css"; - } - return "text/plain"; - } - - public String getContentType() { - return contentType; - } - - public String getContent() { - return content; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Response.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Response.java deleted file mode 100644 index 311c156f24..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/Response.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.apache.coyote.http11.response; - -import org.apache.coyote.http11.request.Request; - -public class Response { - public static String writeResponse(Request request, String contentType, String content) { - return String.join("\r\n", - String.format("%s 200 OK ", request.getHttpVersion()), - String.format("Content-Type: %s;charset=utf-8 ",contentType), - "Content-Length: " + content.getBytes().length + " ", - "", - content); - } -} 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..ba63df869d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java @@ -0,0 +1,34 @@ +package org.apache.coyote.http11.session; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public Object getAttribute(final String name) { + return values.get(name); + } + + public void setAttribute(final String name, final Object value) { + values.put(name, value); + } + + public void removeAttribute(final String name) { + values.remove(name); + } + + public void invalidate() { + values.clear(); + } +} 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..277c87c3e1 --- /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.HashMap; +import java.util.Map; + +import org.apache.catalina.Manager; + +public class SessionManager implements Manager { + // static! + private static final Map SESSIONS = new HashMap<>(); + private static SessionManager instance; + + private SessionManager() {} + + public static SessionManager getInstance() { + if (instance == null) { + instance = new SessionManager(); + } + return instance; + } + + @Override + public void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public Session findSession(final String id) { + return SESSIONS.get(id); + } + + @Override + public void remove(final String id) { + SESSIONS.remove(id); + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..4db95d061f 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

로그인

-
+
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 02e44f4a83..a521e0059a 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -7,10 +7,13 @@ import java.net.URL; import java.nio.file.Files; +import org.apache.coyote.http11.session.Session; +import org.apache.coyote.http11.session.SessionManager; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.databind.ObjectMapper; import com.techcourse.db.InMemoryUserRepository; import com.techcourse.model.User; @@ -29,21 +32,19 @@ void process() { processor.process(socket); // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", + assertThat(socket.output()).contains("HTTP/1.1 200 OK ", "Content-Type: text/html;charset=utf-8 ", "Content-Length: 12 ", "", "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); } @Test + @DisplayName("루트 요청에 대해 index.html 페이지 응답") void index() throws IOException { // given final String httpRequest = String.join("\r\n", - "GET /index.html HTTP/1.1 ", + "GET / HTTP/1.1 ", "Host: localhost:8080 ", "Connection: keep-alive ", "", @@ -57,69 +58,247 @@ void index() throws IOException { // 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: 5564 \r\n" + - "\r\n" + - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - assertThat(socket.output()).isEqualTo(expected); + String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + assertThat(socket.output()).contains( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + String.format("Content-Length: %d \r\n", responseBody.getBytes().length), + "\r\n", + responseBody + ); } - @Test - void css() throws IOException { - // given - final String httpRequest = String.join("\r\n", - "GET /css/styles.css HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "", - ""); + @Nested + @DisplayName("정적 리소스 조회") + class GetStaticResourceTest { - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + @Test + @DisplayName("index.html 페이지 조회") + void index() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /index.html HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); - // when - processor.process(socket); + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); - // then - final URL resource = getClass().getClassLoader().getResource("static/css/styles.css"); - String contentBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/css;charset=utf-8 \r\n" + - String.format("Content-Length: %d \r\n", contentBody.getBytes().length) + - "\r\n" + - contentBody; - - assertThat(socket.output()).isEqualTo(expected); + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/index.html"); + + String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + assertThat(socket.output()).contains( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + String.format("Content-Length: %d \r\n", responseBody.getBytes().length), + "\r\n", + responseBody + ); + } + + @Test + @DisplayName("style.css 파일 테스트") + void css() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /css/styles.css HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/css/styles.css"); + String contentBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).contains("HTTP/1.1 200 OK \r\n", + "Content-Type: text/css;charset=utf-8 \r\n", + String.format("Content-Length: %d \r\n", contentBody.getBytes().length), + "\r\n", + contentBody + ); + } } - @Test - void login() throws IOException { - // given - final String httpRequest = String.join("\r\n", - "GET /login?account=gugu&password=password HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "", - ""); + @Nested + @DisplayName("로그인 테스트") + class LoginTest { - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - final ObjectMapper objectMapper = new ObjectMapper(); + @Test + @DisplayName("로그인 뷰 테스트") + void login() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); - // when - processor.process(socket); + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); - // then - User user = InMemoryUserRepository.findByAccount("gugu").get(); - String contentBody = objectMapper.writeValueAsString(user); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: application/json;charset=utf-8 \r\n" + - String.format("Content-Length: %d \r\n", contentBody.getBytes().length) + - "\r\n" + - contentBody; - - assertThat(socket.output()).isEqualTo(expected); + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/login.html"); + String contentBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).contains("HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + String.format("Content-Length: %d \r\n", contentBody.getBytes().length), + "\r\n", + contentBody); + } + + @Test + @DisplayName("로그인 뷰 테스트 : 이미 로그인이 되어 있을 경우 index.html 로 리다이렉트 시킨다.") + void loginViewWhenLoggedIn() throws IOException { + // given + User user = InMemoryUserRepository.findByAccount("gugu").get(); + String sessionId = "sessionId"; + Session session = new Session(sessionId); + session.setAttribute("user", user); + SessionManager.getInstance().add(session); + + final String httpRequest = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Cookie: JSESSIONID=sessionId", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + assertThat(socket.output()).contains("HTTP/1.1 301 FOUND \r\n", + "Location: /index.html"); + } + + @Test + @DisplayName("로그인 로직 성공 테스트") + void loginSuccess() throws IOException { + // given + String requestBody = "account=gugu&password=password"; + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + requestBody.getBytes().length, + "", + requestBody); + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + assertThat(socket.output()).contains("HTTP/1.1 301 FOUND \r\n", + "Location: /index.html \r\n", + "\r\n"); + } + + @Test + @DisplayName("로그인 로직 실패 테스트") + void loginFailed() throws IOException { + String requestBody = "account=gugu&password=wrong"; + // given + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + requestBody.getBytes().length, + "", + requestBody); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + assertThat(socket.output()).contains("HTTP/1.1 301 FOUND \r\n", + "Location: /401.html \r\n", + "\r\n"); + } + } + + @Nested + @DisplayName("회원가입 테스트") + class ResisterTest { + + @Test + @DisplayName("회원가입 뷰 테스트") + void registerView() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/register.html"); + String contentBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).contains("HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + String.format("Content-Length: %d \r\n", contentBody.getBytes().length), + "\r\n", + contentBody); + } + + @Test + @DisplayName("회원가입 로직 성공 테스트") + void registerSuccess() throws IOException { + // given + String requestBody = "account=HoeSeong123&password=eyeTwinkle&email=chorong@wooteco.com"; + final String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Content-Length: " + requestBody.getBytes().length, + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + requestBody); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + assertThat(socket.output()).contains("HTTP/1.1 301 FOUND \r\n", + "Location: /index.html \r\n", + "\r\n"); + } } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/learningTest/ObjectMapperTest.java b/tomcat/src/test/java/org/apache/coyote/http11/learningTest/ObjectMapperTest.java index 341af4ddec..22ec0e76c7 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/learningTest/ObjectMapperTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/learningTest/ObjectMapperTest.java @@ -1,5 +1,6 @@ package org.apache.coyote.http11.learningTest; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,6 +15,7 @@ public class ObjectMapperTest { @Test @DisplayName("Object -> Json 직렬화 테스트") + @Disabled void serializeTest() throws JsonProcessingException { User user = InMemoryUserRepository.findByAccount("gugu").get(); diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java index 009f03be72..8438feca69 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java @@ -9,6 +9,8 @@ import java.io.InputStream; import java.io.InputStreamReader; +import org.apache.coyote.http11.httpmessage.request.Method; +import org.apache.coyote.http11.httpmessage.request.Request; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -35,7 +37,7 @@ void getRequestConstructTest() throws IOException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(requestStream)); - Request request = Request.parseFrom(bufferedReader.lines().toList()); + Request request = Request.readFrom(bufferedReader); //then assertAll(