001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2015 ForgeRock AS.
015 */
016
017package org.forgerock.http.header;
018
019import static java.util.Collections.*;
020
021import java.text.ParseException;
022import java.text.SimpleDateFormat;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Date;
026import java.util.List;
027import java.util.Locale;
028import java.util.TimeZone;
029
030import org.forgerock.http.protocol.Cookie;
031import org.forgerock.http.protocol.Header;
032import org.forgerock.http.protocol.Response;
033
034/**
035 * Processes the <strong>{@code Set-Cookie}</strong> request message header. For
036 * more information, see the Http State Management Mechanism specification <a
037 * href="http://tools.ietf.org/html/rfc6265">RFC 6265</a>.
038 * <p>
039 * Note: This implementation is designed to be forgiving when parsing malformed
040 * cookies.
041 */
042public class SetCookieHeader extends Header {
043
044    /** The name of this header. */
045    public static final String NAME = "Set-Cookie";
046
047    /**
048     * Constructs a new header, initialized from the specified string value.
049     *
050     * @param value
051     *            The value to initialize the header from.
052     * @return The parsed header.
053     */
054    public static SetCookieHeader valueOf(String value) {
055        return new SetCookieHeader(singletonList(parseCookie(value)));
056    }
057
058    private static Cookie parseCookie(String value) {
059        List<String> parts = Arrays.asList(value.split(";"));
060        Cookie cookie = new Cookie();
061        for (String part : parts) {
062            String[] nvp = part.split("=");
063            if ("Expires".equalsIgnoreCase(nvp[0].trim())) {
064                cookie.setExpires(parseDate(nvp[1].trim()));
065            } else if ("Max-Age".equalsIgnoreCase(nvp[0].trim())) {
066                cookie.setMaxAge(parseInteger(nvp[1].trim()));
067            } else if ("Path".equalsIgnoreCase(nvp[0].trim())) {
068                cookie.setPath(nvp[1]);
069            } else if ("Domain".equalsIgnoreCase(nvp[0].trim())) {
070                cookie.setDomain(nvp[1]);
071            } else if ("Secure".equalsIgnoreCase(nvp[0].trim())) {
072                cookie.setSecure(true);
073            } else if ("HttpOnly".equalsIgnoreCase(nvp[0].trim())) {
074                cookie.setHttpOnly(true);
075            } else if (cookie.getName() == null || cookie.getName().isEmpty()) {
076                cookie.setName(nvp[0].trim());
077                cookie.setValue(nvp[1].trim());
078            }
079        }
080        if (cookie.getName() == null || cookie.getName().isEmpty()) {
081            cookie = new Cookie();
082        }
083        return cookie;
084    }
085
086    /**
087     * Constructs a new header, initialized from the specified response message.
088     *
089     * @param response
090     *            The response message to initialize the header from.
091     * @return The parsed header.
092     */
093    public static SetCookieHeader valueOf(Response response) {
094        if (response == null || !response.getHeaders().containsKey(NAME)) {
095            return null;
096        }
097        return valueOf(response.getHeaders().get(NAME).getValues());
098    }
099
100    /**
101     * Constructs a new header, initialized from the specified list of Set-Cookie values.
102     *
103     * @param values
104     *            The values to initialize the header from.
105     * @return The parsed header.
106     */
107    public static SetCookieHeader valueOf(List<String> values) {
108        if (values == null) {
109            return null;
110        }
111        List<Cookie> cookies = new ArrayList<>();
112        for (String headerValue : values) {
113            cookies.add(parseCookie(headerValue));
114        }
115        return new SetCookieHeader(unmodifiableList(cookies));
116    }
117
118    private static Integer parseInteger(String s) {
119        try {
120            return Integer.valueOf(s);
121        } catch (NumberFormatException nfe) {
122            return null;
123        }
124    }
125
126    private static final String EXPIRES_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z";
127
128    private static SimpleDateFormat getDateFormatter() {
129        SimpleDateFormat formatter = new SimpleDateFormat(EXPIRES_DATE_FORMAT, Locale.ROOT);
130        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
131        return formatter;
132    }
133
134    private static Date parseDate(String s) {
135        try {
136            return getDateFormatter().parse(s);
137        } catch (ParseException e) {
138            return null;
139        }
140    }
141
142    private final List<Cookie> cookies;
143    private final List<String> values;
144
145    /**
146     * Constructs a new header with the provided cookies.
147     *
148     * @param cookies The cookies.
149     */
150    public SetCookieHeader(List<Cookie> cookies) {
151        this.cookies = cookies;
152        if (cookies != null) {
153            this.values = new ArrayList<>();
154            for (Cookie cookie : cookies) {
155                values.add(toString(cookie));
156            }
157        } else {
158            values = null;
159        }
160    }
161
162    @Override
163    public String getName() {
164        return NAME;
165    }
166
167    @Override
168    public List<String> getValues() {
169        return values;
170    }
171
172    /**
173     * Returns the cookies.
174     *
175     * @return The cookies.
176     */
177    public List<Cookie> getCookies() {
178        return cookies;
179    }
180
181    private String toString(Cookie cookie) {
182        StringBuilder sb = new StringBuilder();
183        if (cookie.getName() != null) {
184            sb.append(cookie.getName()).append("=").append(cookie.getValue());
185            if (cookie.getExpires() != null) {
186                sb.append("; ").append("Expires").append("=").append(getDateFormatter().format(cookie.getExpires()));
187            }
188            if (cookie.getMaxAge() != null && cookie.getMaxAge() > 0) {
189                sb.append("; ").append("Max-Age").append("=").append(cookie.getMaxAge());
190            }
191            if (cookie.getPath() != null) {
192                sb.append("; ").append("Path").append("=").append(cookie.getPath());
193            }
194            if (cookie.getDomain() != null) {
195                sb.append("; ").append("Domain").append("=").append(cookie.getDomain());
196            }
197            if (cookie.isSecure() != null && cookie.isSecure()) {
198                sb.append("; ").append("Secure");
199            }
200            if (cookie.isHttpOnly() != null && cookie.isHttpOnly()) {
201                sb.append("; ").append("HttpOnly");
202            }
203        }
204        return sb.toString();
205    }
206
207    static class Factory extends HeaderFactory<SetCookieHeader> {
208
209        @Override
210        public SetCookieHeader parse(String value) {
211            return valueOf(value);
212        }
213
214        @Override
215        public SetCookieHeader parse(List<String> values) {
216            return valueOf(values);
217        }
218    }
219}