diff --git a/README.md b/README.md index 235fddaea..09ddb4b63 100644 --- a/README.md +++ b/README.md @@ -15,22 +15,157 @@ * 각 요구사항을 구현하는 것이 중요한 것이 아니라 구현 과정을 통해 학습한 내용을 인식하는 것이 배움에 중요하다. ### 요구사항 1 - http://localhost:8080/index.html로 접속시 응답 -* +*네트워크 요청 받아드리기* +1. java.net.ServerSocket(portNumber)를 생성하면 해당포트로 연결되는 네트워크 접속을 감지한다. +2. (무한루프를 통해서) 소켓접속이 감지되면, 해당 소켓을 분석하여서 적절한 로직을 수행한다. +3. 소켓의 inputStream에는 요청정보들이 담겨있고, outputStream에 응답정보를 작성할 수 있다. ### 요구사항 2 - get 방식으로 회원가입 -* +* 요청 path에 queryString으로 정보들이 들어온다. ### 요구사항 3 - post 방식으로 회원가입 -* +*HTTP 프로토콜* +- 요청 +``` +GET /path HTTP/1.1 +Accept: application/json +Authorization: Bearer UExBMDFUMDRQV1MwMnzpdvtYYNWMSJ7CL8h0zM6q6a9ntw +... (`key: value` 형태의 헤더정보) +(빈줄) +...(바디정보) +``` + +- 응답 +``` +HTTP/1.1 200 OK +Date: Mon, 23 May 2005 22:38:34 GMT +Content-Type: text/html; charset=UTF-8 +Content-Encoding: UTF-8 +... (`key: value` 형태의 헤더정보) +(빈줄) +...(바디정보) +... +``` + +- (HTTP) 프로토콜이란, 결국 약속! 이다. *문자열*로 요청을 보내지만 클라이언트와 서버간에 *약속되어져 있는 형태*로 보내면 된다. 약속된 형태의 *문자열*을 클라이언트와 서버가 subString하여 파싱하고 정보를 추출하는 것이다. + - 예를들어 헤더정보는 반드시 `key: value` 형태여야 한다. 콜론뒤에 빈칸도 포함되어있어야 한다! + - 예를들어 순서는 반드시 `요청라인(첫줄)-헤더정보-빈줄-바디정보` 여야한다. + - 바디정보에 form 정보가 `key1=value1;key2=value2` 형태로 드러있다. ### 요구사항 4 - redirect 방식으로 이동 -* +* HTTP 응답라인(첫줄)에 status 코드가 300번대라면, 클라이언트에게 리다이렉트를 하라는 프로토콜이다. +* 헤더정보에 `Location: /redirect-url` 형태로 보내면 어떤 주소로 리다이렉트 해야하는지 클라이언트에게 알려줄 수 있다. +* *HTTP는 프로토콜(약속)* 이고, 실제 형태는 문자열일뿐이다. ### 요구사항 5 - cookie -* +* 요청 헤더에 `Cookie: key1=value1;key2=value2` 형태로 클라이언트가 가지고 있는 쿠키 값을 서버에게 보낸다. +* 응답 헤더에 `Set-Cookie: key1=value1;key2=value2` 형태로 서버가 클라이언트에게 쿠키 값을 설정하라고 알려준다. +* *HTTP는 프로토콜(약속)* 이고, 실제 형태는 문자열일뿐이다. ### 요구사항 6 - stylesheet 적용 -* +* 응답 헤더에 `Content-Type: text/css` 형태로 서버가 클라이언트에게 css 파일이라는 점을 알려준다. +* *HTTP는 프로토콜(약속)* 이고, 실제 형태는 문자열일뿐이다. + +### 설계적 영감 +- path를 매핑하는 작업에서 전략패턴을 적용하고 각 컨트롤러는 Constructor를 이용하여서 매핑해두었다 :thumbup: +- 컨트롤러들은 하나씩만 필요하여 싱글톤으로 만들었다. +- 스프링은 전략패턴과 싱글톤을 '쉽게' 구현하도록 도와준다. 그래서 개발자들이 이 패턴들을 사용하지 않아도 되는 편의가 생겼다. +- 싱글톤으로 분리하면, 이를 이용하면서 파생되는 DTO(Data,Domain..) 객체들이 *필연적으로* 만들어진다. 이를 잘 인지하고 분리하자! (HttpRuqest, HttpResponse) +- AOP같이 모든 작업들의 선작업 혹은 후작업이 필요하다면, 여기서 상속을 사용해보는 것을 고려해보자! (중복때문에 상속사용하는게 아니다. AOP를 구현하기 위함) + +### ec2 서버에 배포 후 +*EC2 접속하기* +1. chmod 400 jwp-book.pem + - 키파일 퍼미션을 400으로 두어야 ssh가 작동한다. (라고 aws에서 안내해줌) +2. ssh -i "jwp-book.pem" [ubuntu@ec2-13-124-248-99.ap-northeast-2.compute.amazonaws.com](mailto:ubuntu@ec2-13-124-248-99.ap-northeast-2.compute.amazonaws.com) + - ssh로 키파일을 이용하여서 ec2에 접속 + - ubuntu@ 는 계정명을 말하는것 같다. 뒤에 주소는 ec2에서 명시하는 퍼블릭IP + - 그래서 아마존 리눅스로 다시 만드니까 유저명이 ec2-user였고 ec2-user@로 해야 됐다! + +*한글 로케일 설정하기* +1. sudo locale-gen ko_KR.EUC-KR ko_KR.UTF-8 +2. sudo dpkg-reconfigure locales +3. .bash_profile 파일에 설정추가 + ``` + LANG="ko_KR.UTF-8" + LANGUAGE="ko_KR:ko:en_US:en" + ``` +4. source .bash_profile + +*JDK와 메이븐 설치하기* +1. wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" [https://download.java.net/openjdk/jdk8u41/ri/openjdk-8u41-b04-linux-x64-14_jan_2020.tar.gz](https://download.java.net/openjdk/jdk8u41/ri/openjdk-8u41-b04-linux-x64-14_jan_2020.tar.gz) + - jdk 다운로드 +2. gunzip openjdk-8u41-b04-linux-x64-14_jan_2020.tar.gz + - 압축풀기 gz +3. tar -xvf openjdk-8u41-b04-linux-x64-14_jan_2020.tar + - 압축풀기 tar (따로따로 풀어야하네) +4. ln -s java-se-8u41-ri/ java + - 압축푼 디렉토리 'java'로 심볼릭 링크 만들기 +5. 프로필 파일에 path 추가 (로케일 설정시에 사용했던 .bash_profile에다가 했다) + ``` + bash + export JAVA_HOME=~/java + export PATH=$PATH:$JAVA_HOME/bin + ``` + - JAVA_HOME에 (심볼릭 설정한) java 폴더 설정하고 + - PATH설정 추가! +6. source .bash_profile + - 설정 반영 +7. java -version + - path설정이 잘 되었나 확인 +8. wget [http://apache.mirror.cdnetworks.com/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz](http://apache.mirror.cdnetworks.com/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz) + - 메이븐 다운로드 +9. gunzip apache-maven-3.6.3-bin.tar.gz + - 압축풀기 +10. tar -xvf apache-maven-3.6.3-bin.tar + - 압축풀기 +11. 프로필 파일에 메이븐 path 추가 + ``` + bash + export JAVA_HOME=~/java + export MAVEN_HOME=~/maven + export PATH=$PATH:$JAVA_HOME/bin:$MAVEN_HOME/bin + ``` +12. source .bash_profile +13. env + - 환경설정 확인 +14. mvn -version + - path 설정 잘 됐나 확인 + +*JDK, 메이븐 설치하기 (apt-get으로)* +- 이렇게 하니까 메이븐 에러가 안남.. 아니면 우분투 버전을 16버전으로 하니까 안나는 걸 수 도있어 +- sudo apt-get install openjdk-8-jdk +- sudo apt-get install maven + +*Git 설치하고 프로젝트 받기* +1. (sudo apt-get install git) + - 보통 설치가 되어있음 +2. git —version + - git 설치되었는지 확인 +3. git clone [https://github.com/siyoon210/web-application-server](https://github.com/siyoon210/web-application-server) + - 배포할 레포 git clone +4. mvn clean package + - 클론해온 레포 디렉토리 안에서 실행 + - 메이븐으로 삭제(clean) 후 빌드(package) + - 결과물은 target 디렉토리 안에 생김 +5. git clone [https://github.com/siyoon210/web-application-server](https://github.com/siyoon210/web-application-server) + - 배포할 레포 git clone +6. mvn clean package + - 클론해온 레포 디렉토리 안에서 실행 + - 메이븐으로 삭제(clean) 후 빌드(package) + - 결과물은 target 디렉토리 안에 생김 -### heroku 서버에 배포 후 -* \ No newline at end of file +*ec2 포트열기* +- ec2 정보에서 `보안그룹`을 선택한다. (보통 launch-wizard-4 이런식에 이름으로 되어있음) +- `사용자 지정 TCP` 8080(원하는 번호) 포트를 공개한다. (혹은 HTTP 80을 오픈해도 됨) + - 소스가 0.0.0.0/0, 이랑 ::/0 두개가 생기는데 뭔지는 잘 모르겠음 + +*빌드 배포하기* +- mvn clean package +- java -cp target/classes:target/dependecy/* webserver.Webserver 8080 +- 재배포하기 위해서 프로세스 끄기 + - ps -ef | grep webserver + - 실행한 프로세스 아이디(PID)를 찾고 + - kill -9 $PID + - 웹서버 종료 + - 위에 배포코드실행 \ No newline at end of file diff --git a/pom.xml b/pom.xml index a98034b22..ea1ec9ecf 100644 --- a/pom.xml +++ b/pom.xml @@ -14,9 +14,16 @@ - junit - junit - 4.11 + org.junit.jupiter + junit-jupiter-api + 5.6.2 + test + + + + org.junit.vintage + junit-vintage-engine + 5.6.2 test @@ -26,6 +33,12 @@ 18.0 + + org.assertj + assertj-core + 3.16.1 + test + ch.qos.logback diff --git a/src/main/java/controller/AbstractController.java b/src/main/java/controller/AbstractController.java new file mode 100644 index 000000000..c186c2baf --- /dev/null +++ b/src/main/java/controller/AbstractController.java @@ -0,0 +1,30 @@ +package controller; + +import webserver.model.HttpRequest; +import webserver.model.HttpResponse; +import model.HttpMethod; + +import java.io.IOException; + +public abstract class AbstractController implements Controller { + public final HttpResponse process(HttpRequest httpRequest) throws IOException, IllegalAccessException { + final HttpMethod method = HttpMethod.valueOf(httpRequest.getMethod()); + + switch (method) { + case GET: + return doGet(httpRequest); + case POST: + return doPost(httpRequest); + default: + throw new IllegalArgumentException("Illegal Http Method"); + } + } + + protected HttpResponse doGet(HttpRequest httpRequest) throws IOException, IllegalAccessException { + throw new IllegalAccessException("doGet() is not overridden."); + } + + protected HttpResponse doPost(HttpRequest httpRequest) throws IOException, IllegalAccessException { + throw new IllegalAccessException("doPost() is not overridden."); + } +} diff --git a/src/main/java/controller/Controller.java b/src/main/java/controller/Controller.java new file mode 100644 index 000000000..70169e297 --- /dev/null +++ b/src/main/java/controller/Controller.java @@ -0,0 +1,10 @@ +package controller; + +import webserver.model.HttpRequest; +import webserver.model.HttpResponse; + +import java.io.IOException; + +public interface Controller { + HttpResponse process(HttpRequest httpRequest) throws IOException, IllegalAccessException; +} diff --git a/src/main/java/controller/ControllerConstructor.java b/src/main/java/controller/ControllerConstructor.java new file mode 100644 index 000000000..0bd0a1dc5 --- /dev/null +++ b/src/main/java/controller/ControllerConstructor.java @@ -0,0 +1,38 @@ +package controller; + +import webserver.model.HttpRequest; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class ControllerConstructor { + private final static Map pathAndControllers = new HashMap<>(); + + private ControllerConstructor() { + } + + static { + pathAndControllers.put("/user/create", UserCreateController.getInstance()); + pathAndControllers.put("/user/login", UserLoginController.getInstance()); + pathAndControllers.put("/user/list", UserListController.getInstance()); + } + + public static Controller getOf(HttpRequest request) { + String path = request.getPath(); + if (hasQueryString(path)) { + path = subStringQueryString(path); + } + + final Controller controller = pathAndControllers.get(path); + return Objects.isNull(controller) ? DefaultController.getInstance() : controller; + } + + private static String subStringQueryString(String path) { + return path.split("\\?")[0]; + } + + private static boolean hasQueryString(String path) { + return path.contains("?"); + } +} diff --git a/src/main/java/controller/DefaultController.java b/src/main/java/controller/DefaultController.java new file mode 100644 index 000000000..9487088e8 --- /dev/null +++ b/src/main/java/controller/DefaultController.java @@ -0,0 +1,35 @@ +package controller; + +import webserver.model.HttpRequest; +import webserver.model.HttpResponse; + +import java.io.IOException; + +class DefaultController extends AbstractController { + private static final Controller instance = new DefaultController(); + + private DefaultController() { + } + + public static Controller getInstance() { + return instance; + } + + @Override + protected HttpResponse doGet(HttpRequest request) throws IOException { + final String path = getPath(request); + + return HttpResponse.builder() + .status(200) + .forward(path) + .build(); + } + + private String getPath(HttpRequest request) { + final String path = request.getPath(); + if (path.equals("/")) { + return "/index.html"; + } + return path; + } +} diff --git a/src/main/java/controller/UserCreateController.java b/src/main/java/controller/UserCreateController.java new file mode 100644 index 000000000..5dc521752 --- /dev/null +++ b/src/main/java/controller/UserCreateController.java @@ -0,0 +1,35 @@ +package controller; + +import webserver.model.HttpRequest; +import db.DataBase; +import webserver.model.HttpResponse; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +class UserCreateController extends AbstractController { + private static final Controller instance = new UserCreateController(); + private static final Logger log = LoggerFactory.getLogger(UserCreateController.class); + + private UserCreateController() {} + + public static Controller getInstance() { + return instance; + } + + @Override + protected HttpResponse doPost(HttpRequest request) { + final Map content = request.getParsedBody(); + final User newUser = new User(content); + + log.info("Create User: {}", newUser); + DataBase.addUser(newUser); + + return HttpResponse.builder() + .status(302) + .redirect("/") + .build(); + } +} diff --git a/src/main/java/controller/UserListController.java b/src/main/java/controller/UserListController.java new file mode 100644 index 000000000..afb4425fc --- /dev/null +++ b/src/main/java/controller/UserListController.java @@ -0,0 +1,58 @@ +package controller; + +import db.DataBase; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.model.HttpRequest; +import webserver.model.HttpResponse; +import webserver.model.HttpSession; + +import java.io.IOException; +import java.util.Collection; + +class UserListController extends AbstractController { + private static final Controller instance = new UserListController(); + private static final Logger log = LoggerFactory.getLogger(UserListController.class); + + private UserListController() {} + + public static Controller getInstance() { + return instance; + } + + @Override + protected HttpResponse doGet(HttpRequest request) throws IOException { + if (isLogined(request)) { + log.info("Login user"); + final byte[] body = getUserListAsBytes(); + return HttpResponse.builder() + .status(200) + .header("Content-Type", "text/html;charset=utf-8") + .header("Content-Length", body.length) + .body(body) + .build(); + } else { + log.info("Logout user"); + return HttpResponse.builder() + .status(302) + .redirect("/user/login.html") + .build(); + } + } + + private boolean isLogined(HttpRequest request) { + final HttpSession session = request.getSession(); + final Object user = session.get("user"); + return user != null; + } + + private byte[] getUserListAsBytes() { + final Collection all = DataBase.findAll(); + final StringBuilder sb = new StringBuilder(); + for (User user : all) { + sb.append(user.getName()).append(": ").append(user.getEmail()).append("
"); + } + return sb.toString().getBytes(); + } +} diff --git a/src/main/java/controller/UserLoginController.java b/src/main/java/controller/UserLoginController.java new file mode 100644 index 000000000..5e6f38830 --- /dev/null +++ b/src/main/java/controller/UserLoginController.java @@ -0,0 +1,52 @@ +package controller; + +import db.DataBase; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.model.HttpRequest; +import webserver.model.HttpResponse; +import webserver.model.HttpSession; + +import java.io.IOException; +import java.util.Map; + +class UserLoginController extends AbstractController { + private static final Controller instance = new UserLoginController(); + private static final Logger log = LoggerFactory.getLogger(UserLoginController.class); + + private UserLoginController() { + } + + public static Controller getInstance() { + return instance; + } + + @Override + protected HttpResponse doPost(HttpRequest request) throws IOException { + final Map content = request.getParsedBody(); + final HttpSession session = request.getSession(); + final User user = DataBase.findUserById(content.get("userId")); + + if (isLoginSuccess(content, user)) { + log.debug("Login success: {}", user.getName()); + session.set("user", user); + return HttpResponse.builder() + .status(302) + .redirect("/") + .cookie("logined", true) + .build(); + } else { + log.debug("Login fail: {}", user == null ? "[No user]" : user.getName()); + return HttpResponse.builder() + .status(302) + .redirect("/user/login_failed.html") + .cookie("logined", false) + .build(); + } + } + + private boolean isLoginSuccess(Map content, User user) { + return user != null && user.getPassword().equals(content.get("password")); + } +} diff --git a/src/main/java/model/HttpMethod.java b/src/main/java/model/HttpMethod.java new file mode 100644 index 000000000..2716903b4 --- /dev/null +++ b/src/main/java/model/HttpMethod.java @@ -0,0 +1,6 @@ +package model; + +public enum HttpMethod { + GET, + POST +} diff --git a/src/main/java/model/MediaType.java b/src/main/java/model/MediaType.java new file mode 100644 index 000000000..96431a5c6 --- /dev/null +++ b/src/main/java/model/MediaType.java @@ -0,0 +1,25 @@ +package model; + +public enum MediaType { + HTML("text/html"), + CSS("text/css"), + JS("text/javascript"), + PNG("image/png"), + ICO("image/ico"), + WOFF("font/woff"); + + private final String mediaType; + + MediaType(String mediaType) { + this.mediaType = mediaType; + } + + public static String getMediaType(String fileType) { + try { + return valueOf(fileType.toUpperCase()).mediaType; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return "unknown"; + } + } +} diff --git a/src/main/java/model/User.java b/src/main/java/model/User.java index b7abb7304..584d5bb14 100644 --- a/src/main/java/model/User.java +++ b/src/main/java/model/User.java @@ -1,5 +1,7 @@ package model; +import java.util.Map; + public class User { private String userId; private String password; @@ -13,6 +15,13 @@ public User(String userId, String password, String name, String email) { this.email = email; } + public User(Map content) { + this.userId = content.get("userId"); + this.password = content.get("password"); + this.name = content.get("name"); + this.email = content.get("email"); + } + public String getUserId() { return userId; } diff --git a/src/main/java/util/HttpRequestUtils.java b/src/main/java/util/HttpRequestUtils.java index c4cd95c0d..e54cba985 100644 --- a/src/main/java/util/HttpRequestUtils.java +++ b/src/main/java/util/HttpRequestUtils.java @@ -1,7 +1,13 @@ package util; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import com.google.common.base.Strings; diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 90195ec4e..48aa4ac2d 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -1,55 +1,52 @@ package webserver; -import java.io.DataOutputStream; +import controller.Controller; +import controller.ControllerConstructor; +import webserver.model.HttpCookie; +import webserver.model.HttpRequest; +import webserver.model.HttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.model.HttpSession; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; public class RequestHandler extends Thread { private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); - private Socket connection; + private final Socket connection; public RequestHandler(Socket connectionSocket) { this.connection = connectionSocket; } + @Override public void run() { log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort()); - try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { - // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다. - DataOutputStream dos = new DataOutputStream(out); - byte[] body = "Hello World".getBytes(); - response200Header(dos, body.length); - responseBody(dos, body); - } catch (IOException e) { + try (InputStream in = connection.getInputStream(); + OutputStream out = connection.getOutputStream()) { + final HttpRequest httpRequest = HttpRequest.from(in); + final Controller controller = ControllerConstructor.getOf(httpRequest); + final HttpResponse httpResponse = controller.process(httpRequest); + checkSessionId(httpRequest, httpResponse); + httpResponse.write(out); + } catch (IOException | IllegalAccessException e) { log.error(e.getMessage()); } } - private void response200Header(DataOutputStream dos, int lengthOfBodyContent) { - try { - dos.writeBytes("HTTP/1.1 200 OK \r\n"); - dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); - dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.error(e.getMessage()); - } - } - - private void responseBody(DataOutputStream dos, byte[] body) { - try { - dos.write(body, 0, body.length); - dos.flush(); - } catch (IOException e) { - log.error(e.getMessage()); + private void checkSessionId(HttpRequest httpRequest, HttpResponse httpResponse) { + final HttpCookie cookies = httpRequest.getCookies(); + if (Objects.isNull(cookies.get(HttpSession.SESSION_ID_KEY))) { + httpResponse.addCookie(HttpSession.SESSION_ID_KEY, UUID.randomUUID()); } } } diff --git a/src/main/java/webserver/WebServer.java b/src/main/java/webserver/WebServer.java index 91f4a0fbc..4d9aac835 100644 --- a/src/main/java/webserver/WebServer.java +++ b/src/main/java/webserver/WebServer.java @@ -2,7 +2,10 @@ import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import com.google.common.collect.Queues; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,24 +13,22 @@ public class WebServer { private static final Logger log = LoggerFactory.getLogger(WebServer.class); private static final int DEFAULT_PORT = 8080; - public static void main(String args[]) throws Exception { - int port = 0; + public static void main(String[] args) throws Exception { + int port; if (args == null || args.length == 0) { port = DEFAULT_PORT; } else { port = Integer.parseInt(args[0]); } - // 서버소켓을 생성한다. 웹서버는 기본적으로 8080번 포트를 사용한다. + final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, Queues.newLinkedBlockingQueue(6)); try (ServerSocket listenSocket = new ServerSocket(port)) { log.info("Web Application Server started {} port.", port); - // 클라이언트가 연결될때까지 대기한다. Socket connection; while ((connection = listenSocket.accept()) != null) { - RequestHandler requestHandler = new RequestHandler(connection); - requestHandler.start(); + threadPoolExecutor.execute(new RequestHandler(connection)); } } } diff --git a/src/main/java/webserver/model/HttpCookie.java b/src/main/java/webserver/model/HttpCookie.java new file mode 100644 index 000000000..ba4a3e7b8 --- /dev/null +++ b/src/main/java/webserver/model/HttpCookie.java @@ -0,0 +1,17 @@ +package webserver.model; + +import java.util.Map; + +import static util.HttpRequestUtils.parseCookies; + +public class HttpCookie { + private final Map cookies; + + public HttpCookie(String cookieAsString) { + this.cookies = parseCookies(cookieAsString); + } + + public String get(String key) { + return cookies.get(key); + } +} diff --git a/src/main/java/webserver/model/HttpRequest.java b/src/main/java/webserver/model/HttpRequest.java new file mode 100644 index 000000000..1d7bf352c --- /dev/null +++ b/src/main/java/webserver/model/HttpRequest.java @@ -0,0 +1,112 @@ +package webserver.model; + +import util.HttpRequestUtils; +import util.IOUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static util.HttpRequestUtils.parseHeader; +import static util.HttpRequestUtils.parseQueryString; + +public class HttpRequest { + private final Map requestInfo; + private HttpCookie httpCookie; + + private HttpRequest(InputStream in) throws IOException { + requestInfo = new HashMap<>(); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in)); + int contentLength = parseHeaders(bufferedReader); + if (contentLength > 0) { + parseBody(bufferedReader, contentLength); + } + } + + public static HttpRequest from(InputStream in) throws IOException { + return new HttpRequest(in); + } + + private int parseHeaders(BufferedReader bufferedReader) throws IOException { + String line; + int contentLength = 0; + while (!"".equals(line = bufferedReader.readLine())) { + if (Objects.isNull(line)) { + break; + } + + if (requestInfo.isEmpty()) { + parseHttpMethodAndPath(line); + continue; + } + + final HttpRequestUtils.Pair pair = parseHeader(line); + if (Objects.isNull(pair)) { + continue; + } + + requestInfo.put(pair.getKey(), pair.getValue()); + + if (pair.getKey().equals("Content-Length")) { + contentLength = Integer.parseInt(pair.getValue()); + } + } + return contentLength; + } + + private void parseHttpMethodAndPath(String line) { + final String[] s = line.split(" "); + requestInfo.put("Method", s[0]); + requestInfo.put("Path", s[1]); + requestInfo.put("Version", s[2]); + } + + private void parseBody(BufferedReader bufferedReader, int contentLength) throws IOException { + String body = IOUtils.readData(bufferedReader, contentLength); + requestInfo.put("body", body); + } + + public String get(String key) { + return requestInfo.get(key); + } + + public String getPath() { + return requestInfo.get("Path"); + } + + public String getMethod() { + return requestInfo.get("Method"); + } + + public HttpCookie getCookies() { + if (Objects.isNull(httpCookie)) { + httpCookie = new HttpCookie(requestInfo.get("Cookie")); + } + + return httpCookie; + } + + public HttpSession getSession() { + final HttpCookie cookies = getCookies(); + final String jsessionid = cookies.get(HttpSession.SESSION_ID_KEY); + if (Objects.isNull(jsessionid)) { + throw new IllegalStateException("Session ID dose not exist."); + } + return HttpSessions.get(jsessionid); + } + + public Map getParsedBody() { + return parseQueryString(requestInfo.get("body")); + } + + @Override + public String toString() { + return "HttpRequest{" + + "requestInfo=" + requestInfo + + '}'; + } +} diff --git a/src/main/java/webserver/model/HttpResponse.java b/src/main/java/webserver/model/HttpResponse.java new file mode 100644 index 000000000..91d14607b --- /dev/null +++ b/src/main/java/webserver/model/HttpResponse.java @@ -0,0 +1,143 @@ +package webserver.model; + +import model.MediaType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import static java.util.stream.Collectors.joining; + +public class HttpResponse { + private static final Logger log = LoggerFactory.getLogger(HttpResponse.class); + private static final String STATIC_FILE_PATH = "./webapp"; + + private final int status; + private final Map headers; + private final byte[] body; + + public HttpResponse(int status, Map headers, byte[] body) { + this.status = status; + this.headers = headers; + this.body = body; + } + + public static Builder builder() { + return new Builder(); + } + + public HttpResponse addCookie(String key, UUID uuid) { + final String cookieAsString = key + "=" + uuid; + headers.merge("Set-Cookie", cookieAsString, (existedValue, newValue) -> existedValue + ";" + cookieAsString); + return this; + } + + public void write(OutputStream out) { + DataOutputStream dos = new DataOutputStream(out); + writeHeader(dos); + writeBody(dos); + } + + private void writeHeader(DataOutputStream dos) { + try { + dos.writeBytes("HTTP/1.1 " + status + " \r\n"); + for (Map.Entry header : headers.entrySet()) { + dos.writeBytes(header.getKey() + ": " + header.getValue() + "\r\n"); + } + dos.writeBytes("\r\n"); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + + private void writeBody(DataOutputStream dos) { + try { + if (Objects.nonNull(body)) { + dos.write(body, 0, body.length); + } + dos.flush(); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + + public static class Builder { + private int status; + private final Map headers; + private final Map cookies; + private byte[] body; + + public Builder() { + headers = new HashMap<>(); + cookies = new HashMap<>(); + } + + public Builder status(int status) { + this.status = status; + return this; + } + + public Builder header(String key, String value) { + headers.put(key, value); + return this; + } + + public Builder header(String key, int value) { + headers.put(key, String.valueOf(value)); + return this; + } + + public Builder cookie(String key, boolean value) { + cookies.put(key, String.valueOf(value)); + return this; + } + + public Builder redirect(String location) { + headers.put("Location", location); + return this; + } + + public Builder forward(String path) throws IOException { + body(Files.readAllBytes(new File(STATIC_FILE_PATH + path).toPath())); + header("Content-Type", getMediaType(path) + ";charset=utf-8"); + header("Content-Length", body.length); + return this; + } + + public Builder body(byte[] body) { + this.body = body; + return this; + } + + public HttpResponse build() { + putCookiesOnHeader(); + return new HttpResponse(status, headers, body); + } + + private String getMediaType(String path) { + final String fileType = path.substring(path.lastIndexOf('.') + 1); + return MediaType.getMediaType(fileType); + } + + private void putCookiesOnHeader() { + if (cookies.isEmpty()) { + return; + } + + final String joinedCookies = this.cookies.entrySet() + .stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(joining(";")); + + headers.put("Set-Cookie", joinedCookies); + } + } +} diff --git a/src/main/java/webserver/model/HttpSession.java b/src/main/java/webserver/model/HttpSession.java new file mode 100644 index 000000000..0b0f698a8 --- /dev/null +++ b/src/main/java/webserver/model/HttpSession.java @@ -0,0 +1,27 @@ +package webserver.model; + +import java.util.HashMap; +import java.util.Map; + +public class HttpSession { + public static final String SESSION_ID_KEY = "JSESSIONID"; + private final Map value; + private final String id; + + public HttpSession(String id) { + this.value = new HashMap<>(); + this.id = id; + } + + public void set(String key, Object value) { + this.value.put(key, value); + } + + public Object get(String key) { + return value.get(key); + } + + public void remove(String key) { + value.remove(key); + } +} diff --git a/src/main/java/webserver/model/HttpSessions.java b/src/main/java/webserver/model/HttpSessions.java new file mode 100644 index 000000000..ba21d16e2 --- /dev/null +++ b/src/main/java/webserver/model/HttpSessions.java @@ -0,0 +1,28 @@ +package webserver.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class HttpSessions { + private static final Map sessions = new HashMap<>(); + + public static void set(String key, HttpSession session) { + sessions.put(key, session); + } + + public static HttpSession get(String sessionId) { + HttpSession httpSession = sessions.get(sessionId); + + if (Objects.isNull(httpSession)) { + httpSession = new HttpSession(sessionId); + set(sessionId, httpSession); + } + + return httpSession; + } + + public static void remove(String key) { + sessions.remove(key); + } +} diff --git a/src/test/java/util/HttpRequestUtilsTest.java b/src/test/java/util/HttpRequestUtilsTest.java index a4265f5e7..7a955d97a 100644 --- a/src/test/java/util/HttpRequestUtilsTest.java +++ b/src/test/java/util/HttpRequestUtilsTest.java @@ -1,12 +1,11 @@ package util; import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.MatcherAssert.*; import java.util.Map; -import org.junit.Test; - +import org.junit.jupiter.api.Test; import util.HttpRequestUtils.Pair; public class HttpRequestUtilsTest { diff --git a/src/test/java/util/IOUtilsTest.java b/src/test/java/util/IOUtilsTest.java index 3c00cc4fa..eb8812be6 100644 --- a/src/test/java/util/IOUtilsTest.java +++ b/src/test/java/util/IOUtilsTest.java @@ -3,7 +3,7 @@ import java.io.BufferedReader; import java.io.StringReader; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/test/java/util/UUIDTest.java b/src/test/java/util/UUIDTest.java new file mode 100644 index 000000000..08239fd1c --- /dev/null +++ b/src/test/java/util/UUIDTest.java @@ -0,0 +1,12 @@ +package util; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +public class UUIDTest { + @Test + public void uuid() { + System.out.println("UUID.randomUUID() = " + UUID.randomUUID()); + } +} diff --git a/src/test/java/webserver/HttpCookieTest.java b/src/test/java/webserver/HttpCookieTest.java new file mode 100644 index 000000000..545b2331b --- /dev/null +++ b/src/test/java/webserver/HttpCookieTest.java @@ -0,0 +1,37 @@ +package webserver; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import webserver.model.HttpCookie; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +public class HttpCookieTest { + @Test + @DisplayName("문자열 쿠키값으로 HttpCookie로 파싱하기") + public void createHttpCookie() { + //given + String cookieAsString = "key1=value1;key2=value2"; + + //when + final HttpCookie httpCookie = new HttpCookie(cookieAsString); + + //then + assertThat(httpCookie.get("key1")).isEqualTo("value1"); + assertThat(httpCookie.get("key2")).isEqualTo("value2"); + assertThat(httpCookie.get("key3")).isEqualTo(null); + } + + @Test + @DisplayName("빈 문자열 쿠키값으로 HttpCookie로 파싱하면 객체만 반환되고 아무 값도 담지 않는다.") + public void createHttpCookieByNull() { + //given + String cookieAsString = null; + + //when + final HttpCookie httpCookie = new HttpCookie(cookieAsString); + + //then + assertThat(httpCookie).isNotNull(); + } +} diff --git a/src/test/java/webserver/HttpRequestTest.java b/src/test/java/webserver/HttpRequestTest.java new file mode 100644 index 000000000..75dcfbf4e --- /dev/null +++ b/src/test/java/webserver/HttpRequestTest.java @@ -0,0 +1,74 @@ +package webserver; + +import webserver.model.HttpCookie; +import webserver.model.HttpRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +public class HttpRequestTest { + private HttpRequest httpRequest; + + @BeforeEach + public void init() throws IOException { + //given + final String request = "POST /user/create HTTP/1.1\n" + + "Host: localhost:8080\n" + + "Connection: keep-alive\n" + + "Content-Length: 46\n" + + "Content-Type: application/x-www-form-urlencoded\n" + + "Accept: */*\n" + + "Cookie: isLogined=false;cookie2=value2\n" + + "\n" + + "userId=puru&password=1234&name=siyoon"; + + final InputStream in = new ByteArrayInputStream(request.getBytes()); + + //when + this.httpRequest = HttpRequest.from(in); + } + + @Test + @DisplayName("HTTP요청에서 요청라인(HTTP메서드와 경로)를 파싱한다.") + public void getHttpMethodFromRequest() { + assertThat(httpRequest.getMethod()).isEqualTo("POST"); + assertThat(httpRequest.getPath()).isEqualTo("/user/create"); + } + + @Test + @DisplayName("HTTP요청에서 헤더정보를 파싱한다.") + public void getHttpHeaderInfosFromRequest() { + assertThat(httpRequest.get("Host")).isEqualTo("localhost:8080"); + assertThat(httpRequest.get("Connection")).isEqualTo("keep-alive"); + assertThat(httpRequest.get("Content-Length")).isEqualTo("46"); + assertThat(httpRequest.get("Content-Type")).isEqualTo("application/x-www-form-urlencoded"); + assertThat(httpRequest.get("Accept")).isEqualTo("*/*"); + } + + @Test + @DisplayName("HTTP요청에서 쿠키정보를 파싱한다.") + public void getHttpCookieInfosFromRequest() { + final HttpCookie cookies = httpRequest.getCookies(); + assertThat(cookies).isNotNull(); + assertThat(cookies.get("isLogined")).isEqualTo("false"); + assertThat(cookies.get("cookie2")).isEqualTo("value2"); + } + + @Test + @DisplayName("HTTP요청에서 바디정보를 파싱한다.") + public void getHttpBodyInfosFromRequest() { + final Map body = httpRequest.getParsedBody(); + assertThat(body).isNotNull(); + assertThat(body.get("userId")).isEqualTo("puru"); + assertThat(body.get("password")).isEqualTo("1234"); + assertThat(body.get("name")).isEqualTo("siyoon"); + } +} diff --git a/webapp/user/form.html b/webapp/user/form.html index 96fe1bd3a..f7a3b5612 100644 --- a/webapp/user/form.html +++ b/webapp/user/form.html @@ -75,7 +75,7 @@
-
+