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.handler.codec.http.cookie.CookieUtil.stripTrailingSeparatorOrNull;
23 import static io.netty.util.internal.ObjectUtil.checkNotNull;
24 import io.netty.handler.codec.http.HttpRequest;
25 import io.netty.util.internal.InternalThreadLocalMap;
26
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Comparator;
30 import java.util.Iterator;
31 import java.util.List;
32
33 /**
34 * A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie encoder to be used client side, so
35 * only name=value pairs are sent.
36 *
37 * User-Agents are not supposed to interpret cookies, so, if present, {@link Cookie#rawValue()} will be used.
38 * Otherwise, {@link Cookie#value()} will be used unquoted.
39 *
40 * Note that multiple cookies are supposed to be sent at once in a single "Cookie" header.
41 *
42 * <pre>
43 * // Example
44 * {@link HttpRequest} req = ...;
45 * res.setHeader("Cookie", {@link ClientCookieEncoder}.encode("JSESSIONID", "1234"));
46 * </pre>
47 *
48 * @see ClientCookieDecoder
49 */
50 public final class ClientCookieEncoder extends CookieEncoder {
51
52 /**
53 * Strict encoder that validates that name and value chars are in the valid scope and (for methods that accept
54 * multiple cookies) sorts cookies into order of decreasing path length, as specified in RFC6265.
55 */
56 public static final ClientCookieEncoder STRICT = new ClientCookieEncoder(true);
57
58 /**
59 * Lax instance that doesn't validate name and value, and (for methods that accept multiple cookies) keeps
60 * cookies in the order in which they were given.
61 */
62 public static final ClientCookieEncoder LAX = new ClientCookieEncoder(false);
63
64 private ClientCookieEncoder(boolean strict) {
65 super(strict);
66 }
67
68 /**
69 * Encodes the specified cookie into a Cookie header value.
70 *
71 * @param name
72 * the cookie name
73 * @param value
74 * the cookie value
75 * @return a Rfc6265 style Cookie header value
76 */
77 public String encode(String name, String value) {
78 return encode(new DefaultCookie(name, value));
79 }
80
81 /**
82 * Encodes the specified cookie into a Cookie header value.
83 *
84 * @param specified
85 * the cookie
86 * @return a Rfc6265 style Cookie header value
87 */
88 public String encode(Cookie cookie) {
89 StringBuilder buf = stringBuilder();
90 encode(buf, checkNotNull(cookie, "cookie"));
91 return stripTrailingSeparator(buf);
92 }
93
94 /**
95 * Sort cookies into decreasing order of path length, breaking ties by sorting into increasing chronological
96 * order of creation time, as recommended by RFC 6265.
97 */
98 private static final Comparator<Cookie> COOKIE_COMPARATOR = new Comparator<Cookie>() {
99 @Override
100 public int compare(Cookie c1, Cookie c2) {
101 String path1 = c1.path();
102 String path2 = c2.path();
103 // Cookies with unspecified path default to the path of the request. We don't
104 // know the request path here, but we assume that the length of an unspecified
105 // path is longer than any specified path (i.e. pathless cookies come first),
106 // because setting cookies with a path longer than the request path is of
107 // limited use.
108 int len1 = path1 == null ? Integer.MAX_VALUE : path1.length();
109 int len2 = path2 == null ? Integer.MAX_VALUE : path2.length();
110 int diff = len2 - len1;
111 if (diff != 0) {
112 return diff;
113 }
114 // Rely on Java's sort stability to retain creation order in cases where
115 // cookies have same path length.
116 return -1;
117 }
118 };
119
120 /**
121 * Encodes the specified cookies into a single Cookie header value.
122 *
123 * @param cookies
124 * some cookies
125 * @return a Rfc6265 style Cookie header value, null if no cookies are passed.
126 */
127 public String encode(Cookie... cookies) {
128 if (checkNotNull(cookies, "cookies").length == 0) {
129 return null;
130 }
131
132 StringBuilder buf = stringBuilder();
133 if (strict) {
134 if (cookies.length == 1) {
135 encode(buf, cookies[0]);
136 } else {
137 Cookie[] cookiesSorted = Arrays.copyOf(cookies, cookies.length);
138 Arrays.sort(cookiesSorted, COOKIE_COMPARATOR);
139 for (Cookie c : cookiesSorted) {
140 encode(buf, c);
141 }
142 }
143 } else {
144 for (Cookie c : cookies) {
145 encode(buf, c);
146 }
147 }
148 return stripTrailingSeparatorOrNull(buf);
149 }
150
151 /**
152 * Encodes the specified cookies into a single Cookie header value.
153 *
154 * @param cookies
155 * some cookies
156 * @return a Rfc6265 style Cookie header value, null if no cookies are passed.
157 */
158 public String encode(Collection<? extends Cookie> cookies) {
159 if (checkNotNull(cookies, "cookies").isEmpty()) {
160 return null;
161 }
162
163 StringBuilder buf = stringBuilder();
164 if (strict) {
165 if (cookies.size() == 1) {
166 encode(buf, cookies.iterator().next());
167 } else {
168 Cookie[] cookiesSorted = cookies.toArray(new Cookie[cookies.size()]);
169 Arrays.sort(cookiesSorted, COOKIE_COMPARATOR);
170 for (Cookie c : cookiesSorted) {
171 encode(buf, c);
172 }
173 }
174 } else {
175 for (Cookie c : cookies) {
176 encode(buf, c);
177 }
178 }
179 return stripTrailingSeparatorOrNull(buf);
180 }
181
182 /**
183 * Encodes the specified cookies into a single Cookie header value.
184 *
185 * @param cookies some cookies
186 * @return a Rfc6265 style Cookie header value, null if no cookies are passed.
187 */
188 public String encode(Iterable<? extends Cookie> cookies) {
189 Iterator<? extends Cookie> cookiesIt = checkNotNull(cookies, "cookies").iterator();
190 if (!cookiesIt.hasNext()) {
191 return null;
192 }
193
194 StringBuilder buf = stringBuilder();
195 if (strict) {
196 Cookie firstCookie = cookiesIt.next();
197 if (!cookiesIt.hasNext()) {
198 encode(buf, firstCookie);
199 } else {
200 List<Cookie> cookiesList = InternalThreadLocalMap.get().arrayList();
201 cookiesList.add(firstCookie);
202 while (cookiesIt.hasNext()) {
203 cookiesList.add(cookiesIt.next());
204 }
205 Cookie[] cookiesSorted = cookiesList.toArray(new Cookie[cookiesList.size()]);
206 Arrays.sort(cookiesSorted, COOKIE_COMPARATOR);
207 for (Cookie c : cookiesSorted) {
208 encode(buf, c);
209 }
210 }
211 } else {
212 while (cookiesIt.hasNext()) {
213 encode(buf, cookiesIt.next());
214 }
215 }
216 return stripTrailingSeparatorOrNull(buf);
217 }
218
219 private void encode(StringBuilder buf, Cookie c) {
220 final String name = c.name();
221 final String value = c.value() != null ? c.value() : "";
222
223 validateCookie(name, value);
224
225 if (c.wrap()) {
226 addQuoted(buf, name, value);
227 } else {
228 add(buf, name, value);
229 }
230 }
231 }