diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..87a1f0313c 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,7 +19,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'ch.qos.logback:logback-classic:1.5.7' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..4ed8036e67 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,18 +1,17 @@ package cache.com.example; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.http.HttpServletResponse; - @Controller public class GreetingController { @GetMapping("/") public String index() { - return "index"; + return "index.html"; } /** @@ -30,7 +29,7 @@ public String cacheControl(final HttpServletResponse response) { @GetMapping("/etag") public String etag() { - return "index"; + return "index.html"; } @GetMapping("/resource-versioning") 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..c36fc0cc20 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,21 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; 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) { + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping( + CacheControl.noCache().cachePrivate(), + "/**" + ); + registry.addInterceptor(webContentInterceptor); } } 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..a0e10ae8b1 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,28 @@ package cache.com.example.etag; +import cache.com.example.version.ResourceVersion; +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; + +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + private final ResourceVersion resourceVersion; + + public EtagFilterConfiguration(ResourceVersion resourceVersion) { + this.resourceVersion = resourceVersion; + } + + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterFilterRegistrationBean + = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + filterFilterRegistrationBean.addUrlPatterns("/etag"); + filterFilterRegistrationBean.addUrlPatterns(PREFIX_STATIC_RESOURCES + "/" + resourceVersion.getVersion() + "/*"); + 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..6b8f137d59 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -2,8 +2,10 @@ 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 java.time.Duration; @Configuration public class CacheBustingWebConfig implements WebMvcConfigurer { @@ -20,6 +22,7 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..7277616bc7 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,8 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 10 \ No newline at end of file diff --git a/study/src/main/resources/templates/index.html b/study/src/main/resources/static/index.html similarity index 100% rename from study/src/main/resources/templates/index.html rename to study/src/main/resources/static/index.html diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..03ad07e471 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,18 +1,16 @@ package org.apache.catalina; -import jakarta.servlet.http.HttpSession; +import org.apache.coyote.http11.Session; import java.io.IOException; /** - * A Manager manages the pool of Sessions that are associated with a - * particular Container. Different Manager implementations may support - * value-added features such as the persistent storage of session data, - * as well as migrating sessions for distributable web applications. + * A Manager manages the pool of Sessions that are associated with a particular Container. Different Manager + * implementations may support value-added features such as the persistent storage of session data, as well as migrating + * sessions for distributable web applications. *

- * In order for a Manager implementation to successfully operate - * with a Context implementation that implements reloading, it - * must obey the following constraints: + * In order for a Manager implementation to successfully operate with a Context implementation + * that implements reloading, it must obey the following constraints: *

    *
  • Must implement Lifecycle so that the Context can indicate * that a restart is required. @@ -29,28 +27,23 @@ 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 - * specified session id (if any); otherwise return null. + * Return the active Session, associated with this Manager, with the specified session id (if any); otherwise return + * null. * * @param id The session id for the session to be returned - * - * @exception IllegalStateException if a new session cannot be - * instantiated for any reason - * @exception IOException if an input/output error occurs while - * processing this request - * - * @return the request session or {@code null} if a session with the - * requested ID could not be found + * @return the request session or {@code null} if a session with the requested ID could not be found + * @throws IllegalStateException if a new session cannot be instantiated for any reason + * @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(Session 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 bb14184757..5f5d21eddb 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,12 +1,19 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.model.User; import org.apache.coyote.Processor; +import org.apache.coyote.http11.controller.ResourceLoader; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import java.io.IOException; import java.net.Socket; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.UUID; public class Http11Processor implements Runnable, Processor { @@ -29,19 +36,88 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; + HttpRequest httpRequest = HttpRequest.from(inputStream); + + String version = httpRequest.getVersion(); + Map httpRequestHeaders = httpRequest.getHeaders(); + String httpMethod = httpRequest.getHttpMethod(); + String page = httpRequest.getPath(); + HttpResponse httpResponse; + + String responseBody; + if (page.equals("/")) { + responseBody = "Hello world!"; + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + } else if (page.startsWith("/login") && httpMethod.equals("POST")) { + String requestBody = httpRequest.getBody(); + String account = requestBody.split("&")[0].split("=")[1]; + String password = requestBody.split("&")[1].split("=")[1]; + + User user = InMemoryUserRepository.findByAccount(account).get(); + + if (user.checkPassword(password)) { + log.info("user : {}", user); + SessionManager sessionManager = new SessionManager(); + UUID jSessionId = UUID.randomUUID(); + Session session = new Session(jSessionId.toString()); + session.setAttribute("user", user); + sessionManager.add(session); + + responseBody = new String(ResourceLoader.loadResource("static/index.html")); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + httpResponse.addHeader("Location", "/index.html"); + httpResponse.addHeader("Set-Cookie", "JSESSIONID=" + jSessionId); + } else { + responseBody = new String(ResourceLoader.loadResource("static/401.html")); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + httpResponse.addHeader("Location", "/401.html"); + } + } else if (page.startsWith("/login") && httpMethod.equals("GET")) { + SessionManager sessionManager = new SessionManager(); - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); + if (httpRequestHeaders.containsKey("Cookie") && + httpRequestHeaders.get("Cookie").startsWith("JSESSIONID=")) { + String jSessionId = httpRequestHeaders.get("Cookie").split("=")[1]; + Session session = sessionManager.findSession(jSessionId); - outputStream.write(response.getBytes()); - outputStream.flush(); + if (session != null && session.getAttribute("user") != null) { + responseBody = new String(ResourceLoader.loadResource("static/index.html")); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + } else { + responseBody = new String(ResourceLoader.loadResource("static" + page + ".html")); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + } + } else { + responseBody = new String(ResourceLoader.loadResource("static" + page + ".html")); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + } + } else if (page.equals("/register") && httpMethod.equals("POST")) { + String requestBody = httpRequest.getBody(); + String account = requestBody.split("&")[0].split("=")[1]; + String email = requestBody.split("&")[1].split("=")[1]; + String password = requestBody.split("&")[2].split("=")[1]; + InMemoryUserRepository.save(new User(account, email, password)); + responseBody = new String(ResourceLoader.loadResource("static/index.html")); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + httpResponse.addHeader("Location", "/index.html"); + } else if (page.startsWith("/css/")) { + responseBody = new String(ResourceLoader.loadResource("static" + page)); + httpResponse = HttpResponse.of(version, 200, "text/css", responseBody); + } else if (page.contains(".js")) { + responseBody = new String(ResourceLoader.loadResource("static" + page)); + httpResponse = HttpResponse.of(version, 200, "text/javascript", responseBody); + } else if (page.endsWith(".html")) { + responseBody = new String(ResourceLoader.loadResource("static" + page)); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + } else { + responseBody = new String(ResourceLoader.loadResource("static" + page + ".html")); + httpResponse = HttpResponse.of(version, 200, "text/html", responseBody); + } + httpResponse.send(outputStream); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); + } catch (URISyntaxException e) { + throw new RuntimeException(e); } } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/Session.java new file mode 100644 index 0000000000..1679c17d28 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Session.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private static final Map values = new HashMap<>(); + private final String id; + + 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); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java new file mode 100644 index 0000000000..0383d6998b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java @@ -0,0 +1,29 @@ +package org.apache.coyote.http11; + +import org.apache.catalina.Manager; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new HashMap<>(); + + public SessionManager() { + } + + @Override + public void add(Session session) { + SESSIONS.put(session.getId(), new Session(session.getId())); + } + + @Override + public Session findSession(String id) throws IOException { + return SESSIONS.get(id); + } + + @Override + public void remove(Session session) { + SESSIONS.remove(session.getId()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/ResourceLoader.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/ResourceLoader.java new file mode 100644 index 0000000000..bce0bb3c39 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/ResourceLoader.java @@ -0,0 +1,20 @@ +package org.apache.coyote.http11.controller; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ResourceLoader { + + public static byte[] loadResource(String resourceName) throws URISyntaxException, IOException { + URL url = ResourceLoader.class.getClassLoader().getResource(resourceName); + if (url == null) { + throw new IllegalArgumentException("존재하지 않는 리소스 입니다." + resourceName); + } + + Path path = Path.of(url.toURI()); + return Files.readAllBytes(path); + } +} 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..d2f95eedac --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,77 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +public class HttpRequest { + + private static final String HEADER_SPLIT_DELIMITER = ": "; + + private final String method; + private final String path; + private final String version; + private final Map headers; + private final String body; + + public HttpRequest(String method, String path, String version, Map headers, String body) { + this.method = method; + this.path = path; + this.version = version; + this.headers = headers; + this.body = body; + } + + public static HttpRequest from(InputStream inputStream) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + String startLine = bufferedReader.readLine(); + + String[] parsedStartLine = HttpRequestParser.parseStartLine(startLine); + String method = parsedStartLine[0]; + String path = parsedStartLine[1]; + String version = parsedStartLine[2]; + + Map headers = new HashMap<>(); + while (!startLine.isEmpty()) { + String line = bufferedReader.readLine(); + if (line.isEmpty()) { + break; + } + String[] headerParts = line.split(HEADER_SPLIT_DELIMITER, 2); + headers.put(headerParts[0], headerParts[1]); + } + + if (method.equals("POST")) { + int contentLength = Integer.parseInt(headers.get("Content-Length")); + char[] buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + String body = new String(buffer); + return new HttpRequest(method, path, version, headers, body); + } + + return new HttpRequest(method, path, version, headers, null); + } + + public String getHttpMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getVersion() { + return version; + } + + public Map getHeaders() { + return headers; + } + + public String getBody() { + return body; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java new file mode 100644 index 0000000000..9e04e37860 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java @@ -0,0 +1,12 @@ +package org.apache.coyote.http11.request; + +public class HttpRequestParser { + + public static String[] parseStartLine(String startLine) { + String[] parsedStartLine = startLine.split(" "); + if (parsedStartLine.length != 3) { + throw new IllegalArgumentException("Invalid start line: " + startLine); + } + return parsedStartLine; + } +} 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..7eea9700d7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,57 @@ +package org.apache.coyote.http11.response; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +public class HttpResponse { + + private final String version; + private final int statusCode; + private final String statusMessage; + private final Map headers; + private final String body; + + public HttpResponse(String version, int statusCode, String statusMessage, Map headers, + String body) { + this.version = version; + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.headers = headers; + this.body = body; + } + + public static HttpResponse of(String version, int statusCode, + String contentType, String body) { + Map headers = new HashMap<>(); + headers.put("Content-Type", contentType + ";charset=utf-8"); + headers.put("Content-Length", body.getBytes().length); + + return new HttpResponse(version, statusCode, "OK", headers, body); + } + + public void addHeader(String key, String value) { + headers.put(key, value); + } + + public void send(OutputStream outputStream) { + try { + StringBuilder sb = new StringBuilder(); + sb.append(version).append(" ").append(statusCode).append(" ").append(statusMessage).append(" \r\n"); + for (Entry entry : headers.entrySet()) { + sb.append(entry.getKey()); + sb.append(": "); + sb.append(entry.getValue()); + sb.append(" \r\n"); + } + sb.append("\r\n"); + sb.append(body); + outputStream.write(sb.toString().getBytes()); + outputStream.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..bc933357f2 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 2aba8c56e0..fe20ebd29f 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -24,8 +24,8 @@ void process() { // then var expected = String.join("\r\n", "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", "Content-Length: 12 ", + "Content-Type: text/html;charset=utf-8 ", "", "Hello world!"); @@ -51,8 +51,8 @@ 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" + + "Content-Type: text/html;charset=utf-8 \r\n" + "\r\n"+ new String(Files.readAllBytes(new File(resource.getFile()).toPath()));