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