Skip to content
Open
3 changes: 2 additions & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/main/java/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;

import java.io.IOException;

public interface Controller {
void execute(HttpRequest request, HttpResponse response) throws IOException;
}
24 changes: 24 additions & 0 deletions src/main/java/controller/ForwardController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ForwardController implements Controller {
private static final Logger log = Logger.getLogger(ForwardController.class.getName());

@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath();
if (path == null || path.isEmpty() || "/".equals(path)) {
path = "index.html";
} else if (path.startsWith("/")) {
path = path.substring(1);
}
log.log(Level.INFO, "Forward static resource path={0}", path);
response.forward(path);
}
}
13 changes: 13 additions & 0 deletions src/main/java/controller/HomeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package controller;

import http.HttpRequest;
import http.HttpResponse;

import java.io.IOException;

public class HomeController implements Controller {
@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
response.forward("index.html");
}
}
48 changes: 48 additions & 0 deletions src/main/java/controller/ListController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package controller;

import http.CookieName;
import http.HttpRequest;
import http.HttpResponse;
import webserver.UrlPath;

import java.io.IOException;

public class ListController implements Controller {
@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
String logined = request.getCookie(CookieName.LOGINED.key());
if (!"true".equalsIgnoreCase(logined)) {
response.forward(UrlPath.USER_LOGIN_PAGE.value());
return;
}

response.forward(UrlPath.USER_LIST_HTML.value());

// String path = request.getPath();
// if (path == null) path = "/user/list";
// int q = path.indexOf('?');
// if (q >= 0) path = path.substring(0, q);
// String normalized = path.startsWith("/") ? path.substring(1) : path;
//
// // 3) 표준 경로만 서비스 (user/list, user/list.html, user/list/ 허용)
// if (normalized.equals("user/list") || normalized.equals("user/list.html") || normalized.equals("user/list/")) {
// response.forward("user/list.html"); // 실제 파일로 고정
// return;
// }
//
// // 4) 과거/실수 경로 보정: /user/userList* → /user/list 로 리다이렉트
// if (normalized.equals("user/userList") || normalized.equals("user/userList.html") || normalized.equals("user/userList/")) {
// response.response302Header("/user/list.html");
// return;
// }

// if (normalized.startsWith("user/userList")) {
// response.forward("user/list.html");
// return;
// }

// 4) 그 외는 필요에 따라 404 또는 다른 처리
response.send404("/user/userList");

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

import db.MemoryUserRepository;
import db.Repository;
import http.*;
import model.User;
import model.UserQueryKey;
import webserver.UrlPath;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LoginController implements Controller {
private static final Logger log = Logger.getLogger(LoginController.class.getName());
private final Repository repository = MemoryUserRepository.getInstance();

@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
if (request.getMethod() == HttpMethod.GET) {
response.forward("user/login.html");
return;
}

if (request.getMethod() != HttpMethod.POST) {
response.response302Header(UrlPath.USER_LOGIN_FAILED.value());
return;
}

String userId = request.getParameter(UserQueryKey.USER_ID.key());
User user = repository.findUserById(userId);
log.log(Level.INFO, "Login attempt userId={0}", userId);
if (user != null && user.getPassword().equals(request.getParameter(UserQueryKey.PASSWORD.key()))) {
Comment on lines +30 to +33
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

입력 검증 및 null 안전성 검증이 필요합니다.

request.getParameter()가 null을 반환할 수 있으므로, Line 34와 Line 36에서 NPE가 발생할 수 있습니다. userId와 password에 대한 null 체크를 추가하세요.

다음 diff를 적용하여 null 체크를 추가하세요:

 String userId = request.getParameter(UserQueryKey.USER_ID.key());
+if (userId == null || userId.isEmpty()) {
+    log.log(Level.INFO, "Login failed: missing userId");
+    response.response302Header(UrlPath.USER_LOGIN_FAILED.value());
+    return;
+}
+
 User user = repository.findUserById(userId);
 log.log(Level.INFO, "Login attempt userId={0}", userId);
-if (user != null && user.getPassword().equals(request.getParameter(UserQueryKey.PASSWORD.key()))) {
+String password = request.getParameter(UserQueryKey.PASSWORD.key());
+if (password == null || password.isEmpty()) {
+    log.log(Level.INFO, "Login failed userId={0}: missing password", userId);
+    response.response302Header(UrlPath.USER_LOGIN_FAILED.value());
+    return;
+}
+if (user != null && user.getPassword().equals(password)) {
📝 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
String userId = request.getParameter(UserQueryKey.USER_ID.key());
User user = repository.findUserById(userId);
log.log(Level.INFO, "Login attempt userId={0}", userId);
if (user != null && user.getPassword().equals(request.getParameter(UserQueryKey.PASSWORD.key()))) {
String userId = request.getParameter(UserQueryKey.USER_ID.key());
if (userId == null || userId.isEmpty()) {
log.log(Level.INFO, "Login failed: missing userId");
response.response302Header(UrlPath.USER_LOGIN_FAILED.value());
return;
}
User user = repository.findUserById(userId);
log.log(Level.INFO, "Login attempt userId={0}", userId);
String password = request.getParameter(UserQueryKey.PASSWORD.key());
if (password == null || password.isEmpty()) {
log.log(Level.INFO, "Login failed userId={0}: missing password", userId);
response.response302Header(UrlPath.USER_LOGIN_FAILED.value());
return;
}
if (user != null && user.getPassword().equals(password)) {
🤖 Prompt for AI Agents
In src/main/java/controller/LoginController.java around lines 33 to 36,
request.getParameter(...) can return null causing NPEs; ensure you validate
userId and password before use by: check for null/empty userId after fetching it
and short-circuit (log and return an error/unauthorized response) if missing;
when comparing passwords, null-check the retrieved user and the password
parameter (or use a null-safe comparison such as
Objects.equals(user.getPassword(), passwordParam)) so you never call methods on
a null reference; update the log to reflect missing parameters and handle the
early return flow accordingly.

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

평문 비밀번호 비교는 보안 위험입니다.

현재 구현은 평문으로 비밀번호를 비교합니다. 실제 프로덕션 환경에서는 BCrypt, Argon2 등의 해시 알고리즘을 사용하여 비밀번호를 안전하게 비교해야 합니다.

학습/데모 목적이라면 현재 구현이 적절할 수 있지만, 실제 배포 환경에서는 비밀번호 해싱을 반드시 구현해야 합니다.


🏁 Script executed:

#!/bin/bash
# Search for BCrypt usage in Java files
rg -n --type=java 'BCrypt' -g '*.java'

# Search for PasswordEncoder usage in Java files
rg -n --type=java 'PasswordEncoder' -g '*.java'

# Display imports and password-related declarations in LoginController.java
sed -n '1,80p' src/main/java/controller/LoginController.java

Length of output: 1638


비밀번호 평문 비교 제거 및 해시 알고리즘 적용
src/main/java/controller/LoginController.java:36
현재 사용자 비밀번호를 평문으로 직접 비교하고 있어 보안 취약점이 있습니다. BCrypt(또는 Argon2) 기반 해시 및 비교 로직으로 변경해야 합니다.

🤖 Prompt for AI Agents
In src/main/java/controller/LoginController.java around line 36, the code
directly compares plain-text passwords which is insecure; change it to use a
password-hashing check (e.g., BCrypt or Argon2). Replace the plain equality
check with a call to a password encoder's match method (for Spring, inject/use
BCryptPasswordEncoder or Argon2PasswordEncoder and call
encoder.matches(request.getParameter(PASSWORD), user.getPassword())). Ensure
user.getPassword() contains the stored hash, handle possible nulls, and do not
log the raw password; if the project does not yet persist hashed passwords,
migrate stored passwords to hashes before enabling this check.

response.addHeader(HttpHeader.SET_COOKIE, CookieName.LOGINED.key() + "=true; Path=/; HttpOnly; SameSite=Lax");

log.log(Level.INFO, "Login success userId={0}", userId);
response.response302Header(UrlPath.INDEX.value());
} else {
log.log(Level.INFO, "Login failed userId={0}", userId);
response.response302Header(UrlPath.USER_LOGIN_FAILED.value());
}
}
}
67 changes: 67 additions & 0 deletions src/main/java/controller/SignUpController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package controller;

import db.MemoryUserRepository;
import db.Repository;
import http.HttpMethod;
import http.HttpRequest;
import http.HttpResponse;
import model.User;
import model.UserQueryKey;
import webserver.UrlPath;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class SignUpController implements Controller {
private static final Logger log = Logger.getLogger(SignUpController.class.getName());
private final Repository repository = MemoryUserRepository.getInstance();
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

MemoryUserRepository는 스레드 안전하지 않습니다(싱글턴/저장 Map 모두).

요청은 멀티스레드로 처리되므로, 현재 HashMap과 비동기 안전한 getInstance 구현은 경쟁 상태, 데이터 손상 위험이 있습니다. ConcurrentHashMap과 안전한 싱글턴으로 교체하세요.

아래와 같이 리팩터링(별도 파일 수정):

// in MemoryUserRepository.java
import java.util.concurrent.ConcurrentHashMap;

public class MemoryUserRepository implements Repository {
    private final Map<String, User> users = new ConcurrentHashMap<>();
    private static final MemoryUserRepository INSTANCE = new MemoryUserRepository();
    private MemoryUserRepository() {}
    public static MemoryUserRepository getInstance() {
        return INSTANCE;
    }
    public void addUser(User user) { users.put(user.getUserId(), user); }
    // 기타 메서드 동일
}
🤖 Prompt for AI Agents
In src/main/java/controller/SignUpController.java around line 18, the controller
currently uses MemoryUserRepository.getInstance() which returns a
non-thread-safe singleton backed by a HashMap; replace MemoryUserRepository with
a thread-safe implementation: change the backing Map to a
java.util.concurrent.ConcurrentHashMap, implement an eager, thread-safe
singleton (private constructor + private static final INSTANCE and a simple
getInstance() returning INSTANCE), and keep repository methods the same but
operating on the ConcurrentHashMap so concurrent requests cannot corrupt state.


@Override
public void execute(HttpRequest request, HttpResponse response) throws IOException {
HttpMethod method = request.getMethod();

// Requirement 2 : GET 방식으로 회원가입하기
// if (method != HttpMethod.GET && method != HttpMethod.POST) {
// response.send404(request.getPath());
// return;
// }
Comment on lines +24 to +28
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

주석 처리된 코드를 제거하세요.

더 이상 사용하지 않는 코드는 버전 관리 시스템에 기록되어 있으므로 삭제하는 것이 좋습니다. 코드베이스를 깔끔하게 유지할 수 있습니다.

다음과 같이 주석 처리된 코드를 제거하세요:

-        // Requirement 2 : GET 방식으로 회원가입하기
-//        if (method != HttpMethod.GET && method != HttpMethod.POST) {
-//                response.send404(request.getPath());
-//                return;
-//        }
-
         // Requirement 3 : POST 방식으로 회원가입하기
📝 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
// Requirement 2 : GET 방식으로 회원가입하기
// if (method != HttpMethod.GET && method != HttpMethod.POST) {
// response.send404(request.getPath());
// return;
// }
// Requirement 3 : POST 방식으로 회원가입하기
🤖 Prompt for AI Agents
In src/main/java/controller/SignUpController.java around lines 24 to 28, remove
the commented-out HTTP method check block (the three-line commented code that
checks if method is GET or POST) entirely — delete those commented lines from
the file, ensure no other code depends on them, recompile/run tests to verify
nothing breaks, and commit the cleaned-up file.


// Requirement 3 : POST 방식으로 회원가입하기
if (method == HttpMethod.GET) {
// response.forward("user/form.html");
response.response302Header("/index.html");
return;
}

if (method != HttpMethod.POST) {
response.send404(request.getPath());
return;
}

String userId = normalized(request, UserQueryKey.USER_ID);
String password = normalized(request, UserQueryKey.PASSWORD);
String name = normalized(request, UserQueryKey.NAME);
String email = normalized(request, UserQueryKey.EMAIL);

if (userId.isEmpty() || password.isEmpty() || name.isEmpty() || email.isEmpty()) {
log.log(Level.WARNING, "Missing sign-up parameters method={0} userId={1}", new Object[]{method, userId});
response.response302Header(UrlPath.USER_SIGNUP_FORM.value());
return;
}

User user = new User(
userId,
password,
name,
email);
repository.addUser(user);
log.log(Level.INFO, "User signup method={0} userId={1}", new Object[]{method, user.getUserId()});
response.response302Header(UrlPath.INDEX.value());
}
Comment on lines +20 to +61
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

가입은 POST 전용으로 제한하고, GET은 폼 포워딩으로 분리하세요. 중복 가입도 막아야 합니다.

현재 GET에서도 모든 파라미터가 채워지면 가입이 진행됩니다. 이는 보안/정합성 측면에서 위험합니다. 가입은 POST에서만 처리하고, GET은 폼을 포워딩하세요. 또한 동일 userId 덮어쓰기를 방지하기 위해 중복 체크가 필요합니다.

아래와 같이 분기 구조를 단순화해 주세요(중복 체크는 Repository 인터페이스에 findUserById가 있다면 추가).

-    public void execute(HttpRequest request, HttpResponse response) throws IOException {
-        HttpMethod method = request.getMethod();
-        if (method != HttpMethod.GET && method != HttpMethod.POST) {
-            response.send404(request.getPath());
-            return;
-        }
-
-        String userId = normalized(request, UserQueryKey.USER_ID);
-        String password = normalized(request, UserQueryKey.PASSWORD);
-        String name = normalized(request, UserQueryKey.NAME);
-        String email = normalized(request, UserQueryKey.EMAIL);
-
-        if (userId.isEmpty() || password.isEmpty() || name.isEmpty() || email.isEmpty()) {
-            log.log(Level.WARNING, "Missing sign-up parameters method={0} userId={1}", new Object[]{method, userId});
-            response.response302Header(UrlPath.USER_SIGNUP_FORM.value());
-            return;
-        }
-
-        User user = new User(
-                userId,
-                password,
-                name,
-                email);
-        repository.addUser(user);
-        log.log(Level.INFO, "User signup method={0} userId={1}", new Object[]{method, user.getUserId()});
-        response.response302Header(UrlPath.INDEX.value());
-    }
+    public void execute(HttpRequest request, HttpResponse response) throws IOException {
+        HttpMethod method = request.getMethod();
+
+        // GET: 폼만 제공
+        if (method == HttpMethod.GET) {
+            response.forward(UrlPath.USER_SIGNUP_FORM.value());
+            return;
+        }
+        // POST 이외: 거부
+        if (method != HttpMethod.POST) {
+            response.send404(request.getPath());
+            return;
+        }
+
+        String userId = normalized(request, UserQueryKey.USER_ID);
+        String password = normalized(request, UserQueryKey.PASSWORD);
+        String name = normalized(request, UserQueryKey.NAME);
+        String email = normalized(request, UserQueryKey.EMAIL);
+
+        if (userId.isEmpty() || password.isEmpty() || name.isEmpty() || email.isEmpty()) {
+            log.log(Level.WARNING, "Missing sign-up parameters method={0} userId={1}", new Object[]{method, userId});
+            response.response302Header(UrlPath.USER_SIGNUP_FORM.value());
+            return;
+        }
+
+        // TODO: Repository에 findUserById가 있다면 중복 체크 추가
+        // if (repository.findUserById(userId) != null) {
+        //     response.response302Header(UrlPath.USER_SIGNUP_FORM.value());
+        //     return;
+        // }
+
+        User user = new User(userId, password, name, email);
+        repository.addUser(user);
+        log.log(Level.INFO, "User signup method={0} userId={1}", new Object[]{method, user.getUserId()});
+        response.response302Header(UrlPath.INDEX.value());
+    }

Repository에 중복 조회 메서드 존재 여부 확인용 스크립트:


🏁 Script executed:

#!/bin/bash
# Repository 인터페이스/구현에 사용자 조회 메서드가 있는지 확인
rg -nP -C2 '(interface|class)\s+Repository\b' | sed -n '1,120p'
rg -nP -C3 'class\s+MemoryUserRepository\b'
rg -nP -C3 'findUserById\s*\('

Length of output: 2375


GET 요청은 폼 렌더링으로만 처리하고, POST에서만 가입 로직을 수행하세요
repository.findUserById(String userId)를 사용해 중복 userId를 검사하고, 중복 시 가입을 차단해야 합니다.

🤖 Prompt for AI Agents
In src/main/java/controller/SignUpController.java around lines 20 to 47, the
controller currently runs signup logic for both GET and POST and does not check
for duplicate userIds; update it so GET only renders the signup form and all
signup processing runs only on POST, validate parameters inside the POST branch,
call repository.findUserById(userId) before creating a new User and if a user
already exists log a warning and redirect back to the signup form (no creation),
otherwise create and add the user and redirect to the index; keep the existing
logging and redirects for success/failure paths.


private String normalized(HttpRequest request, UserQueryKey key) {
String value = request.getParameter(key.key());
return value == null ? "" : value.trim();
}
}
8 changes: 8 additions & 0 deletions src/main/java/http/CookieName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package http;

public enum CookieName {
LOGINED("logined");
private final String key;
CookieName(String key){ this.key = key; }
public String key(){ return key; }
}
34 changes: 34 additions & 0 deletions src/main/java/http/FormatType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package http;
import java.util.Set;

public enum FormatType {
HTML("text/html; charset=utf-8", Set.of(".html", ".htm")),
CSS("text/css", Set.of(".css")),
JS("application/javascript; charset=utf-8", Set.of(".js")),
JSON("application/json; charset=utf-8", Set.of(".json")),
SVG("image/svg+xml", Set.of(".svg")),
PNG("image/png", Set.of(".png")),
JPEG("image/jpeg", Set.of(".jpg", ".jpeg")),
GIF("image/gif", Set.of(".gif")),
ICO("image/x-icon", Set.of(".ico")),
OCTET("application/octet-stream",Set.of());

private final String contentType;
private final Set<String> exts;

FormatType(String contentType, Set<String> exts) {
this.contentType = contentType;
this.exts = exts;
}
public String contentType() { return contentType; }

public static String fromFilename(String name) {
String n = name.toLowerCase();
for (FormatType mt : values()) {
for (String ext : mt.exts) {
if (n.endsWith(ext)) return mt.contentType;
}
}
return OCTET.contentType;
}
}
19 changes: 19 additions & 0 deletions src/main/java/http/HttpHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package http;

public enum HttpHeader {
CONTENT_TYPE("Content-Type"),
CONTENT_LENGTH("Content-Length"),
LOCATION("Location"),
SET_COOKIE("Set-Cookie"),
COOKIE("Cookie");

private final String value;

HttpHeader(String value) {
this.value = value;
}

public String value() {
return value;
}
}
14 changes: 14 additions & 0 deletions src/main/java/http/HttpMethod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package http;

public enum HttpMethod {
GET,
POST;

public boolean isEqual(HttpMethod method) {
return this == method;
}

public boolean isEqual(String rawMethod) {
return rawMethod != null && name().equalsIgnoreCase(rawMethod);
}
}
Loading