Skip to content

Commit

Permalink
[톰캣 구현하기 - 1,2단계] 엔델(김민준) 미션 제출합니다. (#331)
Browse files Browse the repository at this point in the history
* 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() 메서드를 사용해 생성하도록 메서드 네이밍 변경
  • Loading branch information
SproutMJ authored Sep 5, 2023
1 parent 68db530 commit 5b5ee3f
Show file tree
Hide file tree
Showing 13 changed files with 447 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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> shallowEtagHeaderFilter() {
// return null;
// }
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new ShallowEtagHeaderFilter());
filterFilterRegistrationBean.addUrlPatterns("/etag");
filterFilterRegistrationBean.addUrlPatterns("/resources-versioning/*");
filterFilterRegistrationBean.setOrder(1);
return filterFilterRegistrationBean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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("/**"));
}
}
3 changes: 3 additions & 0 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ server:
max-connections: 1
threads:
max: 2
compression:
enabled: true
min-response-size: 10
216 changes: 205 additions & 11 deletions tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, String> requestLine = extractRequestLine(reader);
final Map<String, String> httpRequestHeaders = extractRequestHeaders(reader);
final Map<String, String> requestBody = extractRequestBody(httpRequestHeaders, reader);
final String method = requestLine.get(HTTP_METHOD);
final String path = parsingPath(requestLine.get(URL));
final Map<String, String> 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<String, String> httpRequestHeader, final String method, final String path, final Map<String, String> queryString, final Map<String, String> 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> 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<String, String> parsingQueryString(final String path) {
final Map<String, String> 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<String, String> extractRequestLine(final BufferedReader reader) throws IOException {
final String requestLine = reader.readLine();
final String[] parts = requestLine.split(" ");

final Map<String, String> request = new HashMap<>();
request.put(HTTP_METHOD, parts[0]);
request.put(URL, parts[1]);
request.put(HTTP_VERSION, parts[2]);

return request;
}

private Map<String, String> extractRequestHeaders(final BufferedReader reader) throws IOException {
final Map<String, String> 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<String, String> extractRequestBody(final Map<String, String> httpRequestHeaders, final BufferedReader reader) throws IOException {
final Map<String, String> 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;
}
}
42 changes: 42 additions & 0 deletions tomcat/src/main/java/org/apache/http/ContentType.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 5b5ee3f

Please sign in to comment.