test/jdk/java/net/httpclient/MockServer.java
author chegar
Thu, 08 Mar 2018 17:42:16 +0000
branchhttp-client-branch
changeset 56265 ec34ae013fbe
parent 56233 1753108d07b9
child 56451 9585061fdb04
permissions -rw-r--r--
http-client-branch: tests should bind to the loopback only

/*
 * Copyright (c) 2015, 2018, 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.
 */

import com.sun.net.httpserver.HttpServer;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLServerSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static java.nio.charset.StandardCharsets.ISO_8859_1;

/**
 * A cut-down Http/1 Server for testing various error situations
 *
 * use interrupt() to halt
 */
public class MockServer extends Thread implements Closeable {

    final ServerSocket ss;
    private final List<Connection> sockets;
    private final List<Connection> removals;
    private final List<Connection> additions;
    AtomicInteger counter = new AtomicInteger(0);
    // if specified (not null), only requests which
    // contain this value in their status line
    // will be taken into account and returned by activity().
    // Other requests will get summarily closed.
    // When specified, this can prevent answering to rogue
    // (external) clients that might be lurking
    // on the test machine instead of answering
    // to the test client.
   final String root;

    // waits up to 2000 seconds for something to happen
    // dont use this unless certain activity coming.
    public Connection activity() {
        for (int i = 0; i < 80 * 100; i++) {
            doRemovalsAndAdditions();
            for (Connection c : sockets) {
                if (c.poll()) {
                    if (root != null) {
                        // if a root was specified in MockServer
                        // constructor, rejects (by closing) all
                        // requests whose statusLine does not contain
                        // root.
                        if (!c.statusLine.contains(root)) {
                            System.out.println("Bad statusLine: "
                                    + c.statusLine
                                    + " closing connection");
                            c.close();
                            continue;
                        }
                    }
                    return c;
                }
            }
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    private void doRemovalsAndAdditions() {
        synchronized (removals) {
            Iterator<Connection> i = removals.iterator();
            while (i.hasNext()) {
                Connection c = i.next();
                System.out.println("socket removed: " + c);
                sockets.remove(c);
            }
            removals.clear();
        }

        synchronized (additions) {
            Iterator<Connection> i = additions.iterator();
            while (i.hasNext()) {
                Connection c = i.next();
                System.out.println("socket added: " + c);
                sockets.add(c);
            }
            additions.clear();
        }
    }

    // clears all current connections on Server.
    public void reset() {
        for (Connection c : sockets) {
            c.close();
        }
    }

    /**
     * Reads data into an ArrayBlockingQueue<String> where each String
     * is a line of input, that was terminated by CRLF (not included)
     */
    class Connection extends Thread {
        Connection(Socket s) throws IOException {
            this.socket = s;
            id = counter.incrementAndGet();
            is = s.getInputStream();
            os = s.getOutputStream();
            incoming = new ArrayBlockingQueue<>(100);
            setName("Server-Connection");
            setDaemon(true);
        }
        final Socket socket;
        final int id;
        final InputStream is;
        final OutputStream os;
        final ArrayBlockingQueue<String> incoming;
        volatile String statusLine;

        final static String CRLF = "\r\n";

        // sentinel indicating connection closed
        final static String CLOSED = "C.L.O.S.E.D";
        volatile boolean closed = false;
        volatile boolean released = false;

        @Override
        public void run() {
            byte[] buf = new byte[256];
            String s = "";
            try {
                while (true) {
                    int n = is.read(buf);
                    if (n == -1) {
                        cleanup();
                        return;
                    }
                    String s0 = new String(buf, 0, n, ISO_8859_1);
                    s = s + s0;
                    int i;
                    while ((i=s.indexOf(CRLF)) != -1) {
                        String s1 = s.substring(0, i+2);
                        System.out.println("Server got: " + s1.substring(0,i));
                        if (statusLine == null) statusLine = s1.substring(0,i);
                        incoming.put(s1);
                        if (i+2 == s.length()) {
                            s = "";
                            break;
                        }
                        s = s.substring(i+2);
                    }
                }
            } catch (IOException |InterruptedException e1) {
                cleanup();
            } catch (Throwable t) {
                System.out.println("X: " + t);
                cleanup();
            }
        }

        @Override
        public String toString() {
            return "Server.Connection: " + socket.toString();
        }

        public void sendHttpResponse(int code, String body, String... headers)
            throws IOException
        {
            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
            for (int i=0; i<headers.length; i+=2) {
                r1 += headers[i] + ": " + headers[i+1] + CRLF;
            }
            int clen = body == null ? 0 : body.getBytes(ISO_8859_1).length;
            r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
            r1 += CRLF;
            if (body != null) {
                r1 += body;
            }
            send(r1);
        }

        // content-length is 10 bytes too many
        public void sendIncompleteHttpResponseBody(int code) throws IOException {
            String body = "Hello World Helloworld Goodbye World";
            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
            int clen = body.getBytes(ISO_8859_1).length + 10;
            r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
            r1 += CRLF;
            if (body != null) {
                r1 += body;
            }
            send(r1);
        }

        public void sendIncompleteHttpResponseHeaders(int code)
            throws IOException
        {
            String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
            send(r1);
        }

        public void send(String r) throws IOException {
            try {
                os.write(r.getBytes(ISO_8859_1));
            } catch (IOException x) {
                IOException suppressed =
                        new IOException("MockServer["
                            + ss.getLocalPort()
                            +"] Failed while writing bytes: "
                            +  x.getMessage());
                x.addSuppressed(suppressed);
                System.err.println("WARNING: " + suppressed);
                throw x;
            }
        }

        public synchronized void close() {
            cleanup();
            closed = true;
            incoming.clear();
        }

        public String nextInput(long timeout, TimeUnit unit) {
            String result = "";
            while (poll()) {
                try {
                    String s = incoming.poll(timeout, unit);
                    if (s == null && closed) {
                        return CLOSED;
                    } else {
                        result += s;
                    }
                } catch (InterruptedException e) {
                    return null;
                }
            }
            return result;
        }

        public String nextInput() {
            return nextInput(0, TimeUnit.SECONDS);
        }

        public boolean poll() {
            return incoming.peek() != null;
        }

        private void cleanup() {
            if (released) return;
            synchronized(this) {
                if (released) return;
                released = true;
            }
            try {
                socket.close();
            } catch (IOException e) {}
            synchronized (removals) {
                removals.add(this);
            }
        }
    }

    MockServer(int port, ServerSocketFactory factory, String root) throws IOException {
        ss = factory.createServerSocket();
        ss.setReuseAddress(false);
        ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
        this.root = root; // if specified, any request which don't have this value
                          // in their statusLine will be rejected.
        sockets = Collections.synchronizedList(new LinkedList<>());
        removals = new LinkedList<>();
        additions = new LinkedList<>();
        setName("Test-Server");
        setDaemon(true);
    }

    MockServer(int port, ServerSocketFactory factory) throws IOException {
        this(port, factory, "/foo/");
    }

    MockServer(int port) throws IOException {
        this(port, ServerSocketFactory.getDefault());
    }

    MockServer() throws IOException {
        this(0);
    }

    int port() {
        return ss.getLocalPort();
    }

    String serverAuthority() {
        return InetAddress.getLoopbackAddress().getHostName() + ":" + port();
    }

    public String getURL() {
        if (ss instanceof SSLServerSocket) {
            return "https://" + serverAuthority() + "/foo/";
        } else {
            return "http://" + serverAuthority() + "/foo/";
        }
    }

    private volatile boolean closed;

    @Override
    public void close() {
        closed = true;
        try {
            ss.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        for (Connection c : sockets) {
            c.close();
        }
    }

    @Override
    public void run() {
        try {
            while (!closed) {
                try {
                    System.out.println("Server waiting for connection");
                    Socket s = ss.accept();
                    Connection c = new Connection(s);
                    c.start();
                    System.out.println("Server got new connection: " + c);
                    synchronized (additions) {
                        additions.add(c);
                    }
                } catch (IOException e) {
                    if (closed)
                        return;
                    e.printStackTrace(System.out);
                }
            }
        } catch (Throwable t) {
            System.out.println("Unexpected exception in accept loop: " + t);
            t.printStackTrace(System.out);
        } finally {
            if (closed) {
                System.out.println("Server closed: exiting accept loop");
            } else {
                System.out.println("Server not closed: exiting accept loop and closing");
                close();
            }
        }
    }

}