Skip to content

3주차 미션 / 서버 5조 진수민#15

Open
jsm040605 wants to merge 1 commit intoKonkuk-KUIT:mainfrom
jsm040605:jsm040605
Open

3주차 미션 / 서버 5조 진수민#15
jsm040605 wants to merge 1 commit intoKonkuk-KUIT:mainfrom
jsm040605:jsm040605

Conversation

@jsm040605
Copy link

@jsm040605 jsm040605 commented Oct 3, 2025

Summary by CodeRabbit

  • 신기능
    • 기본 라우팅과 정적 파일 전달/리다이렉트 지원이 추가되어 페이지 이동이 원활해졌습니다.
    • 회원가입, 로그인, 홈, 사용자 목록 페이지 처리를 지원합니다. 로그인 상태에 따라 사용자 목록 접근이 제어됩니다.
    • HTTP 요청/응답 처리가 개선되어 다양한 메서드와 헤더를 안정적으로 처리합니다.
  • 변경
    • 회원가입 폼 전송 방식이 GET에서 POST로 변경되었습니다.
  • 리팩터링
    • 요청 처리 흐름을 중앙화하여 성능과 유지보수성을 향상했습니다.
  • 기타
    • IDE 설정 및 VCS 매핑 구성이 추가되었습니다.
    • 서버 코드에 주석이 보강되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Oct 3, 2025

Walkthrough

IDE VCS/Gradle config updates; introduction of HTTP request/response abstractions; addition of routing via RequestMapper; new controller interface and implementations (Home, Forward, List, Login, SignUp); WebServer comments; signup form method changed to POST.

Changes

Cohort / File(s) Summary
IDE 설정
.idea/gradle.xml, .idea/vcs.xml
GradleMigrationSettings 추가, Git VCS 매핑 파일 신규 추가.
컨트롤러 추가
src/main/java/controller/Controller.java, .../ForwardController.java, .../HomeController.java, .../ListController.java, .../LoginController.java, .../SignUpController.java
공통 인터페이스 Controller 도입 및 다수 컨트롤러 구현: 포워드, 홈(/index.html), 로그인, 회원가입, 로그인 여부에 따른 리스트 리다이렉트.
HTTP 열거형
src/main/java/enums/HttpHeader.java, src/main/java/enums/HttpMethod.java
HTTP 헤더 및 메서드 enum 신규 도입, 문자열 값 반환 메서드 제공.
웹서버 핵심 로직
src/main/java/webserver/HttpRequest.java, .../HttpResponse.java, .../RequestMapper.java, .../RequestHandler.java
요청 파싱/응답 헬퍼 클래스 추가, URL 기반 라우팅 RequestMapper 도입, RequestHandler가 매퍼를 통해 처리하도록 변경.
서버 부가 변경
src/main/java/webserver/WebServer.java
주석 추가(동작 변화 없음).
웹앱 템플릿
webapp/user/form.html
회원가입 폼 메서드 GET→POST로 변경.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant WS as WebServer
  participant RH as RequestHandler
  participant HRq as HttpRequest
  participant HRs as HttpResponse
  participant RM as RequestMapper
  participant CTR as Controller

  C->>WS: TCP 연결
  WS->>RH: 소켓 핸들러 실행
  RH->>HRq: BufferedReader로 from(...) 파싱
  RH->>HRs: DataOutputStream으로 생성
  RH->>RM: new RequestMapper(HRq, HRs)
  RM->>RM: URL→컨트롤러 매핑 조회
  RM->>CTR: execute(request, response)
  alt Forward
    CTR->>HRs: forward(request.getUrl())
  else Home
    CTR->>HRs: forward("/index.html")
  else Login
    CTR->>HRs: redirect("/index.html?logined=true")
  else SignUp
    CTR->>HRs: redirect("/index.html")
  else List
    CTR->>HRs: redirect("/user/login.html") or redirect("/user/list.html")
  end
  HRs-->>C: HTTP 응답 전송
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • chore: readme추가 #10 — 동일한 .idea/gradle.xml의 GradleMigrationSettings 및 .idea/vcs.xml VCS 매핑 추가로 구성 변경이 일치.

Poem

새벽 포트에 톰캣 꿈, 라우터 길이 폈다냥(…아니 띠용, 토끼!)
요청 hop, 응답 hop, 맵퍼는 길을 잇고
컨트롤러 폴짝폴짝, 홈으로 포워드 쓩—
쿠키 냠, 로그인 휙, 사인업 POST 찰칵!
깃 바람 솔솔, 서버는 오늘도 경쾌한 발놀림 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning PR 제목이 과제 주차와 작성자 정보만 나열하고 실제 구현된 HTTP 요청/응답 처리 로직과 컨트롤러 추가 등 핵심 변경 사항을 반영하지 않아 변경 내용을 요약하는 역할을 수행하지 못합니다. PR 제목을 핵심 구현 기능을 간결하게 요약하도록 수정해야 합니다. 예를 들어 “커스텀 Tomcat 서버에 HttpRequest/Response 및 RequestMapper 기반 컨트롤러 추가”와 같이 변경 사항을 구체적으로 반영해 주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.71% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jsm040605 jsm040605 changed the title 요구사항 구형 3주차 미션 / 서버 5조 진수민 Oct 3, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (7)
src/main/java/enums/HttpHeader.java (1)

16-18: 메서드 이름이 HttpMethod와 일관성이 없습니다.

HttpHeader.value()HttpMethod.getValue()의 메서드 명명이 다릅니다. API 일관성을 위해 둘 중 하나로 통일하는 것이 좋습니다.

옵션 1: HttpHeader를 getValue()로 변경

-    public String value() {
+    public String getValue() {
         return value;
     }

옵션 2: HttpMethod를 value()로 변경 (HttpMethod.java에서)

-    public String getValue() {
+    public String value() {
         return value;
     }

권장: getValue()로 통일 (Java 관례상 더 명확함)

src/main/java/controller/LoginController.java (1)

15-15: 필드를 final로 선언하세요.

memoryUserRepository는 재할당되지 않으므로 final로 선언하여 불변성을 보장하는 것이 좋습니다.

-    MemoryUserRepository memoryUserRepository = MemoryUserRepository.getInstance();
+    private final MemoryUserRepository memoryUserRepository = MemoryUserRepository.getInstance();
src/main/java/webserver/RequestMapper.java (2)

16-24: 생성자에서 null 체크 필요

httpRequest 또는 httpResponse 파라미터가 null인 경우 NPE가 발생할 수 있습니다. 생성자 시작 부분에서 null 체크를 추가하는 것을 권장합니다.

 public RequestMapper(HttpRequest httpRequest, HttpResponse httpResponse) {
+    if (httpRequest == null || httpResponse == null) {
+        throw new IllegalArgumentException("HttpRequest and HttpResponse must not be null");
+    }
     this.request = httpRequest;
     this.response = httpResponse;

14-14: 컨트롤러 맵을 정적 final 필드로 고려

컨트롤러 맵은 모든 요청에 대해 동일하므로, 매 요청마다 새로 생성하는 대신 정적 final 필드로 선언하여 메모리 효율성을 개선할 수 있습니다.

+private static final Map<String, Controller> CONTROLLERS = new HashMap<>();
+
+static {
+    CONTROLLERS.put("/", new HomeController());
+    CONTROLLERS.put("/user/signup", new SignUpController());
+    CONTROLLERS.put("/user/login", new LoginController());
+    CONTROLLERS.put("/user/userList", new ListController());
+}
+
 HttpRequest request;
 HttpResponse response;
-Map<String, Controller> controllers = new HashMap<>();

 public RequestMapper(HttpRequest httpRequest, HttpResponse httpResponse) {
     this.request = httpRequest;
     this.response = httpResponse;
-
-    controllers.put("/", new HomeController());
-    controllers.put("/user/signup", new SignUpController());
-    controllers.put("/user/login", new LoginController());
-    controllers.put("/user/userList", new ListController());
 }
src/main/java/webserver/RequestHandler.java (1)

97-100: 예외 스택 트레이스 로깅 개선

Arrays.toString(e.getStackTrace())보다는 e를 직접 로깅하거나 log.log(Level.SEVERE, "Error", e)를 사용하는 것이 스택 트레이스를 더 명확하게 출력합니다.

 } catch (Exception e) {
-    log.log(Level.SEVERE, e.getMessage());
-    System.out.println(Arrays.toString(e.getStackTrace()));
+    log.log(Level.SEVERE, "Error processing request", e);
 }
src/main/java/webserver/HttpRequest.java (1)

12-16: 필드를 private final로 변경 권장

현재 필드들이 package-private이어서 같은 패키지 내에서 직접 수정 가능합니다. 불변성을 보장하기 위해 private final로 선언하는 것을 권장합니다.

-String method;
-String path;
-String version;
-Map<String,String> headers;
-String body;
+private final String method;
+private final String path;
+private final String version;
+private final Map<String,String> headers;
+private final String body;
src/main/java/webserver/HttpResponse.java (1)

13-13: 필드를 private final로 변경 권장

dos 필드가 package-private이며 재할당 가능합니다. 불변성을 위해 private final로 선언하는 것을 권장합니다.

-DataOutputStream dos;
+private final DataOutputStream dos;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dba7017 and 9797e9e.

📒 Files selected for processing (16)
  • .idea/gradle.xml (1 hunks)
  • .idea/vcs.xml (1 hunks)
  • src/main/java/controller/Controller.java (1 hunks)
  • src/main/java/controller/ForwardController.java (1 hunks)
  • src/main/java/controller/HomeController.java (1 hunks)
  • src/main/java/controller/ListController.java (1 hunks)
  • src/main/java/controller/LoginController.java (1 hunks)
  • src/main/java/controller/SignUpController.java (1 hunks)
  • src/main/java/enums/HttpHeader.java (1 hunks)
  • src/main/java/enums/HttpMethod.java (1 hunks)
  • src/main/java/webserver/HttpRequest.java (1 hunks)
  • src/main/java/webserver/HttpResponse.java (1 hunks)
  • src/main/java/webserver/RequestHandler.java (2 hunks)
  • src/main/java/webserver/RequestMapper.java (1 hunks)
  • src/main/java/webserver/WebServer.java (1 hunks)
  • webapp/user/form.html (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
src/main/java/controller/ListController.java (2)
src/main/java/webserver/HttpRequest.java (1)
  • HttpRequest (10-77)
src/main/java/webserver/HttpResponse.java (1)
  • HttpResponse (11-102)
src/main/java/controller/Controller.java (2)
src/main/java/webserver/HttpRequest.java (1)
  • HttpRequest (10-77)
src/main/java/webserver/HttpResponse.java (1)
  • HttpResponse (11-102)
src/main/java/webserver/HttpRequest.java (1)
src/main/java/http/util/IOUtils.java (1)
  • IOUtils (6-21)
src/main/java/controller/SignUpController.java (5)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/util/HttpRequestUtils.java (1)
  • HttpRequestUtils (8-20)
src/main/java/model/User.java (1)
  • User (5-46)
src/main/java/webserver/HttpRequest.java (1)
  • HttpRequest (10-77)
src/main/java/webserver/HttpResponse.java (1)
  • HttpResponse (11-102)
src/main/java/controller/HomeController.java (2)
src/main/java/webserver/HttpRequest.java (1)
  • HttpRequest (10-77)
src/main/java/webserver/HttpResponse.java (1)
  • HttpResponse (11-102)
src/main/java/webserver/RequestHandler.java (5)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/util/HttpRequestUtils.java (1)
  • HttpRequestUtils (8-20)
src/main/java/http/util/IOUtils.java (1)
  • IOUtils (6-21)
src/main/java/model/User.java (1)
  • User (5-46)
src/main/java/webserver/HttpRequest.java (1)
  • HttpRequest (10-77)
src/main/java/controller/ForwardController.java (2)
src/main/java/webserver/HttpRequest.java (1)
  • HttpRequest (10-77)
src/main/java/webserver/HttpResponse.java (1)
  • HttpResponse (11-102)
src/main/java/controller/LoginController.java (4)
src/main/java/db/MemoryUserRepository.java (1)
  • MemoryUserRepository (9-35)
src/main/java/http/util/HttpRequestUtils.java (1)
  • HttpRequestUtils (8-20)
src/main/java/model/User.java (1)
  • User (5-46)
src/main/java/webserver/HttpResponse.java (1)
  • HttpResponse (11-102)
🔇 Additional comments (6)
.idea/gradle.xml (1)

3-3: Gradle 마이그레이션 설정 추가 문제없음

IDE 메타데이터에 GradleMigrationSettings가 추가된 것으로 보이며, 실행 동작에 영향이 없어 보입니다.

webapp/user/form.html (1)

58-58: 회원가입 폼 메서드를 POST로 변경한 점 좋습니다.

민감한 사용자 정보(비밀번호 등)를 URL에 노출시키지 않기 위해 GET 대신 POST를 사용하는 것이 보안상 올바른 선택입니다.

src/main/java/webserver/WebServer.java (1)

24-30: 한글 주석이 코드 이해에 도움이 됩니다.

ServerSocket, accept 루프, 스레드 풀 제출 과정에 대한 설명이 명확합니다.

src/main/java/controller/HomeController.java (1)

8-13: 깔끔한 구현입니다.

홈 페이지로 포워딩하는 간단하고 명확한 컨트롤러 구현입니다.

src/main/java/enums/HttpMethod.java (1)

3-23: HttpMethod 열거형 구현이 잘 되어 있습니다.

표준 HTTP 메서드들을 타입 안전하게 관리할 수 있는 열거형입니다. 현재 코드에서는 사용되지 않지만 향후 HttpRequest에서 문자열 대신 이 enum을 사용하면 타입 안전성이 향상됩니다.

참고: Line 12의 후행 쉼표(trailing comma)는 Java에서 유효한 문법이며, 향후 enum 값 추가 시 diff를 깔끔하게 유지하는 장점이 있습니다.

src/main/java/controller/Controller.java (1)

8-12: 인터페이스 설계 우수

컨트롤러 인터페이스가 명확하고 간결합니다. execute 메서드의 시그니처가 적절하며 IOException 선언도 올바릅니다.

Comment on lines +11 to +12
public void execute(HttpRequest request, HttpResponse response) throws IOException {
response.forward(request.getUrl());
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

경로 탐색(Path Traversal) 취약점 가능성이 있습니다.

request.getUrl()의 값을 검증 없이 response.forward()에 전달하면 ../ 같은 상대 경로를 통해 웹 애플리케이션 루트 디렉토리 외부의 파일에 접근할 수 있습니다.

HttpResponse.forward()new File("./webapp" + path)를 사용하므로, 악의적인 요청 (예: GET /../../etc/passwd)이 시스템 파일을 노출시킬 수 있습니다.

다음 스크립트로 HttpResponse.forward의 경로 검증 여부를 확인하세요:

경로 검증이 없다면 다음과 같이 수정하세요:

     @Override
     public void execute(HttpRequest request, HttpResponse response) throws IOException {
-        response.forward(request.getUrl());
+        String path = request.getUrl();
+        // 경로 탐색 방지: 상대 경로 제거
+        if (path.contains("..") || !path.startsWith("/")) {
+            response.forward("/error/400.html");
+            return;
+        }
+        response.forward(path);
     }

또는 HttpResponse.forward() 내부에서 경로를 정규화하여 보안을 강화하세요.


🏁 Script executed:

#!/bin/bash
# HttpResponse.forward 메서드의 경로 검증 로직 확인
rg -A 10 'public void forward' src/main/java/webserver/HttpResponse.java

Length of output: 489


경로 탐색(Path Traversal) 취약점 확인 및 방어 구현 필요
request.getUrl()에서 전달받은 경로가 검증·정규화되지 않아 ../ 입력으로 웹앱 외부 파일(예: /etc/passwd)에 접근될 수 있습니다.
HttpResponse.forward() 내부 또는 호출부에서 입력 경로를 Path.normalize() 등으로 정규화하고, 허용된 웹앱 디렉터리 경로로만 제한(예: normalizedPath.startsWith(webappRoot))한 뒤 처리하세요.

🤖 Prompt for AI Agents
In src/main/java/controller/ForwardController.java around lines 11-12, the
controller forwards using request.getUrl() without validation which allows path
traversal; normalize and resolve the requested path against the webapp root and
reject any paths that escape it: obtain the webapp root Path, resolve the
request URL against it, call normalize() (and preferably toRealPath() or
toAbsolutePath() to canonicalize), verify the resolved path startsWith the
webapp root canonical path, and if not respond with an error (400/403) and log
the event; only call response.forward() with the validated normalized path.

Comment on lines +14 to +20
String cookie = request.getHeaders().get(HttpHeader.COOKIE.value());

if(cookie == null) {
response.redirect("/user/login.html", null);
}else if(cookie.equals("logined=true")) {
response.redirect("/user/list.html", null);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

쿠키 기반 인증에 심각한 보안 취약점이 있습니다.

현재 구현은 다음 문제들을 가지고 있습니다:

  1. 인증 우회 가능: 클라이언트가 브라우저에서 logined=true 쿠키를 직접 설정하면 인증 없이 사용자 목록에 접근할 수 있습니다.
  2. 서버 측 검증 부재: 세션 토큰이나 서명된 쿠키 없이 단순 문자열만 비교합니다.
  3. 불완전한 로직: 쿠키가 존재하지만 "logined=true"가 아닌 경우 응답을 보내지 않습니다.

다음 개선 사항을 적용하세요:

+import java.util.UUID;
+// 서버 측에 세션 저장소 추가 필요
+
 public class ListController implements Controller {
 
     @Override
     public void execute(HttpRequest request, HttpResponse response) throws IOException {
 
         String cookie = request.getHeaders().get(HttpHeader.COOKIE.value());
 
-        if(cookie == null) {
+        // 세션 ID 검증 로직 추가
+        String sessionId = extractSessionId(cookie);
+        if(sessionId == null || !isValidSession(sessionId)) {
             response.redirect("/user/login.html", null);
-        }else if(cookie.equals("logined=true")) {
+            return;
+        }
+        
+        if(isValidSession(sessionId)) {
             response.redirect("/user/list.html", null);
+            return;
         }
-
+        
+        response.redirect("/user/login.html", null);
     }
+    
+    private String extractSessionId(String cookie) {
+        // 쿠키에서 세션 ID 추출
+    }
+    
+    private boolean isValidSession(String sessionId) {
+        // 서버 측 세션 저장소에서 검증
+    }
 }

또는 최소한 다음과 같이 수정하여 빠진 else 케이스를 처리하세요:

         if(cookie == null) {
             response.redirect("/user/login.html", null);
+            return;
         }else if(cookie.equals("logined=true")) {
             response.redirect("/user/list.html", null);
+            return;
         }
-
+        
+        // 쿠키가 있지만 유효하지 않은 경우
+        response.redirect("/user/login.html", null);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/controller/ListController.java around lines 14 to 20 the
current logic trusts a plain "logined=true" cookie and omits handling for other
cookie values, creating an authentication bypass and missing server-side
validation; replace the simple string check with server-side session validation
(e.g., read a session ID or signed token from the cookie and validate it against
your session store or verify its signature and expiry), ensure the else branch
handles invalid/missing tokens by redirecting to /user/login.html or returning
401, and set secure cookie attributes (HttpOnly, Secure, SameSite) when issuing
auth cookies; in short: validate tokens server-side, handle the missing/invalid
cookie path, and enforce secure cookie properties.

Comment on lines +21 to +27
Map<String, String> queryParameter = HttpRequestUtils.parseQueryParameter(request.getBody());
User loginUser = memoryUserRepository.findUserById(queryParameter.get("username"));

if(loginUser != null) {
response.redirect("/index.html", "logined=true");
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

비밀번호 검증이 누락되어 인증 우회가 가능합니다.

현재 구현은 사용자 존재 여부만 확인하고 비밀번호를 검증하지 않습니다. 데이터베이스에 존재하는 임의의 사용자 이름만 알면 비밀번호 없이 로그인할 수 있는 심각한 보안 취약점입니다.

다음과 같이 수정하세요:

         Map<String, String> queryParameter = HttpRequestUtils.parseQueryParameter(request.getBody());
-        User loginUser = memoryUserRepository.findUserById(queryParameter.get("username"));
+        String username = queryParameter.get("username");
+        String password = queryParameter.get("password");
+        
+        User loginUser = memoryUserRepository.findUserById(username);
 
-        if(loginUser != null) {
+        if(loginUser != null && loginUser.getPassword().equals(password)) {
             response.redirect("/index.html", "logined=true");
             return;
         }

추가 권장사항: 실제 프로덕션 환경에서는 평문 비밀번호 비교 대신 bcrypt 등의 해시 함수를 사용해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Map<String, String> queryParameter = HttpRequestUtils.parseQueryParameter(request.getBody());
User loginUser = memoryUserRepository.findUserById(queryParameter.get("username"));
if(loginUser != null) {
response.redirect("/index.html", "logined=true");
return;
}
Map<String, String> queryParameter = HttpRequestUtils.parseQueryParameter(request.getBody());
String username = queryParameter.get("username");
String password = queryParameter.get("password");
User loginUser = memoryUserRepository.findUserById(username);
if (loginUser != null && loginUser.getPassword().equals(password)) {
response.redirect("/index.html", "logined=true");
return;
}
🤖 Prompt for AI Agents
In src/main/java/controller/LoginController.java around lines 21 to 27, the code
only checks that a user exists and skips password verification, allowing
authentication bypass; retrieve the password from queryParameter (e.g.,
"password"), compare it against the stored password for loginUser and only call
response.redirect("/index.html", "logined=true") when the passwords match; on
mismatch or missing password return an appropriate failure response (e.g.,
redirect back with an error flag or set 401), and in comments or docs note to
use a secure hash (bcrypt) and compare hashed values in production instead of
plain-text comparison.

Comment on lines +23 to +28
if (request.getMethod().equals(HttpMethod.GET.getValue())) {
String[] tokens = request.getUrl().split("\\?");
queryString = tokens[1];
} else if (request.getMethod().equals(HttpMethod.POST.getValue())) {
queryString = request.getBody();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

URL 파싱 시 ArrayIndexOutOfBoundsException 위험

GET 요청 시 URL에 ?가 없으면 Line 25에서 tokens[1]에 접근할 때 ArrayIndexOutOfBoundsException이 발생합니다.

 //get 방식인 경우
 if (request.getMethod().equals(HttpMethod.GET.getValue())) {
     String[] tokens = request.getUrl().split("\\?");
-    queryString = tokens[1];
+    if (tokens.length > 1) {
+        queryString = tokens[1];
+    }
 } else if (request.getMethod().equals(HttpMethod.POST.getValue())) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (request.getMethod().equals(HttpMethod.GET.getValue())) {
String[] tokens = request.getUrl().split("\\?");
queryString = tokens[1];
} else if (request.getMethod().equals(HttpMethod.POST.getValue())) {
queryString = request.getBody();
}
if (request.getMethod().equals(HttpMethod.GET.getValue())) {
String[] tokens = request.getUrl().split("\\?");
if (tokens.length > 1) {
queryString = tokens[1];
}
} else if (request.getMethod().equals(HttpMethod.POST.getValue())) {
queryString = request.getBody();
}
🤖 Prompt for AI Agents
In src/main/java/controller/SignUpController.java around lines 23 to 28, the GET
branch assumes request.getUrl() contains a '?' and uses tokens[1], risking
ArrayIndexOutOfBoundsException; change the logic to check for the presence of
'?' (or verify tokens.length > 1) before accessing tokens[1], and if absent set
queryString to an empty string (or null) so the code safely handles URLs without
query parameters; also defensively ensure the POST branch uses a non-null body
(fallback to empty string) to avoid NPEs.

Comment on lines +30 to +38
Map<String, String> queryParameter = HttpRequestUtils.parseQueryParameter(queryString);

User user = new User(
queryParameter.get("username"),
queryParameter.get("password"),
queryParameter.get("email"),
queryParameter.get("phone")
);
memoryUserRepository.addUser(user);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

입력 값 검증 및 중복 사용자 체크 누락

사용자 입력에 대한 검증(null 체크, 빈 문자열 체크)이 없으며, 이미 존재하는 사용자 ID에 대한 중복 체크도 없습니다.

 Map<String, String> queryParameter = HttpRequestUtils.parseQueryParameter(queryString);

+String username = queryParameter.get("username");
+String password = queryParameter.get("password");
+String name = queryParameter.get("name");
+String email = queryParameter.get("email");
+
+// 입력 값 검증
+if (username == null || username.trim().isEmpty() ||
+    password == null || password.trim().isEmpty()) {
+    response.redirect("/user/form.html", null);
+    return;
+}
+
+// 중복 사용자 체크
+if (memoryUserRepository.findUserById(username) != null) {
+    response.redirect("/user/form.html", null);
+    return;
+}
+
 User user = new User(
-        queryParameter.get("username"),
-        queryParameter.get("password"),
-        queryParameter.get("email"),
-        queryParameter.get("phone")
+        username,
+        password,
+        name,
+        email
 );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/controller/SignUpController.java around lines 30 to 38, the
code constructs a User directly from parsed query parameters without validating
inputs or checking for duplicate usernames; update the handler to (1) validate
each required parameter (username, password, email, phone) for null and empty
values and return a client error if any are missing/invalid, (2) optionally
validate formats (e.g., email regex, minimal password length) and reject invalid
formats, (3) query memoryUserRepository to detect an existing user with the same
username and return a conflict error if found, and (4) only create and add the
new User when all validations pass; ensure error responses include a clear
message and appropriate HTTP status codes.

Comment on lines +22 to +35
public void forward(String path) throws IOException {

File file = new File("./webapp"+path);

if(file.exists()) { //존재한다면 파일 출력
byte[] body = java.nio.file.Files.readAllBytes(file.toPath());
response200Header(dos, body.length, getContentType(path));
responseBody(dos, body);
} else { //존재하지 않는다면 404 not found 출력
byte[] body = "<h1>404 Not Found</h1>".getBytes();
response404Header(dos, body.length);
responseBody(dos, body);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

경로 순회(Path Traversal) 취약점

Line 24에서 path 파라미터를 검증 없이 파일 경로로 사용하고 있습니다. 공격자가 ../../../etc/passwd 같은 경로를 전달하면 웹 루트 외부의 파일에 접근할 수 있습니다.

 public void forward(String path) throws IOException {
+    // 경로 정규화 및 검증
+    String normalizedPath = new File(path).getCanonicalPath();
+    String webappPath = new File("./webapp").getCanonicalPath();
+    
+    if (!normalizedPath.startsWith(webappPath)) {
+        byte[] body = "<h1>403 Forbidden</h1>".getBytes();
+        response404Header(dos, body.length);
+        responseBody(dos, body);
+        return;
+    }

-    File file = new File("./webapp"+path);
+    File file = new File(webappPath + path);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/webserver/HttpResponse.java around lines 22 to 35, the forward
method uses the incoming path directly to construct a File, allowing path
traversal (e.g. ../../../etc/passwd). Normalize and validate the requested path
before opening the file: URL-decode the input, reject any null bytes, resolve it
against a fixed web root (e.g. new File(webRoot, path)), obtain the canonical
path of both the web root and the resolved file, and only serve the file if the
file's canonical path startsWith the web root's canonical path; otherwise return
404 or 400. Also disallow segments like ".." after decoding and consider mapping
directory requests to an index file.

Comment on lines +58 to +67
private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) {
try {
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes(HttpHeader.CONTENT_TYPE.value() + ": " + contentType + ";charset=utf-8\r\n");
dos.writeBytes( HttpHeader.CONTENT_LENGTH.value() + ": " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.log(Level.SEVERE, e.getMessage());
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

IOException 처리 방식 개선 필요

Lines 64-66에서 IOException을 catch하여 로깅만 하고 있습니다. 이는 호출자에게 오류를 전달하지 않아 응답이 불완전하게 전송될 수 있습니다. 또한 flush()가 누락되어 있습니다.

-private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) {
+private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) throws IOException {
-    try {
-        dos.writeBytes("HTTP/1.1 200 OK \r\n");
-        dos.writeBytes(HttpHeader.CONTENT_TYPE.value() + ": " + contentType + ";charset=utf-8\r\n");
-        dos.writeBytes( HttpHeader.CONTENT_LENGTH.value() + ": " + lengthOfBodyContent + "\r\n");
-        dos.writeBytes("\r\n");
-    } catch (IOException e) {
-        log.log(Level.SEVERE, e.getMessage());
-    }
+    dos.writeBytes("HTTP/1.1 200 OK \r\n");
+    dos.writeBytes(HttpHeader.CONTENT_TYPE.value() + ": " + contentType + ";charset=utf-8\r\n");
+    dos.writeBytes( HttpHeader.CONTENT_LENGTH.value() + ": " + lengthOfBodyContent + "\r\n");
+    dos.writeBytes("\r\n");
+    dos.flush();
 }

response404Header와 responseBody 메서드도 동일하게 수정이 필요합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) {
try {
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes(HttpHeader.CONTENT_TYPE.value() + ": " + contentType + ";charset=utf-8\r\n");
dos.writeBytes( HttpHeader.CONTENT_LENGTH.value() + ": " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.log(Level.SEVERE, e.getMessage());
}
}
private void response200Header(DataOutputStream dos, int lengthOfBodyContent, String contentType) throws IOException {
dos.writeBytes("HTTP/1.1 200 OK \r\n");
dos.writeBytes(HttpHeader.CONTENT_TYPE.value() + ": " + contentType + ";charset=utf-8\r\n");
dos.writeBytes(HttpHeader.CONTENT_LENGTH.value() + ": " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
dos.flush();
}
🤖 Prompt for AI Agents
In src/main/java/webserver/HttpResponse.java around lines 58 to 67, the methods
catch IOException and only log it (and omit flush), which hides errors and can
leave incomplete responses; update response200Header (and mirror changes in
response404Header and responseBody) to propagate the IOException instead of
swallowing it by either declaring throws IOException and removing the try/catch
or rethrowing the caught exception after logging, and ensure you call
dos.flush() after writing headers and after writing the body so data is sent;
also adjust any callers to handle or declare the propagated IOException.

Comment on lines +20 to +74
//public class RequestHandler implements Runnable {
// Socket connection;
// private static final Logger log = Logger.getLogger(RequestHandler.class.getName());
//
// private final Repository repository;
// private Controller controller = new ForwardController();
//
//
// public RequestHandler(Socket connection) {
// this.connection = connection;
// repository = MemoryUserRepository.getInstance();
// }
//
// @Override
// public void run() {
// log.log(Level.INFO, "New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort());
// try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
// BufferedReader br = new BufferedReader(new InputStreamReader(in));
// DataOutputStream dos = new DataOutputStream(out);
//
// // Header 분석
// HttpRequest httpRequest = HttpRequest.from(br);
// HttpResponse httpResponse = new HttpResponse(dos);
//
// // 요구 사항 1번
// if (httpRequest.getMethod().equals(HttpMethod.GET.getValue()) && httpRequest.getUrl().endsWith(".html")) {
// controller = new ForwardController();
// }
//
// if (httpRequest.getUrl().equals("/")) {
// controller = new HomeController();
// }
//
// // 요구 사항 2,3,4번
// if (httpRequest.getUrl().equals("/user/signup")) {
// controller = new SignUpController();
// }
//
// // 요구 사항 5번
// if (httpRequest.getUrl().equals("/user/login")) {
// controller = new LoginController();
// }
//
// // 요구 사항 6번
// if (httpRequest.getUrl().equals("/user/userList")) {
// controller = new ListController();
// }
// controller.execute(httpRequest, httpResponse);
//
// } catch (Exception e) {
// log.log(Level.SEVERE, e.getMessage());
// System.out.println(Arrays.toString(e.getStackTrace()));
// }
// }
//}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

주석 처리된 코드 제거 필요

이전 구현이 주석으로 남아있습니다. 버전 관리 시스템(Git)에서 이력을 확인할 수 있으므로, 주석 처리된 코드는 제거하는 것이 좋습니다.

-//public class RequestHandler implements Runnable {
-//    Socket connection;
-//    private static final Logger log = Logger.getLogger(RequestHandler.class.getName());
-//
-//    private final Repository repository;
-//    private Controller controller = new ForwardController();
-//
-//
-//    public RequestHandler(Socket connection) {
-//        this.connection = connection;
-//        repository = MemoryUserRepository.getInstance();
-//    }
-//
-//    @Override
-//    public void run() {
-//        log.log(Level.INFO, "New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort());
-//        try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
-//            BufferedReader br = new BufferedReader(new InputStreamReader(in));
-//            DataOutputStream dos = new DataOutputStream(out);
-//
-//            // Header 분석
-//            HttpRequest httpRequest = HttpRequest.from(br);
-//            HttpResponse httpResponse = new HttpResponse(dos);
-//
-//            // 요구 사항 1번
-//            if (httpRequest.getMethod().equals(HttpMethod.GET.getValue()) && httpRequest.getUrl().endsWith(".html")) {
-//                controller = new ForwardController();
-//            }
-//
-//            if (httpRequest.getUrl().equals("/")) {
-//                controller = new HomeController();
-//            }
-//
-//            // 요구 사항 2,3,4번
-//            if (httpRequest.getUrl().equals("/user/signup")) {
-//                controller = new SignUpController();
-//            }
-//
-//            // 요구 사항 5번
-//            if (httpRequest.getUrl().equals("/user/login")) {
-//                controller = new LoginController();
-//            }
-//
-//            // 요구 사항 6번
-//            if (httpRequest.getUrl().equals("/user/userList")) {
-//                controller = new ListController();
-//            }
-//            controller.execute(httpRequest, httpResponse);
-//
-//        } catch (Exception e) {
-//            log.log(Level.SEVERE, e.getMessage());
-//            System.out.println(Arrays.toString(e.getStackTrace()));
-//        }
-//    }
-//}
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//public class RequestHandler implements Runnable {
// Socket connection;
// private static final Logger log = Logger.getLogger(RequestHandler.class.getName());
//
// private final Repository repository;
// private Controller controller = new ForwardController();
//
//
// public RequestHandler(Socket connection) {
// this.connection = connection;
// repository = MemoryUserRepository.getInstance();
// }
//
// @Override
// public void run() {
// log.log(Level.INFO, "New Client Connect! Connected IP : " + connection.getInetAddress() + ", Port : " + connection.getPort());
// try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
// BufferedReader br = new BufferedReader(new InputStreamReader(in));
// DataOutputStream dos = new DataOutputStream(out);
//
// // Header 분석
// HttpRequest httpRequest = HttpRequest.from(br);
// HttpResponse httpResponse = new HttpResponse(dos);
//
// // 요구 사항 1번
// if (httpRequest.getMethod().equals(HttpMethod.GET.getValue()) && httpRequest.getUrl().endsWith(".html")) {
// controller = new ForwardController();
// }
//
// if (httpRequest.getUrl().equals("/")) {
// controller = new HomeController();
// }
//
// // 요구 사항 2,3,4번
// if (httpRequest.getUrl().equals("/user/signup")) {
// controller = new SignUpController();
// }
//
// // 요구 사항 5번
// if (httpRequest.getUrl().equals("/user/login")) {
// controller = new LoginController();
// }
//
// // 요구 사항 6번
// if (httpRequest.getUrl().equals("/user/userList")) {
// controller = new ListController();
// }
// controller.execute(httpRequest, httpResponse);
//
// } catch (Exception e) {
// log.log(Level.SEVERE, e.getMessage());
// System.out.println(Arrays.toString(e.getStackTrace()));
// }
// }
//}
🤖 Prompt for AI Agents
In src/main/java/webserver/RequestHandler.java around lines 20 to 74, remove the
large block of commented-out legacy RequestHandler implementation (the entire
commented class) to clean up the file; after removal, tidy imports and any
unused variables or logger references introduced by the cleanup, ensure the
active implementation remains intact and compiles, and run the build/tests to
verify no regressions before committing.

Comment on lines +91 to +95
HttpRequest httpRequest = HttpRequest.from(br);
HttpResponse httpResponse = new HttpResponse(dos);

} catch (IOException e) {
log.log(Level.SEVERE,e.getMessage());
}
}
RequestMapper requestMapper = new RequestMapper(httpRequest, httpResponse);
requestMapper.proceed();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

HttpRequest가 null인 경우 처리 필요

HttpRequest.from(br)은 Line 47에서 null을 반환할 수 있습니다 (빈 라인이거나 null인 경우). null 체크 없이 RequestMapper를 생성하면 NPE가 발생합니다.

 HttpRequest httpRequest = HttpRequest.from(br);
+if (httpRequest == null) {
+    log.log(Level.WARNING, "Invalid HTTP request received");
+    return;
+}
 HttpResponse httpResponse = new HttpResponse(dos);

 RequestMapper requestMapper = new RequestMapper(httpRequest, httpResponse);
🤖 Prompt for AI Agents
In src/main/java/webserver/RequestHandler.java around lines 91 to 95,
HttpRequest.from(br) can return null (e.g., for empty lines) and the code
currently constructs RequestMapper without a null check causing an NPE; add a
null check immediately after obtaining httpRequest and if it's null, either
close the socket/streams or write an appropriate error response (e.g., 400 Bad
Request) via the existing HttpResponse/dos and return without creating
RequestMapper, ensuring resources are cleaned up.

Comment on lines +26 to +35
public void proceed() throws IOException {

Controller controller = controllers.get(request.getUrl());

if(controller == null) {
controller = new ForwardController();
}

controller.execute(request, response);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

request.getUrl() 호출 전 null 체크 필요

Line 28에서 request.getUrl()을 호출하기 전에 request가 null이 아닌지 확인해야 합니다. HttpRequest.from()이 null을 반환할 수 있으므로 NPE 위험이 있습니다.

 public void proceed() throws IOException {
+    if (request == null) {
+        throw new IllegalStateException("HttpRequest is null");
+    }

     Controller controller = controllers.get(request.getUrl());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void proceed() throws IOException {
Controller controller = controllers.get(request.getUrl());
if(controller == null) {
controller = new ForwardController();
}
controller.execute(request, response);
}
public void proceed() throws IOException {
if (request == null) {
throw new IllegalStateException("HttpRequest is null");
}
Controller controller = controllers.get(request.getUrl());
if (controller == null) {
controller = new ForwardController();
}
controller.execute(request, response);
}
🤖 Prompt for AI Agents
In src/main/java/webserver/RequestMapper.java around lines 26 to 35, the code
calls request.getUrl() without checking if request is null; add a null check for
request before invoking getUrl() and handle the null case (e.g., set controller
= new ForwardController() or return an appropriate error response), then only
call controllers.get(request.getUrl()) when request is non-null; also optionally
log the null-request case for diagnostics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant