diff --git a/study/build.gradle b/study/build.gradle index 7bdce4f751..c4d76b6702 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -21,8 +21,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.apache.commons:commons-lang3:3.14.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' - implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' + implementation 'com.github.jknack:handlebars-springmvc:4.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.assertj:assertj-core:3.26.0' diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java b/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java deleted file mode 100644 index ed47d0541a..0000000000 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheHandlerInterceptor.java +++ /dev/null @@ -1,18 +0,0 @@ -package cache.com.example.cachecontrol; - -import com.google.common.net.HttpHeaders; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.CacheControl; -import org.springframework.web.servlet.HandlerInterceptor; - -public class CacheHandlerInterceptor implements HandlerInterceptor { - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - CacheControl cacheControl = CacheControl.noCache().cachePrivate(); - response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue()); - - return true; - } -} diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index f3e04d3a3d..823229ca53 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,28 +1,28 @@ package cache.com.example.cachecontrol; -import com.google.common.net.HttpHeaders; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; + import java.time.Duration; import org.springframework.context.annotation.Configuration; import org.springframework.http.CacheControl; -import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { - registry.addInterceptor(new CacheHandlerInterceptor()).addPathPatterns("/"); - registry.addInterceptor(new HandlerInterceptor() { - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)).cachePublic(); - response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl.getHeaderValue()); - return true; - } - }).addPathPatterns("/resources/**"); + WebContentInterceptor cacheInterceptor = new WebContentInterceptor(); + cacheInterceptor.setCacheControl(CacheControl.noCache().cachePrivate()); + registry.addInterceptor(cacheInterceptor) + .addPathPatterns("/"); + + WebContentInterceptor resourceInterceptor = new WebContentInterceptor(); + CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)).cachePublic(); + resourceInterceptor.setCacheControl(cacheControl); + registry.addInterceptor(resourceInterceptor) + .addPathPatterns(PREFIX_STATIC_RESOURCES + "/**"); } } 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 c51500216c..bd497e9c11 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -10,6 +10,11 @@ public class EtagFilterConfiguration { @Bean public FilterRegistrationBean shallowEtagHeaderFilter() { - return new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + FilterRegistrationBean filterFilterRegistrationBean = new FilterRegistrationBean<>( + new ShallowEtagHeaderFilter()); + filterFilterRegistrationBean.addUrlPatterns("/etag"); + filterFilterRegistrationBean.addUrlPatterns("/resources/*"); + + return filterFilterRegistrationBean; } } diff --git a/study/src/main/java/cache/com/example/version/HandlebarsConfig.java b/study/src/main/java/cache/com/example/version/HandlebarsConfig.java new file mode 100644 index 0000000000..79f73b97e9 --- /dev/null +++ b/study/src/main/java/cache/com/example/version/HandlebarsConfig.java @@ -0,0 +1,26 @@ +package cache.com.example.version; + +import com.github.jknack.handlebars.springmvc.HandlebarsViewResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.ViewResolver; + +@Configuration +public class HandlebarsConfig { + + private final VersionHandlebarsHelper versionHandlebarsHelper; + + public HandlebarsConfig(VersionHandlebarsHelper versionHandlebarsHelper) { + this.versionHandlebarsHelper = versionHandlebarsHelper; + } + + @Bean + public ViewResolver handlebarsViewResolver() { + HandlebarsViewResolver viewResolver = new HandlebarsViewResolver(); + viewResolver.registerHelper("staticUrls", versionHandlebarsHelper); + viewResolver.setPrefix("classpath:/templates/"); + viewResolver.setSuffix(".html"); + + return viewResolver; + } +} \ No newline at end of file diff --git a/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java b/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java index a8e004466a..8bf617189f 100644 --- a/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java +++ b/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java @@ -1,13 +1,14 @@ package cache.com.example.version; +import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import pl.allegro.tech.boot.autoconfigure.handlebars.HandlebarsHelper; +import org.springframework.stereotype.Component; -@HandlebarsHelper -public class VersionHandlebarsHelper { +@Component +public class VersionHandlebarsHelper implements Helper { private static final Logger log = LoggerFactory.getLogger(VersionHandlebarsHelper.class); @@ -22,4 +23,9 @@ public String staticUrls(String path, Options options) { log.debug("static url : {}", path); return String.format("/resources/%s%s", version.getVersion(), path); } + + @Override + public Object apply(Object context, Options options) { + return staticUrls(context.toString(), options); + } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 8b74bdfd88..db798e1815 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -1,6 +1,3 @@ -handlebars: - suffix: .html - server: tomcat: accept-count: 1 diff --git a/tomcat/src/main/java/com/techcourse/controller/AbstractController.java b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java index cada57aae4..ed9a3e1d0c 100644 --- a/tomcat/src/main/java/com/techcourse/controller/AbstractController.java +++ b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java @@ -1,16 +1,17 @@ package com.techcourse.controller; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.Method; +import org.apache.coyote.http11.response.HttpResponse; public abstract class AbstractController implements Controller { @Override public void service(HttpRequest request, HttpResponse.Builder responseBuilder) { - if (request.method().equals("GET")) { + if (request.isMethod(Method.GET)) { doGet(request, responseBuilder); } - if (request.method().equals("POST")) { + if (request.isMethod(Method.POST)) { doPost(request, responseBuilder); } } diff --git a/tomcat/src/main/java/com/techcourse/controller/Controller.java b/tomcat/src/main/java/com/techcourse/controller/Controller.java index ef38bd1ec4..4afd05b7f5 100644 --- a/tomcat/src/main/java/com/techcourse/controller/Controller.java +++ b/tomcat/src/main/java/com/techcourse/controller/Controller.java @@ -1,7 +1,7 @@ package com.techcourse.controller; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; public interface Controller { void service(HttpRequest request, HttpResponse.Builder responseBuilder); diff --git a/tomcat/src/main/java/com/techcourse/controller/HomeController.java b/tomcat/src/main/java/com/techcourse/controller/HomeController.java index 47141dcc32..42a256cd1e 100644 --- a/tomcat/src/main/java/com/techcourse/controller/HomeController.java +++ b/tomcat/src/main/java/com/techcourse/controller/HomeController.java @@ -1,11 +1,15 @@ package com.techcourse.controller; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; -import org.apache.coyote.http11.Status; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class HomeController extends AbstractController { + private static final Logger log = LoggerFactory.getLogger(HomeController.class); + @Override protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { responseBuilder.status(Status.OK) diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java index 2b8f24a9fb..a1241e1148 100644 --- a/tomcat/src/main/java/com/techcourse/controller/LoginController.java +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -5,15 +5,13 @@ import com.techcourse.db.InMemoryUserRepository; import com.techcourse.model.User; import jakarta.servlet.http.HttpSession; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; +import java.util.Map; import org.apache.catalina.session.JSession; import org.apache.catalina.session.SessionManager; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; -import org.apache.coyote.http11.HttpResponse.Builder; -import org.apache.coyote.http11.Status; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponse.Builder; +import org.apache.coyote.http11.response.Status; public class LoginController extends AbstractController { @@ -23,52 +21,50 @@ public LoginController() { this.resourceController = new ResourceController(); } - private static void processSessionLogin(Builder responseBuilder, HttpSession session) { - User user = (User) Objects.requireNonNull(session).getAttribute("user"); - - log.info("이미 로그인한 사용자 입니다. - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); - - responseBuilder.status(Status.FOUND) - .location("/index.html"); - } - - private static void processAccountLogin(Builder responseBuilder, User user) { - String sessionId = UUID.randomUUID().toString(); - JSession jSession = new JSession(sessionId); - jSession.setAttribute("user", user); - SessionManager.getInstance().add(jSession); - - log.info("계정 정보 로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), sessionId); - - responseBuilder.status(Status.FOUND) - .location("/index.html") - .addCookie(JSession.COOKIE_NAME, sessionId); - } - @Override protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) { - HttpSession session = SessionManager.getInstance().getSession(request); - if (session != null) { - processSessionLogin(responseBuilder, session); + if (SessionManager.getInstance().getSession(request) != null) { + responseBuilder.status(Status.FOUND) + .location("/index.html"); return; } + resourceController.doGet(request.updatePath("login.html"), responseBuilder); + } - if (request.parameters().containsKey("account") && request.parameters().containsKey("password")) { - findValidUser(request).ifPresentOrElse( - user -> processAccountLogin(responseBuilder, user), - () -> responseBuilder.status(Status.FOUND).location("/401.html") - ); + @Override + protected void doPost(HttpRequest request, Builder responseBuilder) { + Map body = request.extractUrlEncodedBody(); + if (isInvalidBody(body)) { + responseBuilder.status(Status.FOUND) + .location("/login"); return; } + processAccountLogin(body, responseBuilder); + } - resourceController.doGet(request.updatePath("login.html"), responseBuilder); + private boolean isInvalidBody(Map body) { + return !body.containsKey("account") || + !body.containsKey("password"); } - private Optional findValidUser(HttpRequest request) { - String account = request.parameters().get("account"); - String password = request.parameters().get("password"); + private void processAccountLogin(Map body, HttpResponse.Builder responseBuilder) { + String account = body.get("account"); + String password = body.get("password"); - return InMemoryUserRepository.findByAccount(account) - .filter(user -> user.checkPassword(password)); + InMemoryUserRepository.findByAccount(account) + .filter(user -> user.checkPassword(password)) + .ifPresentOrElse( + user -> processLoginSuccess(responseBuilder, user), + () -> responseBuilder.status(Status.FOUND).location("/401.html")); + } + + private void processLoginSuccess(Builder responseBuilder, User user) { + HttpSession session = SessionManager.getInstance().createSession(user); + + log.info("로그인 성공! - 아이디 : {}, 세션 ID : {}", user.getAccount(), session.getId()); + + responseBuilder.status(Status.FOUND) + .location("/index.html") + .addCookie(JSession.COOKIE_NAME, session.getId()); } } diff --git a/tomcat/src/main/java/com/techcourse/controller/RegisterController.java b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java index 5ba88730f2..8c0f740c68 100644 --- a/tomcat/src/main/java/com/techcourse/controller/RegisterController.java +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -3,9 +3,9 @@ import com.techcourse.db.InMemoryUserRepository; import com.techcourse.model.User; import java.util.Map; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; -import org.apache.coyote.http11.Status; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.Status; public class RegisterController extends AbstractController { @@ -22,22 +22,30 @@ protected void doGet(HttpRequest request, HttpResponse.Builder responseBuilder) @Override protected void doPost(HttpRequest request, HttpResponse.Builder responseBuilder) { - Map body = HttpRequest.extractParameters(request.body()); - - if (!body.containsKey("account") || - !body.containsKey("password") || - !body.containsKey("email")) { + Map body = request.extractUrlEncodedBody(); + if (isInvalidBody(body)) { responseBuilder.status(Status.BAD_REQUEST); return; } - - String account = body.get("account"); - if (InMemoryUserRepository.findByAccount(account).isPresent()) { + if (existsAccount(body.get("account"))) { responseBuilder.status(Status.CONFLICT); return; } + saveUserAndRedirectHome(responseBuilder, body); + } + + private boolean isInvalidBody(Map body) { + return !body.containsKey("account") || + !body.containsKey("password") || + !body.containsKey("email"); + } + + private boolean existsAccount(String account) { + return InMemoryUserRepository.findByAccount(account).isPresent(); + } - InMemoryUserRepository.save(new User(account, body.get("password"), body.get("email"))); + private void saveUserAndRedirectHome(HttpResponse.Builder responseBuilder, Map body) { + InMemoryUserRepository.save(new User(body)); responseBuilder.status(Status.FOUND) .location("/index.html"); } diff --git a/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java index a7587aac2d..fcc4cdb234 100644 --- a/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java +++ b/tomcat/src/main/java/com/techcourse/controller/RequestMapping.java @@ -2,7 +2,7 @@ import java.util.Map; import java.util.function.Predicate; -import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.request.HttpRequest; public class RequestMapping { diff --git a/tomcat/src/main/java/com/techcourse/controller/ResourceController.java b/tomcat/src/main/java/com/techcourse/controller/ResourceController.java index fb596f3721..2c95125a17 100644 --- a/tomcat/src/main/java/com/techcourse/controller/ResourceController.java +++ b/tomcat/src/main/java/com/techcourse/controller/ResourceController.java @@ -3,9 +3,9 @@ import com.techcourse.StaticResourceReader; import java.io.IOException; import java.net.URLConnection; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; -import org.apache.coyote.http11.Status; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/tomcat/src/main/java/com/techcourse/model/User.java b/tomcat/src/main/java/com/techcourse/model/User.java index e8cf4c8e68..8c37286fdf 100644 --- a/tomcat/src/main/java/com/techcourse/model/User.java +++ b/tomcat/src/main/java/com/techcourse/model/User.java @@ -1,5 +1,7 @@ package com.techcourse.model; +import java.util.Map; + public class User { private final Long id; @@ -18,6 +20,10 @@ public User(String account, String password, String email) { this(null, account, password, email); } + public User(Map body) { + this(body.get("account"), body.get("password"), body.get("email")); + } + public boolean checkPassword(String password) { return this.password.equals(password); } diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index efa41aaeb8..d576d18130 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -1,15 +1,18 @@ package org.apache.catalina.session; +import com.techcourse.model.User; import jakarta.servlet.http.HttpSession; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import org.apache.catalina.Manager; -import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.request.HttpRequest; public class SessionManager implements Manager { private static final Map SESSIONS = new HashMap<>(); private static final SessionManager SESSION_MANAGER = new SessionManager(); + private static final String ATTRIBUTE_USER_NAME = "user"; private SessionManager() { } @@ -27,7 +30,7 @@ public void add(HttpSession session) { public HttpSession findSession(String id) { HttpSession session = SESSIONS.get(id); - if (session == null || session.getAttribute("user") == null) { + if (session == null || session.getAttribute(ATTRIBUTE_USER_NAME) == null) { return null; } @@ -39,12 +42,21 @@ public void remove(HttpSession session) { SESSIONS.remove(session.getId()); } + public HttpSession createSession(User user) { + String sessionId = UUID.randomUUID().toString(); + JSession jSession = new JSession(sessionId); + jSession.setAttribute(ATTRIBUTE_USER_NAME, user); + add(jSession); + + return jSession; + } + public HttpSession getSession(HttpRequest request) { String sessionId = request.cookies().get(JSession.COOKIE_NAME); if (sessionId == null) { return null; } - return SESSIONS.get(sessionId); + return findSession(sessionId); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java index 290fb164df..c29ee2b7ae 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11InputStreamReader.java @@ -46,9 +46,6 @@ private static List readHeaders(BufferedReader reader) throws IOExceptio break; } } - if (line == null) { - log.warn("Incomplete headers received"); - } return lines; } 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 874a906045..5d68784e1b 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -6,13 +6,16 @@ import java.net.Socket; import java.util.List; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - private static final RequestMapping REQUEST_MAPPING = new RequestMapping(); + private static final RequestMapping requestMapping = new RequestMapping(); private final Socket connection; @@ -31,11 +34,21 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { List requestLines = Http11InputStreamReader.read(inputStream); - HttpRequest request = HttpRequest.parse(requestLines); -// log.debug(request.toString()); + HttpRequest request; HttpResponse.Builder responseBuilder = HttpResponse.builder(); - REQUEST_MAPPING.getController(request) + try { + request = HttpRequest.parse(requestLines); +// log.debug(request.toString()); + } catch (IllegalArgumentException e) { + HttpResponse response = responseBuilder.status(Status.BAD_REQUEST) + .build(); + outputStream.write(response.toMessage()); + outputStream.flush(); + return; + } + + requestMapping.getController(request) .service(request, responseBuilder); HttpResponse response = responseBuilder.build(); // log.debug(response.toString()); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java deleted file mode 100644 index 6a06e4c756..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.apache.coyote.http11; - -import java.net.URLDecoder; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public record HttpRequest( - String method, - String path, - Map parameters, - Map headers, - Map cookies, - String protocolVersion, - String body) { - - public static HttpRequest parse(List lines) { - String[] startLineParts = lines.getFirst().split(" "); - String method = startLineParts[0]; - - String path = ""; - Map parameters = Map.of(); - Pattern pattern = Pattern.compile("([^?]+)(\\?(.*))?"); - Matcher matcher = pattern.matcher(startLineParts[1]); - if (matcher.find()) { - path = matcher.group(1); - parameters = extractParameters(matcher.group(3)); - } - - String protocolVersion = startLineParts[2]; - Map headers = extractHeaders(lines); - Map cookies = extractCookies(headers.get("Cookie")); - - String body = extractBody(lines); - - return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); - } - - private static String extractBody(List lines) { - if (lines.size() > 1 && lines.get(lines.size() - 2).isEmpty()) { - return lines.getLast(); - } - return null; - } - - private static Map extractHeaders(List lines) { - Map headers = new HashMap<>(); - - for (int i = 1; i < lines.size() - 2; i++) { - String[] lineParts = lines.get(i).trim().split(": "); - if (lineParts.length >= 2) { - headers.put(lineParts[0], lineParts[1]); - } - } - - return headers; - } - - private static Map extractCookies(String cookieMessage) { - if (cookieMessage == null) { - return Map.of(); - } - - Map cookies = new HashMap<>(); - - for (String entry : cookieMessage.split("; ")) { - int delimiterIndex = entry.indexOf("="); - if (delimiterIndex == -1) { - continue; - } - - String key = entry.substring(0, delimiterIndex).trim(); - String value = entry.substring(delimiterIndex + 1).trim(); - cookies.put(key, value); - } - - return cookies; - } - - public static Map extractParameters(String query) { - Map parameters = new HashMap<>(); - - if (query != null) { - String[] pairs = query.split("&"); - for (String pair : pairs) { - String[] keyValue = pair.split("="); - if (keyValue.length == 2) { - String key = keyValue[0]; - String value = URLDecoder.decode(keyValue[1], Charset.defaultCharset()); - parameters.put(key, value); - } - } - } - - return parameters; - } - - public HttpRequest updatePath(String path) { - return new HttpRequest(method, path, parameters, headers, cookies, protocolVersion, body); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java deleted file mode 100644 index 24f32edce7..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -public record HttpResponse(String protocolVersion, int statusCode, String statusText, - Map headers, Map cookies, byte[] body) { - - private static final String CRLF = "\r\n"; - - public static Builder builder() { - return new Builder().protocolVersion("HTTP/1.1"); - } - - private static byte[] mergeByteArrays(byte[] array1, byte[] array2) { - byte[] mergedArray = new byte[array1.length + array2.length]; - - System.arraycopy(array1, 0, mergedArray, 0, array1.length); - System.arraycopy(array2, 0, mergedArray, array1.length, array2.length); - - return mergedArray; - } - - public byte[] toMessage() { - StringBuilder builder = new StringBuilder(); - builder.append(protocolVersion).append(" ").append(statusCode).append(" ").append(statusText).append(" "); - headers.forEach((key, value) -> builder.append(CRLF).append(key).append(": ").append(value).append(" ")); - - if (!cookies.isEmpty()) { - String cookiesMessage = cookies.entrySet().stream() - .map(Entry::toString) - .collect(Collectors.joining("; ")); - builder.append(CRLF).append("Set-Cookie: ").append(cookiesMessage).append(" "); - } - - if (body != null && body.length > 0) { - builder.append(CRLF).append("Content-Length: ").append(body.length).append(" "); - builder.append(CRLF.repeat(2)); - return mergeByteArrays(builder.toString().getBytes(), body); - } - - return builder.toString().getBytes(); - } - - public static class Builder { - private final Map headers; - private final Map cookies; - private String protocolVersion; - private int statusCode; - private String statusText; - private byte[] body; - - private Builder() { - headers = new HashMap<>(); - cookies = new HashMap<>(); - } - - public Builder protocolVersion(String protocolVersion) { - this.protocolVersion = protocolVersion; - return this; - } - - public Builder status(Status status) { - this.statusCode = status.getCode(); - this.statusText = status.getMessage(); - return this; - } - - public Builder addHeader(String key, String value) { - headers.put(key, value); - return this; - } - - public Builder addCookie(String key, String value) { - cookies.put(key, value); - return this; - } - - public Builder contentType(String value) { - headers.put("Content-Type", value + ";charset=utf-8"); - return this; - } - - public Builder location(String value) { - headers.put("Location", value); - return this; - } - - public Builder body(byte[] body) { - this.body = body; - return this; - } - - public HttpResponse build() { - return new HttpResponse(protocolVersion, statusCode, statusText, headers, cookies, body); - } - - @Override - public String toString() { - return "Builder{" + - "headers=" + headers + - ", cookies=" + cookies + - ", protocolVersion='" + protocolVersion + '\'' + - ", statusCode=" + statusCode + - ", statusText='" + statusText + '\'' + - ", body=" + Arrays.toString(body) + - '}'; - } - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookies.java new file mode 100644 index 0000000000..6eafff3131 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookies.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11.common; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public class HttpCookies { + + private static final String DELIMITER_SEMICOLON = "; "; + + private final Map values = new HashMap<>(); + + public void put(String key, String value) { + values.put(key, value); + } + + public String get(String key) { + return values.get(key); + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public String toMessage() { + return values.entrySet().stream() + .map(Entry::toString) + .collect(Collectors.joining(DELIMITER_SEMICOLON)); + } + + @Override + public String toString() { + return "HttpCookies{" + + "values=" + values + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java new file mode 100644 index 0000000000..53f68751b4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java @@ -0,0 +1,29 @@ +package org.apache.coyote.http11.common; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +public class HttpHeaders { + + private final Map values = new HashMap<>(); + + public void put(String key, String value) { + values.put(key, value); + } + + public String get(String key) { + return values.get(key); + } + + public void forEach(BiConsumer consumer) { + values.forEach(consumer); + } + + @Override + public String toString() { + return "HttpHeaders{" + + "values=" + values + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/ProtocolVersion.java b/tomcat/src/main/java/org/apache/coyote/http11/common/ProtocolVersion.java new file mode 100644 index 0000000000..a2807b8cdb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/ProtocolVersion.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11.common; + +import java.util.Arrays; + +public enum ProtocolVersion { + HTTP1("HTTP/1.0"), + HTTP11("HTTP/1.1"), + HTTP2("HTTP/2.0"), + ; + + private static final ProtocolVersion DEFAULT_VERSION = HTTP11; + private final String value; + + ProtocolVersion(String value) { + this.value = value; + } + + public static ProtocolVersion from(String value) { + return Arrays.stream(values()) + .filter(version -> version.value.equals(value)) + .findAny() + .orElse(DEFAULT_VERSION); + } + + public String getValue() { + return value; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpParameters.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpParameters.java new file mode 100644 index 0000000000..84776e3920 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpParameters.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class HttpParameters { + + private final Map values = new HashMap<>(); + + public void put(String key, String value) { + values.put(key, value); + } + + public String get(String key) { + return values.get(key); + } + + public boolean containsKey(String key) { + return values.containsKey(key); + } + + @Override + public String toString() { + return "HttpParameters{" + + "values=" + values + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..c4dcb876e4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,133 @@ +package org.apache.coyote.http11.request; + +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.coyote.http11.common.HttpCookies; +import org.apache.coyote.http11.common.HttpHeaders; +import org.apache.coyote.http11.common.ProtocolVersion; + +public record HttpRequest( + Method method, + String path, + ProtocolVersion protocolVersion, + HttpParameters parameters, + HttpHeaders headers, + HttpCookies cookies, + String body +) { + + private static final String DELIMITER_COOKIE = "; "; + private static final String DELIMITER_HEADER = ": "; + private static final String DELIMITER_SPACE = " "; + private static final String DELIMITER_VALUE = "="; + private static final String DELIMITER_PARAMETER_ENTRY = "&"; + private static final String HEADER_NAME_COOKIE = "Cookie"; + private static final int REQUIRED_REQUEST_LINE_ITEMS_COUNT = 3; + + public static HttpRequest parse(List lines) { + String[] startLineParts = splitRequestLine(lines.getFirst()); + Method method = Method.from(startLineParts[0]); + + String path = ""; + HttpParameters parameters = new HttpParameters(); + Pattern pattern = Pattern.compile("([^?]+)(\\?(.*))?"); + Matcher matcher = pattern.matcher(startLineParts[1]); + if (matcher.find()) { + path = matcher.group(1); + extractParameters(matcher.group(3)).forEach(parameters::put); + } + + ProtocolVersion protocolVersion = ProtocolVersion.from(startLineParts[2]); + HttpHeaders headers = extractHeaders(lines); + HttpCookies cookies = extractCookies(headers.get(HEADER_NAME_COOKIE)); + + String body = extractBody(lines); + + return new HttpRequest(method, path, protocolVersion, parameters, headers, cookies, body); + } + + private static String[] splitRequestLine(String first) { + String[] result = first.split(DELIMITER_SPACE); + if (result.length < REQUIRED_REQUEST_LINE_ITEMS_COUNT) { + throw new IllegalArgumentException("Request line items count is incorrect: " + first); + } + return result; + } + + private static Map extractParameters(String query) { + if (query == null) { + return Map.of(); + } + return Arrays.stream(query.split(DELIMITER_PARAMETER_ENTRY)) + .map(keyValue -> keyValue.split(DELIMITER_VALUE)) + .filter(HttpRequest::isLengthTwo) + .collect(Collectors.toMap( + entry -> entry[0], + entry -> URLDecoder.decode(entry[1], Charset.defaultCharset()) + )); + } + + private static HttpHeaders extractHeaders(List lines) { + int endHeaderIndex = IntStream.range(0, lines.size()) + .filter(i -> lines.get(i).isEmpty()) + .findFirst() + .orElse(lines.size()); + + HttpHeaders httpHeaders = new HttpHeaders(); + lines.stream() + .limit(endHeaderIndex) + .map(line -> line.split(DELIMITER_HEADER)) + .filter(HttpRequest::isLengthTwo) + .forEach(entry -> httpHeaders.put(entry[0], entry[1])); + + return httpHeaders; + } + + private static boolean isLengthTwo(String[] entry) { + return entry.length == 2; + } + + private static HttpCookies extractCookies(String cookieMessage) { + if (cookieMessage == null) { + return new HttpCookies(); + } + + HttpCookies httpCookies = new HttpCookies(); + Arrays.stream(cookieMessage.split(DELIMITER_COOKIE)) + .map(cookie -> cookie.split(DELIMITER_VALUE)) + .filter(HttpRequest::isLengthTwo) + .forEach(entry -> httpCookies.put(entry[0], entry[1])); + + return httpCookies; + } + + private static String extractBody(List lines) { + if (lines.size() > 1 && existsBodySeparatorEmptyLine(lines)) { + return lines.getLast(); + } + return null; + } + + private static boolean existsBodySeparatorEmptyLine(List lines) { + return lines.get(lines.size() - 2).isEmpty(); + } + + public HttpRequest updatePath(String path) { + return new HttpRequest(method, path, protocolVersion, parameters, headers, cookies, body); + } + + public Map extractUrlEncodedBody() { + return extractParameters(body); + } + + public boolean isMethod(Method method) { + return method.equals(this.method); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Method.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Method.java new file mode 100644 index 0000000000..4b7cfd981d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Method.java @@ -0,0 +1,22 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; + +public enum Method { + GET("GET"), + POST("POST"), + ; + + private final String value; + + Method(String value) { + this.value = value; + } + + public static Method from(String value) { + return Arrays.stream(Method.values()) + .filter(method -> method.value.equals(value)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("HTTP Method not found: " + value)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..f5c1aaf041 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,138 @@ +package org.apache.coyote.http11.response; + +import java.util.Arrays; +import org.apache.coyote.http11.common.HttpCookies; +import org.apache.coyote.http11.common.HttpHeaders; +import org.apache.coyote.http11.common.ProtocolVersion; + +public record HttpResponse( + ProtocolVersion protocolVersion, + Status status, + HttpHeaders headers, + HttpCookies cookies, + byte[] body +) { + + private static final String CRLF = "\r\n"; + private static final ProtocolVersion DEFAULT_PROTOCOL = ProtocolVersion.HTTP11; + private static final String DELIMITER_SEMICOLON = ";"; + private static final String DELIMITER_COLON = ": "; + private static final String DELIMITER_SPACE = " "; + private static final String HEADER_NAME_SET_COOKIE = "Set-Cookie: "; + private static final String HEADER_NAME_CONTENT_LENGTH = "Content-Length: "; + private static final String HEADER_NAME_CONTENT_TYPE = "Content-Type"; + private static final String HEADER_NAME_LOCATION = "Location"; + private static final String HEADER_VALUE_CONTENT_TYPE_CHARSET = "charset=utf-8"; + + public static Builder builder() { + return new Builder().protocolVersion(DEFAULT_PROTOCOL) + .status(Status.OK); + } + + private static byte[] mergeByteArrays(byte[] array1, byte[] array2) { + byte[] mergedArray = new byte[array1.length + array2.length]; + + System.arraycopy(array1, 0, mergedArray, 0, array1.length); + System.arraycopy(array2, 0, mergedArray, array1.length, array2.length); + + return mergedArray; + } + + public byte[] toMessage() { + StringBuilder builder = new StringBuilder(); + buildFirstLine(builder); + buildHeaders(builder); + buildCookies(builder); + + if (body != null && body.length > 0) { + buildBody(builder); + return mergeByteArrays(builder.toString().getBytes(), body); + } + return builder.toString().getBytes(); + } + + private void buildFirstLine(StringBuilder builder) { + builder.append(protocolVersion.getValue()).append(DELIMITER_SPACE) + .append(status.getCode()).append(DELIMITER_SPACE) + .append(status.getMessage()).append(DELIMITER_SPACE); + } + + private void buildHeaders(StringBuilder builder) { + headers.forEach((key, value) -> builder.append(CRLF) + .append(key).append(DELIMITER_COLON) + .append(value).append(DELIMITER_SPACE)); + } + + private void buildCookies(StringBuilder builder) { + if (!cookies.isEmpty()) { + String cookiesMessage = cookies.toMessage(); + builder.append(CRLF) + .append(HEADER_NAME_SET_COOKIE).append(cookiesMessage).append(DELIMITER_SPACE); + } + } + + private void buildBody(StringBuilder builder) { + builder.append(CRLF).append(HEADER_NAME_CONTENT_LENGTH).append(body.length).append(DELIMITER_SPACE) + .append(CRLF) + .append(CRLF); + } + + public static class Builder { + private final HttpHeaders headers; + private final HttpCookies cookies; + private ProtocolVersion protocolVersion; + private Status status; + private byte[] body; + + private Builder() { + headers = new HttpHeaders(); + cookies = new HttpCookies(); + } + + public Builder protocolVersion(ProtocolVersion protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + + public Builder status(Status status) { + this.status = status; + return this; + } + + public Builder addCookie(String key, String value) { + cookies.put(key, value); + return this; + } + + public Builder contentType(String value) { + headers.put(HEADER_NAME_CONTENT_TYPE, + String.join(DELIMITER_SEMICOLON, value, HEADER_VALUE_CONTENT_TYPE_CHARSET)); + return this; + } + + public Builder location(String value) { + headers.put(HEADER_NAME_LOCATION, value); + return this; + } + + public Builder body(byte[] body) { + this.body = body; + return this; + } + + public HttpResponse build() { + return new HttpResponse(protocolVersion, status, headers, cookies, body); + } + + @Override + public String toString() { + return "Builder{" + + "headers=" + headers + + ", cookies=" + cookies + + ", protocolVersion=" + protocolVersion + + ", status=" + status + + ", body=" + Arrays.toString(body) + + '}'; + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Status.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Status.java similarity index 69% rename from tomcat/src/main/java/org/apache/coyote/http11/Status.java rename to tomcat/src/main/java/org/apache/coyote/http11/response/Status.java index 86b7b16bb3..6d2f41b078 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Status.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/Status.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.response; public enum Status { OK(200, "OK"), @@ -24,4 +24,12 @@ public int getCode() { public String getMessage() { return message; } + + @Override + public String toString() { + return "Status{" + + "code=" + code + + ", message='" + message + '\'' + + '}'; + } } diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..101b5feefb 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -1,66 +1,69 @@ - - - - - - - 로그인 - - - - -
-
-
-
-
-
-
-

로그인

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