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