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 2010–2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2015 ForgeRock AS.
016 */
017
018package org.forgerock.http.header;
019
020import static java.util.Collections.*;
021import static org.forgerock.http.header.HeaderUtil.*;
022
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026
027import org.forgerock.http.protocol.Cookie;
028import org.forgerock.http.protocol.Header;
029import org.forgerock.http.protocol.Request;
030
031/**
032 * Processes the <strong>{@code Cookie}</strong> request message header. For
033 * more information, see the original <a href=
034 * "http://web.archive.org/web/20070805052634/http://wp.netscape.com/newsref/std/cookie_spec.html"
035 * > Netscape specification</a>, <a
036 * href="http://www.ietf.org/rfc/rfc2109.txt">RFC 2109</a> and <a
037 * href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a>.
038 * <p>
039 * Note: This implementation is designed to be forgiving when parsing malformed
040 * cookies.
041 */
042public class CookieHeader extends Header {
043    private static CookieHeader valueOf(final List<String> values) {
044        List<Cookie> cookies = new ArrayList<>(values.size());
045        Integer version = null;
046        Cookie cookie = new Cookie();
047        for (String s1 : values) {
048            for (String s2 : HeaderUtil.split(s1, ';')) {
049                String[] nvp = HeaderUtil.parseParameter(s2);
050                if (nvp[0].length() > 0 && nvp[0].charAt(0) != '$') {
051                    if (cookie.getName() != null) {
052                        // existing cookie was being parsed
053                        cookies.add(cookie);
054                    }
055                    cookie = new Cookie();
056                    // inherit previous parsed version
057                    cookie.setVersion(version);
058                    cookie.setName(nvp[0]);
059                    cookie.setValue(nvp[1]);
060                } else if ("$Version".equalsIgnoreCase(nvp[0])) {
061                    cookie.setVersion(version = parseInteger(nvp[1]));
062                } else if ("$Path".equalsIgnoreCase(nvp[0])) {
063                    cookie.setPath(nvp[1]);
064                } else if ("$Domain".equalsIgnoreCase(nvp[0])) {
065                    cookie.setDomain(nvp[1]);
066                } else if ("$Port".equalsIgnoreCase(nvp[0])) {
067                    cookie.getPort().clear();
068                    parsePorts(cookie.getPort(), nvp[1]);
069                }
070            }
071        }
072        if (cookie.getName() != null) {
073            // last cookie being parsed
074            cookies.add(cookie);
075        }
076        return new CookieHeader(cookies);
077    }
078
079    private static void parsePorts(List<Integer> list, String s) {
080        for (String port : s.split(",")) {
081            Integer p = parseInteger(port);
082            if (p != null) {
083                list.add(p);
084            }
085        }
086    }
087
088    private static Integer parseInteger(String s) {
089        try {
090            return Integer.valueOf(s);
091        } catch (NumberFormatException nfe) {
092            return null;
093        }
094    }
095
096    /**
097     * Constructs a new header, initialized from the specified request message.
098     *
099     * @param message
100     *            The request message to initialize the header from.
101     * @return The parsed header.
102     */
103    public static CookieHeader valueOf(final Request message) {
104        return valueOf(parseMultiValuedHeader(message, NAME));
105    }
106
107    /**
108     * Constructs a new header, initialized from the specified string value.
109     *
110     * @param string
111     *            The value to initialize the header from.
112     * @return The parsed header.
113     */
114    public static CookieHeader valueOf(final String string) {
115        return valueOf(parseMultiValuedHeader(string));
116    }
117
118    /** The name of this header. */
119    public static final String NAME = "Cookie";
120
121    /** Request message cookies. */
122    private final List<Cookie> cookies;
123
124    /**
125     * Constructs a new empty header.
126     */
127    public CookieHeader() {
128        this(new ArrayList<Cookie>(1));
129    }
130
131    /**
132     * Constructs a new header with the provided cookies.
133     *
134     * @param cookies
135     *            The cookies.
136     */
137    public CookieHeader(List<Cookie> cookies) {
138        this.cookies = cookies;
139    }
140
141    /**
142     * Returns the cookies' request list.
143     *
144     * @return The cookies' request list.
145     */
146    public List<Cookie> getCookies() {
147        return cookies;
148    }
149
150    @Override
151    public String getName() {
152        return NAME;
153    }
154
155    @Override
156    public List<String> getValues() {
157        boolean quoted = false;
158        Integer version = null;
159        for (Cookie cookie : cookies) {
160            if (cookie.getVersion() != null && (version == null || cookie.getVersion() > version)) {
161                version = cookie.getVersion();
162            } else if (version == null && (cookie.getPath() != null || cookie.getDomain() != null)) {
163                // presence of extended fields makes it version 1 at minimum
164                version = 1;
165            }
166        }
167        StringBuilder sb = new StringBuilder();
168        if (version != null) {
169            sb.append("$Version=").append(version.toString());
170            quoted = true;
171        }
172        for (Cookie cookie : cookies) {
173            if (cookie.getName() != null) {
174                if (sb.length() > 0) {
175                    sb.append("; ");
176                }
177                sb.append(cookie.getName()).append('=');
178                sb.append(quoted ? HeaderUtil.quote(cookie.getValue()) : cookie.getValue());
179                if (cookie.getPath() != null) {
180                    sb.append("; $Path=").append(HeaderUtil.quote(cookie.getPath()));
181                }
182                if (cookie.getDomain() != null) {
183                    sb.append("; $Domain=").append(HeaderUtil.quote(cookie.getDomain()));
184                }
185                if (cookie.getPort().size() > 0) {
186                    sb.append("; $Port=").append(HeaderUtil.quote(portList(cookie.getPort())));
187                }
188            }
189        }
190        // return null if empty
191        return sb.length() > 0 ? singletonList(sb.toString()) : Collections.<String>emptyList();
192    }
193
194    private String portList(List<Integer> ports) {
195        StringBuilder sb = new StringBuilder();
196        for (Integer port : ports) {
197            if (sb.length() > 0) {
198                sb.append(',');
199            }
200            sb.append(port.toString());
201        }
202        return sb.toString();
203    }
204
205    static class Factory extends HeaderFactory<CookieHeader> {
206
207        @Override
208        public CookieHeader parse(String value) {
209            return valueOf(value);
210        }
211
212        @Override
213        public CookieHeader parse(List<String> values) {
214            return valueOf(values);
215        }
216    }
217}