8061729: Update java/net tests to eliminate dependency on sun.net.www.MessageHeader and some other internal APIs

Reviewed-by: dfuchs
This commit is contained in:
Mahendra Chhipa 2022-02-16 16:43:15 +00:00 committed by Daniel Fuchs
parent 395bc141f2
commit 0f3d3ac32c
11 changed files with 966 additions and 57 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2006, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2006, 2022, 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
@ -25,14 +25,22 @@
* @test
* @bug 6498566
* @summary URL.openConnection(Proxy.NO_PROXY) may connect through a proxy.
* @modules java.base/sun.net.www
* @library /test/lib
* @run main/othervm ProxyFromCache
*/
import java.net.*;
import java.io.*;
import sun.net.www.MessageHeader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import jdk.test.lib.net.HttpHeaderParser;
import jdk.test.lib.net.URIBuilder;
/* Creates a simple proxy and http server that just return 200 OK.
@ -135,15 +143,12 @@ class SimpleServer extends Thread
connectionCount++;
InputStream is = sock.getInputStream();
OutputStream os = sock.getOutputStream();
MessageHeader headers = new MessageHeader (is);
HttpHeaderParser httpHeaderParser = new HttpHeaderParser(is);
os.write(replyOK.getBytes("UTF-8"));
headers = new MessageHeader (is);
httpHeaderParser = new HttpHeaderParser(is);
// If we get here then we received a second request.
connectionCount++;
os.write(replyOK.getBytes("UTF-8"));
sock.close();
} catch (Exception e) {
//e.printStackTrace();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2006, 2022, 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
@ -25,13 +25,14 @@
* @test
* @bug 6469663
* @summary HTTP Request-URI contains fragment when connecting through proxy
* @modules java.base/sun.net.www
* @library /test/lib
* @run main/othervm RequestURI
*/
import java.net.*;
import java.io.*;
import sun.net.www.MessageHeader;
import jdk.test.lib.net.HttpHeaderParser;
// Create a Server listening on port 5001 to act as the proxy. Requests
// never need to be forwared from it. We are only interested in the
@ -91,8 +92,8 @@ class RequestURIServer extends Thread
InputStream is = sock.getInputStream();
OutputStream os = sock.getOutputStream();
MessageHeader headers = new MessageHeader (is);
String requestLine = headers.getValue(0);
HttpHeaderParser headers = new HttpHeaderParser (is);
String requestLine = headers.getRequestDetails();
int first = requestLine.indexOf(' ');
int second = requestLine.lastIndexOf(' ');

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2004, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2004, 2022, 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
@ -24,16 +24,23 @@
/**
* @test
* @bug 6189206
* @modules java.base/sun.net.www
* @library /test/lib
* @run main/othervm -Dhttp.keepAlive=false CloseOptionHeader
* @summary HTTP client should set "Connection: close" header in request when keepalive is disabled
*/
import java.net.*;
import java.util.*;
import java.io.*;
import sun.net.www.MessageHeader;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.List;
import jdk.test.lib.net.HttpHeaderParser;
import jdk.test.lib.net.URIBuilder;
public class CloseOptionHeader implements Runnable {
@ -49,10 +56,15 @@ public class CloseOptionHeader implements Runnable {
/* check the request to find close connection option header */
InputStream is = s.getInputStream ();
MessageHeader mh = new MessageHeader(is);
String connHeader = mh.findValue("Connection");
if (connHeader != null && connHeader.equalsIgnoreCase("close")) {
hasCloseHeader = true;
HttpHeaderParser mh = new HttpHeaderParser(is);
List <String> connHeader = mh.getHeaderValue("Connection");
if (connHeader != null) {
for(String value : connHeader) {
if (value.equalsIgnoreCase("close")) {
hasCloseHeader = true;
break;
}
}
}
PrintStream out = new PrintStream(

View File

@ -0,0 +1,498 @@
/*
* Copyright (c) 2022, 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/* @test
* @bug 8061729
* @library /test/lib
* @summary Sanity check that HttpHeaderParser works same as MessageHeader
* @modules java.base/sun.net.www java.base/sun.net.www.protocol.http:open
* @run testng/othervm HttpHeaderParserTest
*/
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.US_ASCII;
import jdk.test.lib.net.HttpHeaderParser;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import sun.net.www.MessageHeader;
public class HttpHeaderParserTest {
@DataProvider(name = "responses")
public Object[][] responses() {
List<String> responses = new ArrayList<>();
String[] basic =
{ "HTTP/1.1 200 OK\r\n\r\n",
"HTTP/1.1 200 OK\r\n" +
"Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" +
"Server: Apache/1.3.14 (Unix)\r\n" +
"Connection: close\r\n" +
"Content-Type: text/html; charset=iso-8859-1\r\n" +
"Content-Length: 10\r\n\r\n" +
"123456789",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 9\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"X-Header: U\u00ffU\r\n" + // value with U+00FF - Extended Latin-1
"Content-Length: 9\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 9\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" + // more than one SP after ':'
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"Content-Length:\t10\r\n" +
"Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" + // HT separator
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"Content-Length:\t\t10\r\n" +
"Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" + // more than one HT after ':'
"XXXXX",
"HTTP/1.1 407 Proxy Authorization Required\r\n" +
"Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n",
"HTTP/1.1 401 Unauthorized\r\n" +
"WWW-Authenticate: Digest realm=\"wally land\" domain=/ " +
"nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n",
"HTTP/1.1 200 OK\r\n" +
"X-Foo:\r\n\r\n", // no value
"HTTP/1.1 200 OK\r\n" +
"X-Foo:\r\n\r\n" + // no value, with response body
"Some Response Body",
"HTTP/1.1 200 OK\r\n" +
"X-Foo:\r\n" + // no value, followed by another header
"Content-Length: 10\r\n\r\n" +
"Some Response Body",
"HTTP/1.1 200 OK\r\n" +
"X-Foo:\r\n" + // no value, followed by another header, with response body
"Content-Length: 10\r\n\r\n",
"HTTP/1.1 200 OK\r\n" +
"X-Foo: chegar\r\n" +
"X-Foo: dfuchs\r\n" + // same header appears multiple times
"Content-Length: 0\r\n" +
"X-Foo: michaelm\r\n" +
"X-Foo: prappo\r\n\r\n",
"HTTP/1.1 200 OK\r\n" +
"X-Foo:\r\n" + // no value, same header appears multiple times
"X-Foo: dfuchs\r\n" +
"Content-Length: 0\r\n" +
"X-Foo: michaelm\r\n" +
"X-Foo: prappo\r\n\r\n",
"HTTP/1.1 200 OK\r\n" +
"Accept-Ranges: bytes\r\n" +
"Cache-control: max-age=0, no-cache=\"set-cookie\"\r\n" +
"Content-Length: 132868\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"Date: Sun, 05 Nov 2017 22:24:03 GMT\r\n" +
"Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips Communique/4.2.2\r\n" +
"Set-Cookie: AWSELB=AF7927F5100F4202119876ED2436B5005EE;PATH=/;MAX-AGE=900\r\n" +
"Vary: Host,Accept-Encoding,User-Agent\r\n" +
"X-Mod-Pagespeed: 1.12.34.2-0\r\n" +
"Connection: keep-alive\r\n\r\n"
};
Arrays.stream(basic).forEach(responses::add);
// add some tests where some of the CRLF are replaced
// by a single LF
Arrays.stream(basic)
.map(HttpHeaderParserTest::mixedCRLF)
.forEach(responses::add);
String[] foldingTemplate =
{ "HTTP/1.1 200 OK\r\n" +
"Content-Length: 9\r\n" +
"Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r'
" charset=UTF-8\r\n" + // one preceding SP
"Connection: close\r\n\r\n" +
"XXYYZZAABBCCDDEE",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 19\r\n" +
"Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
" charset=UTF-8\r\n" + // more than one preceding SP
"Connection: keep-alive\r\n\r\n" +
"XXYYZZAABBCCDDEEFFGG",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 999\r\n" +
"Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
"\tcharset=UTF-8\r\n" + // one preceding HT
"Connection: close\r\n\r\n" +
"XXYYZZAABBCCDDEE",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 54\r\n" +
"Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
"\t\t\tcharset=UTF-8\r\n" + // more than one preceding HT
"Connection: keep-alive\r\n\r\n" +
"XXYYZZAABBCCDDEEFFGG",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: -1\r\n" +
"Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
"\t \t \tcharset=UTF-8\r\n" + // mix of preceding HT and SP
"Connection: keep-alive\r\n\r\n" +
"XXYYZZAABBCCDDEEFFGGHH",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 65\r\n" +
"Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r
" \t \t charset=UTF-8\r\n" + // mix of preceding SP and HT
"Connection: keep-alive\r\n\r\n" +
"XXYYZZAABBCCDDEEFFGGHHII",
"HTTP/1.1 401 Unauthorized\r\n" +
"WWW-Authenticate: Digest realm=\"wally land\","
+"$NEWLINE domain=/,"
+"$NEWLINE nonce=\"2B7F3A2B\","
+"$NEWLINE\tqop=\"auth\"\r\n\r\n",
};
for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) {
for (String template : foldingTemplate)
responses.add(template.replace("$NEWLINE", newLineChar));
}
// add some tests where some of the CRLF are replaced
// by a single LF
for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) {
for (String template : foldingTemplate)
responses.add(mixedCRLF(template).replace("$NEWLINE", newLineChar));
}
String[] bad = // much of this is to retain parity with legacy MessageHeaders
{ "HTTP/1.1 200 OK\r\n" +
"Connection:\r\n\r\n", // empty value, no body
"HTTP/1.1 200 OK\r\n" +
"Connection:\r\n\r\n" + // empty value, with body
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
": no header\r\n\r\n", // no/empty header-name, no body, no following header
"HTTP/1.1 200 OK\r\n" +
": no; header\r\n" + // no/empty header-name, no body, following header
"Content-Length: 65\r\n\r\n",
"HTTP/1.1 200 OK\r\n" +
": no header\r\n" + // no/empty header-name
"Content-Length: 65\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"X-foo: bar\r\n" +
" : no header\r\n" + // fold, not a blank header-name
"Content-Length: 65\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"X-foo: bar\r\n" +
" \t : no header\r\n" + // fold, not a blank header-name
"Content-Length: 65\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
": no header\r\n\r\n" + // no/empty header-name, followed by header
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"Conte\r" +
"nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXXYYZZ",
"HTTP/1.0 404 Not Found\r\n" +
"header-without-colon\r\n\r\n",
"HTTP/1.0 404 Not Found\r\n" +
"header-without-colon\r\n\r\n" +
"SOMEBODY",
};
Arrays.stream(bad).forEach(responses::add);
return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
}
static final AtomicInteger index = new AtomicInteger();
static final AtomicInteger limit = new AtomicInteger(1);
static final AtomicBoolean useCRLF = new AtomicBoolean();
// A small method to replace part of the CRLF present in a string
// with simple LF. The method uses a deterministic algorithm based
// on current values of static index/limit/useCRLF counters.
// These counters are used to produce a stream of substitutes that
// looks like this:
// LF CRLF LF LF CRLF CRLF LF LF LF CRLF CRLF CRLF (then repeat from start)
static final String mixedCRLF(String headers) {
int next;
int start = 0;
int last = headers.lastIndexOf("\r\n");
String prev = "";
StringBuilder res = new StringBuilder();
while ((next = headers.indexOf("\r\n", start)) > 0) {
res.append(headers.substring(start, next));
if ("\n".equals(prev) && next == last) {
// for some reason the legacy MessageHeader parser will
// not consume the final LF if the headers are terminated
// by <LF><CRLF> instead of <CRLF><CRLF>. It consume
// <LF><CR> but leaves the last <LF> in the stream.
// Here we just make sure to avoid using <LF><CRLF>
// as that would cause the legacy parser to consume
// 1 byte less than the Http1HeadersParser - which
// does consume the last <LF>, as it should.
// if this is the last CRLF and the previous one
// was replaced by LF then use LF.
res.append(prev);
} else {
prev = useCRLF.get() ? "\r\n" : "\n";
res.append(prev);
}
// skip CRLF
start = next + 2;
// The idea is to substitute some of the CRLF with LF.
// Rather than doing this randomly, always use the following
// sequence:
// LF CRLF LF LF CRLF CRLF LF LF LF CRLF CRLF CRLF
index.incrementAndGet();
if (index.get() == limit.get()) {
index.set(0);
if (useCRLF.get()) limit.incrementAndGet();
if (limit.get() > 3) limit.set(1);
useCRLF.set(!useCRLF.get());
}
}
res.append(headers.substring(start));
return res.toString();
}
@Test(dataProvider = "responses")
public void verifyHeaders(String respString) throws Exception {
System.out.println("\ntesting:\n\t" + respString
.replace("\r\n", "<CRLF>")
.replace("\r", "<CR>")
.replace("\n","<LF>")
.replace("LF>", "LF>\n\t"));
byte[] bytes = respString.getBytes(ISO_8859_1);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
MessageHeader m = new MessageHeader(bais);
Map<String,List<String>> messageHeaderMap = m.getHeaders();
int availableBytes = bais.available();
HttpHeaderParser decoder = new HttpHeaderParser();
ByteArrayInputStream headerStream = new ByteArrayInputStream(bytes);
int initialBytes = headerStream.available();
decoder.parse(headerStream);
System.out.printf("HttpHeaderParser parsed %d bytes out of %d%n", initialBytes - headerStream.available(), bytes.length);
Map<String,List<String>> decoderMap1 = decoder.getHeaderMap();
// assert status-line
String statusLine1 = messageHeaderMap.get(null).get(0);
String statusLine2 = decoder.getRequestDetails();
if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line
assertEquals(statusLine2, statusLine1, "Status-line not equal");
} else {
assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1.");
}
// remove the null'th entry with is the status-line
Map<String,List<String>> map = new HashMap<>();
for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) {
if (e.getKey() != null) {
map.put(e.getKey(), e.getValue());
}
}
messageHeaderMap = map;
assertHeadersEqual(messageHeaderMap, decoderMap1,
"messageHeaderMap not equal to decoderMap1");
assertEquals(availableBytes, headerStream.available(),
String.format("stream available (%d) not equal to remaining (%d)",
availableBytes, headerStream.available()));
}
@DataProvider(name = "errors")
public Object[][] errors() {
List<String> responses = new ArrayList<>();
// These responses are parsed, somewhat, by MessageHeaders but give
// nonsensible results. They, correctly, fail with the Http1HeaderParser.
String[] bad =
{// "HTTP/1.1 402 Payment Required\r\n" +
// "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete
"HTTP/1.1 402 Payment Required\r\n" +
"Content-Length: 65\r\n\rT\r\n\r\nGGGGGG",
"HTTP/1.1 200OK\r\n\rT",
"HTTP/1.1 200OK\rT",
"HTTP/1.0 FOO\r\n",
"HTTP/1.1 BAR\r\n",
"HTTP/1.1 +99\r\n",
"HTTP/1.1 -22\r\n",
"HTTP/1.1 -20 \r\n",
"HTTP/1.1 200 OK\r\n" +
"X-fo\u00ffo: foo\r\n" + // invalid char in name
"Content-Length: 5\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"HTTP/1.1 200 OK\r\n" +
"X-foo : bar\r\n" + // trim space after name
"Content-Length: 5\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
" X-foo: bar\r\n" + // trim space before name
"Content-Length: 5\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"X foo: bar\r\n" + // invalid space in name
"Content-Length: 5\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 5\r\n" +
"Content Type: text/html; charset=UTF-8\r\n\r\n" + // invalid space in name
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
"Conte\r" +
" nt-Length: 9\r\n" + // fold results in space in header name
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
" : no header\r\n" + // all blank header-name (not fold)
"Content-Length: 65\r\n\r\n" +
"XXXXX",
"HTTP/1.1 200 OK\r\n" +
" \t : no header\r\n" + // all blank header-name (not fold)
"Content-Length: 65\r\n\r\n" +
"XXXXX",
};
Arrays.stream(bad).forEach(responses::add);
return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new);
}
@Test(dataProvider = "errors", expectedExceptions = IOException.class)
public void errors(String respString) throws IOException {
byte[] bytes = respString.getBytes(US_ASCII);
HttpHeaderParser decoder = new HttpHeaderParser();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
decoder.parse(bais);
}
void assertHeadersEqual(Map<String,List<String>> expected,
Map<String,List<String>> actual,
String msg) {
if (expected.equals(actual))
return;
assertEquals(expected.size(), actual.size(),
format("%s. Expected size %d, actual size %s. %nexpected= %s,%n actual=%s.",
msg, expected.size(), actual.size(), mapToString(expected), mapToString(actual)));
for (Map.Entry<String,List<String>> e : expected.entrySet()) {
String key = e.getKey();
List<String> values = e.getValue();
boolean found = false;
for (Map.Entry<String,List<String>> other: actual.entrySet()) {
if (key.equalsIgnoreCase(other.getKey())) {
found = true;
List<String> otherValues = other.getValue();
assertEquals(values.size(), otherValues.size(),
format("%s. Expected list size %d, actual size %s",
msg, values.size(), otherValues.size()));
if (!(values.containsAll(otherValues) && otherValues.containsAll(values)))
assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues));
break;
}
}
assertTrue(found, format("header name, %s, not found in %s", key, actual));
}
}
static String mapToString(Map<String,List<String>> map) {
StringBuilder sb = new StringBuilder();
List<String> sortedKeys = new ArrayList(map.keySet());
Collections.sort(sortedKeys);
for (String key : sortedKeys) {
List<String> values = map.get(key);
sb.append("\n\t" + key + " | " + values);
}
return sb.toString();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2007, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2007, 2022, 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
@ -24,7 +24,6 @@
/*
* @test
* @bug 6520665 6357133
* @modules java.base/sun.net.www
* @library /test/lib
* @run main/othervm NTLMTest
* @summary 6520665 & 6357133: NTLM authentication issues.
@ -32,7 +31,8 @@
import java.net.*;
import java.io.*;
import sun.net.www.MessageHeader;
import jdk.test.lib.net.HttpHeaderParser;
import jdk.test.lib.net.URIBuilder;
public class NTLMTest
@ -160,7 +160,7 @@ public class NTLMTest
OutputStream os = s.getOutputStream();
for (int i=start; i<end; i++) {
MessageHeader header = new MessageHeader (s.getInputStream());
HttpHeaderParser httpHeaderParser = new HttpHeaderParser(s.getInputStream());
//System.out.println("Input :" + header);
//System.out.println("Output:" + resp[i]);
os.write(resp[i].getBytes("ASCII"));

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2013, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2013, 2022, 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
@ -26,8 +26,7 @@
* @library /test/lib
* @summary Sanity check that NTLM will not be selected by the http protocol
* handler when running on a profile that does not support NTLM
* @modules java.base/sun.net.www
* java.base/sun.net.www.protocol.http:open
* @modules java.base/sun.net.www.protocol.http:open
* @run main/othervm NoNTLM
* @run main/othervm -Djava.net.preferIPv6Addresses=true NoNTLM
*/
@ -42,8 +41,9 @@ import java.net.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import jdk.test.lib.net.HttpHeaderParser;
import jdk.test.lib.net.URIBuilder;
import sun.net.www.MessageHeader;
public class NoNTLM {
@ -163,7 +163,7 @@ public class NoNTLM {
// client ---- GET ---> server
// client <--- 401 ---- server
try (Socket s = ss.accept()) {
new MessageHeader().parseHeader(s.getInputStream());
new HttpHeaderParser().parse(s.getInputStream());
s.getOutputStream().write(reply.getBytes("US-ASCII"));
}
@ -171,10 +171,10 @@ public class NoNTLM {
// client <--- 200 ---- server
String auth;
try (Socket s = ss.accept()) {
MessageHeader mh = new MessageHeader();
mh.parseHeader(s.getInputStream());
HttpHeaderParser mh = new HttpHeaderParser();
mh.parse(s.getInputStream());
s.getOutputStream().write(OKAY.getBytes("US-ASCII"));
auth = mh.findValue("Authorization");
auth = mh.getHeaderValue("Authorization").get(0);
}
// check Authorization header
@ -208,7 +208,7 @@ public class NoNTLM {
// client ---- GET ---> server
// client <--- 401 ---- client
try (Socket s = ss.accept()) {
new MessageHeader().parseHeader(s.getInputStream());
new HttpHeaderParser().parse(s.getInputStream());
s.getOutputStream().write(reply.getBytes("US-ASCII"));
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2003, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2003, 2022, 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
@ -26,13 +26,13 @@
* @bug 4772077
* @library /test/lib
* @summary using defaultReadTimeout appear to retry request upon timeout
* @modules java.base/sun.net.www
*/
import java.net.*;
import java.io.*;
import jdk.test.lib.net.HttpHeaderParser;
import jdk.test.lib.net.URIBuilder;
import sun.net.www.*;
public class RetryUponTimeout implements Runnable {
// run server
@ -42,7 +42,7 @@ public class RetryUponTimeout implements Runnable {
for (int i = 0; i < 2; i++) {
socket = server.accept();
InputStream is = socket.getInputStream ();
MessageHeader header = new MessageHeader (is);
HttpHeaderParser header = new HttpHeaderParser (is);
count++;
}
} catch (Exception ex) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2001, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2001, 2022, 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
@ -25,7 +25,6 @@
* @test
* @bug 4512200
* @library /test/lib
* @modules java.base/sun.net.www
* @run main/othervm -Dhttp.agent=foo UserAgent
* @run main/othervm -Dhttp.agent=foo -Djava.net.preferIPv6Addresses=true UserAgent
* @summary HTTP header "User-Agent" format incorrect
@ -34,8 +33,9 @@
import java.io.*;
import java.util.*;
import java.net.*;
import jdk.test.lib.net.HttpHeaderParser;
import jdk.test.lib.net.URIBuilder;
import sun.net.www.MessageHeader;
class Server extends Thread {
Server (ServerSocket server) {
@ -46,8 +46,8 @@ class Server extends Thread {
String version = System.getProperty ("java.version");
String expected = "foo Java/"+version;
Socket s = server.accept ();
MessageHeader header = new MessageHeader (s.getInputStream());
String v = header.findValue ("User-Agent");
HttpHeaderParser header = new HttpHeaderParser (s.getInputStream());
String v = header.getHeaderValue ("User-Agent").get(0);
if (!expected.equals (v)) {
error ("Got unexpected User-Agent: " + v);
} else {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, 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
@ -25,7 +25,7 @@
* @test
* @bug 6226610 6973030
* @summary HTTP tunnel connections send user headers to proxy
* @modules java.base/sun.net.www
* @library /test/lib
* @run main/othervm B6226610
*/
@ -37,7 +37,9 @@
import java.io.*;
import java.net.*;
import sun.net.www.MessageHeader;
import jdk.test.lib.net.HttpHeaderParser;
public class B6226610 {
static HeaderCheckerProxyTunnelServer proxy;
@ -138,21 +140,21 @@ class HeaderCheckerProxyTunnelServer extends Thread
private void processRequests() throws IOException
{
InputStream in = clientSocket.getInputStream();
MessageHeader mheader = new MessageHeader(in);
String statusLine = mheader.getValue(0);
HttpHeaderParser mheader = new HttpHeaderParser(in);
String statusLine = mheader.getRequestDetails();
if (statusLine.startsWith("CONNECT")) {
// retrieve the host and port info from the status-line
retrieveConnectInfo(statusLine);
if (mheader.findValue("X-TestHeader") != null) {
if (mheader.getHeaderValue("X-TestHeader") != null) {
System.out.println("Proxy should not receive user defined headers for tunneled requests");
failed = true;
}
// 6973030
String value;
if ((value = mheader.findValue("Proxy-Connection")) == null ||
if ((value = mheader.getHeaderValue("Proxy-Connection").get(0)) == null ||
!value.equals("keep-alive")) {
System.out.println("Proxy-Connection:keep-alive not being sent");
failed = true;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, 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
@ -45,7 +45,8 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import sun.net.www.MessageHeader;
import jdk.test.lib.net.HttpHeaderParser;
public class TunnelProxy {
@ -261,7 +262,7 @@ public class TunnelProxy {
try {
InputStream is = new BufferedInputStream (new NioInputStream (chan));
String requestline = readLine (is);
MessageHeader mhead = new MessageHeader (is);
HttpHeaderParser mhead = new HttpHeaderParser (is);
String[] req = requestline.split (" ");
if (req.length < 2) {
/* invalid request line */

View File

@ -0,0 +1,390 @@
/*
* Copyright (c) 2022, 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.test.lib.net;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static java.util.Objects.requireNonNull;
public class HttpHeaderParser {
private static final char CR = '\r';
private static final char LF = '\n';
private static final char HT = '\t';
private static final char SP = ' ';
// ABNF primitives defined in RFC 7230
private static boolean[] tchar = new boolean[256];
private static boolean[] fieldvchar = new boolean[256];
static {
char[] allowedTokenChars =
("!#$%&'*+-.^_`|~0123456789" +
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();
for (char c : allowedTokenChars) {
tchar[c] = true;
}
for (char c = 0x21; c <= 0xFF; c++) {
fieldvchar[c] = true;
}
fieldvchar[0x7F] = false; // a little hole (DEL) in the range
}
private StringBuilder sb = new StringBuilder();
private Map <String, List<String>> headerMap = new LinkedHashMap<>();
private List <String> keyList = new ArrayList<>();
private String requestOrStatusLine;
private int responseCode;
private boolean eof;
enum State { INITIAL,
STATUS_OR_REQUEST_LINE,
STATUS_OR_REQUEST_LINE_FOUND_CR,
STATUS_OR_REQUEST_LINE_FOUND_LF,
STATUS_OR_REQUEST_LINE_END,
STATUS_OR_REQUEST_LINE_END_CR,
STATUS_OR_REQUEST_LINE_END_LF,
HEADER,
HEADER_FOUND_CR,
HEADER_FOUND_LF,
HEADER_FOUND_CR_LF,
HEADER_FOUND_CR_LF_CR,
FINISHED }
private HttpHeaderParser.State state = HttpHeaderParser.State.INITIAL;
public HttpHeaderParser() {
}
public HttpHeaderParser(InputStream is) throws IOException, ProtocolException {
parse(is);
}
public Map<String, List<String>> getHeaderMap() {
return headerMap;
}
public List<String> getHeaderValue(String key) {
if(headerMap.containsKey(key.toLowerCase(Locale.ROOT))) {
return headerMap.get(key.toLowerCase(Locale.ROOT));
}
return null;
}
public List<String> getValue(int id) {
String key = keyList.get(id);
return headerMap.get(key);
}
public String getRequestDetails() {
return requestOrStatusLine;
}
/**
* Parses HTTP/1.X status-line or request-line and headers from the given input stream.
* @param input Containing the input stream of bytes representing request or response header data
* @return true if the end of the headers block has been reached
*/
public boolean parse(InputStream input) throws IOException {
requireNonNull(input, "null input");
while (canContinueParsing()) {
switch (state) {
case INITIAL -> state = HttpHeaderParser.State.STATUS_OR_REQUEST_LINE;
case STATUS_OR_REQUEST_LINE -> readResumeStatusLine(input);
case STATUS_OR_REQUEST_LINE_FOUND_CR, STATUS_OR_REQUEST_LINE_FOUND_LF -> readStatusLineFeed(input);
case STATUS_OR_REQUEST_LINE_END -> maybeStartHeaders(input);
case STATUS_OR_REQUEST_LINE_END_CR, STATUS_OR_REQUEST_LINE_END_LF -> maybeEndHeaders(input);
case HEADER -> readResumeHeader(input);
case HEADER_FOUND_CR, HEADER_FOUND_LF -> resumeOrLF(input);
case HEADER_FOUND_CR_LF -> resumeOrSecondCR(input);
case HEADER_FOUND_CR_LF_CR -> resumeOrEndHeaders(input);
default -> throw new InternalError("Unexpected state: " + state);
}
}
return state == HttpHeaderParser.State.FINISHED;
}
private boolean canContinueParsing() {
// some states don't require any input to transition
// to the next state.
return switch (state) {
case FINISHED -> false;
case STATUS_OR_REQUEST_LINE_FOUND_LF, STATUS_OR_REQUEST_LINE_END_LF, HEADER_FOUND_LF -> true;
default -> !eof;
};
}
/**
* Returns a character (char) corresponding to the next byte in the
* input, interpreted as an ISO-8859-1 encoded character.
* <p>
* The ISO-8859-1 encoding is a 8-bit character coding that
* corresponds to the first 256 Unicode characters - from U+0000 to
* U+00FF. UTF-16 is backward compatible with ISO-8859-1 - which
* means each byte in the input should be interpreted as an unsigned
* value from [0, 255] representing the character code.
*
* @param input a {@code InputStream} containing input stream of Bytes.
* @return the next byte in the input, interpreted as an ISO-8859-1
* encoded char
* @throws IOException
* if an I/O error occurs.
*/
private char get(InputStream input) throws IOException {
int c = input.read();
if(c < 0)
eof = true;
return (char)(c & 0xFF);
}
private void readResumeStatusLine(InputStream input) throws IOException {
char c;
while ((c = get(input)) != CR && !eof) {
if (c == LF) break;
sb.append(c);
}
if (c == CR) {
state = HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_FOUND_CR;
} else if (c == LF) {
state = HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_FOUND_LF;
}
}
private void readStatusLineFeed(InputStream input) throws IOException {
char c = state == HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_FOUND_LF ? LF : get(input);
if (c != LF) {
throw protocolException("Bad trailing char, \"%s\", when parsing status line, \"%s\"",
c, sb.toString());
}
requestOrStatusLine = sb.toString();
sb = new StringBuilder();
if (!requestOrStatusLine.startsWith("HTTP/1.")) {
if(!requestOrStatusLine.startsWith("GET") && !requestOrStatusLine.startsWith("POST") &&
!requestOrStatusLine.startsWith("PUT") && !requestOrStatusLine.startsWith("DELETE") &&
!requestOrStatusLine.startsWith("OPTIONS") && !requestOrStatusLine.startsWith("HEAD") &&
!requestOrStatusLine.startsWith("PATCH") && !requestOrStatusLine.startsWith("CONNECT")) {
throw protocolException("Invalid request Or Status line: \"%s\"", requestOrStatusLine);
} else { //This is request
System.out.println("Request is :"+requestOrStatusLine);
}
} else { //This is response
if (requestOrStatusLine.length() < 12) {
throw protocolException("Invalid status line: \"%s\"", requestOrStatusLine);
}
try {
responseCode = Integer.parseInt(requestOrStatusLine.substring(9, 12));
} catch (NumberFormatException nfe) {
throw protocolException("Invalid status line: \"%s\"", requestOrStatusLine);
}
// response code expected to be a 3-digit integer (RFC-2616, section 6.1.1)
if (responseCode < 100) {
throw protocolException("Invalid status line: \"%s\"", requestOrStatusLine);
}
}
state = HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_END;
}
private void maybeStartHeaders(InputStream input) throws IOException {
assert state == HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_END;
assert sb.length() == 0;
char c = get(input);
if(!eof) {
if (c == CR) {
state = HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_END_CR;
} else if (c == LF) {
state = HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_END_LF;
} else {
sb.append(c);
state = HttpHeaderParser.State.HEADER;
}
}
}
private void maybeEndHeaders(InputStream input) throws IOException {
assert state == HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_END_CR || state == HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_END_LF;
assert sb.length() == 0;
char c = state == HttpHeaderParser.State.STATUS_OR_REQUEST_LINE_END_LF ? LF : get(input);
if (c == LF) {
state = HttpHeaderParser.State.FINISHED; // no headers
} else {
throw protocolException("Unexpected \"%s\", after status line CR", c);
}
}
private void readResumeHeader(InputStream input) throws IOException {
assert state == HttpHeaderParser.State.HEADER;
assert !eof;
char c = get(input);
while (!eof) {
if (c == CR) {
state = HttpHeaderParser.State.HEADER_FOUND_CR;
break;
} else if (c == LF) {
state = HttpHeaderParser.State.HEADER_FOUND_LF;
break;
}
if (c == HT)
c = SP;
sb.append(c);
c = get(input);
}
}
private void addHeaderFromString(String headerString) throws ProtocolException {
assert sb.length() == 0;
int idx = headerString.indexOf(':');
if (idx == -1)
return;
String name = headerString.substring(0, idx);
// compatibility with HttpURLConnection;
if (name.isEmpty()) return;
if (!isValidName(name)) {
throw protocolException("Invalid header name \"%s\"", name);
}
String value = headerString.substring(idx + 1).trim();
if (!isValidValue(value)) {
throw protocolException("Invalid header value \"%s: %s\"", name, value);
}
keyList.add(name);
headerMap.computeIfAbsent(name.toLowerCase(Locale.US),
k -> new ArrayList<>()).add(value);
}
private void resumeOrLF(InputStream input) throws IOException {
assert state == HttpHeaderParser.State.HEADER_FOUND_CR || state == HttpHeaderParser.State.HEADER_FOUND_LF;
char c = state == HttpHeaderParser.State.HEADER_FOUND_LF ? LF : get(input);
if (!eof) {
if (c == LF) {
state = HttpHeaderParser.State.HEADER_FOUND_CR_LF;
} else if (c == SP || c == HT) {
sb.append(SP); // parity with MessageHeaders
state = HttpHeaderParser.State.HEADER;
} else {
sb = new StringBuilder();
sb.append(c);
state = HttpHeaderParser.State.HEADER;
}
}
}
private void resumeOrSecondCR(InputStream input) throws IOException {
assert state == HttpHeaderParser.State.HEADER_FOUND_CR_LF;
char c = get(input);
if (!eof) {
if (c == CR || c == LF) {
if (sb.length() > 0) {
// no continuation line - flush
// previous header value.
String headerString = sb.toString();
sb = new StringBuilder();
addHeaderFromString(headerString);
}
if (c == CR) {
state = HttpHeaderParser.State.HEADER_FOUND_CR_LF_CR;
} else {
state = HttpHeaderParser.State.FINISHED;
}
} else if (c == SP || c == HT) {
assert sb.length() != 0;
sb.append(SP); // continuation line
state = HttpHeaderParser.State.HEADER;
} else {
if (sb.length() > 0) {
// no continuation line - flush
// previous header value.
String headerString = sb.toString();
sb = new StringBuilder();
addHeaderFromString(headerString);
}
sb.append(c);
state = HttpHeaderParser.State.HEADER;
}
}
}
private void resumeOrEndHeaders(InputStream input) throws IOException {
assert state == HttpHeaderParser.State.HEADER_FOUND_CR_LF_CR;
char c = get(input);
if (!eof) {
if (c == LF) {
state = HttpHeaderParser.State.FINISHED;
} else {
throw protocolException("Unexpected \"%s\", after CR LF CR", c);
}
}
}
private ProtocolException protocolException(String format, Object ... args) {
return new ProtocolException(String.format(format, args));
}
/*
* Validates a RFC 7230 field-name.
*/
public boolean isValidName(String token) {
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (c > 255 || !tchar[c]) {
return false;
}
}
return !token.isEmpty();
}
/*
* Validates a RFC 7230 field-value.
*
* "Obsolete line folding" rule
*
* obs-fold = CRLF 1*( SP / HTAB )
*
* is not permitted!
*/
public boolean isValidValue(String token) {
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (c > 255) {
return false;
}
if (c == ' ' || c == '\t') {
continue;
} else if (!fieldvchar[c]) {
return false; // forbidden byte
}
}
return true;
}
}