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    *   https://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;
17  
18  import io.netty.handler.codec.DefaultHeaders;
19  import io.netty.handler.codec.DefaultHeaders.NameValidator;
20  import io.netty.handler.codec.DefaultHeaders.ValueValidator;
21  import io.netty.handler.codec.Headers;
22  import io.netty.handler.codec.ValueConverter;
23  import io.netty.util.HashingStrategy;
24  import io.netty.util.internal.StringUtil;
25  
26  import java.util.Collection;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  
31  import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;
32  import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER;
33  import static io.netty.util.internal.ObjectUtil.checkNotNull;
34  import static io.netty.util.internal.StringUtil.COMMA;
35  import static io.netty.util.internal.StringUtil.unescapeCsvFields;
36  
37  /**
38   * Will add multiple values for the same header as single header with a comma separated list of values.
39   * <p>
40   * Please refer to section <a href="https://tools.ietf.org/html/rfc7230#section-3.2.2">RFC 7230, 3.2.2</a>.
41   */
42  public class CombinedHttpHeaders extends DefaultHttpHeaders {
43      /**
44       * Create a combined HTTP header object, with optional validation.
45       *
46       * @param validate Should Netty validate header values to ensure they aren't malicious.
47       * @deprecated Prefer instead to configuring a {@link HttpHeadersFactory}
48       * by calling {@link DefaultHttpHeadersFactory#withCombiningHeaders(boolean) withCombiningHeaders(true)}
49       * on {@link DefaultHttpHeadersFactory#headersFactory()}.
50       */
51      @Deprecated
52      public CombinedHttpHeaders(boolean validate) {
53          super(new CombinedHttpHeadersImpl(CASE_INSENSITIVE_HASHER, valueConverter(), nameValidator(validate),
54                  valueValidator(validate)));
55      }
56  
57      CombinedHttpHeaders(NameValidator<CharSequence> nameValidator, ValueValidator<CharSequence> valueValidator) {
58          super(new CombinedHttpHeadersImpl(
59                  CASE_INSENSITIVE_HASHER,
60                  valueConverter(),
61                  checkNotNull(nameValidator, "nameValidator"),
62                  checkNotNull(valueValidator, "valueValidator")));
63      }
64  
65      CombinedHttpHeaders(
66              NameValidator<CharSequence> nameValidator, ValueValidator<CharSequence> valueValidator, int sizeHint) {
67          super(new CombinedHttpHeadersImpl(
68                  CASE_INSENSITIVE_HASHER,
69                  valueConverter(),
70                  checkNotNull(nameValidator, "nameValidator"),
71                  checkNotNull(valueValidator, "valueValidator"),
72                  sizeHint));
73      }
74  
75      @Override
76      public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
77          return super.containsValue(name, StringUtil.trimOws(value), ignoreCase);
78      }
79  
80      private static final class CombinedHttpHeadersImpl
81              extends DefaultHeaders<CharSequence, CharSequence, CombinedHttpHeadersImpl> {
82          /**
83           * An estimate of the size of a header value.
84           */
85          private static final int VALUE_LENGTH_ESTIMATE = 10;
86          private CsvValueEscaper<Object> objectEscaper;
87          private CsvValueEscaper<CharSequence> charSequenceEscaper;
88  
89          private CsvValueEscaper<Object> objectEscaper() {
90              if (objectEscaper == null) {
91                  objectEscaper = new CsvValueEscaper<Object>() {
92                      @Override
93                      public CharSequence escape(CharSequence name, Object value) {
94                          CharSequence converted;
95                          try {
96                              converted = valueConverter().convertObject(value);
97                          } catch (IllegalArgumentException e) {
98                              throw new IllegalArgumentException(
99                                      "Failed to convert object value for header '" + name + '\'', e);
100                         }
101                         return StringUtil.escapeCsv(converted, true);
102                     }
103                 };
104             }
105             return objectEscaper;
106         }
107 
108         private CsvValueEscaper<CharSequence> charSequenceEscaper() {
109             if (charSequenceEscaper == null) {
110                 charSequenceEscaper = new CsvValueEscaper<CharSequence>() {
111                     @Override
112                     public CharSequence escape(CharSequence name, CharSequence value) {
113                         return StringUtil.escapeCsv(value, true);
114                     }
115                 };
116             }
117             return charSequenceEscaper;
118         }
119 
120         CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy,
121                                 ValueConverter<CharSequence> valueConverter,
122                                 NameValidator<CharSequence> nameValidator,
123                                 ValueValidator<CharSequence> valueValidator) {
124             this(nameHashingStrategy, valueConverter, nameValidator, valueValidator, 16);
125         }
126 
127         CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy,
128                                 ValueConverter<CharSequence> valueConverter,
129                                 NameValidator<CharSequence> nameValidator,
130                                 ValueValidator<CharSequence> valueValidator,
131                                 int sizeHint) {
132             super(nameHashingStrategy, valueConverter, nameValidator, sizeHint, valueValidator);
133         }
134 
135         @Override
136         public Iterator<CharSequence> valueIterator(CharSequence name) {
137             Iterator<CharSequence> itr = super.valueIterator(name);
138             if (!itr.hasNext() || cannotBeCombined(name)) {
139                 return itr;
140             }
141             Iterator<CharSequence> unescapedItr = unescapeCsvFields(itr.next()).iterator();
142             if (itr.hasNext()) {
143                 throw new IllegalStateException("CombinedHttpHeaders should only have one value");
144             }
145             return unescapedItr;
146         }
147 
148         @Override
149         public List<CharSequence> getAll(CharSequence name) {
150             List<CharSequence> values = super.getAll(name);
151             if (values.isEmpty() || cannotBeCombined(name)) {
152                 return values;
153             }
154             if (values.size() != 1) {
155                 throw new IllegalStateException("CombinedHttpHeaders should only have one value");
156             }
157             return unescapeCsvFields(values.get(0));
158         }
159 
160         @Override
161         public CombinedHttpHeadersImpl add(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
162             // Override the fast-copy mechanism used by DefaultHeaders
163             if (headers == this) {
164                 throw new IllegalArgumentException("can't add to itself.");
165             }
166             if (headers instanceof CombinedHttpHeadersImpl) {
167                 if (isEmpty()) {
168                     // Can use the fast underlying copy
169                     addImpl(headers);
170                 } else {
171                     // Values are already escaped so don't escape again
172                     for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
173                         addEscapedValue(header.getKey(), header.getValue());
174                     }
175                 }
176             } else {
177                 for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
178                     add(header.getKey(), header.getValue());
179                 }
180             }
181             return this;
182         }
183 
184         @Override
185         public CombinedHttpHeadersImpl set(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
186             if (headers == this) {
187                 return this;
188             }
189             clear();
190             return add(headers);
191         }
192 
193         @Override
194         public CombinedHttpHeadersImpl setAll(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
195             if (headers == this) {
196                 return this;
197             }
198             for (CharSequence key : headers.names()) {
199                 remove(key);
200             }
201             return add(headers);
202         }
203 
204         @Override
205         public CombinedHttpHeadersImpl add(CharSequence name, CharSequence value) {
206             return addEscapedValue(name, charSequenceEscaper().escape(name, value));
207         }
208 
209         @Override
210         public CombinedHttpHeadersImpl add(CharSequence name, CharSequence... values) {
211             return addEscapedValue(name, commaSeparate(name, charSequenceEscaper(), values));
212         }
213 
214         @Override
215         public CombinedHttpHeadersImpl add(CharSequence name, Iterable<? extends CharSequence> values) {
216             return addEscapedValue(name, commaSeparate(name, charSequenceEscaper(), values));
217         }
218 
219         @Override
220         public CombinedHttpHeadersImpl addObject(CharSequence name, Object value) {
221             return addEscapedValue(name, commaSeparate(name, objectEscaper(), value));
222         }
223 
224         @Override
225         public CombinedHttpHeadersImpl addObject(CharSequence name, Iterable<?> values) {
226             return addEscapedValue(name, commaSeparate(name, objectEscaper(), values));
227         }
228 
229         @Override
230         public CombinedHttpHeadersImpl addObject(CharSequence name, Object... values) {
231             return addEscapedValue(name, commaSeparate(name, objectEscaper(), values));
232         }
233 
234         @Override
235         public CombinedHttpHeadersImpl set(CharSequence name, CharSequence... values) {
236             set(name, commaSeparate(name, charSequenceEscaper(), values));
237             return this;
238         }
239 
240         @Override
241         public CombinedHttpHeadersImpl set(CharSequence name, Iterable<? extends CharSequence> values) {
242             set(name, commaSeparate(name, charSequenceEscaper(), values));
243             return this;
244         }
245 
246         @Override
247         public CombinedHttpHeadersImpl setObject(CharSequence name, Object value) {
248             set(name, commaSeparate(name, objectEscaper(), value));
249             return this;
250         }
251 
252         @Override
253         public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) {
254             set(name, commaSeparate(name, objectEscaper(), values));
255             return this;
256         }
257 
258         @Override
259         public CombinedHttpHeadersImpl setObject(CharSequence name, Iterable<?> values) {
260             set(name, commaSeparate(name, objectEscaper(), values));
261             return this;
262         }
263 
264         private static boolean cannotBeCombined(CharSequence name) {
265             return SET_COOKIE.contentEqualsIgnoreCase(name);
266         }
267 
268         private CombinedHttpHeadersImpl addEscapedValue(CharSequence name, CharSequence escapedValue) {
269             CharSequence currentValue = get(name);
270             if (currentValue == null || cannotBeCombined(name)) {
271                 super.add(name, escapedValue);
272             } else {
273                 set(name, commaSeparateEscapedValues(currentValue, escapedValue));
274             }
275             return this;
276         }
277 
278         private static <T> CharSequence commaSeparate(CharSequence name, CsvValueEscaper<T> escaper, T... values) {
279             StringBuilder sb = new StringBuilder(values.length * VALUE_LENGTH_ESTIMATE);
280             if (values.length > 0) {
281                 int end = values.length - 1;
282                 for (int i = 0; i < end; i++) {
283                     sb.append(escaper.escape(name, values[i])).append(COMMA);
284                 }
285                 sb.append(escaper.escape(name, values[end]));
286             }
287             return sb;
288         }
289 
290         private static <T> CharSequence commaSeparate(CharSequence name, CsvValueEscaper<T> escaper,
291                                                       Iterable<? extends T> values) {
292             @SuppressWarnings("rawtypes")
293             final StringBuilder sb = values instanceof Collection
294                     ? new StringBuilder(((Collection) values).size() * VALUE_LENGTH_ESTIMATE) : new StringBuilder();
295             Iterator<? extends T> iterator = values.iterator();
296             if (iterator.hasNext()) {
297                 T next = iterator.next();
298                 while (iterator.hasNext()) {
299                     sb.append(escaper.escape(name, next)).append(COMMA);
300                     next = iterator.next();
301                 }
302                 sb.append(escaper.escape(name, next));
303             }
304             return sb;
305         }
306 
307         private static CharSequence commaSeparateEscapedValues(CharSequence currentValue, CharSequence value) {
308             return new StringBuilder(currentValue.length() + 1 + value.length())
309                     .append(currentValue)
310                     .append(COMMA)
311                     .append(value);
312         }
313 
314         /**
315          * Escapes comma separated values (CSV).
316          *
317          * @param <T> The type that a concrete implementation handles
318          */
319         private interface CsvValueEscaper<T> {
320             /**
321              * Appends the value to the specified {@link StringBuilder}, escaping if necessary.
322              *
323              * @param name the name of the header for the value being escaped
324              * @param value the value to be appended, escaped if necessary
325              */
326             CharSequence escape(CharSequence name, T value);
327         }
328     }
329 }