8268960: com/sun/net/httpserver/Headers.java: Ensure mutators normalize keys and disallow null for keys and values
Reviewed-by: chegar, dfuchs, michaelm
This commit is contained in:
parent
18f356a38e
commit
82bfc5d45c
src/jdk.httpserver/share/classes/com/sun/net/httpserver
test/jdk/com/sun/net/httpserver
@ -30,7 +30,9 @@ import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* HTTP request and response headers are represented by this class which
|
||||
@ -63,14 +65,8 @@ import java.util.Set;
|
||||
* value given overwriting any existing values in the value list.
|
||||
* </ul>
|
||||
*
|
||||
* <p> All methods in this class accept {@code null} values for keys and values.
|
||||
* However, {@code null} keys will never will be present in HTTP request
|
||||
* headers, and will not be output/sent in response headers. Null values can be
|
||||
* represented as either a {@code null} entry for the key (i.e. the list is
|
||||
* {@code null}) or where the key has a list, but one (or more) of the list's
|
||||
* values is {@code null}. Null values are output as a header line containing
|
||||
* the key but no associated value.
|
||||
*
|
||||
* <p> All methods in this class reject {@code null} values for keys and values.
|
||||
* {@code null} keys will never be present in HTTP request or response headers.
|
||||
* @since 1.6
|
||||
*/
|
||||
public class Headers implements Map<String,List<String>> {
|
||||
@ -88,9 +84,7 @@ public class Headers implements Map<String,List<String>> {
|
||||
* key is presumed to be {@code ASCII}.
|
||||
*/
|
||||
private String normalize(String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
Objects.requireNonNull(key);
|
||||
int len = key.length();
|
||||
if (len == 0) {
|
||||
return key;
|
||||
@ -110,43 +104,47 @@ public class Headers implements Map<String,List<String>> {
|
||||
return new String(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {return map.size();}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {return map.isEmpty();}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(key instanceof String)) {
|
||||
return false;
|
||||
}
|
||||
return map.containsKey(normalize((String)key));
|
||||
Objects.requireNonNull(key);
|
||||
return key instanceof String k && map.containsKey(normalize(k));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
Objects.requireNonNull(value);
|
||||
return map.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> get(Object key) {
|
||||
return map.get(normalize((String)key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first value from the {@link List} of {@code String}
|
||||
* values for the given key (if at least one exists).
|
||||
* Returns the first value from the {@link List} of {@code String} values
|
||||
* for the given {@code key}, or {@code null} if no mapping for the
|
||||
* {@code key} exists.
|
||||
*
|
||||
* @param key the key to search for
|
||||
* @return the first {@code String} value associated with the key
|
||||
* @return the first {@code String} value associated with the key,
|
||||
* or {@code null} if no mapping for the key exists
|
||||
*/
|
||||
public String getFirst(String key) {
|
||||
List<String> l = map.get(normalize(key));
|
||||
if (l == null) {
|
||||
if (l == null || l.size() == 0) { // no mapping exists
|
||||
return null;
|
||||
}
|
||||
return l.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> put(String key, List<String> value) {
|
||||
for (String v : value)
|
||||
checkValue(v);
|
||||
@ -154,10 +152,10 @@ public class Headers implements Map<String,List<String>> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given value to the list of headers for the given key. If
|
||||
* the mapping does not already exist, then it is created.
|
||||
* Adds the given {@code value} to the list of headers for the given
|
||||
* {@code key}. If the mapping does not already exist, then it is created.
|
||||
*
|
||||
* @param key the header name
|
||||
* @param key the header name
|
||||
* @param value the value to add to the header
|
||||
*/
|
||||
public void add(String key, String value) {
|
||||
@ -196,10 +194,10 @@ public class Headers implements Map<String,List<String>> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given value as the sole header value for the given
|
||||
* key. If the mapping does not already exist, then it is created.
|
||||
* Sets the given {@code value} as the sole header value for the given
|
||||
* {@code key}. If the mapping does not already exist, then it is created.
|
||||
*
|
||||
* @param key the header name
|
||||
* @param key the header name
|
||||
* @param value the header value to set
|
||||
*/
|
||||
public void set(String key, String value) {
|
||||
@ -208,25 +206,52 @@ public class Headers implements Map<String,List<String>> {
|
||||
put(key, l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> remove(Object key) {
|
||||
return map.remove(normalize((String)key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String,? extends List<String>> t) {
|
||||
map.putAll(t);
|
||||
t.forEach(this::put);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {map.clear();}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {return map.keySet();}
|
||||
|
||||
@Override
|
||||
public Collection<List<String>> values() {return map.values();}
|
||||
|
||||
@Override
|
||||
public Set<Map.Entry<String, List<String>>> entrySet() {
|
||||
return map.entrySet();
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {return map.equals(o);}
|
||||
@Override
|
||||
public void replaceAll(BiFunction<? super String, ? super List<String>, ? extends List<String>> function) {
|
||||
var f = function.andThen(values -> {
|
||||
Objects.requireNonNull(values);
|
||||
values.forEach(Headers::checkValue);
|
||||
return values;
|
||||
});
|
||||
Map.super.replaceAll(f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) { return map.equals(o); }
|
||||
|
||||
@Override
|
||||
public int hashCode() {return map.hashCode();}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final var sb = new StringBuilder(Headers.class.getSimpleName());
|
||||
sb.append(" { ");
|
||||
sb.append(map.toString());
|
||||
sb.append(" }");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -23,21 +23,295 @@
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8251496
|
||||
* @bug 8251496 8268960
|
||||
* @summary Tests for methods in Headers class
|
||||
* @modules jdk.httpserver/com.sun.net.httpserver:+open
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.URIBuilder
|
||||
* @run testng/othervm HeadersTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import org.testng.annotations.DataProvider;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertFalse;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
|
||||
public class HeadersTest {
|
||||
|
||||
static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
|
||||
static final Class<IOException> IOE = IOException.class;
|
||||
static final Class<NullPointerException> NPE = NullPointerException.class;
|
||||
|
||||
@Test
|
||||
public void TestDefaultConstructor() {
|
||||
public static void testDefaultConstructor() {
|
||||
var headers = new Headers();
|
||||
assertTrue(headers.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public static void testNull() {
|
||||
final Headers h = new Headers();
|
||||
h.put("Foo", List.of("Bar"));
|
||||
|
||||
final var mapNullKey = new HashMap<String, List<String>>();
|
||||
mapNullKey.put(null, List.of("Bar"));
|
||||
|
||||
final var mapNullList = new HashMap<String, List<String>>();
|
||||
mapNullList.put("Foo", null);
|
||||
|
||||
final var listWithNull = new LinkedList<String>();
|
||||
listWithNull.add(null);
|
||||
|
||||
final var mapNullInList = new HashMap<String, List<String>>();
|
||||
mapNullInList.put("Foo", listWithNull);
|
||||
|
||||
assertThrows(NPE, () -> h.add(null, "Bar"));
|
||||
assertThrows(NPE, () -> h.add("Foo", null));
|
||||
|
||||
assertThrows(NPE, () -> h.compute(null, (k, v) -> List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.compute("Foo", (k, v) -> listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.computeIfAbsent(null, (k) -> List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.computeIfAbsent("Foo-foo", (k) -> listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.computeIfPresent(null, (k, v) -> List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.computeIfPresent("Foo", (k, v) -> listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.containsKey(null));
|
||||
|
||||
assertThrows(NPE, () -> h.containsValue(null));
|
||||
|
||||
assertThrows(NPE, () -> h.get(null));
|
||||
|
||||
assertThrows(NPE, () -> h.getFirst(null));
|
||||
|
||||
assertThrows(NPE, () -> h.getOrDefault(null, List.of("Bar")));
|
||||
|
||||
assertThrows(NPE, () -> h.merge(null, List.of("Bar"), (k, v) -> List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.merge("Foo-foo", null, (k, v) -> List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.merge("Foo-foo", listWithNull, (k, v) -> List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.merge("Foo", List.of("Bar"), (k, v) -> listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.put(null, List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.put("Foo", null));
|
||||
assertThrows(NPE, () -> h.put("Foo", listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.putAll(mapNullKey));
|
||||
assertThrows(NPE, () -> h.putAll(mapNullList));
|
||||
assertThrows(NPE, () -> h.putAll(mapNullInList));
|
||||
|
||||
assertThrows(NPE, () -> h.putIfAbsent(null, List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.putIfAbsent("Foo-foo", null));
|
||||
assertThrows(NPE, () -> h.putIfAbsent("Foo-foo", listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.remove(null));
|
||||
|
||||
assertThrows(NPE, () -> h.remove(null, List.of("Bar")));
|
||||
|
||||
assertThrows(NPE, () -> h.replace(null, List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.replace("Foo", null));
|
||||
assertThrows(NPE, () -> h.replace("Foo", listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.replace(null, List.of("Bar"), List.of("Bar")));
|
||||
assertThrows(NPE, () -> h.replace("Foo", List.of("Bar"), null));
|
||||
assertThrows(NPE, () -> h.replace("Foo", List.of("Bar"), listWithNull));
|
||||
|
||||
assertThrows(NPE, () -> h.replaceAll((k, v) -> listWithNull));
|
||||
assertThrows(NPE, () -> h.replaceAll((k, v) -> null));
|
||||
|
||||
assertThrows(NPE, () -> h.set(null, "Bar"));
|
||||
assertThrows(NPE, () -> h.set("Foo", null));
|
||||
}
|
||||
|
||||
@DataProvider
|
||||
public Object[][] responseHeaders() {
|
||||
final var listWithNull = new LinkedList<String>();
|
||||
listWithNull.add(null);
|
||||
return new Object[][] {
|
||||
{null, List.of("Bar")},
|
||||
{"Foo", null},
|
||||
{"Foo", listWithNull}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms HttpExchange::sendResponseHeaders throws NPE if response headers
|
||||
* contain a null key or value.
|
||||
*/
|
||||
@Test(dataProvider = "responseHeaders")
|
||||
public void testNullResponseHeaders(String headerKey, List<String> headerVal)
|
||||
throws Exception {
|
||||
var handler = new Handler(headerKey, headerVal);
|
||||
var server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
||||
server.createContext("/", handler);
|
||||
server.start();
|
||||
try {
|
||||
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
|
||||
var request = HttpRequest.newBuilder(uri(server, "")).build();
|
||||
assertThrows(IOE, () -> client.send(request, HttpResponse.BodyHandlers.ofString()));
|
||||
assertEquals(throwable.get().getClass(), NPE);
|
||||
assertTrue(Arrays.stream(throwable.get().getStackTrace())
|
||||
.anyMatch(e -> e.getClassName().equals("sun.net.httpserver.HttpExchangeImpl")
|
||||
|| e.getMethodName().equals("sendResponseHeaders")));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<Throwable> throwable = new CompletableFuture<>();
|
||||
|
||||
private record Handler(String headerKey, List<String> headerVal) implements HttpHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try (InputStream is = exchange.getRequestBody();
|
||||
OutputStream os = exchange.getResponseBody()) {
|
||||
is.readAllBytes();
|
||||
var resp = "hello world".getBytes(StandardCharsets.UTF_8);
|
||||
putHeaders(exchange.getResponseHeaders(), headerKey, headerVal);
|
||||
try {
|
||||
exchange.sendResponseHeaders(200, resp.length);
|
||||
} catch (Throwable t) { // expect NPE
|
||||
throwable.complete(t);
|
||||
throw t;
|
||||
}
|
||||
os.write(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static URI uri(HttpServer server, String path) {
|
||||
return URIBuilder.newBuilder()
|
||||
.host("localhost")
|
||||
.port(server.getAddress().getPort())
|
||||
.scheme("http")
|
||||
.path("/" + path)
|
||||
.buildUnchecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets headers reflectively to be able to set a null key or value.
|
||||
*/
|
||||
private static void putHeaders(Headers headers,
|
||||
String headerKey,
|
||||
List<String> headerVal) {
|
||||
try {
|
||||
final var map = new HashMap<String, List<String>>();
|
||||
map.put(headerKey, headerVal);
|
||||
var mapField = Headers.class.getDeclaredField("map");
|
||||
mapField.setAccessible(true);
|
||||
mapField.set(headers, map);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Could not set headers reflectively", e);
|
||||
}
|
||||
}
|
||||
|
||||
@DataProvider
|
||||
public static Object[][] headerPairs() {
|
||||
final var h1 = new Headers();
|
||||
final var h2 = new Headers();
|
||||
final var h3 = new Headers();
|
||||
final var h4 = new Headers();
|
||||
final var h5 = new Headers();
|
||||
h1.put("Accept-Encoding", List.of("gzip, deflate"));
|
||||
h2.put("accept-encoding", List.of("gzip, deflate"));
|
||||
h3.put("AccePT-ENCoding", List.of("gzip, deflate"));
|
||||
h4.put("ACCept-EncodING", List.of("gzip, deflate"));
|
||||
h5.put("ACCEPT-ENCODING", List.of("gzip, deflate"));
|
||||
|
||||
final var headers = List.of(h1, h2, h3, h4, h5);
|
||||
return headers.stream() // cartesian product of headers
|
||||
.flatMap(header1 -> headers.stream().map(header2 -> new Headers[] { header1, header2 }))
|
||||
.toArray(Object[][]::new);
|
||||
}
|
||||
|
||||
@Test(dataProvider = "headerPairs")
|
||||
public static void testEqualsAndHashCode(Headers h1, Headers h2) {
|
||||
// avoid testng's asserts(Map, Map) as they don't call Headers::equals
|
||||
assertTrue(h1.equals(h2), "Headers differ");
|
||||
assertEquals(h1.hashCode(), h2.hashCode(), "hashCode differ for "
|
||||
+ List.of(h1, h2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public static void testEqualsMap() {
|
||||
final var h = new Headers();
|
||||
final var m = new HashMap<String, List<String>>();
|
||||
assertTrue(h.equals(m));
|
||||
assertTrue(m.equals(h));
|
||||
assertFalse(h.equals(null), "null cannot be equal to Headers");
|
||||
}
|
||||
|
||||
@Test
|
||||
public static void testToString() {
|
||||
final var h = new Headers();
|
||||
h.put("Accept-Encoding", List.of("gzip, deflate"));
|
||||
assertTrue(h.toString().equals("Headers { {Accept-encoding=[gzip, deflate]} }"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public static void testPutAll() {
|
||||
final var h0 = new Headers();
|
||||
final var map = new HashMap<String, List<String>>();
|
||||
map.put("a", null);
|
||||
assertThrows(NPE, () -> h0.putAll(map));
|
||||
|
||||
final var list = new ArrayList<String>();
|
||||
list.add(null);
|
||||
assertThrows(NPE, () -> h0.putAll(Map.of("a", list)));
|
||||
assertThrows(IAE, () -> h0.putAll(Map.of("a", List.of("\n"))));
|
||||
|
||||
final var h1 = new Headers();
|
||||
h1.put("a", List.of("1"));
|
||||
h1.put("b", List.of("2"));
|
||||
final var h2 = new Headers();
|
||||
h2.putAll(Map.of("a", List.of("1"), "b", List.of("2")));
|
||||
assertTrue(h1.equals(h2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public static void testReplaceAll() {
|
||||
final var h1 = new Headers();
|
||||
h1.put("a", List.of("1"));
|
||||
h1.put("b", List.of("2"));
|
||||
final var list = new ArrayList<String>();
|
||||
list.add(null);
|
||||
assertThrows(NPE, () -> h1.replaceAll((k, v) -> list));
|
||||
assertThrows(IAE, () -> h1.replaceAll((k, v) -> List.of("\n")));
|
||||
|
||||
h1.replaceAll((k, v) -> {
|
||||
String s = h1.get(k).get(0);
|
||||
return List.of(s+s);
|
||||
});
|
||||
final var h2 = new Headers();
|
||||
h2.put("a", List.of("11"));
|
||||
h2.put("b", List.of("22"));
|
||||
assertTrue(h1.equals(h2));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user