diff --git a/src/main/java/http/request/Cookies.java b/src/main/java/http/request/Cookies.java new file mode 100644 index 000000000..82fdd7d25 --- /dev/null +++ b/src/main/java/http/request/Cookies.java @@ -0,0 +1,47 @@ +package http.request; + +import java.util.*; + +public class Cookies { + private final Map values; + private Cookies(Map values) { + this.values = Collections.unmodifiableMap(values); + } + + public static Cookies empty() { + return new Cookies(new HashMap<>()); + } + + //TODO: XSS 방어 코드 추가 + public static Cookies parse(String rawHeader) { + if (rawHeader == null || rawHeader.isBlank()) return empty(); + + Map 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); + } + } + return new Cookies(map); + } + + public Optional get(String name) { + return Optional.ofNullable(values.get(name)); + } + + public boolean contains(String name) { + return values.containsKey(name); + } + + public Map asMap() { + return values; + } +} diff --git a/src/main/java/http/request/HttpRequest.java b/src/main/java/http/request/HttpRequest.java index 2b73186e0..824551de3 100644 --- a/src/main/java/http/request/HttpRequest.java +++ b/src/main/java/http/request/HttpRequest.java @@ -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 headers; //TODO: Map> 타입으로 변경 + private Cookies cookies; private String httpVersion; private final URI uri; private String contentType; @@ -28,30 +30,55 @@ private HttpRequest (HttpMethod method, this.uri = URI.create(target); this.httpVersion = httpVersion; this.headers = new HashMap<>(); + this.cookies = Cookies.empty(); + } + + 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 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 getHeaders(){ return headers.keySet().stream().toList(); } + public Cookies getCookies(){ + return this.cookies; + } + + public Optional getCookieValue(String key){ + return this.cookies.get(key); + } + public String getPath(){ return uri.getPath(); } @@ -72,20 +99,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; } diff --git a/src/main/java/http/response/CookieBuilder.java b/src/main/java/http/response/CookieBuilder.java new file mode 100644 index 000000000..41efb7422 --- /dev/null +++ b/src/main/java/http/response/CookieBuilder.java @@ -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); + } + + 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); + + 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(); + } +} diff --git a/src/main/java/http/response/ResponseCookie.java b/src/main/java/http/response/ResponseCookie.java new file mode 100644 index 000000000..9d6f44db1 --- /dev/null +++ b/src/main/java/http/response/ResponseCookie.java @@ -0,0 +1,5 @@ +package http.response; + +public interface ResponseCookie { + String toHeaderValue(); +} diff --git a/src/main/java/http/response/SimpleCookieBuilder.java b/src/main/java/http/response/SimpleCookieBuilder.java new file mode 100644 index 000000000..0c8f31e12 --- /dev/null +++ b/src/main/java/http/response/SimpleCookieBuilder.java @@ -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; + } +} diff --git a/src/main/java/web/renderer/StaticViewRenderer.java b/src/main/java/web/renderer/StaticViewRenderer.java index dae1a9b84..d61dc0d01 100644 --- a/src/main/java/web/renderer/StaticViewRenderer.java +++ b/src/main/java/web/renderer/StaticViewRenderer.java @@ -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; } catch (IOException e) { diff --git a/src/main/java/web/response/HandlerResponse.java b/src/main/java/web/response/HandlerResponse.java index c2fe1ac04..884d8df86 100644 --- a/src/main/java/web/response/HandlerResponse.java +++ b/src/main/java/web/response/HandlerResponse.java @@ -1,15 +1,30 @@ package web.response; import http.HttpStatus; +import http.response.ResponseCookie; + +import java.util.ArrayList; +import java.util.List; public abstract class HandlerResponse { protected final HttpStatus status; + protected final List cookies; + protected HandlerResponse(HttpStatus status) { this.status = status; + this.cookies = new ArrayList<>(); } public HttpStatus getStatus(){ return status; } + + public void setCookie(ResponseCookie cookie){ + this.cookies.add(cookie); + } + + public List getCookies(){ + return this.cookies.stream().map(ResponseCookie::toHeaderValue).toList(); + } }