-
Notifications
You must be signed in to change notification settings - Fork 0
[Http] 쿠키 개발 #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Http] 쿠키 개발 #42
Changes from 3 commits
67d2a22
43fd32e
7c9b243
60821bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package http.request; | ||
|
|
||
| import java.util.*; | ||
|
|
||
| public class Cookies { | ||
| private final Map<String, String> values; | ||
| private Cookies(Map<String, String> values) { | ||
| this.values = Collections.unmodifiableMap(values); | ||
| } | ||
|
|
||
| public static Cookies empty() { | ||
| return new Cookies(new HashMap<>()); | ||
| } | ||
|
|
||
| public static Cookies parse(String rawHeader) { | ||
| if (rawHeader == null || rawHeader.isBlank()) return empty(); | ||
|
|
||
| Map<String, String> map = new LinkedHashMap<>(); | ||
| String[] parts = rawHeader.split(";"); | ||
| for (String part : parts) { | ||
| String token = part.trim(); | ||
| if (token.isEmpty()) continue; | ||
|
|
||
| String[] kv = token.split("=", 2); | ||
| String name = kv[0].trim(); | ||
| String value = kv.length == 2 ? kv[1].trim() : ""; | ||
|
|
||
| if (!name.isEmpty()) { | ||
| map.put(name, value); // 동일 name은 마지막 값으로 덮어씀(브라우저/서버 실무 관행) | ||
| } | ||
| } | ||
| return new Cookies(map); | ||
| } | ||
|
|
||
| public Optional<String> get(String name) { | ||
| return Optional.ofNullable(values.get(name)); | ||
| } | ||
|
|
||
| public boolean contains(String name) { | ||
| return values.containsKey(name); | ||
| } | ||
|
|
||
| public Map<String, String> asMap() { | ||
| return values; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,10 +10,12 @@ | |
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
||
| public class HttpRequest { | ||
| private final HttpMethod method; | ||
| private final Map<String, String> headers; //TODO: Map<String, List<String>> 타입으로 변경 | ||
| private Cookies cookies; | ||
| private String httpVersion; | ||
| private final URI uri; | ||
| private String contentType; | ||
|
|
@@ -30,28 +32,52 @@ private HttpRequest (HttpMethod method, | |
| this.headers = new HashMap<>(); | ||
| } | ||
|
|
||
| public static HttpRequest from(String requestLine){ | ||
| String[] parts = requestLine.split(" "); | ||
| 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"); | ||
| } | ||
| } | ||
|
|
||
|
Comment on lines
41
to
49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NPE 위험:
해결책: 생성자에서
Comment on lines
45
to
49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NPE 위험:
해결책: |
||
| public byte[] getBody() { | ||
| return body; | ||
| } | ||
|
|
||
| public String getContentType() { | ||
| return contentType; | ||
| } | ||
| public String getHeader(String key){ | ||
| return headers.get(key.toLowerCase()); | ||
| } | ||
|
|
||
| public void setHeader(String key, String value){ | ||
| headers.put(key.toLowerCase(), value); | ||
| if (key.equalsIgnoreCase("Content-Type")) { | ||
| this.contentType = value; | ||
| } else if (key.equalsIgnoreCase("Cookie")) { | ||
| this.cookies = Cookies.parse(value); | ||
| } | ||
| } | ||
|
|
||
| public String getHeader(String key){ | ||
| return headers.get(key.toLowerCase()); | ||
| } | ||
|
|
||
| public List<String> getHeaders(){ | ||
| return headers.keySet().stream().toList(); | ||
| } | ||
|
|
||
| public Cookies getCookies(){ | ||
| return this.cookies; | ||
| } | ||
|
|
||
| public Optional<String> getCookieValue(String key){ | ||
| return this.cookies.get(key); | ||
| } | ||
|
|
||
| public String getPath(){ | ||
| return uri.getPath(); | ||
| } | ||
|
|
@@ -72,20 +98,6 @@ public InetAddress getRequestAddress() { | |
| return requestAddress; | ||
| } | ||
|
|
||
| public static HttpRequest from(String requestLine){ | ||
| String[] parts = requestLine.split(" "); | ||
| 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"); | ||
| } | ||
| } | ||
|
|
||
| public void setBody(byte[] body){ | ||
| this.body = body; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package http.response; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.ZonedDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.Objects; | ||
|
|
||
| public class CookieBuilder implements ResponseCookie{ | ||
| private final String name; | ||
| private final String value; | ||
|
|
||
| private String path; | ||
| private String domain; | ||
| private Long maxAge; // seconds | ||
| private ZonedDateTime expires; | ||
| private boolean httpOnly; | ||
| private boolean secure; | ||
| private SameSite sameSite; | ||
|
|
||
| public enum SameSite { STRICT, LAX, NONE } | ||
|
|
||
| private CookieBuilder(String name, String value) { | ||
| if (name == null || name.isBlank()) throw new IllegalArgumentException("cookie name required"); | ||
| this.name = name; | ||
| this.value = value == null ? "" : value; | ||
| } | ||
|
|
||
| public static CookieBuilder of(String name, String value) { | ||
| return new CookieBuilder(name, value); | ||
| } | ||
|
|
||
| public static CookieBuilder delete(String name) { | ||
| return CookieBuilder.of(name, "") | ||
| .path("/") // 보통 명시 | ||
| .maxAge(0); | ||
| } | ||
|
|
||
|
Comment on lines
+33
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 논리 오류: SameSite=None일 때 Secure 플래그가 필수인데, 이를 강제하지 않습니다.
해결책: |
||
| public CookieBuilder path(String path) { | ||
| this.path = path; | ||
| return this; | ||
| } | ||
|
|
||
| public CookieBuilder domain(String domain) { | ||
| this.domain = domain; | ||
| return this; | ||
| } | ||
|
|
||
| public CookieBuilder maxAge(long seconds) { | ||
| if (seconds < 0) throw new IllegalArgumentException("maxAge must be >= 0"); | ||
| this.maxAge = seconds; | ||
| return this; | ||
| } | ||
|
|
||
| public CookieBuilder maxAge(Duration duration) { | ||
| Objects.requireNonNull(duration); | ||
| return maxAge(duration.getSeconds()); | ||
| } | ||
|
|
||
| public CookieBuilder expires(ZonedDateTime expires) { | ||
| this.expires = expires; | ||
| return this; | ||
| } | ||
|
|
||
| public CookieBuilder httpOnly() { | ||
| this.httpOnly = true; | ||
| return this; | ||
| } | ||
|
|
||
| public CookieBuilder secure() { | ||
| this.secure = true; | ||
| return this; | ||
| } | ||
|
|
||
| public CookieBuilder sameSite(SameSite sameSite) { | ||
| this.sameSite = sameSite; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public String toHeaderValue() { | ||
| StringBuilder sb = new StringBuilder(); | ||
| sb.append(name).append("=").append(value); | ||
|
|
||
| if (domain != null && !domain.isBlank()) sb.append("; Domain=").append(domain); | ||
| if (path != null && !path.isBlank()) sb.append("; Path=").append(path); | ||
|
Comment on lines
+77
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. XSS/Injection 위험: 쿠키 값이 검증되지 않은 채로 직접 HTTP 헤더에 추가됩니다.
해결책: 쿠키 값을 검증하거나 인코딩하세요. RFC 6265 표준에 따라 유효한 문자인지 확인하고, 필요시 URL 인코딩을 적용하세요. |
||
|
|
||
| if (maxAge != null) sb.append("; Max-Age=").append(maxAge); | ||
|
|
||
| if (expires != null) { | ||
| sb.append("; Expires=").append(DateTimeFormatter.RFC_1123_DATE_TIME.format(expires)); | ||
| } | ||
|
|
||
| if (secure) sb.append("; Secure"); | ||
| if (httpOnly) sb.append("; HttpOnly"); | ||
|
|
||
| if (sameSite != null) { | ||
| sb.append("; SameSite=").append( | ||
| switch (sameSite) { | ||
| case STRICT -> "Strict"; | ||
| case LAX -> "Lax"; | ||
| case NONE -> "None"; | ||
| } | ||
| ); | ||
| } | ||
| return sb.toString(); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return toHeaderValue(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package http.response; | ||
|
|
||
| public interface ResponseCookie { | ||
| String toHeaderValue(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package http.response; | ||
|
|
||
| public class SimpleCookieBuilder implements ResponseCookie{ | ||
| private final String cookieString; | ||
|
|
||
| private SimpleCookieBuilder(String str){ | ||
| this.cookieString = str; | ||
| } | ||
|
|
||
| public static SimpleCookieBuilder of(String str){ | ||
| return new SimpleCookieBuilder(str); | ||
| } | ||
|
|
||
| @Override | ||
| public String toHeaderValue() { | ||
| return cookieString; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ public HttpResponse handle(HandlerResponse handlerResponse) { | |
|
|
||
| HttpResponse httpResponse = HttpResponse.of(handlerResponse.getStatus()); | ||
| httpResponse.setBody(file, body); | ||
| handlerResponse.getCookies().forEach(cookie->httpResponse.addHeader("Set-Cookie", cookie)); | ||
| return httpResponse; | ||
|
Comment on lines
32
to
34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NPE 위험:
해결책: defensive programming으로 |
||
|
|
||
| } catch (IOException e) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,31 @@ | ||
| package web.response; | ||
|
|
||
| import http.HttpStatus; | ||
| import http.response.CookieBuilder; | ||
| import http.response.ResponseCookie; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| public abstract class HandlerResponse { | ||
| protected final HttpStatus status; | ||
| protected final List<ResponseCookie> cookies; | ||
|
|
||
|
|
||
| protected HandlerResponse(HttpStatus status) { | ||
| this.status = status; | ||
| this.cookies = new ArrayList<>(); | ||
| } | ||
|
|
||
| public HttpStatus getStatus(){ | ||
| return status; | ||
| } | ||
|
|
||
| public void setCookie(CookieBuilder cookie){ | ||
| this.cookies.add(cookie); | ||
|
Comment on lines
20
to
24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 타입 불일치 위험:
해결책: 메서드를 |
||
| } | ||
|
|
||
| public List<String> getCookies(){ | ||
| return this.cookies.stream().map(ResponseCookie::toHeaderValue).toList(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XSS 위험: 요청 쿠키 값이 검증되지 않습니다.
parse()메서드가 쿠키 헤더 값을 그대로 분석하여 저장합니다.values맵에 저장되어 나중에 사용될 때 문제가 될 수 있습니다.해결책: 쿠키 값의 유효성을 검증하거나, 사용 시점에 필요한 인코딩/디코딩을 적용하세요.