Skip to content
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

[두강] 3단계 - HTTP 웹서버 구현 미션 제출합니다. #191

Open
wants to merge 13 commits into
base: parkdoowon
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
# 웹 애플리케이션 서버

## 요구사항
- [x] newFixedThreadPool을 사용해 쓰레드풀을 이용한다.
- [x] HttpRequest, HttpResponse를 구현하여 요청과 응답에 대한 처리 로직을 분리한다.
- [x] 다형성을 활용하여 요청 URL에 대한 분기를 처리한다.
## 요구사항 1
- [x] 로그인 성공 시 `index.html`로 이동하고, 로그인 실패 시 `/user/login_failed.html`로 이동한다.
- [x] 로그인 성공 시 cookie를 활용하여 로그인 상태를 유지해야 한다.
- [x] 로그인이 성공할 경우 요청 header의 Cookie header 값이 logined=true, 로그인이 실패하면 Cookie header 값이 logined=false로 전달되어야 한다.

## 요구사항 2
- [x] 로그인 상태인 경우 사용자 목록을 보여주고, 로그인 상태가 아니면 `login.html`로 이동한다.
- [x] 동적으로 html을 생성하기 위해 handlebars.java template engine을 활용한다.

## 요구사항 3
- [x] 서블릿에서 지원하는 HttpSession API의 일부를 지원한다.
- [x] getId() 메서드를 구현한다.
- [x] setAttribute(String name, Object value) 메서드를 구현한다.
- [x] getAttribute(String name) 메서드를 구현한다.
- [x] removeAttribute(String name) 메서드를 구현한다.
- [x] invalidate() 메서드를 구현한다.

## 진행 방법
* 웹 애플리케이션 서버 요구사항을 파악한다.
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/db/DataBase.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package db;

import java.util.Collection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.google.common.collect.Maps;

import model.User;

public class DataBase {
Expand All @@ -18,7 +18,7 @@ public static User findUserById(String userId) {
return users.get(userId);
}

public static Collection<User> findAll() {
return users.values();
public static List<User> findAll() {
return new ArrayList<>(users.values());
}
}
17 changes: 17 additions & 0 deletions src/main/java/dto/Users.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dto;

import java.util.List;

import model.User;

public class Users {
private List<User> users;

public Users(List<User> users) {
this.users = users;
}

public List<User> getUsers() {
return users;
}
}
4 changes: 4 additions & 0 deletions src/main/java/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public static User of(Map<String, String> userInfo) {
userInfo.get("email"));
}

public boolean validatePassword(String password) {
return this.password.equals(password);
}

public String getUserId() {
return userId;
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/utils/ExtractUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ public class ExtractUtils {

public static Map<String, String> parseRequestBody(String body) throws UnsupportedEncodingException {
String[] infos = body.split(PARAM_DELIMITER);
Map<String, String> userInfo = new HashMap<>();
Map<String, String> parsedBody = new HashMap<>();

for (String info : infos) {
String element = info.split(ELEMENT_DELIMITER)[0];
String content = URLDecoder.decode(info.split(ELEMENT_DELIMITER)[1], "UTF-8");
userInfo.put(element, content);
parsedBody.put(element, content);
}

return userInfo;
return parsedBody;
}
}
24 changes: 24 additions & 0 deletions src/main/java/webserver/HttpCookie.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package webserver;

import java.util.HashMap;
import java.util.Map;

public class HttpCookie {
private static final String COOKIE_DELIMITER = "; ";
private static final String VALUE_DELIMITER = "=";

private final Map<String, String> cookies;

public HttpCookie(String allCookies) {
Map<String, String> cookies = new HashMap<>();
for (String cookie : allCookies.split(COOKIE_DELIMITER)) {
String[] cookieSet = cookie.split(VALUE_DELIMITER);
cookies.put(cookieSet[0], cookieSet[1]);
}
this.cookies = cookies;
}

public String getCookie(String cookieName) {
return cookies.get(cookieName);
}
}
48 changes: 41 additions & 7 deletions src/main/java/webserver/HttpResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,57 @@
import org.slf4j.Logger;

public class HttpResponse {
private static final String LINE_SEPARATOR = System.lineSeparator();

public static void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType,
Logger logger) {
try {
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes(String.format("Content-Type: %s\r\n", contentType));
dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
dos.writeBytes("HTTP/1.1 200 OK " + LINE_SEPARATOR);
dos.writeBytes(String.format("Content-Type: %s" + LINE_SEPARATOR, contentType));
dos.writeBytes("Content-Length: " + lengthOfBodyContent + LINE_SEPARATOR);
dos.writeBytes(LINE_SEPARATOR);
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public static void response302Header(DataOutputStream dos, String url, Logger logger) {
try {
dos.writeBytes("HTTP/1.1 302 FOUND \r\n");
dos.writeBytes(String.format("Location: %s\r\n", url));
dos.writeBytes("\r\n");
dos.writeBytes("HTTP/1.1 302 FOUND " + LINE_SEPARATOR);
dos.writeBytes(String.format("Location: %s" + LINE_SEPARATOR, url));
dos.writeBytes(LINE_SEPARATOR);
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public static void responseLogin302Header(DataOutputStream dos, LoginStatus loginStatus, HttpSession httpSession,
Logger logger) {
try {
dos.writeBytes("HTTP/1.1 302 FOUND " + LINE_SEPARATOR);
dos.writeBytes(String.format("Location: %s" + LINE_SEPARATOR, loginStatus.getRedirectUrl()));
dos.writeBytes(String.format("Set-Cookie: sessionId=%s; Path=/" + LINE_SEPARATOR, httpSession.getId()));
dos.writeBytes(LINE_SEPARATOR);
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public static void responseUserList200Header(DataOutputStream dos, byte[] body, Logger logger) {
try {
dos.writeBytes("HTTP/1.1 200 OK " + LINE_SEPARATOR);
dos.writeBytes("Content-Type: text/html;charset=UTF-8" + LINE_SEPARATOR);
dos.write(body, 0, body.length);
dos.flush();
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public static void responseNotLogin302Header(DataOutputStream dos, Logger logger) {
try {
dos.writeBytes("HTTP/1.1 302 FOUND " + LINE_SEPARATOR);
dos.writeBytes(String.format("Location: %s" + LINE_SEPARATOR, "/user/login.html"));
} catch (IOException e) {
logger.error(e.getMessage());
}
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/webserver/HttpSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package webserver;

import java.util.HashMap;
import java.util.Map;

public class HttpSession {
private final String id;
private final Map<String, Object> attributes = new HashMap<>();

public HttpSession(String id) {
this.id = id;
}

public String getId() {
return this.id;
}

public void setAttribute(String name, Object value) {
this.attributes.put(name, value);
}

public Object getAttribute(String name) {
return this.attributes.get(name);
}

public void removeAttribute(String name) {
this.attributes.remove(name);
}

public void invalidate() {
this.attributes.clear();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단순히 값들을 삭제할 뿐만 아니라 invalidated 된 Session은 사용 할 수 없는 것으로 알고 있어요.
혹시 모르니 조금 더 찾아보세요! :)

https://love2taeyeon.tistory.com/entry/invalidate%EC%9D%80-%EC%84%B8%EC%85%98%EC%9D%84-%EC%86%8C%EB%A9%B8%EC%8B%9C%ED%82%A4%EB%8A%94-%EA%B2%83%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EB%AC%B4%ED%9A%A8%ED%99%94-%EC%8B%9C%ED%82%AC%EB%BF%90%EC%9D%B4%EB%8B%A4

}
}
31 changes: 31 additions & 0 deletions src/main/java/webserver/LoginStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package webserver;

import java.util.Arrays;

public enum LoginStatus {
SUCCESS("/index.html", true),
FAIL("/user/login_failed.html", false);

private final String redirectUrl;
private final boolean validated;

LoginStatus(String redirectUrl, boolean validated) {
this.redirectUrl = redirectUrl;
this.validated = validated;
}

public static LoginStatus of(boolean validated) {
return Arrays.stream(LoginStatus.values())
.filter(loginStatus -> loginStatus.validated == validated)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 로그인 상태입니다."));
}

public String getRedirectUrl() {
return redirectUrl;
}

public boolean isValidated() {
return validated;
}
}
11 changes: 6 additions & 5 deletions src/main/java/webserver/ResourceTypeMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ public enum ResourceTypeMatcher {
HTML(".html", "./templates", "text/html;charset=UTF-8"),
JS(".js", "./static", "application/javascript;charset=UTF-8"),
CSS(".css", "./static", "text/css;charset=UTF-8"),
WOFF(".woff", "./static", "font/woff;charset=UTF-8");
WOFF(".woff", "./static", "font/woff;charset=UTF-8"),
ICO(".ico", "./templates", "image/x-icon;charset=UTF-8");

private final String fileType;
private final String filePath;
Expand All @@ -18,9 +19,9 @@ public enum ResourceTypeMatcher {
this.contentType = contentType;
}

public static ResourceTypeMatcher findContentType(String path) {
public static ResourceTypeMatcher findContentType(String url) {
return Arrays.stream(values())
.filter(type -> path.endsWith(type.fileType))
.filter(type -> url.endsWith(type.fileType))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 파일 타입입니다."));
}
Expand All @@ -29,9 +30,9 @@ public String parseFilePath(String path) {
return this.filePath + path;
}

public static boolean isContainType(String path) {
public static boolean isContainType(String url) {
return Arrays.stream(values())
.anyMatch(type -> path.endsWith(type.fileType));
.anyMatch(type -> url.endsWith(type.fileType));
}

public String getContentType() {
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/webserver/SessionStorage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package webserver;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

public class SessionStorage {
private static final Map<String, HttpSession> sessions = new HashMap<>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

웹서버는 멀티스레드로 동작하는데 static 타입의 필드를 접근시 Thread Safe 할까요? 🤔


public static HttpSession getSession(String id) {
if (Objects.isNull(id)) {
id = UUID.randomUUID().toString();
}
if (!sessions.containsKey(id)) {
sessions.put(id, new HttpSession(id));
}
return sessions.get(id);
}
}
4 changes: 2 additions & 2 deletions src/main/java/webserver/handler/CreateUserHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public void handleRequest(HttpRequest httpRequest, DataOutputStream dos) throws
}

@Override
public boolean canHandle(String path) {
return path.equals(USER_CREATE_URL);
public boolean canHandle(String url) {
return url.equals(USER_CREATE_URL);
}
}
2 changes: 1 addition & 1 deletion src/main/java/webserver/handler/Handler.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ public abstract class Handler {
public abstract void handleRequest(HttpRequest httpRequest, DataOutputStream dos) throws
IOException, URISyntaxException;

public abstract boolean canHandle(String path);
public abstract boolean canHandle(String url);
}
6 changes: 4 additions & 2 deletions src/main/java/webserver/handler/HandlerStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ public class HandlerStorage {
static {
handlers.add(new CreateUserHandler());
handlers.add(new StaticResourceHandler());
handlers.add(new LoginHandler());
handlers.add(new UserListHandler());
}

public static Handler findHandler(String path) {
public static Handler findHandler(String url) {
return handlers.stream()
.filter(handler -> handler.canHandle(path))
.filter(handler -> handler.canHandle(url))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 요청입니다."));
}
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/webserver/handler/LoginHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package webserver.handler;

import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;

import db.DataBase;
import model.User;
import utils.ExtractUtils;
import webserver.HttpCookie;
import webserver.HttpRequest;
import webserver.HttpResponse;
import webserver.HttpSession;
import webserver.LoginStatus;
import webserver.SessionStorage;

public class LoginHandler extends Handler {
private static final String USER_LOGIN_URL = "/user/login";

@Override
public void handleRequest(HttpRequest httpRequest, DataOutputStream dos) throws IOException {
Map<String, String> loginInfo = ExtractUtils.parseRequestBody(httpRequest.getBody());
String userId = loginInfo.get("userId");
String password = loginInfo.get("password");
User user = Optional.ofNullable(DataBase.findUserById(userId))
.orElseThrow(() -> new IllegalArgumentException("해당 유저가 없습니다."));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 실패시 /user/login_failed.html 로 이동해야 할 것 같은데 그냥 로그만 찍히고 에러 화면으로 넘어가 버리네요 ㅜㅜ

LoginStatus loginStatus = LoginStatus.of(user.validatePassword(password));
HttpCookie httpCookie = new HttpCookie(httpRequest.getHeader("Cookie"));
HttpSession httpSession = SessionStorage.getSession(httpCookie.getCookie("sessionId"));
httpSession.setAttribute("logined", true);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

미션 요구사항은 cookie에다 logind 값을 지정하도록 되어있는데 세션에 담은 이유가 있나요?


HttpResponse.responseLogin302Header(dos, loginStatus, httpSession, logger);
}

@Override
public boolean canHandle(String url) {
return USER_LOGIN_URL.equals(url);
}
}
Loading