diff --git a/build.gradle b/build.gradle index 25dd8fcb7..c7b2ba3f1 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,12 @@ dependencies { implementation 'ch.qos.logback:logback-classic:1.2.3' testImplementation 'org.assertj:assertj-core:3.16.1' + testImplementation 'org.mockito:mockito-core:4.11.0' + testImplementation('org.mockito:mockito-junit-jupiter:4.11.0') { + exclude group: 'org.junit.jupiter' + exclude group: 'org.junit.platform' + } + } diff --git a/src/main/java/db/Database.java b/src/main/java/app/db/Database.java similarity index 91% rename from src/main/java/db/Database.java rename to src/main/java/app/db/Database.java index b7a2cdc74..820a1050b 100644 --- a/src/main/java/db/Database.java +++ b/src/main/java/app/db/Database.java @@ -1,6 +1,6 @@ -package db; +package app.db; -import model.User; +import app.model.User; import java.util.Collection; import java.util.HashMap; diff --git a/src/main/java/app/handler/RegisterHandlerImpl.java b/src/main/java/app/handler/RegisterHandlerImpl.java new file mode 100644 index 000000000..e3afec809 --- /dev/null +++ b/src/main/java/app/handler/RegisterHandlerImpl.java @@ -0,0 +1,25 @@ +package app.handler; + +import app.db.Database; +import app.model.User; +import http.HttpMethod; +import http.request.HttpRequest; +import web.handler.DynamicViewHandler; +import web.response.ViewResponse; + +public class RegisterHandlerImpl extends DynamicViewHandler { + public RegisterHandlerImpl() { + super(HttpMethod.GET, + "/create"); + } + + @Override + public ViewResponse handle(HttpRequest request) { + String userId = request.getQueryValue("userId"); + String password = request.getQueryValue("password"); + String name = request.getQueryValue("name"); + String email = request.getQueryValue("email"); + Database.addUser(new User(userId, password, name, email)); + return ViewResponse.of("/login"); + } +} diff --git a/src/main/java/model/User.java b/src/main/java/app/model/User.java similarity index 97% rename from src/main/java/model/User.java rename to src/main/java/app/model/User.java index b7abb7304..4010a8432 100644 --- a/src/main/java/model/User.java +++ b/src/main/java/app/model/User.java @@ -1,4 +1,4 @@ -package model; +package app.model; public class User { private String userId; diff --git a/src/main/java/webserver/WebServer.java b/src/main/java/bootstrap/WebServer.java similarity index 53% rename from src/main/java/webserver/WebServer.java rename to src/main/java/bootstrap/WebServer.java index eba161289..9c9926c84 100644 --- a/src/main/java/webserver/WebServer.java +++ b/src/main/java/bootstrap/WebServer.java @@ -1,14 +1,20 @@ -package webserver; +package bootstrap; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import config.DependencyLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import web.dispatch.ConnectionHandler; public class WebServer { private static final Logger logger = LoggerFactory.getLogger(WebServer.class); private static final int DEFAULT_PORT = 8080; + private static final DependencyLoader LOADER = new DependencyLoader(); + private static final ExecutorService executor = Executors.newFixedThreadPool(32); public static void main(String args[]) throws Exception { int port = 0; @@ -25,8 +31,15 @@ public static void main(String args[]) throws Exception { // 클라이언트가 연결될때까지 대기한다. Socket connection; while ((connection = listenSocket.accept()) != null) { - Thread thread = new Thread(new RequestHandler(connection)); - thread.start(); + Socket singleConnection = connection; + executor.submit(() -> { + ConnectionHandler connectionHandler = new ConnectionHandler(LOADER.dispatcher, + LOADER.exceptionHandlerMapping, + LOADER.httpResponseConverter, + LOADER.httpRequestConverter, + singleConnection); + connectionHandler.run(); + }); } } } diff --git a/src/main/java/config/AppConfig.java b/src/main/java/config/AppConfig.java new file mode 100644 index 000000000..ca8fd9e69 --- /dev/null +++ b/src/main/java/config/AppConfig.java @@ -0,0 +1,94 @@ +package config; + +import app.handler.RegisterHandlerImpl; +import exception.ExceptionHandlerMapping; +import exception.handler.ErrorExceptionHandler; +import exception.handler.ServiceExceptionHandler; +import exception.handler.UnhandledErrorHandler; +import http.request.HttpBufferedReaderRequestConverter; +import http.request.HttpRequestConverter; +import http.response.HttpBufferedStreamResponseConverter; +import http.response.HttpResponseConverter; +import web.dispatch.Dispatcher; +import web.handler.StaticContentHandler; +import web.handler.WebHandler; +import web.posthandler.StaticContentResponseHandler; +import web.posthandler.ViewResponseHandler; +import web.posthandler.WebHandlerResponseHandler; + +import java.util.List; + +public class AppConfig { + //Http + public HttpBufferedReaderRequestConverter httpBufferedReaderRequestConverter(){ + return new HttpBufferedReaderRequestConverter(); + } + + public HttpBufferedStreamResponseConverter httpBufferedStreamResponseConverter(){ + return new HttpBufferedStreamResponseConverter(); + } + + public HttpRequestConverter httpRequestConverter(){ + return httpBufferedReaderRequestConverter(); + } + public HttpResponseConverter httpResponseConverter(){ + return httpBufferedStreamResponseConverter(); + } + + + //Web + public Dispatcher wasServlet(){ + return new Dispatcher( + webHandlerList(), + webHandlerResponseHandlerList() + ); + } + + private List webHandlerList(){ + return List.of( + staticContentHandler(), + registerHandlerImpl() + ); + } + private RegisterHandlerImpl registerHandlerImpl(){ + return new RegisterHandlerImpl(); + } + + private List webHandlerResponseHandlerList(){ + return List.of( + staticContentResponseHandler(), + viewResponseHandler() + ); + } + private StaticContentHandler staticContentHandler(){ + return new StaticContentHandler(); + } + private ViewResponseHandler viewResponseHandler(){ + return new ViewResponseHandler(); + } + private StaticContentResponseHandler staticContentResponseHandler(){ + return new StaticContentResponseHandler(); + } + + + //Exception + public ExceptionHandlerMapping exceptionHandlerMapping(){ + return new ExceptionHandlerMapping( + List.of( + serviceExceptionHandler(), + errorExceptionHandler(), + unhandledErrorHandler() + ) + ); + } + + private ServiceExceptionHandler serviceExceptionHandler(){ + return new ServiceExceptionHandler(); + } + private UnhandledErrorHandler unhandledErrorHandler(){ + return new UnhandledErrorHandler(); + } + private ErrorExceptionHandler errorExceptionHandler(){ + return new ErrorExceptionHandler(); + } +} diff --git a/src/main/java/config/DependencyLoader.java b/src/main/java/config/DependencyLoader.java new file mode 100644 index 000000000..32905c171 --- /dev/null +++ b/src/main/java/config/DependencyLoader.java @@ -0,0 +1,23 @@ +package config; + +import exception.ExceptionHandlerMapping; +import http.request.HttpRequestConverter; +import http.response.HttpResponseConverter; +import web.dispatch.Dispatcher; + +public class DependencyLoader { + private final AppConfig appConfig; + + public final HttpRequestConverter httpRequestConverter; + public final HttpResponseConverter httpResponseConverter; + public final ExceptionHandlerMapping exceptionHandlerMapping; + public final Dispatcher dispatcher; + + public DependencyLoader(){ + this.appConfig = new AppConfig(); + this.httpRequestConverter = appConfig.httpRequestConverter(); + this.httpResponseConverter = appConfig.httpResponseConverter(); + this.exceptionHandlerMapping = appConfig.exceptionHandlerMapping(); + this.dispatcher = appConfig.wasServlet(); + } +} diff --git a/src/main/java/config/VariableConfig.java b/src/main/java/config/VariableConfig.java new file mode 100644 index 000000000..239bbc662 --- /dev/null +++ b/src/main/java/config/VariableConfig.java @@ -0,0 +1,9 @@ +package config; + +import java.util.List; + +public class VariableConfig { + public static final List STATIC_RESOURCE_ROOTS = List.of( + "./src/main/resources", + "./src/main/resources/static"); +} diff --git a/src/main/java/exception/ErrorCode.java b/src/main/java/exception/ErrorCode.java new file mode 100644 index 000000000..e64d63116 --- /dev/null +++ b/src/main/java/exception/ErrorCode.java @@ -0,0 +1,44 @@ +package exception; + + +import http.HttpStatus; + +public enum ErrorCode { + /* Internal Error */ + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500_INTERNAL", "서버 내부 오류가 발생했습니다."), + + /* Request Error */ + INVALID_INPUT(HttpStatus.BAD_REQUEST, "400_INVALID_INPUT", "입력 값이 올바르지 않습니다."), + MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "400_MISSING_PARAM", "필수 파라미터가 누락되었습니다."), + VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "400_VALIDATION_FAIL", "유효성 검증에 실패했습니다."), + + FORBIDDEN(HttpStatus.FORBIDDEN, "403_FORBIDDEN", "권한이 없습니다."), + + NO_SUCH_RESOURCE(HttpStatus.NOT_FOUND, "404_NO_SUCH_RESOURCE", "요청한 리소스를 찾을 수 없습니다."), + + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "405_METHOD_NOT_ALLOWED", "허용되지 않은 HTTP 메서드입니다."), + ; + + + private final HttpStatus status; + private final String code; + private final String message; + + public HttpStatus getStatus() { + return status; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + ErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/exception/ErrorException.java b/src/main/java/exception/ErrorException.java new file mode 100644 index 000000000..0991599da --- /dev/null +++ b/src/main/java/exception/ErrorException.java @@ -0,0 +1,26 @@ +package exception; + +public class ErrorException extends RuntimeException { + private final ErrorCode errorCode; + private final Throwable throwable; + + public ErrorException(String message) { + super(message); + this.errorCode = ErrorCode.INTERNAL_ERROR; + this.throwable = null; + } + + public ErrorException(String message, Throwable t) { + super(message); + this.errorCode = ErrorCode.INTERNAL_ERROR; + this.throwable = t; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public Throwable getThrowable() { + return throwable; + } +} diff --git a/src/main/java/exception/ExceptionHandler.java b/src/main/java/exception/ExceptionHandler.java new file mode 100644 index 000000000..9f82663dd --- /dev/null +++ b/src/main/java/exception/ExceptionHandler.java @@ -0,0 +1,8 @@ +package exception; + +import java.net.Socket; + +public interface ExceptionHandler { + boolean support(Throwable e); + void handle(Throwable e, Socket connection); +} diff --git a/src/main/java/webserver/exception/ExceptionHandlerMapping.java b/src/main/java/exception/ExceptionHandlerMapping.java similarity index 50% rename from src/main/java/webserver/exception/ExceptionHandlerMapping.java rename to src/main/java/exception/ExceptionHandlerMapping.java index 802a8e1c9..bf186b9ec 100644 --- a/src/main/java/webserver/exception/ExceptionHandlerMapping.java +++ b/src/main/java/exception/ExceptionHandlerMapping.java @@ -1,4 +1,4 @@ -package webserver.exception; +package exception; import java.net.Socket; import java.util.List; @@ -10,9 +10,9 @@ public ExceptionHandlerMapping (List handlers){ this.handlers = handlers; } - public void handle(Exception e, Socket connection){ - ExceptionHandler handlerAdaptor = handlers.stream().filter(handler -> handler.support(e)).findFirst().orElseThrow(); - handlerAdaptor.handle(e, connection); + public void handle(Throwable e, Socket connection){ + ExceptionHandler handler = handlers.stream().filter(h -> h.support(e)).findFirst().orElseThrow(); + handler.handle(e, connection); } } diff --git a/src/main/java/exception/ServiceException.java b/src/main/java/exception/ServiceException.java new file mode 100644 index 000000000..4c525207a --- /dev/null +++ b/src/main/java/exception/ServiceException.java @@ -0,0 +1,19 @@ +package exception; + +public class ServiceException extends RuntimeException { + private final ErrorCode errorCode; + + public ServiceException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ServiceException(ErrorCode errorCode, String customMsg) { + super((customMsg == null || customMsg.isBlank()) ? errorCode.getMessage() : customMsg); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/exception/handler/ErrorExceptionHandler.java b/src/main/java/exception/handler/ErrorExceptionHandler.java new file mode 100644 index 000000000..79bb821a9 --- /dev/null +++ b/src/main/java/exception/handler/ErrorExceptionHandler.java @@ -0,0 +1,66 @@ +package exception.handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import exception.ErrorCode; +import exception.ErrorException; +import exception.ExceptionHandler; +import http.HttpStatus; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class ErrorExceptionHandler implements ExceptionHandler { + private final Logger logger = LoggerFactory.getLogger(ErrorException.class); + @Override + public boolean support(Throwable e) { + return e instanceof ErrorException; + } + + @Override + public void handle(Throwable t, Socket connection) { + ErrorException error = (ErrorException) t; + logger.debug(error.getThrowable().toString()); + ErrorCode errorCode = error.getErrorCode(); + HttpStatus status = errorCode.getStatus(); + + String body = toJson(errorCode.getCode(), error.getMessage()); + byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + + StringBuilder sb = new StringBuilder(); + sb.append("HTTP/1.1 ") + .append(status.getCode()).append("\r\n"); + + sb.append("Date: ") + .append(DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())) + .append("\r\n"); + sb.append("Server: be-was\r\n"); + sb.append("Connection: close\r\n"); + + sb.append("Content-Type: application/json; charset=utf-8\r\n"); + sb.append("Content-Length: ").append(bodyBytes.length).append("\r\n"); + sb.append("\r\n"); + + try (OutputStream out = connection.getOutputStream()) { + out.write(sb.toString().getBytes(StandardCharsets.US_ASCII)); + out.write(bodyBytes); + out.flush(); + } catch (IOException io) { + throw new RuntimeException(io); + } + } + + private String toJson(String code, String message) { + String safeMsg = message == null ? "" : message + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r"); + + return "{\"code\":\"" + code + "\",\"message\":\"" + safeMsg + "\"}"; + } +} diff --git a/src/main/java/exception/handler/ServiceExceptionHandler.java b/src/main/java/exception/handler/ServiceExceptionHandler.java new file mode 100644 index 000000000..3a5e277ed --- /dev/null +++ b/src/main/java/exception/handler/ServiceExceptionHandler.java @@ -0,0 +1,62 @@ +package exception.handler; + +import exception.ErrorCode; +import exception.ServiceException; +import exception.ExceptionHandler; +import http.HttpStatus; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class ServiceExceptionHandler implements ExceptionHandler { + @Override + public boolean support(Throwable e) { + return e instanceof ServiceException; + } + + @Override + public void handle(Throwable t, Socket connection) { + ServiceException error = (ServiceException) t; + ErrorCode errorCode = error.getErrorCode(); + HttpStatus status = errorCode.getStatus(); + + String body = toJson(errorCode.getCode(), error.getMessage()); + byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + + StringBuilder sb = new StringBuilder(); + sb.append("HTTP/1.1 ") + .append(status.getCode()).append("\r\n"); + + sb.append("Date: ") + .append(DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())) + .append("\r\n"); + sb.append("Server: be-was\r\n"); + sb.append("Connection: close\r\n"); + + sb.append("Content-Type: application/json; charset=utf-8\r\n"); + sb.append("Content-Length: ").append(bodyBytes.length).append("\r\n"); + sb.append("\r\n"); + + try (OutputStream out = connection.getOutputStream()) { + out.write(sb.toString().getBytes(StandardCharsets.US_ASCII)); + out.write(bodyBytes); + out.flush(); + } catch (IOException io) { + throw new RuntimeException(io); + } + } + + private String toJson(String code, String message) { + String safeMsg = message == null ? "" : message + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r"); + + return "{\"code\":\"" + code + "\",\"message\":\"" + safeMsg + "\"}"; + } +} diff --git a/src/main/java/exception/handler/UnhandledErrorHandler.java b/src/main/java/exception/handler/UnhandledErrorHandler.java new file mode 100644 index 000000000..ad3a4bb00 --- /dev/null +++ b/src/main/java/exception/handler/UnhandledErrorHandler.java @@ -0,0 +1,42 @@ +package exception.handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import exception.ExceptionHandler; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class UnhandledErrorHandler implements ExceptionHandler { + private final Logger logger = LoggerFactory.getLogger(UnhandledErrorHandler.class); + @Override + public boolean support(Throwable e) { + return e instanceof Throwable; + } + + @Override + public void handle(Throwable t, Socket connection) { + String body = "{\"code\":\"500_UNHANDLED\",\"message\":\"서버 내부 오류가 발생했습니다.\"}"; + byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + logger.debug(t.getMessage()); + String header = + "HTTP/1.1 500 Internal Server Error\r\n" + + "Date: " + DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now()) + "\r\n" + + "Server: be-was\r\n" + + "Connection: close\r\n" + + "Content-Type: application/json; charset=utf-8\r\n" + + "Content-Length: " + bodyBytes.length + "\r\n" + + "\r\n"; + + try (OutputStream out = connection.getOutputStream()) { + out.write(header.getBytes(StandardCharsets.US_ASCII)); + out.write(bodyBytes); + out.flush(); + } catch (IOException ignored) { + } + } +} diff --git a/src/main/java/webserver/http/HttpMethod.java b/src/main/java/http/HttpMethod.java similarity index 76% rename from src/main/java/webserver/http/HttpMethod.java rename to src/main/java/http/HttpMethod.java index 68f9cdac2..a9fdc6dd8 100644 --- a/src/main/java/webserver/http/HttpMethod.java +++ b/src/main/java/http/HttpMethod.java @@ -1,4 +1,4 @@ -package webserver.http; +package http; public enum HttpMethod { GET, diff --git a/src/main/java/webserver/http/HttpStatus.java b/src/main/java/http/HttpStatus.java similarity index 80% rename from src/main/java/webserver/http/HttpStatus.java rename to src/main/java/http/HttpStatus.java index 46f7c71a0..af8b95af0 100644 --- a/src/main/java/webserver/http/HttpStatus.java +++ b/src/main/java/http/HttpStatus.java @@ -1,4 +1,4 @@ -package webserver.http; +package http; public enum HttpStatus { OK(200), @@ -8,7 +8,10 @@ public enum HttpStatus { BAD_REQUEST(400), UNAUTHORIZED(401), + FORBIDDEN(403), NOT_FOUND(404), + METHOD_NOT_ALLOWED(405), + CONFLICT(409), INTERNAL_SERVER_ERROR(500); diff --git a/src/main/java/webserver/http/request/HttpBufferedReaderRequestConverter.java b/src/main/java/http/request/HttpBufferedReaderRequestConverter.java similarity index 75% rename from src/main/java/webserver/http/request/HttpBufferedReaderRequestConverter.java rename to src/main/java/http/request/HttpBufferedReaderRequestConverter.java index 86b574ac4..5abaf4f24 100644 --- a/src/main/java/webserver/http/request/HttpBufferedReaderRequestConverter.java +++ b/src/main/java/http/request/HttpBufferedReaderRequestConverter.java @@ -1,4 +1,4 @@ -package webserver.http.request; +package http.request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,12 +11,13 @@ public class HttpBufferedReaderRequestConverter implements HttpRequestConverter private static final Logger logger = LoggerFactory.getLogger(HttpBufferedReaderRequestConverter.class); public HttpRequest parseRequest(Socket connection){ - try (InputStream in = connection.getInputStream()) { + try { + InputStream in = connection.getInputStream(); InputStreamReader reader = new InputStreamReader(in); BufferedReader bufferedReader = new BufferedReader(reader); String firstLine = bufferedReader.readLine(); HttpRequest request = HttpRequest.from(firstLine); - + request.setRequestAddress(connection.getInetAddress()); while (bufferedReader.ready()) { String line = bufferedReader.readLine().strip(); @@ -24,10 +25,12 @@ public HttpRequest parseRequest(Socket connection){ int idx = line.indexOf(':'); //TODO: idx == -1 일 경우 Throw Exception - request.setHeader(line.substring(0, idx), line.substring(idx)); - logger.debug("New Header Added:{} - {}", line.substring(0, idx), line.substring(idx)); + request.setHeader(line.substring(0, idx).strip(), line.substring(idx+1).strip()); +// logger.debug("New Header Added:{} - {}", line.substring(0, idx), line.substring(idx)); } + //TODO: Body 파싱 추가 + return request; } catch (IOException e) { diff --git a/src/main/java/webserver/http/request/HttpRequest.java b/src/main/java/http/request/HttpRequest.java similarity index 68% rename from src/main/java/webserver/http/request/HttpRequest.java rename to src/main/java/http/request/HttpRequest.java index c59b5c60c..c01976186 100644 --- a/src/main/java/webserver/http/request/HttpRequest.java +++ b/src/main/java/http/request/HttpRequest.java @@ -1,9 +1,12 @@ -package webserver.http.request; +package http.request; -import webserver.http.HttpMethod; +import exception.ErrorCode; +import exception.ErrorException; +import exception.ServiceException; +import http.HttpMethod; import java.io.UnsupportedEncodingException; -import java.net.InetSocketAddress; +import java.net.InetAddress; import java.net.URI; import java.net.URLDecoder; import java.util.HashMap; @@ -11,22 +14,22 @@ import java.util.Map; public class HttpRequest { - private HttpMethod method; + private final HttpMethod method; + private final Map headers; + private final URI uri; private String httpVersion; private String contentType; - private URI uri; - private Map headers; private Map queryMap; private byte[] body; - private InetSocketAddress requestAddress; + private InetAddress requestAddress; public String getHeader(String key){ - return headers.get(key); + return headers.get(key.toLowerCase()); } public void setHeader(String key, String value){ - headers.put(key, value); + headers.put(key.toLowerCase(), value); } public List getHeaders(){ @@ -52,8 +55,6 @@ public HttpMethod getMethod(){ return this.method; } - - private HttpRequest (){} private HttpRequest (HttpMethod method, String target, String httpVersion) { @@ -64,12 +65,26 @@ private HttpRequest (HttpMethod method, } + public void setRequestAddress(InetAddress requestAddress) { + this.requestAddress = requestAddress; + } + + public InetAddress getRequestAddress() { + return requestAddress; + } + public static HttpRequest from(String requestLine){ String[] parts = requestLine.split(" "); - return new HttpRequest( - HttpMethod.valueOf(parts[0].strip().toUpperCase()), - parts[1].strip(), - parts[2].strip()); + try { + return new HttpRequest( + HttpMethod.valueOf(parts[0].strip().toUpperCase()), + parts[1].strip(), + parts[2].strip()); + } catch (IllegalArgumentException e) { + throw new ServiceException(ErrorCode.METHOD_NOT_ALLOWED); + } catch (NullPointerException e){ + throw new ErrorException("Http method error"); + } } diff --git a/src/main/java/webserver/http/request/HttpRequestConverter.java b/src/main/java/http/request/HttpRequestConverter.java similarity index 78% rename from src/main/java/webserver/http/request/HttpRequestConverter.java rename to src/main/java/http/request/HttpRequestConverter.java index e4e717860..1aa010194 100644 --- a/src/main/java/webserver/http/request/HttpRequestConverter.java +++ b/src/main/java/http/request/HttpRequestConverter.java @@ -1,4 +1,4 @@ -package webserver.http.request; +package http.request; import java.net.Socket; diff --git a/src/main/java/http/response/HttpBufferedStreamResponseConverter.java b/src/main/java/http/response/HttpBufferedStreamResponseConverter.java new file mode 100644 index 000000000..7ad59fbb2 --- /dev/null +++ b/src/main/java/http/response/HttpBufferedStreamResponseConverter.java @@ -0,0 +1,48 @@ +package http.response; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class HttpBufferedStreamResponseConverter implements HttpResponseConverter { + + @Override + public boolean support() { + return true; + } + + @Override + public void sendResponse(HttpResponse response, Socket connection) { + try { + OutputStream raw = connection.getOutputStream(); + BufferedOutputStream out = new BufferedOutputStream(raw); + + byte[] body = response.getBody(); + if (body == null) body = new byte[0]; + + String statusLine = + "HTTP/1.1 " + response.getStatus().getCode() + " " + response.getStatus() + "\r\n"; + out.write(statusLine.getBytes(StandardCharsets.ISO_8859_1)); + + for (String key : response.getHeaders()) { + List values = response.getHeader(key); + for (String value : values) { + String headerLine = key + ": " + value + "\r\n"; + out.write(headerLine.getBytes(StandardCharsets.ISO_8859_1)); + } + } + + out.write("\r\n".getBytes(StandardCharsets.ISO_8859_1)); + if (body.length > 0) { + out.write(body); + } + out.flush(); + + } catch (IOException e) { + throw new IllegalStateException("Failed to send HTTP response", e); + } + } +} diff --git a/src/main/java/http/response/HttpResponse.java b/src/main/java/http/response/HttpResponse.java new file mode 100644 index 000000000..9e11f9620 --- /dev/null +++ b/src/main/java/http/response/HttpResponse.java @@ -0,0 +1,87 @@ +package http.response; + +import http.HttpStatus; + +import java.io.File; +import java.net.URLConnection; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HttpResponse { + private final HttpStatus status; + private final Map> headers; + private byte[] body; + + private HttpResponse (HttpStatus status){ + this.status = status; + this.headers = new HashMap<>(); + setHeader("Date", DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now())); + setHeader("Server", "be-was"); + setHeader("Connection", "close"); + } + + public static HttpResponse of (HttpStatus status){ + return new HttpResponse(status); + } + + public HttpStatus getStatus() { + return status; + } + + public List getHeaders() { + return headers.keySet().stream().toList(); + } + + public List getHeader(String key){ + List values = headers.get(key.toLowerCase()); + return values != null ? values : new ArrayList<>(); + } + + public void addHeader(String key, String value){ + if(!headers.containsKey(key.toLowerCase())) + headers.put(key.toLowerCase(), new ArrayList<>()); + headers.get(key.toLowerCase()).add(value); + } + + public void setHeader(String key, String value){ + headers.put(key.toLowerCase(), new ArrayList<>()); + headers.get(key.toLowerCase()).add(value); + } + + public byte[] getBody() { + return body; + } + + public void setBody(byte[] body) { + this.body = body; + setHeader("Content-Length", String.valueOf(body.length)); + } + + public void setBody(File file, byte[] body) { + this.body = body; + setHeader("Content-Type", guessContentType(file)); + setHeader("Content-Length", String.valueOf(body.length)); + } + + private String guessContentType(File file) { + String byName = URLConnection.guessContentTypeFromName(file.getName()); + if (byName != null) return byName; + + String name = file.getName().toLowerCase(); + if (name.endsWith(".html") || name.endsWith(".htm")) return "text/html; charset=utf-8"; + if (name.endsWith(".css")) return "text/css; charset=utf-8"; + if (name.endsWith(".js")) return "application/javascript; charset=utf-8"; + if (name.endsWith(".json")) return "application/json; charset=utf-8"; + if (name.endsWith(".png")) return "image/png"; + if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg"; + if (name.endsWith(".gif")) return "image/gif"; + if (name.endsWith(".svg")) return "image/svg+xml"; + if (name.endsWith(".txt")) return "text/plain; charset=utf-8"; + + return "application/octet-stream"; + } +} \ No newline at end of file diff --git a/src/main/java/webserver/http/response/HttpResponseConverter.java b/src/main/java/http/response/HttpResponseConverter.java similarity index 82% rename from src/main/java/webserver/http/response/HttpResponseConverter.java rename to src/main/java/http/response/HttpResponseConverter.java index 328c2d1b8..e72ce0e62 100644 --- a/src/main/java/webserver/http/response/HttpResponseConverter.java +++ b/src/main/java/http/response/HttpResponseConverter.java @@ -1,4 +1,4 @@ -package webserver.http.response; +package http.response; import java.net.Socket; diff --git a/src/main/java/web/dispatch/ConnectionHandler.java b/src/main/java/web/dispatch/ConnectionHandler.java new file mode 100644 index 000000000..0ea2ae576 --- /dev/null +++ b/src/main/java/web/dispatch/ConnectionHandler.java @@ -0,0 +1,51 @@ +package web.dispatch; + +import exception.ExceptionHandlerMapping; +import http.response.HttpResponseConverter; +import http.request.HttpRequestConverter; +import http.request.HttpRequest; +import http.response.HttpResponse; + +import java.net.Socket; + +public class ConnectionHandler implements Runnable{ + private final Socket connection; + private final HttpRequestConverter requestConverter; + private final HttpResponseConverter responseConverter; + private final ExceptionHandlerMapping exceptionHandlerMapping; + private final Dispatcher dispatcher; + + public ConnectionHandler(Dispatcher dispatcher, + ExceptionHandlerMapping exceptionHandlerMapping, + HttpResponseConverter responseConverter, + HttpRequestConverter requestConverter, + Socket connection) { + this.dispatcher = dispatcher; + this.exceptionHandlerMapping = exceptionHandlerMapping; + this.responseConverter = responseConverter; + this.requestConverter = requestConverter; + this.connection = connection; + } + + @Override + public void run() { + + try { + + HttpRequest request = requestConverter.parseRequest(connection); + HttpResponse response = dispatcher.handle(request); + responseConverter.sendResponse(response, connection); + + } catch (Exception e){ + /** + * TODO: + * ExceptionHandler 또한 HttpResponse를 반환하게 하고 + * finally에 `responseConverter.sendResponse(response, connection);` 를 넣어 + * socket에 write를 하는 포인트를 단일 포인트로 관리 + */ + exceptionHandlerMapping.handle(e, connection); + } finally { + try { connection.close(); } catch (Exception ignore) {} + } + } +} diff --git a/src/main/java/webserver/web/WasServlet.java b/src/main/java/web/dispatch/Dispatcher.java similarity index 53% rename from src/main/java/webserver/web/WasServlet.java rename to src/main/java/web/dispatch/Dispatcher.java index 08343c3a5..0004d4f99 100644 --- a/src/main/java/webserver/web/WasServlet.java +++ b/src/main/java/web/dispatch/Dispatcher.java @@ -1,19 +1,25 @@ -package webserver.web; +package web.dispatch; -import webserver.http.HttpMethod; -import webserver.http.request.HttpRequest; -import webserver.http.response.HttpResponse; -import webserver.web.handler.WebHandler; -import webserver.web.handler.response.WebHandlerResponse; -import webserver.web.handler.response.handler.WebHandlerResponseHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import exception.ErrorCode; +import exception.ErrorException; +import exception.ServiceException; +import http.HttpMethod; +import http.request.HttpRequest; +import http.response.HttpResponse; +import web.handler.WebHandler; +import web.response.WebHandlerResponse; +import web.posthandler.WebHandlerResponseHandler; import java.util.*; -public class WasServlet { +public class Dispatcher { private final Map> handlerMapping; private final List responseHandlerList; + private final Logger logger = LoggerFactory.getLogger(Dispatcher.class); - public WasServlet(List handlerMapping, List responseHandlerList) { + public Dispatcher(List handlerMapping, List responseHandlerList) { this.responseHandlerList = responseHandlerList; this.handlerMapping = new HashMap<>(); Arrays.stream(HttpMethod.values()).forEach(m -> this.handlerMapping.put(m, new ArrayList<>())); @@ -22,13 +28,16 @@ public WasServlet(List handlerMapping, List h.checkEndpoint(method, request.getPath())) - .findFirst().orElseThrow(); //TODO: Add Handler not match exception -> no content + .findFirst().orElseThrow(()-> new ServiceException(ErrorCode.NO_SUCH_RESOURCE)); WebHandlerResponse response = handler.handle(request); + WebHandlerResponseHandler responseHandler = responseHandlerList.stream() .filter(rh -> rh.supports(response)) - .findFirst().orElseThrow();//TODO: Add Response handler not match exception -> internal error + .findFirst().orElseThrow(()-> new ErrorException("Post handler not exists")); return responseHandler.handle(response); } } diff --git a/src/main/java/web/handler/DynamicViewHandler.java b/src/main/java/web/handler/DynamicViewHandler.java new file mode 100644 index 000000000..3f4602d9e --- /dev/null +++ b/src/main/java/web/handler/DynamicViewHandler.java @@ -0,0 +1,31 @@ +package web.handler; + +import http.HttpMethod; +import http.request.HttpRequest; +import web.response.ViewResponse; + +public abstract class DynamicViewHandler implements WebHandler{ + protected final HttpMethod method; + protected final String path; + + protected DynamicViewHandler(HttpMethod method, String path) { + this.method = method; + this.path = path; + } + + public String getPath() { + return path; + } + + @Override + public HttpMethod getMethod() { + return method; + } + + @Override + public boolean checkEndpoint(HttpMethod method, String path) { + return this.method.equals(method) && this.path.equals(path); + } + + public abstract ViewResponse handle(HttpRequest request); +} diff --git a/src/main/java/web/handler/StaticContentHandler.java b/src/main/java/web/handler/StaticContentHandler.java new file mode 100644 index 000000000..a999aa03a --- /dev/null +++ b/src/main/java/web/handler/StaticContentHandler.java @@ -0,0 +1,37 @@ +package web.handler; + +import config.VariableConfig; +import http.HttpMethod; +import http.request.HttpRequest; +import web.response.WebHandlerResponse; +import web.response.StaticContentResponse; + +import java.io.File; +import java.util.List; + +/** + * 정 + */ +public class StaticContentHandler implements WebHandler{ + private final List staticResourceRoots = VariableConfig.STATIC_RESOURCE_ROOTS; + @Override + public HttpMethod getMethod() { + return HttpMethod.GET; + } + + @Override + public boolean checkEndpoint(HttpMethod method, String path) { + if(!method.equals(HttpMethod.GET)) return false; + return staticResourceRoots.stream().anyMatch(root ->{ + File requestedFile = new File(root + path); + String indexFilePath = path + (path.endsWith("/") ? "index.html" : "/index.html"); + File indexFile = new File(root + indexFilePath); + return (requestedFile.exists() && requestedFile.isFile()) || (indexFile.exists() && indexFile.isFile()); + }); + } + + @Override + public WebHandlerResponse handle(HttpRequest request) { + return StaticContentResponse.of(request.getPath()); + } +} diff --git a/src/main/java/webserver/web/handler/WebHandler.java b/src/main/java/web/handler/WebHandler.java similarity index 51% rename from src/main/java/webserver/web/handler/WebHandler.java rename to src/main/java/web/handler/WebHandler.java index 8884f0410..33fced66b 100644 --- a/src/main/java/webserver/web/handler/WebHandler.java +++ b/src/main/java/web/handler/WebHandler.java @@ -1,8 +1,8 @@ -package webserver.web.handler; +package web.handler; -import webserver.http.HttpMethod; -import webserver.http.request.HttpRequest; -import webserver.web.handler.response.WebHandlerResponse; +import http.HttpMethod; +import http.request.HttpRequest; +import web.response.WebHandlerResponse; public interface WebHandler { HttpMethod getMethod(); diff --git a/src/main/java/web/posthandler/StaticContentResponseHandler.java b/src/main/java/web/posthandler/StaticContentResponseHandler.java new file mode 100644 index 000000000..16b6eabef --- /dev/null +++ b/src/main/java/web/posthandler/StaticContentResponseHandler.java @@ -0,0 +1,52 @@ +package web.posthandler; + +import config.VariableConfig; +import exception.ErrorException; +import http.HttpStatus; +import http.response.HttpResponse; +import web.response.WebHandlerResponse; +import web.response.StaticContentResponse; + +import java.io.*; +import java.util.List; +import java.util.Optional; + +public class StaticContentResponseHandler implements WebHandlerResponseHandler{ + private final List staticResourceRoots = VariableConfig.STATIC_RESOURCE_ROOTS; + @Override + public boolean supports(WebHandlerResponse response) { + return response instanceof StaticContentResponse; + } + + @Override + public HttpResponse handle(WebHandlerResponse handlerResponse) { + StaticContentResponse staticResponse = (StaticContentResponse) handlerResponse; + String path = staticResponse.getPath(); + + File file = resolveStaticFile(path) + .orElseThrow(() -> new ErrorException("Static content path Error")); + + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) { + byte[] body = in.readAllBytes(); + + HttpResponse httpResponse = HttpResponse.of(HttpStatus.OK); + httpResponse.setBody(file, body); + return httpResponse; + + } catch (IOException e) { + throw new ErrorException("Static content Read IO-Error"); + } + } + + private Optional resolveStaticFile(String path){ + for(String root : staticResourceRoots){ + File requestedFile = new File(root + path); + if(requestedFile.exists() && requestedFile.isFile()) return Optional.of(requestedFile); + + String indexFilePath = path + (path.endsWith("/") ? "index.html" : "/index.html"); + File indexFile = new File(root + indexFilePath); + if(indexFile.exists() && indexFile.isFile()) return Optional.of(indexFile); + } + return Optional.empty(); + } +} diff --git a/src/main/java/web/posthandler/ViewResponseHandler.java b/src/main/java/web/posthandler/ViewResponseHandler.java new file mode 100644 index 000000000..9ad539d04 --- /dev/null +++ b/src/main/java/web/posthandler/ViewResponseHandler.java @@ -0,0 +1,55 @@ +package web.posthandler; + +import config.VariableConfig; +import exception.ErrorException; +import http.HttpStatus; +import http.response.HttpResponse; +import web.response.WebHandlerResponse; +import web.response.ViewResponse; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +public class ViewResponseHandler implements WebHandlerResponseHandler { + private final List staticResourceRoots = VariableConfig.STATIC_RESOURCE_ROOTS; + @Override + public boolean supports(WebHandlerResponse response) { + return response instanceof ViewResponse; + } + + @Override + public HttpResponse handle(WebHandlerResponse handlerResponse) { + ViewResponse staticResponse = (ViewResponse) handlerResponse; + String path = staticResponse.getViewPath(); + + File file = resolveStaticFile(path) + .orElseThrow(() -> new ErrorException("Static-View path Error")); + + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) { + byte[] body = in.readAllBytes(); + + HttpResponse httpResponse = HttpResponse.of(HttpStatus.OK); + httpResponse.setBody(file, body); + return httpResponse; + + } catch (IOException e) { + throw new ErrorException("Static-View Read IO-Error"); + } + } + + private Optional resolveStaticFile(String path){ + for(String root : staticResourceRoots){ + File requestedFile = new File(root + path); + if(requestedFile.exists() && requestedFile.isFile()) return Optional.of(requestedFile); + + String indexFilePath = path + (path.endsWith("/") ? "index.html" : "/index.html"); + File indexFile = new File(root + indexFilePath); + if(indexFile.exists() && indexFile.isFile()) return Optional.of(indexFile); + } + return Optional.empty(); + } +} diff --git a/src/main/java/webserver/web/handler/response/handler/WebHandlerResponseHandler.java b/src/main/java/web/posthandler/WebHandlerResponseHandler.java similarity index 50% rename from src/main/java/webserver/web/handler/response/handler/WebHandlerResponseHandler.java rename to src/main/java/web/posthandler/WebHandlerResponseHandler.java index 6cfc6a81a..7c8ef55ca 100644 --- a/src/main/java/webserver/web/handler/response/handler/WebHandlerResponseHandler.java +++ b/src/main/java/web/posthandler/WebHandlerResponseHandler.java @@ -1,7 +1,7 @@ -package webserver.web.handler.response.handler; +package web.posthandler; -import webserver.http.response.HttpResponse; -import webserver.web.handler.response.WebHandlerResponse; +import http.response.HttpResponse; +import web.response.WebHandlerResponse; public interface WebHandlerResponseHandler { boolean supports(WebHandlerResponse response); diff --git a/src/main/java/webserver/web/handler/response/staticcontent/StaticContentResponse.java b/src/main/java/web/response/StaticContentResponse.java similarity index 75% rename from src/main/java/webserver/web/handler/response/staticcontent/StaticContentResponse.java rename to src/main/java/web/response/StaticContentResponse.java index a9d6c9900..dcc56d620 100644 --- a/src/main/java/webserver/web/handler/response/staticcontent/StaticContentResponse.java +++ b/src/main/java/web/response/StaticContentResponse.java @@ -1,14 +1,8 @@ -package webserver.web.handler.response.staticcontent; - -import webserver.web.handler.response.WebHandlerResponse; +package web.response; public class StaticContentResponse implements WebHandlerResponse { private final String path; - public String getPath() { - return path; - } - private StaticContentResponse(String path) { this.path = path; } @@ -16,4 +10,8 @@ private StaticContentResponse(String path) { public static StaticContentResponse of(String path){ return new StaticContentResponse(path); } + + public String getPath() { + return path; + } } diff --git a/src/main/java/web/response/ViewResponse.java b/src/main/java/web/response/ViewResponse.java new file mode 100644 index 000000000..c431530bc --- /dev/null +++ b/src/main/java/web/response/ViewResponse.java @@ -0,0 +1,17 @@ +package web.response; + +public class ViewResponse implements WebHandlerResponse { + private final String viewPath; + + public ViewResponse(String viewPath) { + this.viewPath = viewPath; + } + + public static ViewResponse of (String path){ + return new ViewResponse(path); + } + + public String getViewPath() { + return viewPath; + } +} diff --git a/src/main/java/webserver/web/handler/response/WebHandlerResponse.java b/src/main/java/web/response/WebHandlerResponse.java similarity index 50% rename from src/main/java/webserver/web/handler/response/WebHandlerResponse.java rename to src/main/java/web/response/WebHandlerResponse.java index 76bd83b61..9e47c986d 100644 --- a/src/main/java/webserver/web/handler/response/WebHandlerResponse.java +++ b/src/main/java/web/response/WebHandlerResponse.java @@ -1,4 +1,4 @@ -package webserver.web.handler.response; +package web.response; public interface WebHandlerResponse { } diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java deleted file mode 100644 index 8c20bbf52..000000000 --- a/src/main/java/webserver/RequestHandler.java +++ /dev/null @@ -1,68 +0,0 @@ -package webserver; - -import java.io.*; -import java.net.Socket; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class RequestHandler implements Runnable { - private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); - - private Socket connection; - - public RequestHandler(Socket connectionSocket) { - this.connection = connectionSocket; - } - - public void run() { - logger.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), - connection.getPort()); - - try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { - // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다. - InputStreamReader reader = new InputStreamReader(in); - BufferedReader bufferedReader = new BufferedReader(reader); - String firstLine = bufferedReader.readLine(); - logger.info(firstLine); - while (bufferedReader.ready()) { - logger.info(bufferedReader.readLine()); - } - - String[] requests = firstLine.split(" "); - String rPath = requests[1]; - DataOutputStream dos = new DataOutputStream(out); - Path path = Path.of("src", "main", "resources", "static", rPath).toAbsolutePath().normalize(); - byte[] body = Files.readAllBytes(path); - response200Header(dos, body.length); - responseBody(dos, body); - } catch (IOException e) { - logger.error(e.getMessage()); - logger.error(Arrays.toString(e.getStackTrace())); - logger.error(String.valueOf(e.getClass())); - } - } - - 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) { - logger.error(e.getMessage()); - } - } - - private void responseBody(DataOutputStream dos, byte[] body) { - try { - dos.write(body, 0, body.length); - dos.flush(); - } catch (IOException e) { - logger.error(e.getMessage()); - } - } -} diff --git a/src/main/java/webserver/exception/ExceptionHandler.java b/src/main/java/webserver/exception/ExceptionHandler.java deleted file mode 100644 index cd23aab2d..000000000 --- a/src/main/java/webserver/exception/ExceptionHandler.java +++ /dev/null @@ -1,8 +0,0 @@ -package webserver.exception; - -import java.net.Socket; - -public interface ExceptionHandler { - boolean support(Exception e); - void handle(Exception e, Socket connection); -} diff --git a/src/main/java/webserver/http/HttpServlet.java b/src/main/java/webserver/http/HttpServlet.java deleted file mode 100644 index 5265fc37e..000000000 --- a/src/main/java/webserver/http/HttpServlet.java +++ /dev/null @@ -1,42 +0,0 @@ -package webserver.http; - -import webserver.exception.ExceptionHandlerMapping; -import webserver.http.response.HttpResponseConverter; -import webserver.web.WasServlet; -import webserver.http.request.HttpRequestConverter; -import webserver.http.request.HttpRequest; -import webserver.http.response.HttpResponse; - -import java.net.Socket; - -public class HttpServlet implements Runnable{ - private final Socket connection; - private final HttpRequestConverter requestConverter; - private final HttpResponseConverter responseConverter; - private ExceptionHandlerMapping handlerMapping; - private WasServlet servlet; - - public HttpServlet(WasServlet servlet, - ExceptionHandlerMapping handlerMapping, - HttpResponseConverter responseConverter, - HttpRequestConverter requestConverter, - Socket connection) { - this.servlet = servlet; - this.handlerMapping = handlerMapping; - this.responseConverter = responseConverter; - this.requestConverter = requestConverter; - this.connection = connection; - } - - @Override - public void run() { - - try { - HttpRequest request = requestConverter.parseRequest(connection); - HttpResponse response = servlet.handle(request); - responseConverter.sendResponse(response, connection); - } catch (Exception e){ - handlerMapping.handle(e, connection); - } - } -} diff --git a/src/main/java/webserver/http/response/HttpResponse.java b/src/main/java/webserver/http/response/HttpResponse.java deleted file mode 100644 index a91467ec6..000000000 --- a/src/main/java/webserver/http/response/HttpResponse.java +++ /dev/null @@ -1,55 +0,0 @@ -package webserver.http.response; - -import webserver.http.HttpStatus; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class HttpResponse { - private final HttpStatus status; - private Map> headers; - private byte[] body; - - private HttpResponse (HttpStatus status){ - this.status = status; - this.headers = new HashMap<>(); - } - - public static HttpResponse of (HttpStatus status){ - return new HttpResponse(status); - } - - public HttpStatus getStatus() { - return status; - } - - public List getHeaders() { - return headers.keySet().stream().toList(); - } - - public List getHeader(String key){ - List values = headers.get(key); - return values != null ? values : new ArrayList<>(); - } - - public void addHeader(String key, String value){ - if(!headers.containsKey(key)) - headers.put(key, new ArrayList<>()); - headers.get(key).add(value); - } - - public void setHeader(String key, String value){ - headers.put(key, new ArrayList<>()); - headers.get(key).add(value); - } - - public byte[] getBody() { - return body; - } - - public void setBody(byte[] body) { - this.body = body; - } -} \ No newline at end of file diff --git a/src/main/java/webserver/web/handler/StaticContentHandler.java b/src/main/java/webserver/web/handler/StaticContentHandler.java deleted file mode 100644 index bb8191294..000000000 --- a/src/main/java/webserver/web/handler/StaticContentHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package webserver.web.handler; - -import webserver.http.HttpMethod; -import webserver.http.request.HttpRequest; -import webserver.web.handler.response.WebHandlerResponse; -import webserver.web.handler.response.staticcontent.StaticContentResponse; - -import java.io.File; - -/** - * 정 - */ -public class StaticContentHandler implements WebHandler{ - private final String DEFAULT_PATH = "/src/main/java/resource"; - @Override - public HttpMethod getMethod() { - return HttpMethod.GET; - } - - @Override - public boolean checkEndpoint(HttpMethod method, String path) { - if(!method.equals(HttpMethod.GET)) return false; - File file = new File(DEFAULT_PATH + path); - //TODO: Prevent path traversal attack - return file.exists() && file.isFile(); - } - - @Override - public WebHandlerResponse handle(HttpRequest request) { - return StaticContentResponse.of(request.getPath()); - } -} diff --git a/src/main/java/webserver/web/handler/response/handler/StaticContentResponseHandler.java b/src/main/java/webserver/web/handler/response/handler/StaticContentResponseHandler.java deleted file mode 100644 index 322b7d908..000000000 --- a/src/main/java/webserver/web/handler/response/handler/StaticContentResponseHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -package webserver.web.handler.response.handler; - -import webserver.http.HttpStatus; -import webserver.http.response.HttpResponse; -import webserver.web.handler.response.WebHandlerResponse; -import webserver.web.handler.response.staticcontent.StaticContentResponse; - -import java.io.*; -import java.net.URLConnection; - -public class StaticContentResponseHandler implements WebHandlerResponseHandler{ - private final String DEFAULT_PATH = "/src/main/java/resource"; - @Override - public boolean supports(WebHandlerResponse response) { - return response instanceof StaticContentResponse; - } - - @Override - public HttpResponse handle(WebHandlerResponse handlerResponse) { - StaticContentResponse staticResponse = (StaticContentResponse) handlerResponse; - - String path = staticResponse.getPath(); - File file = new File(DEFAULT_PATH + path); - - //TODO: Prevent path traversal attack - if (!file.exists() || !file.isFile()) { - //TODO: Change to Throw Exception - HttpResponse httpResponse = HttpResponse.of(HttpStatus.NOT_FOUND); - httpResponse.setHeader("Content-Length", "0"); - return httpResponse; - } - - try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) { - byte[] body = in.readAllBytes(); - - HttpResponse httpResponse = HttpResponse.of(HttpStatus.OK); - httpResponse.setBody(body); - - String contentType = guessContentType(file); - httpResponse.setHeader("Content-Type", contentType); - httpResponse.setHeader("Content-Length", String.valueOf(body.length)); - return httpResponse; - - } catch (IOException e) { - //TODO: Change to Throw Exception - HttpResponse httpResponse = HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR); - httpResponse.setHeader("Content-Length", "0"); - return httpResponse; - } - } - - private String guessContentType(File file) { - String byName = URLConnection.guessContentTypeFromName(file.getName()); - if (byName != null) return byName; - - String name = file.getName().toLowerCase(); - if (name.endsWith(".html") || name.endsWith(".htm")) return "text/html; charset=utf-8"; - if (name.endsWith(".css")) return "text/css; charset=utf-8"; - if (name.endsWith(".js")) return "application/javascript; charset=utf-8"; - if (name.endsWith(".json")) return "application/json; charset=utf-8"; - if (name.endsWith(".png")) return "image/png"; - if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg"; - if (name.endsWith(".gif")) return "image/gif"; - if (name.endsWith(".svg")) return "image/svg+xml"; - if (name.endsWith(".txt")) return "text/plain; charset=utf-8"; - - return "application/octet-stream"; - } -} diff --git a/src/main/resources/static/registration/index.html b/src/main/resources/static/registration/index.html index 1936b5b28..7e109c32e 100644 --- a/src/main/resources/static/registration/index.html +++ b/src/main/resources/static/registration/index.html @@ -23,11 +23,12 @@

회원가입

-
+

아이디

@@ -36,6 +37,7 @@

회원가입

닉네임

@@ -44,6 +46,7 @@

회원가입

비밀번호

회원가입 id="registration-btn" class="btn btn_contained btn_size_m" style="margin-top: 24px" - type="button" + type="submit" > 회원가입 diff --git a/src/test/java/web/handler/StaticContentHandlerTest.java b/src/test/java/web/handler/StaticContentHandlerTest.java new file mode 100644 index 000000000..b9675f1a5 --- /dev/null +++ b/src/test/java/web/handler/StaticContentHandlerTest.java @@ -0,0 +1,14 @@ +package web.handler; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import http.HttpMethod; + +class StaticContentHandlerTest { + private final StaticContentHandler handler = new StaticContentHandler(); + @Test + void support_test(){ + Assertions.assertThat(handler.checkEndpoint(HttpMethod.GET, "/index.html")).isTrue(); + } + +} \ No newline at end of file diff --git a/src/test/java/web/handler/response/handler/StaticContentResponseHandlerTest.java b/src/test/java/web/handler/response/handler/StaticContentResponseHandlerTest.java new file mode 100644 index 000000000..37160f3e7 --- /dev/null +++ b/src/test/java/web/handler/response/handler/StaticContentResponseHandlerTest.java @@ -0,0 +1,44 @@ +package web.handler.response.handler; + +import org.junit.jupiter.api.*; +import web.response.WebHandlerResponse; +import web.posthandler.StaticContentResponseHandler; +import web.response.StaticContentResponse; + +import java.io.IOException; +import java.nio.file.*; + +import static org.assertj.core.api.Assertions.*; + +class StaticContentResponseHandlerTest { + + private StaticContentResponseHandler handler; + private Path resourceRoot; + + @BeforeEach + void setUp() { + handler = new StaticContentResponseHandler(); + resourceRoot = Paths.get(System.getProperty("user.dir")) + .resolve("src/main/java/resource"); + } + + @AfterEach + void tearDown() throws IOException { + Path index = resourceRoot.resolve("index.html"); + if (Files.exists(index)) { + Files.delete(index); + } + } + + @Test + void supports_true_when_response_is_StaticContentResponse() { + WebHandlerResponse response = StaticContentResponse.of("/index.html"); + assertThat(handler.supports(response)).isTrue(); + } + + @Test + void supports_false_when_response_is_not_StaticContentResponse() { + WebHandlerResponse other = new WebHandlerResponse() {}; + assertThat(handler.supports(other)).isFalse(); + } +} diff --git a/src/test/java/web/http/request/HttpRequestConverterTest.java b/src/test/java/web/http/request/HttpRequestConverterTest.java new file mode 100644 index 000000000..a027926e3 --- /dev/null +++ b/src/test/java/web/http/request/HttpRequestConverterTest.java @@ -0,0 +1,62 @@ +package web.http.request; + +import http.request.HttpBufferedReaderRequestConverter; +import http.request.HttpRequest; +import http.request.HttpRequestConverter; +import org.junit.jupiter.api.Test; + +import java.net.Socket; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +class HttpRequestConverterTest { + private final HttpRequestConverter converter = new HttpBufferedReaderRequestConverter(); + + @Test + void parse_GET_request_test() throws Exception { + String raw = + "GET /hello?name=ta&x=1 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "User-Agent: junit\r\n" + + "\r\n"; + + Socket socket = mock(Socket.class); + when(socket.getInputStream()).thenReturn( + new ByteArrayInputStream(raw.getBytes(StandardCharsets.ISO_8859_1)) + ); + + HttpRequest req = converter.parseRequest(socket); + + assertThat(req.getMethod().name()).isEqualTo("GET"); + assertThat(req.getPath()).isEqualTo("/hello"); + assertThat(req.getQueryValue("name")).isEqualTo("ta"); + assertThat(req.getQueryValue("x")).isEqualTo("1"); + assertThat(req.getHeader("Host")).isEqualTo("localhost"); + assertThat(req.getHeader("User-Agent")).isEqualTo("junit"); + } + + @Test + void parse_GET_request_test2() throws Exception { + String raw = + "GET /index.html HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "User-Agent: junit\r\n" + + "\r\n"; + + Socket socket = mock(Socket.class); + when(socket.getInputStream()).thenReturn( + new ByteArrayInputStream(raw.getBytes(StandardCharsets.ISO_8859_1)) + ); + + HttpRequest req = converter.parseRequest(socket); + + assertThat(req.getMethod().name()).isEqualTo("GET"); + assertThat(req.getPath()).isEqualTo("/index.html"); + assertThat(req.getHeader("Host")).isEqualTo("localhost"); + assertThat(req.getHeader("User-Agent")).isEqualTo("junit"); + } +} \ No newline at end of file diff --git a/src/test/java/web/http/request/HttpRequestTest.java b/src/test/java/web/http/request/HttpRequestTest.java new file mode 100644 index 000000000..008dc8b2f --- /dev/null +++ b/src/test/java/web/http/request/HttpRequestTest.java @@ -0,0 +1,17 @@ +package web.http.request; + +import http.request.HttpRequest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class HttpRequestTest { + + @Test + void query_parameter_parse_test(){ + HttpRequest request = HttpRequest.from("GET /create?userId=user1&nickname=testNick&password=123 HTTP/1.1"); + assertThat(request.getPath()).isEqualTo("/create"); + assertThat(request.getQueryValue("userId")).isEqualTo("user1"); + } + +} \ No newline at end of file diff --git a/src/test/java/web/http/response/HttpResponseConverterTest.java b/src/test/java/web/http/response/HttpResponseConverterTest.java new file mode 100644 index 000000000..e180b8e35 --- /dev/null +++ b/src/test/java/web/http/response/HttpResponseConverterTest.java @@ -0,0 +1,43 @@ +package web.http.response; + +import http.response.HttpBufferedStreamResponseConverter; +import http.response.HttpResponse; +import http.response.HttpResponseConverter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import http.HttpStatus; + +import java.io.ByteArrayOutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +class HttpResponseConverterTest { + private final HttpResponseConverter converter = new HttpBufferedStreamResponseConverter(); + + @Test + void write_response_to_socket_test() throws Exception { + Socket socket = mock(Socket.class); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + when(socket.getOutputStream()).thenReturn(out); + + HttpResponse res = HttpResponse.of(HttpStatus.OK); + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.setBody("PONG".getBytes(StandardCharsets.UTF_8)); + + converter.sendResponse(res, socket); + + String raw = out.toString(StandardCharsets.ISO_8859_1); + + assertThat(raw).startsWith("HTTP/1.1 200"); + assertThat(raw).contains("\r\n\r\n"); + assertThat(raw).containsIgnoringCase("Content-Length: 4"); + assertThat(raw).endsWith("PONG"); + } + +} \ No newline at end of file