2015-08-13 04:05:37 +02:00
|
|
|
/*
|
|
|
|
* Copyright 2015 The Netty Project
|
|
|
|
*
|
|
|
|
* The Netty Project licenses this file to you under the Apache License,
|
|
|
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
|
|
|
* with the License. You may obtain a copy of the License at:
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
* License for the specific language governing permissions and limitations
|
|
|
|
* under the License.
|
|
|
|
*/
|
|
|
|
package io.netty.handler.codec.http;
|
|
|
|
|
|
|
|
import io.netty.handler.codec.DefaultHeaders;
|
2017-02-24 21:45:23 +01:00
|
|
|
import io.netty.handler.codec.Headers;
|
2015-08-13 04:05:37 +02:00
|
|
|
import io.netty.handler.codec.ValueConverter;
|
|
|
|
import io.netty.util.HashingStrategy;
|
|
|
|
import io.netty.util.internal.StringUtil;
|
|
|
|
|
|
|
|
import java.util.Collection;
|
|
|
|
import java.util.Iterator;
|
2016-02-11 05:32:27 +01:00
|
|
|
import java.util.List;
|
2015-10-30 21:24:46 +01:00
|
|
|
import java.util.Map;
|
2015-08-13 04:05:37 +02:00
|
|
|
|
2018-12-01 10:47:18 +01:00
|
|
|
import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;
|
2015-08-13 04:05:37 +02:00
|
|
|
import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER;
|
|
|
|
import static io.netty.util.internal.StringUtil.COMMA;
|
2017-11-12 03:22:22 +01:00
|
|
|
import static io.netty.util.internal.StringUtil.unescapeCsvFields;
|
2015-08-13 04:05:37 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Will add multiple values for the same header as single header with a comma separated list of values.
|
|
|
|
* <p>
|
|
|
|
* Please refer to section <a href="https://tools.ietf.org/html/rfc7230#section-3.2.2">RFC 7230, 3.2.2</a>.
|
|
|
|
*/
|
|
|
|
public class CombinedHttpHeaders extends DefaultHttpHeaders {
|
|
|
|
public CombinedHttpHeaders(boolean validate) {
|
|
|
|
super(new CombinedHttpHeadersImpl(CASE_INSENSITIVE_HASHER, valueConverter(validate), nameValidator(validate)));
|
|
|
|
}
|
|
|
|
|
2017-02-24 21:45:23 +01:00
|
|
|
@Override
|
|
|
|
public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
|
|
|
|
return super.containsValue(name, StringUtil.trimOws(value), ignoreCase);
|
|
|
|
}
|
|
|
|
|
2015-10-27 21:47:47 +01:00
|
|
|
private static final class CombinedHttpHeadersImpl
|
|
|
|
extends DefaultHeaders<CharSequence, CharSequence, CombinedHttpHeadersImpl> {
|
2015-08-13 04:05:37 +02:00
|
|
|
/**
|
|
|
|
* An estimate of the size of a header value.
|
|
|
|
*/
|
|
|
|
private static final int VALUE_LENGTH_ESTIMATE = 10;
|
|
|
|
private CsvValueEscaper<Object> objectEscaper;
|
|
|
|
private CsvValueEscaper<CharSequence> charSequenceEscaper;
|
|
|
|
|
|
|
|
private CsvValueEscaper<Object> objectEscaper() {
|
|
|
|
if (objectEscaper == null) {
|
2019-01-29 14:06:05 +01:00
|
|
|
objectEscaper = value -> StringUtil.escapeCsv(valueConverter().convertObject(value), true);
|
2015-08-13 04:05:37 +02:00
|
|
|
}
|
|
|
|
return objectEscaper;
|
|
|
|
}
|
|
|
|
|
|
|
|
private CsvValueEscaper<CharSequence> charSequenceEscaper() {
|
|
|
|
if (charSequenceEscaper == null) {
|
2019-01-29 14:06:05 +01:00
|
|
|
charSequenceEscaper = value -> StringUtil.escapeCsv(value, true);
|
2015-08-13 04:05:37 +02:00
|
|
|
}
|
|
|
|
return charSequenceEscaper;
|
|
|
|
}
|
|
|
|
|
2019-01-24 16:24:19 +01:00
|
|
|
CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy,
|
2015-08-13 04:05:37 +02:00
|
|
|
ValueConverter<CharSequence> valueConverter,
|
|
|
|
io.netty.handler.codec.DefaultHeaders.NameValidator<CharSequence> nameValidator) {
|
|
|
|
super(nameHashingStrategy, valueConverter, nameValidator);
|
|
|
|
}
|
|
|
|
|
2017-11-12 03:22:22 +01:00
|
|
|
@Override
|
|
|
|
public Iterator<CharSequence> valueIterator(CharSequence name) {
|
|
|
|
Iterator<CharSequence> itr = super.valueIterator(name);
|
2018-12-01 10:47:18 +01:00
|
|
|
if (!itr.hasNext() || cannotBeCombined(name)) {
|
2017-11-12 03:22:22 +01:00
|
|
|
return itr;
|
|
|
|
}
|
|
|
|
Iterator<CharSequence> unescapedItr = unescapeCsvFields(itr.next()).iterator();
|
|
|
|
if (itr.hasNext()) {
|
|
|
|
throw new IllegalStateException("CombinedHttpHeaders should only have one value");
|
|
|
|
}
|
|
|
|
return unescapedItr;
|
|
|
|
}
|
|
|
|
|
2016-02-11 05:32:27 +01:00
|
|
|
@Override
|
|
|
|
public List<CharSequence> getAll(CharSequence name) {
|
|
|
|
List<CharSequence> values = super.getAll(name);
|
2018-12-01 10:47:18 +01:00
|
|
|
if (values.isEmpty() || cannotBeCombined(name)) {
|
2016-02-11 05:32:27 +01:00
|
|
|
return values;
|
|
|
|
}
|
|
|
|
if (values.size() != 1) {
|
|
|
|
throw new IllegalStateException("CombinedHttpHeaders should only have one value");
|
|
|
|
}
|
2017-11-12 03:22:22 +01:00
|
|
|
return unescapeCsvFields(values.get(0));
|
2016-02-11 05:32:27 +01:00
|
|
|
}
|
|
|
|
|
2015-10-30 21:24:46 +01:00
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl add(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
|
|
|
|
// Override the fast-copy mechanism used by DefaultHeaders
|
|
|
|
if (headers == this) {
|
|
|
|
throw new IllegalArgumentException("can't add to itself.");
|
|
|
|
}
|
|
|
|
if (headers instanceof CombinedHttpHeadersImpl) {
|
|
|
|
if (isEmpty()) {
|
|
|
|
// Can use the fast underlying copy
|
|
|
|
addImpl(headers);
|
|
|
|
} else {
|
|
|
|
// Values are already escaped so don't escape again
|
|
|
|
for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
|
|
|
|
addEscapedValue(header.getKey(), header.getValue());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (Map.Entry<? extends CharSequence, ? extends CharSequence> header : headers) {
|
|
|
|
add(header.getKey(), header.getValue());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl set(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
|
|
|
|
if (headers == this) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
clear();
|
|
|
|
return add(headers);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl setAll(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
|
|
|
|
if (headers == this) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
for (CharSequence key : headers.names()) {
|
|
|
|
remove(key);
|
|
|
|
}
|
|
|
|
return add(headers);
|
|
|
|
}
|
|
|
|
|
2015-08-13 04:05:37 +02:00
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl add(CharSequence name, CharSequence value) {
|
2017-02-24 21:45:23 +01:00
|
|
|
return addEscapedValue(name, charSequenceEscaper().escape(value));
|
2015-08-13 04:05:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl add(CharSequence name, CharSequence... values) {
|
|
|
|
return addEscapedValue(name, commaSeparate(charSequenceEscaper(), values));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl add(CharSequence name, Iterable<? extends CharSequence> values) {
|
|
|
|
return addEscapedValue(name, commaSeparate(charSequenceEscaper(), values));
|
|
|
|
}
|
|
|
|
|
2017-02-24 21:45:23 +01:00
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl addObject(CharSequence name, Object value) {
|
|
|
|
return addEscapedValue(name, commaSeparate(objectEscaper(), value));
|
|
|
|
}
|
|
|
|
|
2015-08-13 04:05:37 +02:00
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl addObject(CharSequence name, Iterable<?> values) {
|
|
|
|
return addEscapedValue(name, commaSeparate(objectEscaper(), values));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl addObject(CharSequence name, Object... values) {
|
|
|
|
return addEscapedValue(name, commaSeparate(objectEscaper(), values));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl set(CharSequence name, CharSequence... values) {
|
|
|
|
super.set(name, commaSeparate(charSequenceEscaper(), values));
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl set(CharSequence name, Iterable<? extends CharSequence> values) {
|
|
|
|
super.set(name, commaSeparate(charSequenceEscaper(), values));
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2016-02-11 05:32:27 +01:00
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl setObject(CharSequence name, Object value) {
|
|
|
|
super.set(name, commaSeparate(objectEscaper(), value));
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2015-08-13 04:05:37 +02:00
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) {
|
|
|
|
super.set(name, commaSeparate(objectEscaper(), values));
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CombinedHttpHeadersImpl setObject(CharSequence name, Iterable<?> values) {
|
|
|
|
super.set(name, commaSeparate(objectEscaper(), values));
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-12-01 10:47:18 +01:00
|
|
|
private static boolean cannotBeCombined(CharSequence name) {
|
|
|
|
return SET_COOKIE.contentEqualsIgnoreCase(name);
|
|
|
|
}
|
|
|
|
|
2015-08-13 04:05:37 +02:00
|
|
|
private CombinedHttpHeadersImpl addEscapedValue(CharSequence name, CharSequence escapedValue) {
|
|
|
|
CharSequence currentValue = super.get(name);
|
2018-12-01 10:47:18 +01:00
|
|
|
if (currentValue == null || cannotBeCombined(name)) {
|
2015-08-13 04:05:37 +02:00
|
|
|
super.add(name, escapedValue);
|
|
|
|
} else {
|
|
|
|
super.set(name, commaSeparateEscapedValues(currentValue, escapedValue));
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static <T> CharSequence commaSeparate(CsvValueEscaper<T> escaper, T... values) {
|
|
|
|
StringBuilder sb = new StringBuilder(values.length * VALUE_LENGTH_ESTIMATE);
|
|
|
|
if (values.length > 0) {
|
|
|
|
int end = values.length - 1;
|
|
|
|
for (int i = 0; i < end; i++) {
|
|
|
|
sb.append(escaper.escape(values[i])).append(COMMA);
|
|
|
|
}
|
|
|
|
sb.append(escaper.escape(values[end]));
|
|
|
|
}
|
|
|
|
return sb;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static <T> CharSequence commaSeparate(CsvValueEscaper<T> escaper, Iterable<? extends T> values) {
|
|
|
|
@SuppressWarnings("rawtypes")
|
|
|
|
final StringBuilder sb = values instanceof Collection
|
|
|
|
? new StringBuilder(((Collection) values).size() * VALUE_LENGTH_ESTIMATE) : new StringBuilder();
|
|
|
|
Iterator<? extends T> iterator = values.iterator();
|
|
|
|
if (iterator.hasNext()) {
|
|
|
|
T next = iterator.next();
|
|
|
|
while (iterator.hasNext()) {
|
|
|
|
sb.append(escaper.escape(next)).append(COMMA);
|
|
|
|
next = iterator.next();
|
|
|
|
}
|
|
|
|
sb.append(escaper.escape(next));
|
|
|
|
}
|
|
|
|
return sb;
|
|
|
|
}
|
|
|
|
|
2017-02-22 19:24:24 +01:00
|
|
|
private static CharSequence commaSeparateEscapedValues(CharSequence currentValue, CharSequence value) {
|
2015-08-13 04:05:37 +02:00
|
|
|
return new StringBuilder(currentValue.length() + 1 + value.length())
|
|
|
|
.append(currentValue)
|
|
|
|
.append(COMMA)
|
|
|
|
.append(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Escapes comma separated values (CSV).
|
|
|
|
*
|
|
|
|
* @param <T> The type that a concrete implementation handles
|
|
|
|
*/
|
|
|
|
private interface CsvValueEscaper<T> {
|
|
|
|
/**
|
|
|
|
* Appends the value to the specified {@link StringBuilder}, escaping if necessary.
|
|
|
|
*
|
|
|
|
* @param value the value to be appended, escaped if necessary
|
|
|
|
*/
|
|
|
|
CharSequence escape(T value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|