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:
Julia Boes 2021-07-01 09:56:22 +00:00
parent 18f356a38e
commit 82bfc5d45c
2 changed files with 332 additions and 33 deletions
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));
}
}