8209178: Proxied HttpsURLConnection doesn't send BODY when retrying POST request
Preserve BODY in poster output stream before sending CONNECT request Reviewed-by: dfuchs, vtewari
This commit is contained in:
parent
8e98ce54bf
commit
82747fa960
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 1994, 2016, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 1994, 2019, 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
|
||||
@ -707,11 +707,7 @@ public class HttpClient extends NetworkClient {
|
||||
} else {
|
||||
// try once more
|
||||
openServer();
|
||||
if (needsTunneling()) {
|
||||
MessageHeader origRequests = requests;
|
||||
httpuc.doTunneling();
|
||||
requests = origRequests;
|
||||
}
|
||||
checkTunneling(httpuc);
|
||||
afterConnect();
|
||||
writeRequests(requests, poster);
|
||||
return parseHTTP(responses, pi, httpuc);
|
||||
@ -722,6 +718,18 @@ public class HttpClient extends NetworkClient {
|
||||
|
||||
}
|
||||
|
||||
// Check whether tunnel must be open and open it if necessary
|
||||
// (in the case of HTTPS with proxy)
|
||||
private void checkTunneling(HttpURLConnection httpuc) throws IOException {
|
||||
if (needsTunneling()) {
|
||||
MessageHeader origRequests = requests;
|
||||
PosterOutputStream origPoster = poster;
|
||||
httpuc.doTunneling();
|
||||
requests = origRequests;
|
||||
poster = origPoster;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseHTTPHeader(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc)
|
||||
throws IOException {
|
||||
/* If "HTTP/*" is found in the beginning, return true. Let
|
||||
@ -849,11 +857,7 @@ public class HttpClient extends NetworkClient {
|
||||
closeServer();
|
||||
cachedHttpClient = false;
|
||||
openServer();
|
||||
if (needsTunneling()) {
|
||||
MessageHeader origRequests = requests;
|
||||
httpuc.doTunneling();
|
||||
requests = origRequests;
|
||||
}
|
||||
checkTunneling(httpuc);
|
||||
afterConnect();
|
||||
writeRequests(requests, poster);
|
||||
return parseHTTP(responses, pi, httpuc);
|
||||
|
409
test/jdk/sun/net/www/http/HttpClient/B8209178.java
Normal file
409
test/jdk/sun/net/www/http/HttpClient/B8209178.java
Normal file
@ -0,0 +1,409 @@
|
||||
/*
|
||||
* Copyright (c) 2019, 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 8209178
|
||||
* @modules java.base/sun.net.www java.base/sun.security.x509 java.base/sun.security.tools.keytool
|
||||
* @library /test/lib
|
||||
* @run main/othervm -Dsun.net.http.retryPost=true B8209178
|
||||
* @run main/othervm -Dsun.net.http.retryPost=false B8209178
|
||||
* @summary Proxied HttpsURLConnection doesn't send BODY when retrying POST request
|
||||
*/
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import javax.net.ssl.*;
|
||||
|
||||
import com.sun.net.httpserver.*;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import sun.security.tools.keytool.CertAndKeyGen;
|
||||
import sun.security.x509.X500Name;
|
||||
|
||||
public class B8209178 {
|
||||
static {
|
||||
try {
|
||||
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
|
||||
SSLContext.setDefault(new TestSSLContext().get());
|
||||
} catch (Exception ex) {
|
||||
throw new ExceptionInInitializerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
|
||||
static final String PATH = "/foo/";
|
||||
static final String RETRYPOST = System.getProperty("sun.net.http.retryPost");
|
||||
|
||||
static HttpServer createHttpsServer() throws IOException, NoSuchAlgorithmException {
|
||||
HttpsServer server = HttpsServer.create();
|
||||
HttpContext context = server.createContext(PATH);
|
||||
context.setHandler(new HttpHandler() {
|
||||
|
||||
boolean simulateError = true;
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange he) throws IOException {
|
||||
|
||||
System.out.printf("%s - received request on : %s%n",
|
||||
Thread.currentThread().getName(),
|
||||
he.getRequestURI());
|
||||
System.out.printf("%s - received request headers : %s%n",
|
||||
Thread.currentThread().getName(),
|
||||
new HashMap(he.getRequestHeaders()));
|
||||
|
||||
InputStream requestBody = he.getRequestBody();
|
||||
String body = B8209178.toString(requestBody);
|
||||
|
||||
System.out.printf("%s - received request body : %s%n",
|
||||
Thread.currentThread().getName(), body);
|
||||
|
||||
if (simulateError) {
|
||||
simulateError = false;
|
||||
|
||||
System.out.printf("%s - closing connection unexpectedly ... %n",
|
||||
Thread.currentThread().getName(), he.getRequestHeaders());
|
||||
|
||||
he.close(); // try not to respond anything the first time ...
|
||||
return;
|
||||
}
|
||||
|
||||
he.getResponseHeaders().add("encoding", "UTF-8");
|
||||
he.sendResponseHeaders(200, RESPONSE.length());
|
||||
he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8));
|
||||
he.close();
|
||||
}
|
||||
});
|
||||
|
||||
server.setHttpsConfigurator(new Configurator(SSLContext.getDefault()));
|
||||
server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
||||
return server;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
|
||||
HttpServer server = createHttpsServer();
|
||||
server.start();
|
||||
try {
|
||||
new B8209178().test(server);
|
||||
|
||||
} finally {
|
||||
server.stop(0);
|
||||
System.out.println("Server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public void test(HttpServer server /*, HttpClient.Version version*/) throws IOException {
|
||||
System.out.println("System property retryPost: " + RETRYPOST);
|
||||
System.out.println("Server is: " + server.getAddress());
|
||||
System.out.println("Verifying communication with server");
|
||||
URI uri = URIBuilder.newBuilder()
|
||||
.scheme("https")
|
||||
.host(server.getAddress().getAddress())
|
||||
.port(server.getAddress().getPort())
|
||||
.path(PATH + "x")
|
||||
.buildUnchecked();
|
||||
|
||||
TunnelingProxy proxy = new TunnelingProxy(server);
|
||||
proxy.start();
|
||||
|
||||
try {
|
||||
System.out.println("Proxy started");
|
||||
Proxy p = new Proxy(Proxy.Type.HTTP,
|
||||
InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
|
||||
System.out.println("Verifying communication with proxy");
|
||||
|
||||
callHttpsServerThroughProxy(uri, p);
|
||||
|
||||
} finally {
|
||||
System.out.println("Stopping proxy");
|
||||
proxy.stop();
|
||||
System.out.println("Proxy stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private void callHttpsServerThroughProxy(URI uri, Proxy p) throws IOException {
|
||||
HttpsURLConnection urlConnection = (HttpsURLConnection) uri.toURL().openConnection(p);
|
||||
|
||||
urlConnection.setConnectTimeout(1000);
|
||||
urlConnection.setReadTimeout(3000);
|
||||
urlConnection.setDoInput(true);
|
||||
urlConnection.setDoOutput(true);
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setUseCaches(false);
|
||||
|
||||
urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
|
||||
urlConnection.setRequestProperty("charset", "utf-8");
|
||||
urlConnection.setRequestProperty("Connection", "keep-alive");
|
||||
|
||||
String urlParameters = "param1=a¶m2=b¶m3=c";
|
||||
byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
OutputStream outputStream = urlConnection.getOutputStream();
|
||||
outputStream.write(postData);
|
||||
outputStream.close();
|
||||
|
||||
int responseCode;
|
||||
|
||||
try {
|
||||
responseCode = urlConnection.getResponseCode();
|
||||
System.out.printf(" ResponseCode : %s%n", responseCode);
|
||||
String output;
|
||||
InputStream inputStream = (responseCode < 400) ? urlConnection.getInputStream() : urlConnection.getErrorStream();
|
||||
output = toString(inputStream);
|
||||
inputStream.close();
|
||||
System.out.printf(" Output from server : %s%n", output);
|
||||
|
||||
if (responseCode == 200) { // OK !
|
||||
} else {
|
||||
throw new RuntimeException("Bad response Code : " + responseCode);
|
||||
}
|
||||
} catch (SocketException se) {
|
||||
if (RETRYPOST.equals("true")) { // Should not get here with the fix
|
||||
throw new RuntimeException("Unexpected Socket Exception: " + se);
|
||||
} else {
|
||||
System.out.println("Socket Exception received as expected: " + se);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class TunnelingProxy {
|
||||
final Thread accept;
|
||||
final ServerSocket ss;
|
||||
final boolean DEBUG = false;
|
||||
final HttpServer serverImpl;
|
||||
|
||||
TunnelingProxy(HttpServer serverImpl) throws IOException {
|
||||
this.serverImpl = serverImpl;
|
||||
ss = new ServerSocket();
|
||||
accept = new Thread(this::accept);
|
||||
}
|
||||
|
||||
void start() throws IOException {
|
||||
ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
|
||||
accept.start();
|
||||
}
|
||||
|
||||
// Pipe the input stream to the output stream
|
||||
private synchronized Thread pipe(InputStream is, OutputStream os, char tag) {
|
||||
return new Thread("TunnelPipe(" + tag + ")") {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
try {
|
||||
int c;
|
||||
while ((c = is.read()) != -1) {
|
||||
os.write(c);
|
||||
os.flush();
|
||||
// if DEBUG prints a + or a - for each transferred
|
||||
// character.
|
||||
if (DEBUG) System.out.print(tag);
|
||||
}
|
||||
is.close();
|
||||
} finally {
|
||||
os.close();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
if (DEBUG) ex.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public InetSocketAddress getAddress() {
|
||||
return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());
|
||||
}
|
||||
|
||||
// This is a bit shaky. It doesn't handle continuation
|
||||
// lines, but our client shouldn't send any.
|
||||
// Read a line from the input stream, swallowing the final
|
||||
// \r\n sequence. Stops at the first \n, doesn't complain
|
||||
// if it wasn't preceded by '\r'.
|
||||
//
|
||||
String readLine(InputStream r) throws IOException {
|
||||
StringBuilder b = new StringBuilder();
|
||||
int c;
|
||||
while ((c = r.read()) != -1) {
|
||||
if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
b.appendCodePoint(c);
|
||||
}
|
||||
if (b.codePointAt(b.length() - 1) == '\r') {
|
||||
b.delete(b.length() - 1, b.length());
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public void accept() {
|
||||
Socket clientConnection = null;
|
||||
try {
|
||||
while (true) {
|
||||
System.out.println("Tunnel: Waiting for client");
|
||||
Socket previous = clientConnection;
|
||||
try {
|
||||
clientConnection = ss.accept();
|
||||
} catch (IOException io) {
|
||||
if (DEBUG) io.printStackTrace(System.out);
|
||||
break;
|
||||
} finally {
|
||||
// we have only 1 client at a time, so it is safe
|
||||
// to close the previous connection here
|
||||
if (previous != null) previous.close();
|
||||
}
|
||||
System.out.println("Tunnel: Client accepted");
|
||||
Socket targetConnection = null;
|
||||
InputStream ccis = clientConnection.getInputStream();
|
||||
OutputStream ccos = clientConnection.getOutputStream();
|
||||
Writer w = new OutputStreamWriter(ccos, "UTF-8");
|
||||
PrintWriter pw = new PrintWriter(w);
|
||||
System.out.println("Tunnel: Reading request line");
|
||||
String requestLine = readLine(ccis);
|
||||
System.out.println("Tunnel: Request status line: " + requestLine);
|
||||
if (requestLine.startsWith("CONNECT ")) {
|
||||
// We should probably check that the next word following
|
||||
// CONNECT is the host:port of our HTTPS serverImpl.
|
||||
// Some improvement for a followup!
|
||||
|
||||
// Read all headers until we find the empty line that
|
||||
// signals the end of all headers.
|
||||
while (!requestLine.equals("")) {
|
||||
System.out.println("Tunnel: Reading header: "
|
||||
+ (requestLine = readLine(ccis)));
|
||||
}
|
||||
|
||||
// Open target connection
|
||||
targetConnection = new Socket(
|
||||
serverImpl.getAddress().getAddress(),
|
||||
serverImpl.getAddress().getPort());
|
||||
|
||||
// Then send the 200 OK response to the client
|
||||
System.out.println("Tunnel: Sending "
|
||||
+ "HTTP/1.1 200 OK\r\n\r\n");
|
||||
pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
||||
pw.flush();
|
||||
} else {
|
||||
// This should not happen
|
||||
throw new IOException("Tunnel: Unexpected status line: "
|
||||
+ requestLine);
|
||||
}
|
||||
|
||||
// Pipe the input stream of the client connection to the
|
||||
// output stream of the target connection and conversely.
|
||||
// Now the client and target will just talk to each other.
|
||||
System.out.println("Tunnel: Starting tunnel pipes");
|
||||
Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+');
|
||||
Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-');
|
||||
t1.start();
|
||||
t2.start();
|
||||
|
||||
// We have only 1 client... wait until it has finished before
|
||||
// accepting a new connection request.
|
||||
System.out.println("Tunnel: Waiting for pipes to close");
|
||||
t1.join();
|
||||
t2.join();
|
||||
System.out.println("Tunnel: Done - waiting for next client");
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
try {
|
||||
ss.close();
|
||||
} catch (IOException ex1) {
|
||||
ex.addSuppressed(ex1);
|
||||
}
|
||||
ex.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
void stop() throws IOException {
|
||||
ss.close();
|
||||
}
|
||||
}
|
||||
|
||||
static class Configurator extends HttpsConfigurator {
|
||||
public Configurator(SSLContext ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(HttpsParameters params) {
|
||||
params.setSSLParameters(getSSLContext().getSupportedSSLParameters());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class TestSSLContext {
|
||||
|
||||
SSLContext ssl;
|
||||
|
||||
public TestSSLContext() throws Exception {
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() throws Exception {
|
||||
|
||||
CertAndKeyGen keyGen = new CertAndKeyGen("RSA", "SHA1WithRSA", null);
|
||||
keyGen.generate(1024);
|
||||
|
||||
//Generate self signed certificate
|
||||
X509Certificate[] chain = new X509Certificate[1];
|
||||
chain[0] = keyGen.getSelfCertificate(new X500Name("CN=ROOT"), (long) 365 * 24 * 3600);
|
||||
|
||||
char[] passphrase = "passphrase".toCharArray();
|
||||
|
||||
KeyStore ks = KeyStore.getInstance("JKS");
|
||||
ks.load(null, passphrase); // must be "initialized" ...
|
||||
|
||||
ks.setKeyEntry("server", keyGen.getPrivateKey(), passphrase, chain);
|
||||
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
|
||||
kmf.init(ks, passphrase);
|
||||
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
|
||||
tmf.init(ks);
|
||||
|
||||
ssl = SSLContext.getInstance("TLS");
|
||||
ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
|
||||
}
|
||||
|
||||
public SSLContext get() {
|
||||
return ssl;
|
||||
}
|
||||
}
|
||||
|
||||
// ###############################################################################################
|
||||
|
||||
private static String toString(InputStream inputStream) throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
int i = bufferedReader.read();
|
||||
while (i != -1) {
|
||||
sb.append((char) i);
|
||||
i = bufferedReader.read();
|
||||
}
|
||||
bufferedReader.close();
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user