1/*
2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24import com.sun.net.httpserver.HttpContext;
25import com.sun.net.httpserver.HttpExchange;
26import com.sun.net.httpserver.HttpHandler;
27import com.sun.net.httpserver.HttpServer;
28import com.sun.net.httpserver.HttpsConfigurator;
29import com.sun.net.httpserver.HttpsParameters;
30import com.sun.net.httpserver.HttpsServer;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.OutputStream;
34import java.io.OutputStreamWriter;
35import java.io.PrintWriter;
36import java.io.Writer;
37import java.net.HttpURLConnection;
38import java.net.InetAddress;
39import java.net.InetSocketAddress;
40import java.net.Proxy;
41import java.net.ProxySelector;
42import java.net.ServerSocket;
43import java.net.Socket;
44import java.net.URI;
45import java.net.URISyntaxException;
46import java.nio.charset.StandardCharsets;
47import java.security.NoSuchAlgorithmException;
48import javax.net.ssl.HostnameVerifier;
49import javax.net.ssl.HttpsURLConnection;
50import javax.net.ssl.SSLContext;
51import javax.net.ssl.SSLSession;
52import jdk.incubator.http.HttpClient;
53import jdk.incubator.http.HttpRequest;
54import jdk.incubator.http.HttpResponse;
55import jdk.testlibrary.SimpleSSLContext;
56import java.util.concurrent.*;
57
58/**
59 * @test
60 * @bug 8181422
61 * @summary  Verifies that you can access an HTTP/2 server over HTTPS by
62 *           tunnelling through an HTTP/1.1 proxy.
63 * @modules jdk.incubator.httpclient
64 * @library /lib/testlibrary server
65 * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
66 *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
67 *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
68 * @build jdk.testlibrary.SimpleSSLContext ProxyTest2
69 * @run main/othervm ProxyTest2
70 * @author danielfuchs
71 */
72public class ProxyTest2 {
73
74    static {
75        try {
76            HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
77                    public boolean verify(String hostname, SSLSession session) {
78                        return true;
79                    }
80                });
81            SSLContext.setDefault(new SimpleSSLContext().get());
82        } catch (IOException ex) {
83            throw new ExceptionInInitializerError(ex);
84        }
85    }
86
87    static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
88    static final String PATH = "/foo/";
89
90    static Http2TestServer createHttpsServer(ExecutorService exec) throws Exception {
91        Http2TestServer server = new Http2TestServer(true, 0, exec, SSLContext.getDefault());
92        server.addHandler(new Http2Handler() {
93            @Override
94            public void handle(Http2TestExchange he) throws IOException {
95                he.getResponseHeaders().addHeader("encoding", "UTF-8");
96                he.sendResponseHeaders(200, RESPONSE.length());
97                he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8));
98                he.close();
99            }
100        }, PATH);
101
102        return server;
103    }
104
105    public static void main(String[] args)
106            throws Exception
107    {
108        ExecutorService exec = Executors.newCachedThreadPool();
109        Http2TestServer server = createHttpsServer(exec);
110        server.start();
111        try {
112            // Http2TestServer over HTTPS does not support HTTP/1.1
113            // => only test with a HTTP/2 client
114            test(server, HttpClient.Version.HTTP_2);
115        } finally {
116            server.stop();
117            exec.shutdown();
118            System.out.println("Server stopped");
119        }
120    }
121
122    public static void test(Http2TestServer server, HttpClient.Version version)
123            throws Exception
124    {
125        System.out.println("Server is: " + server.getAddress().toString());
126        URI uri = new URI("https://localhost:" + server.getAddress().getPort() + PATH + "x");
127        TunnelingProxy proxy = new TunnelingProxy(server);
128        proxy.start();
129        try {
130            System.out.println("Proxy started");
131            Proxy p = new Proxy(Proxy.Type.HTTP,
132                    InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
133            System.out.println("Setting up request with HttpClient for version: "
134                    + version.name() + "URI=" + uri);
135            ProxySelector ps = ProxySelector.of(
136                    InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
137            HttpClient client = HttpClient.newBuilder()
138                .version(version)
139                .proxy(ps)
140                .build();
141            HttpRequest request = HttpRequest.newBuilder()
142                .uri(uri)
143                .GET()
144                .build();
145
146            System.out.println("Sending request with HttpClient");
147            HttpResponse<String> response
148                = client.send(request, HttpResponse.BodyHandler.asString());
149            System.out.println("Got response");
150            String resp = response.body();
151            System.out.println("Received: " + resp);
152            if (!RESPONSE.equals(resp)) {
153                throw new AssertionError("Unexpected response");
154            }
155        } finally {
156            System.out.println("Stopping proxy");
157            proxy.stop();
158            System.out.println("Proxy stopped");
159        }
160    }
161
162    static class TunnelingProxy {
163        final Thread accept;
164        final ServerSocket ss;
165        final boolean DEBUG = false;
166        final Http2TestServer serverImpl;
167        TunnelingProxy(Http2TestServer serverImpl) throws IOException {
168            this.serverImpl = serverImpl;
169            ss = new ServerSocket();
170            accept = new Thread(this::accept);
171        }
172
173        void start() throws IOException {
174            ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
175            accept.start();
176        }
177
178        // Pipe the input stream to the output stream.
179        private synchronized Thread pipe(InputStream is, OutputStream os, char tag) {
180            return new Thread("TunnelPipe("+tag+")") {
181                @Override
182                public void run() {
183                    try {
184                        try {
185                            int c;
186                            while ((c = is.read()) != -1) {
187                                os.write(c);
188                                os.flush();
189                                // if DEBUG prints a + or a - for each transferred
190                                // character.
191                                if (DEBUG) System.out.print(tag);
192                            }
193                            is.close();
194                        } finally {
195                            os.close();
196                        }
197                    } catch (IOException ex) {
198                        if (DEBUG) ex.printStackTrace(System.out);
199                    }
200                }
201            };
202        }
203
204        public InetSocketAddress getAddress() {
205            return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());
206        }
207
208        // This is a bit shaky. It doesn't handle continuation
209        // lines, but our client shouldn't send any.
210        // Read a line from the input stream, swallowing the final
211        // \r\n sequence. Stops at the first \n, doesn't complain
212        // if it wasn't preceded by '\r'.
213        //
214        String readLine(InputStream r) throws IOException {
215            StringBuilder b = new StringBuilder();
216            int c;
217            while ((c = r.read()) != -1) {
218                if (c == '\n') break;
219                b.appendCodePoint(c);
220            }
221            if (b.codePointAt(b.length() -1) == '\r') {
222                b.delete(b.length() -1, b.length());
223            }
224            return b.toString();
225        }
226
227        public void accept() {
228            Socket clientConnection = null;
229            try {
230                while (true) {
231                    System.out.println("Tunnel: Waiting for client");
232                    Socket previous = clientConnection;
233                    try {
234                        clientConnection = ss.accept();
235                    } catch (IOException io) {
236                        if (DEBUG) io.printStackTrace(System.out);
237                        break;
238                    } finally {
239                        // we have only 1 client at a time, so it is safe
240                        // to close the previous connection here
241                        if (previous != null) previous.close();
242                    }
243                    System.out.println("Tunnel: Client accepted");
244                    Socket targetConnection = null;
245                    InputStream  ccis = clientConnection.getInputStream();
246                    OutputStream ccos = clientConnection.getOutputStream();
247                    Writer w = new OutputStreamWriter(ccos, "UTF-8");
248                    PrintWriter pw = new PrintWriter(w);
249                    System.out.println("Tunnel: Reading request line");
250                    String requestLine = readLine(ccis);
251                    System.out.println("Tunnel: Request status line: " + requestLine);
252                    if (requestLine.startsWith("CONNECT ")) {
253                        // We should probably check that the next word following
254                        // CONNECT is the host:port of our HTTPS serverImpl.
255                        // Some improvement for a followup!
256
257                        // Read all headers until we find the empty line that
258                        // signals the end of all headers.
259                        while(!requestLine.equals("")) {
260                            System.out.println("Tunnel: Reading header: "
261                                               + (requestLine = readLine(ccis)));
262                        }
263
264                        // Open target connection
265                        targetConnection = new Socket(
266                                serverImpl.getAddress().getAddress(),
267                                serverImpl.getAddress().getPort());
268
269                        // Then send the 200 OK response to the client
270                        System.out.println("Tunnel: Sending "
271                                           + "HTTP/1.1 200 OK\r\n\r\n");
272                        pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
273                        pw.flush();
274                    } else {
275                        // This should not happen.
276                        throw new IOException("Tunnel: Unexpected status line: "
277                                           + requestLine);
278                    }
279
280                    // Pipe the input stream of the client connection to the
281                    // output stream of the target connection and conversely.
282                    // Now the client and target will just talk to each other.
283                    System.out.println("Tunnel: Starting tunnel pipes");
284                    Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+');
285                    Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-');
286                    t1.start();
287                    t2.start();
288
289                    // We have only 1 client... wait until it has finished before
290                    // accepting a new connection request.
291                    // System.out.println("Tunnel: Waiting for pipes to close");
292                    t1.join();
293                    t2.join();
294                    System.out.println("Tunnel: Done - waiting for next client");
295                }
296            } catch (Throwable ex) {
297                try {
298                    ss.close();
299                } catch (IOException ex1) {
300                    ex.addSuppressed(ex1);
301                }
302                ex.printStackTrace(System.err);
303            }
304        }
305
306        void stop() throws IOException {
307            ss.close();
308        }
309
310    }
311
312    static class Configurator extends HttpsConfigurator {
313        public Configurator(SSLContext ctx) {
314            super(ctx);
315        }
316
317        @Override
318        public void configure (HttpsParameters params) {
319            params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
320        }
321    }
322
323}
324