From 5b5ee3fcca8a0c003fcb1a55e13adaf27c0499af Mon Sep 17 00:00:00 2001 From: SproutMJ Date: Tue, 5 Sep 2023 22:10:49 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=86=B0=EC=BA=A3=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20-=201,2=EB=8B=A8=EA=B3=84]=20=EC=97=94?= =?UTF-8?q?=EB=8D=B8(=EA=B9=80=EB=AF=BC=EC=A4=80)=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.=20(#331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 1단계 - HTTP 서버 구현하기 1. GET /index.html 응답하기 2. CSS 지원하기 3. Query String 파싱 * feat: 2단계 - 로그인 구현하기 1. HTTP Status Code 302 2. POST 방식으로 회원가입 3. Cookie에 JSESSIONID 값 저장하기 4. Session 구현하기 * test: 테스트를 통과하도록 수정 * test: 학습 테스트 작성 * refactor: BufferedReader를 try with resource에 넣기 * refactor: HttpHeaders를 클래스로 변경 * refactor: HttpStatus를 열거로 변경 * style: final 추가 * feat: 로그인을 post 방식으로 변경 * refactor: enum의 valueOf를 of로 네이밍 변경, ContentType을 enum으로 변경 * refactor: 빈 Cookie인 경우 .empty() 메서드를 사용해 생성하도록 메서드 네이밍 변경 --- .../example/cachecontrol/CacheWebConfig.java | 15 ++ .../example/etag/EtagFilterConfiguration.java | 16 +- .../version/CacheBustingWebConfig.java | 11 +- study/src/main/resources/application.yml | 3 + .../apache/coyote/http11/Http11Processor.java | 216 +++++++++++++++++- .../java/org/apache/http/ContentType.java | 42 ++++ .../src/main/java/org/apache/http/Cookie.java | 50 ++++ .../java/org/apache/http/HttpHeaders.java | 9 + .../main/java/org/apache/http/HttpStatus.java | 49 ++++ .../main/java/org/apache/session/Session.java | 30 +++ .../org/apache/session/SessionManager.java | 21 ++ tomcat/src/main/resources/static/login.html | 2 +- .../src/main/resources/static/register.html | 2 +- 13 files changed, 447 insertions(+), 19 deletions(-) create mode 100644 tomcat/src/main/java/org/apache/http/ContentType.java create mode 100644 tomcat/src/main/java/org/apache/http/Cookie.java create mode 100644 tomcat/src/main/java/org/apache/http/HttpHeaders.java create mode 100644 tomcat/src/main/java/org/apache/http/HttpStatus.java create mode 100644 tomcat/src/main/java/org/apache/session/Session.java create mode 100644 tomcat/src/main/java/org/apache/session/SessionManager.java diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index 305b1f1e1e..685a5cff6b 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,13 +1,28 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new NestedInterceptor()); + } + + class NestedInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + response.addHeader(HttpHeaders.CACHE_CONTROL, "no-cache"); + response.addHeader(HttpHeaders.CACHE_CONTROL, "private"); + return true; + } } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..995a094194 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -1,12 +1,20 @@ package cache.com.example.etag; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterFilterRegistrationBean = new FilterRegistrationBean<>(); + filterFilterRegistrationBean.setFilter(new ShallowEtagHeaderFilter()); + filterFilterRegistrationBean.addUrlPatterns("/etag"); + filterFilterRegistrationBean.addUrlPatterns("/resources-versioning/*"); + filterFilterRegistrationBean.setOrder(1); + return filterFilterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..48453755f7 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -2,13 +2,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.VersionResourceResolver; + +import java.util.concurrent.TimeUnit; @Configuration public class CacheBustingWebConfig implements WebMvcConfigurer { - public static final String PREFIX_STATIC_RESOURCES = "/resources"; + public static final String PREFIX_STATIC_RESOURCES = "/resources-versioning"; private final ResourceVersion version; @@ -20,6 +24,9 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic()) + .resourceChain(false) + .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**")); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..385c11d5f1 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -7,3 +7,6 @@ server: max-connections: 1 threads: max: 2 + compression: + enabled: true + min-response-size: 10 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 7f1b2c7e96..21d4d95fe0 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,18 +1,44 @@ package org.apache.coyote.http11; +import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.exception.UncheckedServletException; +import nextstep.jwp.model.User; +import org.apache.http.ContentType; +import org.apache.http.Cookie; import org.apache.coyote.Processor; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.session.Session; +import org.apache.session.SessionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; +import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.net.Socket; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final String HTTP_METHOD = "HTTP_METHOD"; + private static final String URL = "URL"; + private static final String HTTP_VERSION = "HTTP_VERSION"; private final Socket connection; + private final SessionManager sessionManager = new SessionManager(); public Http11Processor(final Socket connection) { this.connection = connection; @@ -26,22 +52,190 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); + try (final var reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); final var outputStream = connection.getOutputStream()) { + final Map requestLine = extractRequestLine(reader); + final Map httpRequestHeaders = extractRequestHeaders(reader); + final Map requestBody = extractRequestBody(httpRequestHeaders, reader); + final String method = requestLine.get(HTTP_METHOD); + final String path = parsingPath(requestLine.get(URL)); + final Map parsingQueryString = parsingQueryString(requestLine.get(URL)); - final var responseBody = "Hello world!"; - - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); - + final String response = controller(httpRequestHeaders, method, path, parsingQueryString, requestBody); outputStream.write(response.getBytes()); - outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private String controller(final Map httpRequestHeader, final String method, final String path, final Map queryString, final Map requestBody) throws IOException { + if ("GET".equals(method) && "/".equals(path)) { + return generateResponseBody(200, "html", "Hello world!"); + } + if ("GET".equals(method) && "/index.html".equals(path)) { + return generateResult(path, 200); + } + if ("POST".equals(method) && "/login".equals(path)) { + final Optional user = InMemoryUserRepository.findByAccount(requestBody.get("account")); + if (user.isEmpty()) { + return generateResult("/401.html", 401); + } + if (user.get().checkPassword(requestBody.get("password"))) { + log.info("user : {}", user); + final Cookie cookie = Cookie.empty(); + final String sessionId = UUID.randomUUID().toString(); + final Session session = new Session(sessionId); + session.setAttribute("user", user); + sessionManager.add(session); + cookie.addCookie("JSESSIONID", sessionId); + return generateRedirect("/index.html", 302, cookie); + } + return generateResult("/401.html", 401); + } + if ("GET".equals(method) && "/login".equals(path)) { + Cookie cookie = Cookie.empty(); + if (httpRequestHeader.containsKey("Cookie")) { + cookie = Cookie.parse(httpRequestHeader.get("Cookie")); + } + if (cookie.containsKey("JSESSIONID")) { + return generateRedirect("/index.html", 302); + } + return generateResult(path, 200); + } + if ("POST".equals(method) && "/register".equals(path)) { + InMemoryUserRepository.save(new User(requestBody.get("account"), requestBody.get("password"), requestBody.get("email"))); + return generateRedirect("/index.html", 302); + } + if ("GET".equals(method) && "/register".equals(path)) { + return generateResult(path, 200); + } + + return generateResult(path, 200); + } + + private String generateRedirect(final String location, final int statusCode) { + final HttpStatus httpStatus = HttpStatus.of(statusCode); + return String.join("\r\n", + "HTTP/1.1 " + httpStatus.getStatusCode() + " " + httpStatus.getStatusString() + " ", + HttpHeaders.LOCATION + ": " + location + " "); + } + + private String generateRedirect(final String location, final int statusCode, final Cookie cookie) { + final HttpStatus httpStatus = HttpStatus.of(statusCode); + return String.join("\r\n", + "HTTP/1.1 " + httpStatus.getStatusCode() + " " + httpStatus.getStatusString() + " ", + HttpHeaders.SET_COOKIE + ": " + cookie.generateCookieHeaderValue() + " ", + HttpHeaders.LOCATION + ": " + location + " "); + } + + private String generateResult(final String path, final int statusCode) throws IOException { + return generateResult(path, statusCode, Cookie.empty()); + } + + private String generateResult(final String path, final int statusCode, final Cookie cookie) throws IOException { + final String resourcePath = viewResolve(path); + final URL resource = getClass().getClassLoader().getResource("static" + resourcePath); + if (Objects.isNull(resource)) { + return ""; + } + final Path staticFilePath = new File(resource.getPath()).toPath(); + final int lastDotIndex = staticFilePath.toString().lastIndexOf("."); + final String fileExtension = staticFilePath.toString().substring(lastDotIndex + 1); + final String responseBody = Files.readString(staticFilePath); + + if (cookie.isEmpty()) { + return generateResponseBody(statusCode, fileExtension, responseBody); + } + return generateResponseBody(statusCode, fileExtension, responseBody, cookie); + } + + private String generateResponseBody(final int statusCode, final String fileExtension, final String responseBody) { + final HttpStatus httpStatus = HttpStatus.of(statusCode); + return String.join("\r\n", + "HTTP/1.1 " + httpStatus.getStatusCode() + " " + httpStatus.getStatusString() + " ", + HttpHeaders.CONTENT_TYPE + ": " + ContentType.of(fileExtension).getValue() + " ", + HttpHeaders.CONTENT_LENGTH + ": " + responseBody.getBytes(StandardCharsets.UTF_8).length + " ", + "", + responseBody); + } + + private String generateResponseBody(final int statusCode, final String fileExtension, final String responseBody, final Cookie cookie) { + final HttpStatus httpStatus = HttpStatus.of(statusCode); + return String.join("\r\n", + "HTTP/1.1 " + httpStatus.getStatusCode() + " " + httpStatus.getStatusString() + " ", + HttpHeaders.SET_COOKIE + ": " + cookie.generateCookieHeaderValue() + " ", + HttpHeaders.CONTENT_TYPE + ": " + ContentType.of(fileExtension).getValue() + " ", + HttpHeaders.CONTENT_LENGTH + ": " + responseBody.getBytes(StandardCharsets.UTF_8).length + " ", + "", + responseBody); + } + + private String viewResolve(final String resourcePath) { + if (!resourcePath.contains(".") && !resourcePath.endsWith(".html")) { + return resourcePath + ".html"; + } + + return resourcePath; + } + + private Map parsingQueryString(final String path) { + final Map queryStrings = new HashMap<>(); + if (path.contains("?")) { + final String queryString = path.substring(path.indexOf("?") + 1); + Arrays.stream(queryString.split("&")).forEach(query -> { + String[] keyValue = query.split("="); + queryStrings.put(keyValue[0], keyValue[1]); + }); + } + + return queryStrings; + } + + private String parsingPath(final String path) { + if (path.contains("?")) { + return path.substring(0, path.indexOf("?")); + } + + return path; + } + + private Map extractRequestLine(final BufferedReader reader) throws IOException { + final String requestLine = reader.readLine(); + final String[] parts = requestLine.split(" "); + + final Map request = new HashMap<>(); + request.put(HTTP_METHOD, parts[0]); + request.put(URL, parts[1]); + request.put(HTTP_VERSION, parts[2]); + + return request; + } + + private Map extractRequestHeaders(final BufferedReader reader) throws IOException { + final Map requests = new HashMap<>(); + String line; + while ((line = reader.readLine()) != null && !line.isEmpty()) { + final int colonIndex = line.indexOf(": "); + final String key = line.substring(0, colonIndex); + final String value = line.substring(colonIndex + 2); + requests.put(key, value); + } + return requests; + } + + private Map extractRequestBody(final Map httpRequestHeaders, final BufferedReader reader) throws IOException { + final Map requestBodies = new HashMap<>(); + if (httpRequestHeaders.containsKey("Content-Length")) { + final int contentLength = Integer.parseInt(httpRequestHeaders.get("Content-Length")); + final char[] buffer = new char[contentLength]; + reader.read(buffer, 0, contentLength); + final String requestBody = new String(buffer); + Arrays.stream(requestBody.split("&")).forEach(query -> { + final String[] keyValue = query.split("="); + final String decodedValue = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); + requestBodies.put(keyValue[0], decodedValue); + }); + } + return requestBodies; + } } diff --git a/tomcat/src/main/java/org/apache/http/ContentType.java b/tomcat/src/main/java/org/apache/http/ContentType.java new file mode 100644 index 0000000000..835aee5783 --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/ContentType.java @@ -0,0 +1,42 @@ +package org.apache.http; + +public enum ContentType { + + HTML("text/html;charset=utf-8"), + CSS("text/css"), + JS("application/javascript"), + ICO("image/x-icon"); + + private static final ContentType[] VALUES; + + static { + VALUES = values(); + } + + ContentType(final String value) { + this.value = value; + } + + private final String value; + + public String getValue() { + return this.value; + } + + public static ContentType of(final String fileExtension) { + final ContentType contentType = resolve(fileExtension); + if (contentType == null) { + throw new IllegalArgumentException(); + } + return contentType; + } + + private static ContentType resolve(final String fileExtension) { + for (final ContentType contentType : VALUES) { + if (contentType.name().equalsIgnoreCase(fileExtension)) { + return contentType; + } + } + return null; + } +} diff --git a/tomcat/src/main/java/org/apache/http/Cookie.java b/tomcat/src/main/java/org/apache/http/Cookie.java new file mode 100644 index 0000000000..d1d2f83e76 --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/Cookie.java @@ -0,0 +1,50 @@ +package org.apache.http; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class Cookie { + + private final Map cookies; + + public static Cookie parse(final String cookieValue) { + final Map cookies = new HashMap<>(); + Arrays.stream(cookieValue.split("; ")) + .forEach(cookie -> { + final String[] keyValue = cookie.split("="); + cookies.put(keyValue[0], keyValue[1]); + }); + return new Cookie(cookies); + } + + public static Cookie empty() { + return new Cookie(new HashMap<>()); + } + + private Cookie(Map cookies) { + this.cookies = cookies; + } + + public void addCookie(final String key, final String value) { + cookies.put(key, value); + } + + public boolean containsKey(final String key) { + return cookies.containsKey(key); + } + + public String generateCookieHeaderValue() { + final List cookies = this.cookies.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.toList()); + return String.join("; ", cookies); + } + + public boolean isEmpty() { + return cookies.isEmpty(); + } +} diff --git a/tomcat/src/main/java/org/apache/http/HttpHeaders.java b/tomcat/src/main/java/org/apache/http/HttpHeaders.java new file mode 100644 index 0000000000..65f361e065 --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/HttpHeaders.java @@ -0,0 +1,9 @@ +package org.apache.http; + +public final class HttpHeaders { + + public static final String LOCATION = "Location"; + public static final String SET_COOKIE = "Set-Cookie"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String CONTENT_LENGTH = "Content-Length"; +} diff --git a/tomcat/src/main/java/org/apache/http/HttpStatus.java b/tomcat/src/main/java/org/apache/http/HttpStatus.java new file mode 100644 index 0000000000..f8ed47c723 --- /dev/null +++ b/tomcat/src/main/java/org/apache/http/HttpStatus.java @@ -0,0 +1,49 @@ +package org.apache.http; + +public enum HttpStatus { + + OK(200, "OK"), + CREATED(201, "CREATED"), + FOUND(302, "FOUND"), + UNAUTHORIZED(401, "UNAUTHORIZED"), + NOT_FOUND(404, "NOT FOUND"); + + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int statusCode; + private final String statusString; + + HttpStatus(int statusCode, final String statusString) { + this.statusCode = statusCode; + this.statusString = statusString; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusString() { + return statusString; + } + + public static HttpStatus of(final int statusCode) { + final HttpStatus status = resolve(statusCode); + if (status == null) { + throw new IllegalArgumentException(); + } + return status; + } + + private static HttpStatus resolve(int statusCode) { + for (final HttpStatus status : VALUES) { + if (status.statusCode == statusCode) { + return status; + } + } + return null; + } +} diff --git a/tomcat/src/main/java/org/apache/session/Session.java b/tomcat/src/main/java/org/apache/session/Session.java new file mode 100644 index 0000000000..81b6662787 --- /dev/null +++ b/tomcat/src/main/java/org/apache/session/Session.java @@ -0,0 +1,30 @@ +package org.apache.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 this.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); + } +} diff --git a/tomcat/src/main/java/org/apache/session/SessionManager.java b/tomcat/src/main/java/org/apache/session/SessionManager.java new file mode 100644 index 0000000000..6c8bdde5e9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/session/SessionManager.java @@ -0,0 +1,21 @@ +package org.apache.session; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + + private static final Map SESSIONS = new HashMap<>(); + + public void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + public Session findSession(final String id) { + return SESSIONS.get(id); + } + + 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..0b68cc758e 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/main/resources/static/register.html b/tomcat/src/main/resources/static/register.html index d87de0214c..c8d8905ed7 100644 --- a/tomcat/src/main/resources/static/register.html +++ b/tomcat/src/main/resources/static/register.html @@ -20,7 +20,7 @@

회원 가입

- +