-
Notifications
You must be signed in to change notification settings - Fork 309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[톰캣 구현하기 - 1,2단계] 엔델(김민준) 미션 제출합니다. #331
Changes from 3 commits
cba06e0
9ee956c
2b2558a
d52ab59
a75ccc1
3925657
f3cfc6f
3e77dcb
d9ffa6d
9d87621
e99c5fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package org.apache.cookie; | ||
|
||
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<String, String> cookies; | ||
|
||
public static Cookie parse(final String cookieValue) { | ||
final Map<String, String> 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 newInstance() { | ||
return new Cookie(new HashMap<>()); | ||
} | ||
|
||
private Cookie(Map<String, String> 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<String> cookies = this.cookies.entrySet() | ||
.stream() | ||
.map(entry -> entry.getKey() + "=" + entry.getValue()) | ||
.collect(Collectors.toList()); | ||
return String.join("; ", cookies); | ||
} | ||
|
||
public boolean isEmpty() { | ||
return cookies.isEmpty(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,60 @@ | ||
package org.apache.coyote.http11; | ||
|
||
import nextstep.jwp.db.InMemoryUserRepository; | ||
import nextstep.jwp.exception.UncheckedServletException; | ||
import nextstep.jwp.model.User; | ||
import org.apache.cookie.Cookie; | ||
import org.apache.coyote.Processor; | ||
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.Collections; | ||
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 static final Map<String, String> CONTENT_TYPE; | ||
private static final Map<String, String> HTTP_STATUS; | ||
|
||
private final Socket connection; | ||
private final SessionManager sessionManager = new SessionManager(); | ||
|
||
static { | ||
final Map<String, String> contentType = new HashMap<>(); | ||
contentType.put("html", "text/html;charset=utf-8"); | ||
contentType.put("css", "text/css"); | ||
contentType.put("js", "application/javascript"); | ||
contentType.put("ico", "image/x-icon"); | ||
CONTENT_TYPE = Collections.unmodifiableMap(contentType); | ||
final Map<String, String> httpStatus = new HashMap<>(); | ||
httpStatus.put("OK", "200 OK"); | ||
httpStatus.put("CREATED", "201 CREATED"); | ||
httpStatus.put("FOUND", "302 FOUND"); | ||
httpStatus.put("NOT_FOUND", "404 NOT FOUND"); | ||
httpStatus.put("UNAUTHORIZED", "401 UNAUTHORIZED"); | ||
HTTP_STATUS = Collections.unmodifiableMap(httpStatus); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. enum이라는 선택지도 있었을 텐데, Map을 사용하신 이유를 여쭙고 싶습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일단 직관적인 구현을 위해 이렇게한 것 같습니다. |
||
} | ||
|
||
public Http11Processor(final Socket connection) { | ||
this.connection = connection; | ||
|
@@ -28,20 +70,185 @@ public void run() { | |
public void process(final Socket connection) { | ||
try (final var inputStream = connection.getInputStream(); | ||
final var outputStream = connection.getOutputStream()) { | ||
final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BufferedReader도 try-with-resource 구문 안으로 넣어서 사용 후 자원종료를 해줄 수 있을 것 같아요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 생각하고 있었는데 미처 적용하지 못했네요 감사합니다~ |
||
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, Map<String, String> requestBody) throws IOException { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 마지막 requestBody만 final이 빠져있네요! ( 달게 없을 때 적는 비겁한 리뷰입니다 ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 붙였습니다! |
||
if ("GET".equals(method) && "/".equals(path)) { | ||
return generateResponseBody("OK", "html", "Hello world!"); | ||
} | ||
if ("GET".equals(method) && "/index.html".equals(path)) { | ||
return generateResult(path, "OK"); | ||
} | ||
if ("GET".equals(method) && "/login".equals(path) && queryString.size() == 2) { | ||
final Optional<User> user = InMemoryUserRepository.findByAccount(queryString.get("account")); | ||
if (user.isEmpty()) { | ||
return generateResult("/401.html", "UNAUTHORIZED"); | ||
} | ||
if (user.get().checkPassword(queryString.get("password"))) { | ||
log.info("user : {}", user); | ||
Cookie cookie = Cookie.newInstance(); | ||
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", "FOUND", cookie); | ||
} | ||
return generateResult("/401.html", "UNAUTHORIZED"); | ||
} | ||
if ("GET".equals(method) && "/login".equals(path)) { | ||
Cookie cookie = Cookie.newInstance(); | ||
if (httpRequestHeader.containsKey("Cookie")) { | ||
cookie = Cookie.parse(httpRequestHeader.get("Cookie")); | ||
} | ||
if (cookie.containsKey("JSESSIONID")) { | ||
return generateRedirect("/index.html", "FOUND"); | ||
} | ||
return generateResult(path, "OK"); | ||
} | ||
if ("POST".equals(method) && "/register".equals(path)) { | ||
InMemoryUserRepository.save(new User(requestBody.get("account"), requestBody.get("password"), requestBody.get("email"))); | ||
return generateRedirect("/index.html", "FOUND"); | ||
} | ||
if ("GET".equals(method) && "/register".equals(path)) { | ||
return generateResult(path, "OK"); | ||
} | ||
|
||
return generateResult(path, "OK"); | ||
} | ||
|
||
private String generateRedirect(final String location, final String statusCode) { | ||
return String.join("\r\n", | ||
"HTTP/1.1 " + HTTP_STATUS.get(statusCode) + " ", | ||
"Location: " + location + " "); | ||
} | ||
|
||
private String generateRedirect(final String location, final String statusCode, final Cookie cookie) { | ||
return String.join("\r\n", | ||
"HTTP/1.1 " + HTTP_STATUS.get(statusCode) + " ", | ||
"Set-Cookie: " + cookie.generateCookieHeaderValue() + " ", | ||
"Location: " + location + " "); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 공백, 개행문자, 헤더 key가 다양한 곳에서 쓰일 수 있을 것 같아서 상수화 할 수 있을 것 같습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일단은 헤더의 Key 부분은 HttpHeaders 클래스를 만들어 상수화했습니다. |
||
} | ||
|
||
private String generateResult(final String path, final String statusCode) throws IOException { | ||
return generateResult(path, statusCode, Cookie.newInstance()); | ||
} | ||
|
||
private String generateResult(final String path, final String statusCode, final Cookie cookie) throws IOException { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 쿠키를 담아 보낼 경우 generateResult가 아닌 generateRedirect를 사용중이군요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 구현에 어려움이 있어서 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 좋습니다!! |
||
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("."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 String statusCode, final String fileExtension, final String responseBody) { | ||
return String.join("\r\n", | ||
"HTTP/1.1 " + HTTP_STATUS.get(statusCode) + " ", | ||
"Content-Type: " + CONTENT_TYPE.get(fileExtension) + " ", | ||
"Content-Length: " + responseBody.getBytes(StandardCharsets.UTF_8).length + " ", | ||
"", | ||
responseBody); | ||
} | ||
|
||
private String generateResponseBody(final String statusCode, final String fileExtension, final String responseBody, final Cookie cookie) { | ||
return String.join("\r\n", | ||
"HTTP/1.1 " + HTTP_STATUS.get(statusCode) + " ", | ||
"Set-Cookie: " + cookie.generateCookieHeaderValue() + " ", | ||
"Content-Type: " + CONTENT_TYPE.get(fileExtension) + " ", | ||
"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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인코딩까지 꼼꼼하게 고려해주셨군요👍 |
||
requestBodies.put(keyValue[0], decodedValue); | ||
}); | ||
} | ||
return requestBodies; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Object> 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package org.apache.session; | ||
|
||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
public class SessionManager { | ||
|
||
private static final Map<String, Session> 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,7 @@ | |
<div class="card shadow-lg border-0 rounded-lg mt-5"> | ||
<div class="card-header"><h3 class="text-center font-weight-light my-4">회원 가입</h3></div> | ||
<div class="card-body"> | ||
<form method="post" action="register"> | ||
<form method="post" action="/register"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 남길 곳이 마땅치 않아서 이곳에 남깁니다 2단계 요구사항 중 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어쩐지 뭔가 이상하다고 생각했었는데 미처 적용하지 못했네요 |
||
<div class="form-floating mb-3"> | ||
<input class="form-control" id="inputLoginId" name="account" type="Text" placeholder="아이디를 입력하세요" /> | ||
<label for="inputLoginId">아이디</label> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
미리 생성해둔 객체를 사용하면 좀 더 효율적이지 않을까라는 생각이 들었습니다! (사실 저도 놓친 부분입니다😂)
엔델은 어떻게 생각하시나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 미처 이해를 정확히 하지 못했는데요, 개별적인 빈 캐시를 만들기 위해 만든 메서드입니다.
일단 직관적인 네이밍이 아니어서 empty()로 변경해보았습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 제가 잘못봤네요!
해당 메서드를 통해 빈 쿠키를 반환하는 데만 사용하는 것으로 보고 단 코멘트입니다
혼란을 드려서 죄송합니다🙇🏻♂️