From aed1db7c5152b9ad0fce98f76b24957d8aeee67f Mon Sep 17 00:00:00 2001 From: juha Date: Wed, 4 Sep 2024 14:00:04 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=EC=9A=94=EC=B2=AD=20URI=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/request/Queries.java | 46 +++++++++++++++++++ .../coyote/http11/request/RequestUri.java | 38 +++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/Queries.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Queries.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Queries.java new file mode 100644 index 0000000000..431efd3bdd --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Queries.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class Queries { + + public static final Queries EMPTY_QUERIES = new Queries(Map.of()); + private static final String QUERIES_SEPARATOR = "&"; + private static final String QUERY_SEPARATOR = "="; + + private final Map values; + + public Queries(Map values) { + this.values = values; + } + + public static Queries of(String queries) { + String[] splitQueries = queries.split(QUERIES_SEPARATOR); + Map map = new HashMap<>(); + for (String query : splitQueries) { + if (!isValidQuery(query)) { + continue; + } + int index = query.indexOf(QUERY_SEPARATOR); + String key = query.substring(0, index); + String value = query.substring(index + 1); + map.put(key, value); + } + return new Queries(map); + } + + private static boolean isValidQuery(String query) { + int index = query.indexOf(QUERY_SEPARATOR); + int queryLastIndex = query.length() - 1; + return index != -1 && index != queryLastIndex; + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public String get(String key) { + return values.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java new file mode 100644 index 0000000000..cfe47d8092 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11.request; + +public class RequestUri { + + private static final String QUERY_START = "?"; + + private final String path; + private final Queries queries; + + public RequestUri(String path, Queries queries) { + this.path = path; + this.queries = queries; + } + + public static RequestUri of(String uri) { + boolean hasQueries = hasQueries(uri); + if (!hasQueries) { + return new RequestUri(uri, Queries.EMPTY_QUERIES); + } + int queryStartIndex = uri.indexOf(QUERY_START); + String path = uri.substring(0, queryStartIndex); + String queryString = uri.substring(queryStartIndex + 1); + return new RequestUri(path, Queries.of(queryString)); + } + + private static boolean hasQueries(String uri) { + int queryStartIndex = uri.indexOf(QUERY_START); + return queryStartIndex != -1 && queryStartIndex != uri.length() - 1; + } + + public String getPath() { + return path; + } + + public Queries getQueries() { + return queries; + } +} From 7f082ec274c19a002f7802a4eb259e472e817162 Mon Sep 17 00:00:00 2001 From: juha Date: Wed, 4 Sep 2024 14:01:36 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20HTTP=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EC=B6=94=EC=B6=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coyote/http11/request/HttpMethod.java | 18 ++++++ .../coyote/http11/request/RequestLine.java | 61 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java new file mode 100644 index 0000000000..27810ae23d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -0,0 +1,18 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; + +public enum HttpMethod { + GET; + + private boolean isSameMethod(String method) { + return name().equals(method); + } + + public static HttpMethod getByName(String method) { + return Arrays.stream(values()) + .filter(httpMethod -> httpMethod.isSameMethod(method)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 HTTP 메서드입니다.")); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java new file mode 100644 index 0000000000..0e7a405cea --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -0,0 +1,61 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; +import java.util.List; + +public class RequestLine { + + private static final int SPILT_REQUEST_LINE_COUNT = 3; + private static final int METHOD_INDEX = 0; + private static final int URI_INDEX = 1; + private static final int PROTOCOL_INDEX = 2; + + private final HttpMethod method; + private final RequestUri requestUri; + private final String protocol; + + public RequestLine(HttpMethod method, RequestUri requestUri, String protocol) { + this.method = method; + this.requestUri = requestUri; + this.protocol = protocol; + } + + public static RequestLine of(String requestLine) { + List splitRequestLine = Arrays.stream(requestLine.split(" ")).toList(); + if (splitRequestLine.size() != SPILT_REQUEST_LINE_COUNT) { + throw new IllegalArgumentException("올바르지 않은 request line 형식입니다."); + } + + return new RequestLine( + getMethod(splitRequestLine), + getUri(splitRequestLine), + splitRequestLine.get(PROTOCOL_INDEX) + ); + } + + private static HttpMethod getMethod(List splitRequestLine) { + String methodName = splitRequestLine.get(METHOD_INDEX); + return HttpMethod.getByName(methodName); + } + + private static RequestUri getUri(List splitRequestLine) { + String uri = splitRequestLine.get(URI_INDEX); + return RequestUri.of(uri); + } + + public HttpMethod getMethod() { + return method; + } + + public String getPath() { + return requestUri.getPath(); + } + + public Queries getQueries() { + return requestUri.getQueries(); + } + + public String getProtocol() { + return protocol; + } +} From e5368a4c8e73a6ac84c86f8fef400f9ca135ab07 Mon Sep 17 00:00:00 2001 From: juha Date: Wed, 4 Sep 2024 16:09:06 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20HTTP=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/ResponseFile.java | 35 ++++++++ .../coyote/http11/response/HttpResponse.java | 83 +++++++++++++++++++ .../coyote/http11/response/HttpStatus.java | 24 ++++++ .../coyote/http11/response/Protocol.java | 15 ++++ 4 files changed, 157 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/ResponseFile.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/Protocol.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ResponseFile.java b/tomcat/src/main/java/org/apache/coyote/http11/ResponseFile.java new file mode 100644 index 0000000000..2189d1c5e0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ResponseFile.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ResponseFile { + + private final String contentType; + private final String content; + + public ResponseFile(String contentType, String content) { + this.contentType = contentType; + this.content = content; + } + + public static ResponseFile of(URL resource) throws IOException { + File resourceFile = new File(resource.getFile()); + Path resourceFilePath = resourceFile.toPath(); + String contentType = Files.probeContentType(resourceFilePath) + ";charset=utf-8"; + String responseBody = new String(Files.readAllBytes(resourceFilePath)); + + return new ResponseFile(contentType, responseBody); + } + + public String getContentType() { + return contentType; + } + + public String getContent() { + return content; + } +} 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..fcbfe5cf98 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,83 @@ +package org.apache.coyote.http11.response; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.coyote.http11.ResponseFile; + +public class HttpResponse { + + private static final String LINE_SEPARATOR = "\r\n"; + private static final String STATUS_LINE_FORMAT = "%s %d %s "; + private static final String HEADER_FORMAT = "%s: %s "; + + private final Protocol protocol = Protocol.HTTP11; + private final HttpStatus httpStatus; + private final Map headers; + private final String responseBody; + + public HttpResponse(HttpStatus httpStatus, Map headers) { + this(httpStatus, headers, ""); + } + + public HttpResponse(HttpStatus httpStatus, Map headers, String responseBody) { + this.httpStatus = httpStatus; + this.headers = headers; + this.responseBody = responseBody; + } + + public static HttpResponse createRedirectResponse(HttpStatus httpStatus, String location) { + Map headers = new HashMap<>(); + headers.put("Location", location); + return new HttpResponse( + httpStatus, + headers + ); + } + + public static HttpResponse createFileResponse(ResponseFile responseFile) { + Map headers = new HashMap<>(); + String responseBody = responseFile.getContent(); + int contentLength = responseBody.getBytes().length; + headers.put("Content-Type", responseFile.getContentType()); + headers.put("Content-Length", String.valueOf(contentLength)); + + return new HttpResponse( + HttpStatus.OK, + headers, + responseBody + ); + } + + public String toResponse() { + String statusLine = getStatusLine(); + String headers = getHeaders(); + + return String.join( + LINE_SEPARATOR, + statusLine, + headers, + "", + responseBody + ); + } + + private String getStatusLine() { + return String.format(STATUS_LINE_FORMAT, + protocol.getName(), + httpStatus.getCode(), + httpStatus.getReasonPhrase() + ); + } + + private String getHeaders() { + List formattedHeaders = new ArrayList<>(); + for (String headerKey : headers.keySet()) { + String headerValue = headers.get(headerKey); + String formattedHeader = String.format(HEADER_FORMAT, headerKey, headerValue); + formattedHeaders.add(formattedHeader); + } + return String.join(LINE_SEPARATOR, formattedHeaders); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java new file mode 100644 index 0000000000..8b62a2d458 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.response; + +public enum HttpStatus { + OK(200, "OK"), + FOUND(302, "Found"), + UNAUTHORIZED(401, "Unauthorized"), + NOT_FOUND(404, "Not Found"); + + private final int code; + private final String reasonPhrase; + + HttpStatus(int code, String reasonPhrase) { + this.code = code; + this.reasonPhrase = reasonPhrase; + } + + public int getCode() { + return code; + } + + public String getReasonPhrase() { + return reasonPhrase; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Protocol.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Protocol.java new file mode 100644 index 0000000000..7bb9fb17b3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/Protocol.java @@ -0,0 +1,15 @@ +package org.apache.coyote.http11.response; + +public enum Protocol { + HTTP11("HTTP/1.1"); + + private final String name; + + Protocol(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} From 3bf4a29d9693621707bccf9d591c6d7033d0c63e Mon Sep 17 00:00:00 2001 From: juha Date: Wed, 4 Sep 2024 16:09:53 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20HTTP=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coyote/http11/request/HttpRequest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java 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..644319bbc7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,39 @@ +package org.apache.coyote.http11.request; + +import java.util.List; + +public class HttpRequest { + + private final RequestLine requestLine; + + public HttpRequest(RequestLine requestLine) { + this.requestLine = requestLine; + } + + public static HttpRequest of(List requestLines) { + validateRequest(requestLines); + String firstLine = requestLines.getFirst(); + RequestLine requestLine = RequestLine.of(firstLine); + + return new HttpRequest(requestLine); + } + + private static void validateRequest(List requestLines) { + System.out.println("requestLines = " + requestLines); + if (requestLines.isEmpty()) { + throw new IllegalArgumentException("올바르지 않은 HTTP 요청 형식입니다."); + } + } + + public String getPath() { + return requestLine.getPath(); + } + + public Queries getQueries() { + return requestLine.getQueries(); + } + + public boolean isQueriesEmpty() { + return getQueries().isEmpty(); + } +} From 11864d5a31a680ed42bceda90417525e274942e0 Mon Sep 17 00:00:00 2001 From: juha Date: Wed, 4 Sep 2024 16:16:57 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=EC=A0=95=EC=A0=81=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=98=ED=99=98=20=EB=B0=8F=20login=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/coyote/http11/Http11Processor.java | 109 ++++++++++++++++-- .../coyote/http11/Http11ProcessorTest.java | 32 +---- 2 files changed, 103 insertions(+), 38 deletions(-) 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..196bf29ebb 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,7 +1,21 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.model.User; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.Queries; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,10 +25,15 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final Set endpoints = Set.of( + "login", + "register", + "index" + ); private final Socket connection; - public Http11Processor(final Socket connection) { + public Http11Processor(Socket connection) { this.connection = connection; } @@ -26,22 +45,88 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + try (InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + List lines = new ArrayList<>(); + String line; + while ((line = bufferedReader.readLine()) != null && !line.isEmpty()) { + lines.add(line); + } + log.info("HTTP Request: " + lines); + HttpRequest request = HttpRequest.of(lines); + HttpResponse response = getResponse(request); + String formattedResponse = response.toResponse(); - 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); - - outputStream.write(response.getBytes()); + outputStream.write(formattedResponse.getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private HttpResponse getResponse(HttpRequest request) throws IOException { + String requestPath = request.getPath(); + + if (requestPath.equals("/login")) { + return login(request); + } + + try { + URL resource = getClass().getClassLoader().getResource("static/" + requestPath); + if (resource == null) { + throw new IllegalArgumentException("존재하지 않는 자원입니다."); + } + ResponseFile responseFile = ResponseFile.of(resource); + return HttpResponse.createFileResponse(responseFile); + } catch (Exception e) { + log.error(e.getMessage(), e); + return HttpResponse.createRedirectResponse( + HttpStatus.FOUND, + "/404.html" + ); + } + } + + private HttpResponse login(HttpRequest request) throws IOException { + if (request.isQueriesEmpty()) { + URL resource = getClass().getClassLoader().getResource("static/login.html"); + ResponseFile responseFile = ResponseFile.of(resource); + + return HttpResponse.createFileResponse(responseFile); + } + Queries queries = request.getQueries(); + String account = queries.get("account"); + String password = queries.get("password"); + if (isValidUser(account, password)) { + return HttpResponse.createRedirectResponse( + HttpStatus.FOUND, + "/index.html" + ); + } + return HttpResponse.createRedirectResponse( + HttpStatus.FOUND, + "/401.html" + ); + } + + private boolean isValidUser(String account, String password) { + if (account == null || password == null) { + return false; + } + return InMemoryUserRepository.findByAccount(account) + .map(user -> authenticateUser(user, password)) + .orElse(false); + } + + private boolean authenticateUser(User user, String password) { + if (user.checkPassword(password)) { + log.info("User authenticated: " + user); + return true; + } else { + log.warn("Authentication failed for user: " + user.getAccount()); + return false; + } + } } 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..f4c3b125d3 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -12,26 +12,6 @@ class Http11ProcessorTest { - @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); - } - @Test void index() throws IOException { // given @@ -50,12 +30,12 @@ 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" + - "\r\n"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - assertThat(socket.output()).isEqualTo(expected); + assertThat(socket.output()).contains( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + "Content-Length: 5564 \r\n", + new String(Files.readAllBytes(new File(resource.getFile()).toPath())) + ); } } From b0b08ff38a0881fbeed76cfa21c5c7cad4cd096b Mon Sep 17 00:00:00 2001 From: JUHA <84626225+khabh@users.noreply.github.com> Date: Wed, 4 Sep 2024 22:19:09 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20HttpRequest=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EC=97=90=20=ED=97=A4=EB=8D=94=EC=99=80=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coyote/http11/request/HttpHeaders.java | 57 +++++++++++++++++++ .../coyote/http11/request/HttpRequest.java | 57 ++++++++++++++++--- 2 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/HttpHeaders.java diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpHeaders.java new file mode 100644 index 0000000000..20395524eb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpHeaders.java @@ -0,0 +1,57 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +public class HttpHeaders { + + private static final String HEADER_SEPARATOR = ": "; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map values; + + public HttpHeaders(Map values) { + this.values = values; + } + + public static HttpHeaders of(List headers) { + Map map = new HashMap<>(); + for (String header : headers) { + if (!isValidHeader(header)) { + continue; + } + String[] splitHeader = header.split(HEADER_SEPARATOR); + map.put(splitHeader[KEY_INDEX], splitHeader[VALUE_INDEX]); + } + return new HttpHeaders(map); + } + + private static boolean isValidHeader(String header) { + List splitHeader = Arrays.stream(header.split(HEADER_SEPARATOR)).toList(); + if (splitHeader.size() != 2) { + return false; + } + return splitHeader.stream() + .noneMatch(String::isBlank); + } + + public String get(String key) { + return values.get(key); + } + + public OptionalInt getAsInt(String key) { + String value = get(key); + if (value == null) { + return OptionalInt.empty(); + } + try { + return OptionalInt.of(Integer.parseInt(value)); + } catch (NumberFormatException e) { + return OptionalInt.empty(); + } + } +} 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 index 644319bbc7..8fe93c685e 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -1,30 +1,69 @@ package org.apache.coyote.http11.request; +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; public class HttpRequest { private final RequestLine requestLine; + private final HttpHeaders headers; + private final String body; - public HttpRequest(RequestLine requestLine) { + public HttpRequest(RequestLine requestLine, HttpHeaders headers, String body) { this.requestLine = requestLine; + this.headers = headers; + this.body = body; } - public static HttpRequest of(List requestLines) { - validateRequest(requestLines); - String firstLine = requestLines.getFirst(); - RequestLine requestLine = RequestLine.of(firstLine); + public static HttpRequest of(BufferedReader requestReader) throws IOException { + List requestHead = readRequestHead(requestReader); + validateRequestHead(requestHead); + RequestLine requestLine = getRequestLine(requestHead); + HttpHeaders headers = getHeaders(requestHead); - return new HttpRequest(requestLine); + int contentLength = headers.getAsInt("Content-Length").orElse(0); + String body = readBody(contentLength, requestReader); + + return new HttpRequest(requestLine, headers, body); + } + + private static List readRequestHead(BufferedReader requestReader) throws IOException { + List requestHead = new ArrayList<>(); + String line; + while ((line = requestReader.readLine()) != null && !line.isEmpty()) { + requestHead.add(line); + } + return requestHead; } - private static void validateRequest(List requestLines) { + private static void validateRequestHead(List requestLines) { System.out.println("requestLines = " + requestLines); if (requestLines.isEmpty()) { throw new IllegalArgumentException("올바르지 않은 HTTP 요청 형식입니다."); } } + private static RequestLine getRequestLine(List requestHead) { + String firstLine = requestHead.getFirst(); + return RequestLine.of(firstLine); + } + + private static HttpHeaders getHeaders(List requestHead) { + List headers = new ArrayList<>(requestHead.subList(1, requestHead.size())); + return HttpHeaders.of(headers); + } + + private static String readBody(int contentLength, BufferedReader requestReader) throws IOException { + if (contentLength <= 0) { + return ""; + } + char[] body = new char[contentLength]; + requestReader.read(body, 0, contentLength); + return new String(body); + } + public String getPath() { return requestLine.getPath(); } @@ -36,4 +75,8 @@ public Queries getQueries() { public boolean isQueriesEmpty() { return getQueries().isEmpty(); } + + public HttpMethod getMethod() { + return requestLine.getMethod(); + } } From 82c8a82405173268e23ceb0e6734188444d5b204 Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 08:50:57 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/techcourse/model/User.java | 22 +++++++ .../apache/coyote/http11/Http11Processor.java | 62 +++++++++++-------- .../coyote/http11/request/HttpMethod.java | 3 +- .../coyote/http11/request/HttpRequest.java | 4 ++ .../coyote/http11/response/HttpResponse.java | 13 ++++ .../coyote/http11/response/HttpStatus.java | 3 +- 6 files changed, 78 insertions(+), 29 deletions(-) diff --git a/tomcat/src/main/java/com/techcourse/model/User.java b/tomcat/src/main/java/com/techcourse/model/User.java index e8cf4c8e68..675b6463ca 100644 --- a/tomcat/src/main/java/com/techcourse/model/User.java +++ b/tomcat/src/main/java/com/techcourse/model/User.java @@ -8,6 +8,10 @@ public class User { private final String email; public User(Long id, String account, String password, String email) { + validateAccount(account); + validatePassword(password); + validateEmail(email); + this.id = id; this.account = account; this.password = password; @@ -18,6 +22,24 @@ public User(String account, String password, String email) { this(null, account, password, email); } + private void validateAccount(String account) { + if (account == null || account.isBlank()) { + throw new IllegalArgumentException("account는 비어 있을 수 없습니다."); + } + } + + private void validatePassword(String password) { + if (password == null || password.isBlank()) { + throw new IllegalArgumentException("password는 비어 있을 수 없습니다."); + } + } + + private void validateEmail(String email) { + if (email == null || email.isBlank()) { + throw new IllegalArgumentException("email은 비어 있을 수 없습니다."); + } + } + public boolean checkPassword(String password) { return this.password.equals(password); } 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 196bf29ebb..e61bfe20fc 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -8,10 +8,9 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URL; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpMethod; import org.apache.coyote.http11.request.HttpRequest; import org.apache.coyote.http11.request.Queries; import org.apache.coyote.http11.response.HttpResponse; @@ -25,10 +24,10 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - private static final Set endpoints = Set.of( - "login", - "register", - "index" + private static final Set SERVING_PATHS = Set.of( + "/login", + "/register", + "/index" ); private final Socket connection; @@ -49,13 +48,7 @@ public void process(final Socket connection) { OutputStream outputStream = connection.getOutputStream()) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); - List lines = new ArrayList<>(); - String line; - while ((line = bufferedReader.readLine()) != null && !line.isEmpty()) { - lines.add(line); - } - log.info("HTTP Request: " + lines); - HttpRequest request = HttpRequest.of(lines); + HttpRequest request = HttpRequest.of(bufferedReader); HttpResponse response = getResponse(request); String formattedResponse = response.toResponse(); @@ -69,14 +62,26 @@ public void process(final Socket connection) { private HttpResponse getResponse(HttpRequest request) throws IOException { String requestPath = request.getPath(); + if (SERVING_PATHS.contains(requestPath) && request.isQueriesEmpty() && request.getMethod() == HttpMethod.GET) { + requestPath += ".html"; + } + if (requestPath.contains(".")) { + return getFileResponse(requestPath); + } if (requestPath.equals("/login")) { return login(request); } + if (requestPath.equals("/register") && request.getMethod() == HttpMethod.POST) { + return register(request); + } + throw new IllegalArgumentException("요청을 처리할 수 없습니다."); + } + private HttpResponse getFileResponse(String requestPath) { try { URL resource = getClass().getClassLoader().getResource("static/" + requestPath); if (resource == null) { - throw new IllegalArgumentException("존재하지 않는 자원입니다."); + throw new IllegalArgumentException("존재하지 않는 자원입니다: " + requestPath); } ResponseFile responseFile = ResponseFile.of(resource); return HttpResponse.createFileResponse(responseFile); @@ -89,26 +94,29 @@ private HttpResponse getResponse(HttpRequest request) throws IOException { } } - private HttpResponse login(HttpRequest request) throws IOException { - if (request.isQueriesEmpty()) { - URL resource = getClass().getClassLoader().getResource("static/login.html"); - ResponseFile responseFile = ResponseFile.of(resource); + private HttpResponse register(HttpRequest request) { + Queries queries = Queries.of(request.getBody()); + String account = queries.get("account"); - return HttpResponse.createFileResponse(responseFile); + User user = new User(account, queries.get("password"), queries.get("email")); + + if (InMemoryUserRepository.findByAccount(account).isPresent()) { + String message = "이미 존재하는 사용자입니다: " + account; + log.warn(message); + return HttpResponse.createTextResponse(HttpStatus.CONFLICT, message); } + InMemoryUserRepository.save(user); + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/index.html"); + } + + private HttpResponse login(HttpRequest request) { Queries queries = request.getQueries(); String account = queries.get("account"); String password = queries.get("password"); if (isValidUser(account, password)) { - return HttpResponse.createRedirectResponse( - HttpStatus.FOUND, - "/index.html" - ); + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/index.html"); } - return HttpResponse.createRedirectResponse( - HttpStatus.FOUND, - "/401.html" - ); + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/401.html"); } private boolean isValidUser(String account, String password) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java index 27810ae23d..fb34be1c00 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -3,7 +3,8 @@ import java.util.Arrays; public enum HttpMethod { - GET; + GET, + POST; private boolean isSameMethod(String method) { return name().equals(method); 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 index 8fe93c685e..385e18a714 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -79,4 +79,8 @@ public boolean isQueriesEmpty() { public HttpMethod getMethod() { return requestLine.getMethod(); } + + public String getBody() { + return body; + } } 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 index fcbfe5cf98..6046bac4b0 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -36,6 +36,19 @@ public static HttpResponse createRedirectResponse(HttpStatus httpStatus, String ); } + public static HttpResponse createTextResponse(HttpStatus httpStatus, String responseBody) { + Map headers = new HashMap<>(); + int contentLength = responseBody.getBytes().length; + headers.put("Content-Type", "text/plain;charset=utf-8 "); + headers.put("Content-Length", String.valueOf(contentLength)); + + return new HttpResponse( + httpStatus, + headers, + responseBody + ); + } + public static HttpResponse createFileResponse(ResponseFile responseFile) { Map headers = new HashMap<>(); String responseBody = responseFile.getContent(); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java index 8b62a2d458..8d31b9e449 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatus.java @@ -4,7 +4,8 @@ public enum HttpStatus { OK(200, "OK"), FOUND(302, "Found"), UNAUTHORIZED(401, "Unauthorized"), - NOT_FOUND(404, "Not Found"); + NOT_FOUND(404, "Not Found"), + CONFLICT(409, "Conflict"); private final int code; private final String reasonPhrase; From fed02f6f5f4308400e55c160d9495cad010f5bfb Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 11:11:09 +0900 Subject: [PATCH 08/17] fix: remove implementation logback-classic on gradle (#501) --- study/build.gradle | 1 - 1 file changed, 1 deletion(-) 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' From c1d7cb1191207e0bb7b5aefc5136e52f2da6e885 Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 12:27:02 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20=EC=84=B8=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catalina/{ => session}/Manager.java | 10 ++-- .../org/apache/catalina/session/Session.java | 44 +++++++++++++++ .../catalina/session/SessionManager.java | 27 +++++++++ .../apache/coyote/http11/Http11Processor.java | 56 ++++++++++++------- .../coyote/http11/request/HttpRequest.java | 15 ++++- .../coyote/http11/request/RequestCookies.java | 41 ++++++++++++++ .../coyote/http11/response/HttpResponse.java | 8 ++- .../http11/response/ResponseCookie.java | 24 ++++++++ .../http11/{ => response}/ResponseFile.java | 2 +- tomcat/src/main/resources/static/login.html | 3 +- 10 files changed, 200 insertions(+), 30 deletions(-) rename tomcat/src/main/java/org/apache/catalina/{ => session}/Manager.java (89%) create mode 100644 tomcat/src/main/java/org/apache/catalina/session/Session.java create mode 100644 tomcat/src/main/java/org/apache/catalina/session/SessionManager.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/ResponseCookie.java rename tomcat/src/main/java/org/apache/coyote/http11/{ => response}/ResponseFile.java (95%) diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/session/Manager.java similarity index 89% rename from tomcat/src/main/java/org/apache/catalina/Manager.java rename to tomcat/src/main/java/org/apache/catalina/session/Manager.java index e69410f6a9..c94fdd7650 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/Manager.java @@ -1,6 +1,4 @@ -package org.apache.catalina; - -import jakarta.servlet.http.HttpSession; +package org.apache.catalina.session; import java.io.IOException; @@ -29,7 +27,7 @@ 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 @@ -45,12 +43,12 @@ public interface Manager { * @return the request session or {@code null} if a session with the * requested ID could not be found */ - HttpSession findSession(String id) throws IOException; + Session findSession(String id); /** * 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/catalina/session/Session.java b/tomcat/src/main/java/org/apache/catalina/session/Session.java new file mode 100644 index 0000000000..f116e0423e --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -0,0 +1,44 @@ +package org.apache.catalina.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(String id) { + this.id = id; + } + + public static Session create() { + String sessionId = UUID.randomUUID().toString(); + return new Session(sessionId); + } + + public boolean contains(String name) { + return getAttribute(name) != null; + } + + public Object getAttribute(String name) { + return values.get(name); + } + + public void setAttribute(String name, Object value) { + values.put(name, value); + } + + public void removeAttribute(String name) { + values.remove(name); + } + + public void invalidate() { + values.clear(); + } + + public String getId() { + return id; + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java new file mode 100644 index 0000000000..c14f1703a0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,27 @@ +package org.apache.catalina.session; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new HashMap<>(); + + @Override + public void add(Session session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public Session findSession(String id) { + if (id == null) { + return null; + } + 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/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index e61bfe20fc..ddb37a5d6c 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -8,13 +8,19 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URL; +import java.util.HashMap; +import java.util.Map; import java.util.Set; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; import org.apache.coyote.Processor; import org.apache.coyote.http11.request.HttpMethod; import org.apache.coyote.http11.request.HttpRequest; import org.apache.coyote.http11.request.Queries; import org.apache.coyote.http11.response.HttpResponse; import org.apache.coyote.http11.response.HttpStatus; +import org.apache.coyote.http11.response.ResponseCookie; +import org.apache.coyote.http11.response.ResponseFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +35,7 @@ public class Http11Processor implements Runnable, Processor { "/register", "/index" ); + private final static SessionManager SESSION_MANAGER = new SessionManager(); private final Socket connection; @@ -61,14 +68,16 @@ public void process(final Socket connection) { private HttpResponse getResponse(HttpRequest request) throws IOException { String requestPath = request.getPath(); - + if (requestPath.equals("/login") && request.getMethod() == HttpMethod.GET && isLoginUser(request)) { + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/index.html"); + } if (SERVING_PATHS.contains(requestPath) && request.isQueriesEmpty() && request.getMethod() == HttpMethod.GET) { requestPath += ".html"; } if (requestPath.contains(".")) { return getFileResponse(requestPath); } - if (requestPath.equals("/login")) { + if (requestPath.equals("/login") && request.getMethod() == HttpMethod.POST) { return login(request); } if (requestPath.equals("/register") && request.getMethod() == HttpMethod.POST) { @@ -77,6 +86,11 @@ private HttpResponse getResponse(HttpRequest request) throws IOException { throw new IllegalArgumentException("요청을 처리할 수 없습니다."); } + private boolean isLoginUser(HttpRequest request) { + Session session = request.getSession(); + return session.contains("user"); + } + private HttpResponse getFileResponse(String requestPath) { try { URL resource = getClass().getClassLoader().getResource("static/" + requestPath); @@ -110,31 +124,33 @@ private HttpResponse register(HttpRequest request) { } private HttpResponse login(HttpRequest request) { - Queries queries = request.getQueries(); + String requestBody = request.getBody(); + Queries queries = Queries.of(requestBody); String account = queries.get("account"); String password = queries.get("password"); - if (isValidUser(account, password)) { - return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/index.html"); + validateLoginRequest(account, password); + User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + if (user.checkPassword(password)) { + Session session = request.getSession(); + session.setAttribute("user", user); + SESSION_MANAGER.add(session); + ResponseCookie sessionCookie = ResponseCookie.of(session); + Map headers = new HashMap<>(); + headers.put("Location", "/index.html"); + headers.put("Set-Cookie", sessionCookie.toResponse()); + return new HttpResponse(HttpStatus.FOUND, headers); } return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/401.html"); } - private boolean isValidUser(String account, String password) { - if (account == null || password == null) { - return false; + private void validateLoginRequest(String account, String password) { + if (account == null || account.isEmpty()) { + throw new IllegalArgumentException("account는 비어 있을 수 없습니다."); } - return InMemoryUserRepository.findByAccount(account) - .map(user -> authenticateUser(user, password)) - .orElse(false); - } - - private boolean authenticateUser(User user, String password) { - if (user.checkPassword(password)) { - log.info("User authenticated: " + user); - return true; - } else { - log.warn("Authentication failed for user: " + user.getAccount()); - return false; + if (password == null || password.isEmpty()) { + throw new IllegalArgumentException("password는 비어 있을 수 없습니다."); } } } 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 index 385e18a714..12e3aa9f00 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -4,9 +4,13 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; public class HttpRequest { + private static final SessionManager SESSION_MANAGER = new SessionManager(); + private final RequestLine requestLine; private final HttpHeaders headers; private final String body; @@ -39,7 +43,6 @@ private static List readRequestHead(BufferedReader requestReader) throws } private static void validateRequestHead(List requestLines) { - System.out.println("requestLines = " + requestLines); if (requestLines.isEmpty()) { throw new IllegalArgumentException("올바르지 않은 HTTP 요청 형식입니다."); } @@ -83,4 +86,14 @@ public HttpMethod getMethod() { public String getBody() { return body; } + + public Session getSession() { + RequestCookies requestCookies = RequestCookies.of(headers.get("Cookie")); + String sessionId = requestCookies.get("JSESSIONID"); + Session session = SESSION_MANAGER.findSession(sessionId); + if (session == null) { + return Session.create(); + } + return session; + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java new file mode 100644 index 0000000000..41428d8aa8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java @@ -0,0 +1,41 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class RequestCookies { + + private static final String COOKIES_SEPARATOR = "; "; + private static final String COOKIE_SEPARATOR = "="; + + private final Map properties; + + public RequestCookies(Map values) { + this.properties = values; + } + + public static RequestCookies of(String httpCookies) { + String[] splitHttpCookies = httpCookies.split(COOKIES_SEPARATOR); + Map cookies = new HashMap<>(); + for (String cookie : splitHttpCookies) { + if (!isValidCookie(cookie)) { + continue; + } + int index = cookie.indexOf(COOKIE_SEPARATOR); + String key = cookie.substring(0, index); + String value = cookie.substring(index + 1); + cookies.put(key, value); + } + return new RequestCookies(cookies); + } + + private static boolean isValidCookie(String cookie) { + int index = cookie.indexOf(COOKIE_SEPARATOR); + int cookieLastIndex = cookie.length() - 1; + return index != -1 && index != cookieLastIndex; + } + + public String get(String key) { + return properties.get(key); + } +} 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 index 6046bac4b0..9bdfafea66 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -4,7 +4,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.coyote.http11.ResponseFile; public class HttpResponse { @@ -93,4 +92,11 @@ private String getHeaders() { } return String.join(LINE_SEPARATOR, formattedHeaders); } + +// public void addSession(Session session) { +// if (headers.containsKey("JSESSIONID")) { +// return; +// } +// headers.put("JSESSIONID", session.getId()); +// } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseCookie.java new file mode 100644 index 0000000000..53bbc43b3a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseCookie.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.response; + +import org.apache.catalina.session.Session; + +public class ResponseCookie { + + private static final String COOKIE_FORMAT = "%s=%s"; + + private final String name; + private final String value; + + public ResponseCookie(String name, String value) { + this.name = name; + this.value = value; + } + + public static ResponseCookie of(Session session) { + return new ResponseCookie("JSESSIONID", session.getId()); + } + + public String toResponse() { + return String.format(COOKIE_FORMAT, name, value); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ResponseFile.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseFile.java similarity index 95% rename from tomcat/src/main/java/org/apache/coyote/http11/ResponseFile.java rename to tomcat/src/main/java/org/apache/coyote/http11/response/ResponseFile.java index 2189d1c5e0..2a1f02a235 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/ResponseFile.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseFile.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.response; import java.io.File; import java.io.IOException; diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..457b36255a 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -10,6 +10,7 @@ +
@@ -20,7 +21,7 @@

로그인

-
+
From 7e9135698878932274ddc1f523ba817ed9c56c70 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 5 Sep 2024 13:51:07 +0900 Subject: [PATCH 10/17] fix: add threads min-spare configuration on properties (#502) --- study/src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..e3503a5fb9 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,5 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 From 476edc187407694edc48ba38c2f615ca2e4af36c Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 14:57:54 +0900 Subject: [PATCH 11/17] =?UTF-8?q?fix:=20=EC=9A=94=EC=B2=AD=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20null=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/apache/coyote/http11/request/RequestCookies.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java index 41428d8aa8..530f80a5f4 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java @@ -5,6 +5,7 @@ public class RequestCookies { + private static final RequestCookies EMPTY_COOKIES = new RequestCookies(Map.of()); private static final String COOKIES_SEPARATOR = "; "; private static final String COOKIE_SEPARATOR = "="; @@ -15,6 +16,9 @@ public RequestCookies(Map values) { } public static RequestCookies of(String httpCookies) { + if (httpCookies == null || httpCookies.isBlank()) { + return EMPTY_COOKIES; + } String[] splitHttpCookies = httpCookies.split(COOKIES_SEPARATOR); Map cookies = new HashMap<>(); for (String cookie : splitHttpCookies) { From 6aa1a445298204057a2e8f6701e0974c981f4c1f Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 15:00:49 +0900 Subject: [PATCH 12/17] =?UTF-8?q?feat:=20SessionGenerator=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apache/catalina/connector/Connector.java | 3 +- .../org/apache/catalina/session/Session.java | 6 --- .../catalina/session/SessionGenerator.java | 6 +++ .../session/UuidSessionGenerator.java | 12 ++++++ .../apache/coyote/http11/Http11Processor.java | 37 +++++++++++++++---- .../coyote/http11/request/HttpRequest.java | 14 +++---- 6 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 tomcat/src/main/java/org/apache/catalina/session/SessionGenerator.java create mode 100644 tomcat/src/main/java/org/apache/catalina/session/UuidSessionGenerator.java diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..4490fe7945 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,5 +1,6 @@ package org.apache.catalina.connector; +import org.apache.catalina.session.UuidSessionGenerator; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,7 +67,7 @@ private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); + var processor = new Http11Processor(connection, new UuidSessionGenerator()); new Thread(processor).start(); } diff --git a/tomcat/src/main/java/org/apache/catalina/session/Session.java b/tomcat/src/main/java/org/apache/catalina/session/Session.java index f116e0423e..68df752718 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/Session.java +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -2,7 +2,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.UUID; public class Session { @@ -13,11 +12,6 @@ public Session(String id) { this.id = id; } - public static Session create() { - String sessionId = UUID.randomUUID().toString(); - return new Session(sessionId); - } - public boolean contains(String name) { return getAttribute(name) != null; } diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionGenerator.java b/tomcat/src/main/java/org/apache/catalina/session/SessionGenerator.java new file mode 100644 index 0000000000..059856be39 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionGenerator.java @@ -0,0 +1,6 @@ +package org.apache.catalina.session; + +@FunctionalInterface +public interface SessionGenerator { + Session create(); +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/UuidSessionGenerator.java b/tomcat/src/main/java/org/apache/catalina/session/UuidSessionGenerator.java new file mode 100644 index 0000000000..99546dff57 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/UuidSessionGenerator.java @@ -0,0 +1,12 @@ +package org.apache.catalina.session; + +import java.util.UUID; + +public class UuidSessionGenerator implements SessionGenerator { + + @Override + public Session create() { + String sessionId = UUID.randomUUID().toString(); + return new Session(sessionId); + } +} 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 ddb37a5d6c..5616af0387 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Set; import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionGenerator; import org.apache.catalina.session.SessionManager; import org.apache.coyote.Processor; import org.apache.coyote.http11.request.HttpMethod; @@ -38,9 +39,11 @@ public class Http11Processor implements Runnable, Processor { private final static SessionManager SESSION_MANAGER = new SessionManager(); private final Socket connection; + private final SessionGenerator sessionGenerator; - public Http11Processor(Socket connection) { + public Http11Processor(Socket connection, SessionGenerator sessionGenerator) { this.connection = connection; + this.sessionGenerator = sessionGenerator; } @Override @@ -87,7 +90,16 @@ private HttpResponse getResponse(HttpRequest request) throws IOException { } private boolean isLoginUser(HttpRequest request) { - Session session = request.getSession(); + return request.getSessionId() + .map(this::isLoginUser) + .orElse(false); + } + + private boolean isLoginUser(String sessionId) { + Session session = SESSION_MANAGER.findSession(sessionId); + if (session == null) { + return false; + } return session.contains("user"); } @@ -129,11 +141,10 @@ private HttpResponse login(HttpRequest request) { String account = queries.get("account"); String password = queries.get("password"); validateLoginRequest(account, password); - User user = InMemoryUserRepository.findByAccount(account) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - if (user.checkPassword(password)) { - Session session = request.getSession(); + try { + User user = getLoginUser(account, password); + Session session = sessionGenerator.create(); session.setAttribute("user", user); SESSION_MANAGER.add(session); ResponseCookie sessionCookie = ResponseCookie.of(session); @@ -141,8 +152,20 @@ private HttpResponse login(HttpRequest request) { headers.put("Location", "/index.html"); headers.put("Set-Cookie", sessionCookie.toResponse()); return new HttpResponse(HttpStatus.FOUND, headers); + } catch (Exception e) { + log.warn(e.getMessage(), e); + return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/401.html"); + } + } + + private User getLoginUser(String account, String password) { + User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + if (!user.checkPassword(password)) { + throw new IllegalArgumentException("잘못된 비밀번호입니다."); } - return HttpResponse.createRedirectResponse(HttpStatus.FOUND, "/401.html"); + return user; } private void validateLoginRequest(String account, String password) { 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 index 12e3aa9f00..4da7e0498f 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -4,13 +4,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import org.apache.catalina.session.Session; -import org.apache.catalina.session.SessionManager; +import java.util.Optional; public class HttpRequest { - private static final SessionManager SESSION_MANAGER = new SessionManager(); - private final RequestLine requestLine; private final HttpHeaders headers; private final String body; @@ -87,13 +84,12 @@ public String getBody() { return body; } - public Session getSession() { + public Optional getSessionId() { RequestCookies requestCookies = RequestCookies.of(headers.get("Cookie")); String sessionId = requestCookies.get("JSESSIONID"); - Session session = SESSION_MANAGER.findSession(sessionId); - if (session == null) { - return Session.create(); + if (sessionId == null) { + return Optional.empty(); } - return session; + return Optional.of(sessionId); } } From d4dba28945c558a8339d08087aebde46d2d270f0 Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 15:09:25 +0900 Subject: [PATCH 13/17] =?UTF-8?q?test:=20=EC=A0=95=EC=A0=81=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=98=ED=99=98=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tomcat/build.gradle | 1 + .../techcourse/db/InMemoryUserRepository.java | 4 + .../coyote/http11/Http11ProcessorTest.java | 165 +++++++++++++++--- 3 files changed, 145 insertions(+), 25 deletions(-) diff --git a/tomcat/build.gradle b/tomcat/build.gradle index 21063b298f..ccb3e2d92d 100644 --- a/tomcat/build.gradle +++ b/tomcat/build.gradle @@ -22,6 +22,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' } test { diff --git a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java index d3fa57feeb..10a79ae83c 100644 --- a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java @@ -23,5 +23,9 @@ public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } + public static void delete(User user) { + database.remove(user.getAccount()); + } + private InMemoryUserRepository() {} } 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 f4c3b125d3..b3f464f0a5 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,6 +1,16 @@ package org.apache.coyote.http11; +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.UuidSessionGenerator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import support.StubSocket; import java.io.File; @@ -12,30 +22,135 @@ class Http11ProcessorTest { - @Test - void index() throws IOException { - // given - final String httpRequest= String.join("\r\n", - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "", - ""); - - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - final URL resource = getClass().getClassLoader().getResource("static/index.html"); - - assertThat(socket.output()).contains( - "HTTP/1.1 200 OK \r\n", - "Content-Type: text/html;charset=utf-8 \r\n", - "Content-Length: 5564 \r\n", - new String(Files.readAllBytes(new File(resource.getFile()).toPath())) - ); + @Nested + class ResourceFileTest { + @DisplayName("특정 엔드포인트에 대한 올바른 HTML 파일을 반환한다.") + @ParameterizedTest + @ValueSource(strings = {"/index", "/login", "/register"}) + void getResourceHtml(String requestPath) throws IOException { + String httpRequest = String.join("\r\n", + "GET " + requestPath + " HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, new UuidSessionGenerator()); + + processor.process(socket); + + String responseBody = getFile(requestPath + ".html"); + int contentLength = responseBody.getBytes().length; + + assertThat(socket.output()).contains( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + "Content-Length: " + contentLength + " \r\n", + responseBody + ); + } + + @DisplayName("CSS 파일을 반환한다.") + @Test + void getCssFile() throws IOException { + String httpRequest = String.join("\r\n", + "GET /css/styles.css HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, new UuidSessionGenerator()); + + processor.process(socket); + + String responseBody = getFile("/css/styles.css"); + int contentLength = responseBody.getBytes().length; + + assertThat(socket.output()).contains( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/css;charset=utf-8 \r\n", + "Content-Length: " + contentLength + " \r\n", + responseBody + ); + } + + private String getFile(String fileName) throws IOException { + URL resource = getClass().getClassLoader().getResource("static/" + fileName); + return new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + } + } + + @Nested + class LoginTest { + + private final String account = "account"; + private final String password = "password"; + private final User user = new User(account, password, "aaa@gmail.com"); + + @BeforeEach + void saveUser() { + InMemoryUserRepository.save(user); + } + + @AfterEach + void deleteUser() { + InMemoryUserRepository.delete(user); + } + + @DisplayName("로그인 성공 시 올바르게 응답을 보낸다.") + @Test + void loginSuccess() { + String sessionId = "sessionId"; + Session session = new Session(sessionId); + String body = String.format("account=%s&password=%s", account, password); + int contentLength = body.getBytes().length; + String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + contentLength, + "", + body); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, () -> session); + + processor.process(socket); + + assertThat(socket.output()).contains( + "HTTP/1.1 302 Found \r\n", + "Location: /index.html \r\n", + "Set-Cookie: JSESSIONID=" + sessionId + ); + } + + @DisplayName("로그인 실패 시 올바르게 응답을 보낸다.") + @Test + void loginFail() { + String sessionId = "sessionId"; + Session session = new Session(sessionId); + String body = String.format("account=%s&password=%s", "invalid account", "password"); + int contentLength = body.getBytes().length; + String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + contentLength, + "", + body); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, () -> session); + + processor.process(socket); + + assertThat(socket.output()).contains( + "HTTP/1.1 302 Found \r\n", + "Location: /401.html \r\n" + ); + } } } From 42e833ef1cce951936e10c751821ac579b867bb8 Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 15:10:46 +0900 Subject: [PATCH 14/17] =?UTF-8?q?test:=20Query=20String=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../http11/request/HttpRequestTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java new file mode 100644 index 0000000000..70a6002b52 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java @@ -0,0 +1,32 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpRequestTest { + + @DisplayName("요청 쿼리 스트링을 파싱한다.") + @Test + void parseQuery() throws IOException { + String rawRequest = String.join("\r\n", + "GET /subway?bread=white&sauce=pepper HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + BufferedReader requestReader = new BufferedReader(new StringReader(rawRequest)); + + Queries queries = HttpRequest.of(requestReader).getQueries(); + + assertAll( + () -> assertThat(queries.get("bread")).isEqualTo("white"), + () -> assertThat(queries.get("sauce")).isEqualTo("pepper") + ); + } +} From b3d504bcc19d366bc029257979a65784fdad4240 Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 15:31:22 +0900 Subject: [PATCH 15/17] =?UTF-8?q?test:=20register=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coyote/http11/Http11ProcessorTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) 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 b3f464f0a5..26f6a91b7a 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -19,6 +19,7 @@ import java.nio.file.Files; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; class Http11ProcessorTest { @@ -153,4 +154,40 @@ void loginFail() { ); } } + + @Nested + class RegisterTest { + + @DisplayName("회원 가입 시 올바르게 응답을 보낸다.") + @Test + void register() { + String sessionId = "sessionId"; + String account = "account"; + User user = new User(account, "password", "kkk@gmail.com"); + Session session = new Session(sessionId); + String body = "account=account&password=password&email=kkk@gmail.com"; + int contentLength = body.getBytes().length; + String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: " + contentLength, + "", + body); + + StubSocket socket = new StubSocket(httpRequest); + Http11Processor processor = new Http11Processor(socket, () -> session); + + processor.process(socket); + + assertAll( + () -> assertThat(socket.output()).contains( + "HTTP/1.1 302 Found \r\n", + "Location: /index.html \r\n" + ), + () -> assertThat(InMemoryUserRepository.findByAccount(account)).isNotEmpty() + ); + InMemoryUserRepository.delete(user); + } + } } From 53fa35333ed58cd36d5ceafaf7acfe9533057c57 Mon Sep 17 00:00:00 2001 From: juha Date: Thu, 5 Sep 2024 15:32:34 +0900 Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/apache/coyote/http11/request/RequestCookies.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java index 530f80a5f4..dcff0149c7 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestCookies.java @@ -6,7 +6,7 @@ public class RequestCookies { private static final RequestCookies EMPTY_COOKIES = new RequestCookies(Map.of()); - private static final String COOKIES_SEPARATOR = "; "; + private static final String COOKIES_SEPARATOR = ";"; private static final String COOKIE_SEPARATOR = "="; private final Map properties; @@ -26,8 +26,8 @@ public static RequestCookies of(String httpCookies) { continue; } int index = cookie.indexOf(COOKIE_SEPARATOR); - String key = cookie.substring(0, index); - String value = cookie.substring(index + 1); + String key = cookie.substring(0, index).trim(); + String value = cookie.substring(index + 1).trim(); cookies.put(key, value); } return new RequestCookies(cookies); From b158638d433f99bafba6853c198710afd9a4abaf Mon Sep 17 00:00:00 2001 From: juha Date: Fri, 6 Sep 2024 16:56:46 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20=ED=95=99=EC=8A=B5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cache/com/example/GreetingController.java | 6 +++--- .../example/cachecontrol/CacheWebConfig.java | 6 ++++++ .../example/etag/EtagFilterConfiguration.java | 15 +++++++++---- .../version/CacheBustingWebConfig.java | 5 ++++- study/src/main/resources/application.yml | 3 +++ study/src/main/resources/static/index.html | 10 +++++++++ study/src/test/java/study/FileTest.java | 16 ++++++++------ study/src/test/java/study/IOStreamTest.java | 21 ++++++++++++++----- 8 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 study/src/main/resources/static/index.html diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..5ec03b4a35 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -12,7 +12,7 @@ public class GreetingController { @GetMapping("/") public String index() { - return "index"; + return "index.html"; } /** @@ -25,12 +25,12 @@ public String cacheControl(final HttpServletResponse response) { .cachePrivate() .getHeaderValue(); response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl); - return "index"; + return "index.html"; } @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..e413dd2926 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,19 @@ 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) { + CacheControl cacheControl = CacheControl.noCache().cachePrivate(); + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping(cacheControl, "/**"); + 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..db9507bb93 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,19 @@ 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() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean + = new FilterRegistrationBean<>( new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag"); + filterRegistrationBean.addUrlPatterns("/resources/*"); + return filterRegistrationBean; + } } 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..c7addcdab4 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -1,7 +1,9 @@ package cache.com.example.version; +import java.time.Duration; 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; @@ -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 e3503a5fb9..8b74bdfd88 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -8,3 +8,6 @@ server: threads: min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 10 diff --git a/study/src/main/resources/static/index.html b/study/src/main/resources/static/index.html new file mode 100644 index 0000000000..46cbef0f24 --- /dev/null +++ b/study/src/main/resources/static/index.html @@ -0,0 +1,10 @@ + + + + Getting Started: Serving Web Content + + + +Hello, World! + + diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..d37c48f16f 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,10 +1,13 @@ package study; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.nio.file.Path; -import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -28,7 +31,7 @@ class FileTest { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + final String actual = getClass().getClassLoader().getResource(fileName).getPath(); assertThat(actual).endsWith(fileName); } @@ -40,14 +43,15 @@ class FileTest { * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; + final URL url = getClass().getClassLoader().getResource(fileName); // todo - final Path path = null; + final Path path = new File(url.getFile()).toPath(); // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..ff36c86863 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -53,6 +53,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); @@ -78,6 +79,7 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); @@ -96,6 +98,8 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + } verify(outputStream, atLeastOnce()).close(); } @@ -128,7 +132,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -148,6 +152,8 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } @@ -169,12 +175,12 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -197,15 +203,20 @@ class InputStreamReader_학습_테스트 { * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); final StringBuilder actual = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + actual.append(line).append("\r\n"); + } assertThat(actual).hasToString(emoji); }