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                 // Values are already escaped so don't escape again
168                 for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
169                     addEscapedValue(header.getKey(), header.getValue());
170                 }
171             } else {
172                 for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
173                     add(header.getKey(), header.getValue());
174                 }
175             }
176             return this;
177         }
178 
179         @Override
180         public CombinedHttpHeadersImpl set(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
181             if (headers == this) {
182                 return this;
183             }
184             clear();
185             return add(headers);
186         }
187 
188         @Override
189         public CombinedHttpHeadersImpl setAll(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
190             if (headers == this) {
191                 return this;
192             }
193             for (CharSequence key : headers.names()) {
194                 remove(key);
195             }
196             return add(headers);
197         }
198 
199         @Override
200         public CombinedHttpHeadersImpl add(CharSequence name, CharSequence value) {
201             return addEscapedValue(name, charSequenceEscaper().escape(name, value));
202         }
203 
204         @Override
205         public CombinedHttpHeadersImpl add(CharSequence name, CharSequence... values) {
206             return addEscapedValue(name, commaSeparate(name, charSequenceEscaper(), values));
207         }
208 
209         @Override
210         public CombinedHttpHeadersImpl add(CharSequence name, Iterable<? extends CharSequence> values) {
211             return addEscapedValue(name, commaSeparate(name, charSequenceEscaper(), values));
212         }
213 
214         @Override
215         public CombinedHttpHeadersImpl addObject(CharSequence name, Object value) {
216             return addEscapedValue(name, commaSeparate(name, objectEscaper(), value));
217         }
218 
219         @Override
220         public CombinedHttpHeadersImpl addObject(CharSequence name, Iterable<?> values) {
221             return addEscapedValue(name, commaSeparate(name, objectEscaper(), values));
222         }
223 
224         @Override
225         public CombinedHttpHeadersImpl addObject(CharSequence name, Object... values) {
226             return addEscapedValue(name, commaSeparate(name, objectEscaper(), values));
227         }
228 
229         @Override
230         public CombinedHttpHeadersImpl set(CharSequence name, CharSequence... values) {
231             set(name, commaSeparate(name, charSequenceEscaper(), values));
232             return this;
233         }
234 
235         @Override
236         public CombinedHttpHeadersImpl set(CharSequence name, Iterable<? extends CharSequence> values) {
237             set(name, commaSeparate(name, charSequenceEscaper(), values));
238             return this;
239         }
240 
241         @Override
242         public CombinedHttpHeadersImpl setObject(CharSequence name, Object value) {
243             set(name, commaSeparate(name, objectEscaper(), value));
244             return this;
245         }
246 
247         @Override
248         public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) {
249             set(name, commaSeparate(name, objectEscaper(), values));
250             return this;
251         }
252 
253         @Override
254         public CombinedHttpHeadersImpl setObject(CharSequence name, Iterable<?> values) {
255             set(name, commaSeparate(name, objectEscaper(), values));
256             return this;
257         }
258 
259         private static boolean cannotBeCombined(CharSequence name) {
260             return SET_COOKIE.contentEqualsIgnoreCase(name);
261         }
262 
263         private CombinedHttpHeadersImpl addEscapedValue(CharSequence name, CharSequence escapedValue) {
264             CharSequence currentValue = get(name);
265             if (currentValue == null || cannotBeCombined(name)) {
266                 super.add(name, escapedValue);
267             } else {
268                 set(name, commaSeparateEscapedValues(currentValue, escapedValue));
269             }
270             return this;
271         }
272 
273         private static <T> CharSequence commaSeparate(CharSequence name, CsvValueEscaper<T> escaper, T... values) {
274             StringBuilder sb = new StringBuilder(values.length * VALUE_LENGTH_ESTIMATE);
275             if (values.length > 0) {
276                 int end = values.length - 1;
277                 for (int i = 0; i < end; i++) {
278                     sb.append(escaper.escape(name, values[i])).append(COMMA);
279                 }
280                 sb.append(escaper.escape(name, values[end]));
281             }
282             return sb;
283         }
284 
285         private static <T> CharSequence commaSeparate(CharSequence name, CsvValueEscaper<T> escaper,
286                                                       Iterable<? extends T> values) {
287             @SuppressWarnings("rawtypes")
288             final StringBuilder sb = values instanceof Collection
289                     ? new StringBuilder(((Collection) values).size() * VALUE_LENGTH_ESTIMATE) : new StringBuilder();
290             Iterator<? extends T> iterator = values.iterator();
291             if (iterator.hasNext()) {
292                 T next = iterator.next();
293                 while (iterator.hasNext()) {
294                     sb.append(escaper.escape(name, next)).append(COMMA);
295                     next = iterator.next();
296                 }
297                 sb.append(escaper.escape(name, next));
298             }
299             return sb;
300         }
301 
302         private static CharSequence commaSeparateEscapedValues(CharSequence currentValue, CharSequence value) {
303             return new StringBuilder(currentValue.length() + 1 + value.length())
304                     .append(currentValue)
305                     .append(COMMA)
306                     .append(value);
307         }
308 
309         /**
310          * Escapes comma separated values (CSV).
311          *
312          * @param <T> The type that a concrete implementation handles
313          */
314         private interface CsvValueEscaper<T> {
315             /**
316              * Appends the value to the specified {@link StringBuilder}, escaping if necessary.
317              *
318              * @param name the name of the header for the value being escaped
319              * @param value the value to be appended, escaped if necessary
320              */
321             CharSequence escape(CharSequence name, T value);
322         }
323     }
324 }