View Javadoc
1   /*
2    * Copyright 2015 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http.cookie;
17  
18  import static io.netty.handler.codec.http.cookie.CookieUtil.add;
19  import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted;
20  import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
21  import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator;
22  import static io.netty.util.internal.ObjectUtil.checkNotNull;
23  
24  import io.netty.handler.codec.http.HttpHeaderDateFormat;
25  import io.netty.handler.codec.http.HttpResponse;
26  
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.Date;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  
36  /**
37   * A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie encoder to be used server side,
38   * so some fields are sent (Version is typically ignored).
39   *
40   * As Netty's Cookie merges Expires and MaxAge into one single field, only Max-Age field is sent.
41   *
42   * Note that multiple cookies must be sent as separate "Set-Cookie" headers.
43   *
44   * <pre>
45   * // Example
46   * {@link HttpResponse} res = ...;
47   * res.setHeader("Set-Cookie", {@link ServerCookieEncoder}.encode("JSESSIONID", "1234"));
48   * </pre>
49   *
50   * @see ServerCookieDecoder
51   */
52  public final class ServerCookieEncoder extends CookieEncoder {
53  
54      /**
55       * Strict encoder that validates that name and value chars are in the valid scope
56       * defined in RFC6265, and (for methods that accept multiple cookies) that only
57       * one cookie is encoded with any given name. (If multiple cookies have the same
58       * name, the last one is the one that is encoded.)
59       */
60      public static final ServerCookieEncoder STRICT = new ServerCookieEncoder(true);
61  
62      /**
63       * Lax instance that doesn't validate name and value, and that allows multiple
64       * cookies with the same name.
65       */
66      public static final ServerCookieEncoder LAX = new ServerCookieEncoder(false);
67  
68      private ServerCookieEncoder(boolean strict) {
69          super(strict);
70      }
71  
72      /**
73       * Encodes the specified cookie name-value pair into a Set-Cookie header value.
74       *
75       * @param name the cookie name
76       * @param value the cookie value
77       * @return a single Set-Cookie header value
78       */
79      public String encode(String name, String value) {
80          return encode(new DefaultCookie(name, value));
81      }
82  
83      /**
84       * Encodes the specified cookie into a Set-Cookie header value.
85       *
86       * @param cookie the cookie
87       * @return a single Set-Cookie header value
88       */
89      public String encode(Cookie cookie) {
90          final String name = checkNotNull(cookie, "cookie").name();
91          final String value = cookie.value() != null ? cookie.value() : "";
92  
93          validateCookie(name, value);
94  
95          StringBuilder buf = stringBuilder();
96  
97          if (cookie.wrap()) {
98              addQuoted(buf, name, value);
99          } else {
100             add(buf, name, value);
101         }
102 
103         if (cookie.maxAge() != Long.MIN_VALUE) {
104             add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge());
105             Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis());
106             add(buf, CookieHeaderNames.EXPIRES, HttpHeaderDateFormat.get().format(expires));
107         }
108 
109         if (cookie.path() != null) {
110             add(buf, CookieHeaderNames.PATH, cookie.path());
111         }
112 
113         if (cookie.domain() != null) {
114             add(buf, CookieHeaderNames.DOMAIN, cookie.domain());
115         }
116         if (cookie.isSecure()) {
117             add(buf, CookieHeaderNames.SECURE);
118         }
119         if (cookie.isHttpOnly()) {
120             add(buf, CookieHeaderNames.HTTPONLY);
121         }
122 
123         return stripTrailingSeparator(buf);
124     }
125 
126     /** Deduplicate a list of encoded cookies by keeping only the last instance with a given name.
127      *
128      * @param encoded The list of encoded cookies.
129      * @param nameToLastIndex A map from cookie name to index of last cookie instance.
130      * @return The encoded list with all but the last instance of a named cookie.
131      */
132     private static List<String> dedup(List<String> encoded, Map<String, Integer> nameToLastIndex) {
133         boolean[] isLastInstance = new boolean[encoded.size()];
134         for (int idx : nameToLastIndex.values()) {
135             isLastInstance[idx] = true;
136         }
137         List<String> dedupd = new ArrayList<String>(nameToLastIndex.size());
138         for (int i = 0, n = encoded.size(); i < n; i++) {
139             if (isLastInstance[i]) {
140                 dedupd.add(encoded.get(i));
141             }
142         }
143         return dedupd;
144     }
145 
146     /**
147      * Batch encodes cookies into Set-Cookie header values.
148      *
149      * @param cookies a bunch of cookies
150      * @return the corresponding bunch of Set-Cookie headers
151      */
152     public List<String> encode(Cookie... cookies) {
153         if (checkNotNull(cookies, "cookies").length == 0) {
154             return Collections.emptyList();
155         }
156 
157         List<String> encoded = new ArrayList<String>(cookies.length);
158         Map<String, Integer> nameToIndex = strict && cookies.length > 1 ? new HashMap<String, Integer>() : null;
159         boolean hasDupdName = false;
160         for (int i = 0; i < cookies.length; i++) {
161             Cookie c = cookies[i];
162             encoded.add(encode(c));
163             if (nameToIndex != null) {
164                 hasDupdName |= nameToIndex.put(c.name(), i) != null;
165             }
166         }
167         return hasDupdName ? dedup(encoded, nameToIndex) : encoded;
168     }
169 
170     /**
171      * Batch encodes cookies into Set-Cookie header values.
172      *
173      * @param cookies a bunch of cookies
174      * @return the corresponding bunch of Set-Cookie headers
175      */
176     public List<String> encode(Collection<? extends Cookie> cookies) {
177         if (checkNotNull(cookies, "cookies").isEmpty()) {
178             return Collections.emptyList();
179         }
180 
181         List<String> encoded = new ArrayList<String>(cookies.size());
182         Map<String, Integer> nameToIndex = strict && cookies.size() > 1 ? new HashMap<String, Integer>() : null;
183         int i = 0;
184         boolean hasDupdName = false;
185         for (Cookie c : cookies) {
186             encoded.add(encode(c));
187             if (nameToIndex != null) {
188                 hasDupdName |= nameToIndex.put(c.name(), i++) != null;
189             }
190         }
191         return hasDupdName ? dedup(encoded, nameToIndex) : encoded;
192     }
193 
194     /**
195      * Batch encodes cookies into Set-Cookie header values.
196      *
197      * @param cookies a bunch of cookies
198      * @return the corresponding bunch of Set-Cookie headers
199      */
200     public List<String> encode(Iterable<? extends Cookie> cookies) {
201         Iterator<? extends Cookie> cookiesIt = checkNotNull(cookies, "cookies").iterator();
202         if (!cookiesIt.hasNext()) {
203             return Collections.emptyList();
204         }
205 
206         List<String> encoded = new ArrayList<String>();
207         Cookie firstCookie = cookiesIt.next();
208         Map<String, Integer> nameToIndex = strict && cookiesIt.hasNext() ? new HashMap<String, Integer>() : null;
209         int i = 0;
210         encoded.add(encode(firstCookie));
211         boolean hasDupdName = nameToIndex != null ? nameToIndex.put(firstCookie.name(), i++) != null : false;
212         while (cookiesIt.hasNext()) {
213             Cookie c = cookiesIt.next();
214             encoded.add(encode(c));
215             if (nameToIndex != null) {
216                 hasDupdName |= nameToIndex.put(c.name(), i++) != null;
217             }
218         }
219         return hasDupdName ? dedup(encoded, nameToIndex) : encoded;
220     }
221 }