/*
 * Copyright (c) 2013, 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 8014377
 * @summary Test for interference when two sockets are bound to the same
 *   port but joined to different multicast groups
 * @build Promiscuous  NetworkConfiguration
 * @run main Promiscuous
 * @run main/othervm -Djava.net.preferIPv4Stack=true Promiscuous
 * @key randomness
 */

import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.net.*;
import static java.net.StandardProtocolFamily.*;
import java.util.*;
import java.io.IOException;

public class Promiscuous {

    static final Random rand = new Random();

    static final ProtocolFamily UNSPEC = new ProtocolFamily() {
        public String name() {
            return "UNSPEC";
        }
    };

    /**
     * Sends a datagram to the given multicast group
     */
    static int sendDatagram(NetworkInterface nif,
                            InetAddress group,
                            int port)
        throws IOException
    {
        ProtocolFamily family = (group instanceof Inet6Address) ?
            StandardProtocolFamily.INET6 : StandardProtocolFamily.INET;
        DatagramChannel dc = DatagramChannel.open(family)
            .setOption(StandardSocketOptions.IP_MULTICAST_IF, nif);
        int id = rand.nextInt();
        byte[] msg = Integer.toString(id).getBytes("UTF-8");
        ByteBuffer buf = ByteBuffer.wrap(msg);
        System.out.format("Send message -> group %s (id=0x%x)\n",
            group.getHostAddress(), id);
        dc.send(buf, new InetSocketAddress(group, port));
        dc.close();
        return id;
    }

    /**
     * Wait (with timeout) for datagram. The {@code datagramExepcted}
     * parameter indicates whether a datagram is expected, and if
     * {@true} then {@code id} is the identifier in the payload.
     */
    static void receiveDatagram(DatagramChannel dc,
                                String name,
                                boolean datagramExepcted,
                                int id)
        throws IOException
    {
        System.out.println("Checking if received by " + name);

        Selector sel = Selector.open();
        dc.configureBlocking(false);
        dc.register(sel, SelectionKey.OP_READ);
        ByteBuffer buf = ByteBuffer.allocateDirect(100);

        try {
            for (;;) {
                System.out.println("Waiting to receive message");
                sel.select(5*1000);
                SocketAddress sa = dc.receive(buf);

                // no datagram received
                if (sa == null) {
                    if (datagramExepcted) {
                        throw new RuntimeException("Expected message not recieved");
                    }
                    System.out.println("No message received (correct)");
                    return;
                }

                // datagram received

                InetAddress sender = ((InetSocketAddress)sa).getAddress();
                buf.flip();
                byte[] bytes = new byte[buf.remaining()];
                buf.get(bytes);
                int receivedId = Integer.parseInt(new String(bytes));

                System.out.format("Received message from %s (id=0x%x)\n",
                    sender, receivedId);

                if (!datagramExepcted) {
                    if (receivedId == id)
                        throw new RuntimeException("Message not expected");
                    System.out.println("Message ignored (has wrong id)");
                } else {
                    if (receivedId == id) {
                        System.out.println("Message expected");
                        return;
                    }
                    System.out.println("Message ignored (wrong sender)");
                }

                sel.selectedKeys().clear();
                buf.rewind();
            }
        } finally {
            sel.close();
        }
    }

    static void test(ProtocolFamily family,
                     NetworkInterface nif,
                     InetAddress group1,
                     InetAddress group2)
        throws IOException
    {

        System.out.format("%nTest family=%s%n", family.name());

        DatagramChannel dc1 = (family == UNSPEC) ?
            DatagramChannel.open() : DatagramChannel.open(family);
        DatagramChannel dc2 = (family == UNSPEC) ?
            DatagramChannel.open() : DatagramChannel.open(family);

        try {
            dc1.setOption(StandardSocketOptions.SO_REUSEADDR, true);
            dc2.setOption(StandardSocketOptions.SO_REUSEADDR, true);

            dc1.bind(new InetSocketAddress(0));
            int port = dc1.socket().getLocalPort();
            dc2.bind(new InetSocketAddress(port));

            System.out.format("dc1 joining [%s]:%d @ %s\n",
                group1.getHostAddress(), port, nif.getName());
            System.out.format("dc2 joining [%s]:%d @ %s\n",
                group2.getHostAddress(), port, nif.getName());

            dc1.join(group1, nif);
            dc2.join(group2, nif);

            int id = sendDatagram(nif, group1, port);

            receiveDatagram(dc1, "dc1", true, id);
            receiveDatagram(dc2, "dc2", false, id);

            id = sendDatagram(nif, group2, port);

            receiveDatagram(dc1, "dc1", false, id);
            receiveDatagram(dc2, "dc2", true, id);

        } finally {
            dc1.close();
            dc2.close();
        }
    }

    public static void main(String[] args) throws IOException {
        String os = System.getProperty("os.name");

        // Requires IP_MULTICAST_ALL on Linux (new since 2.6.31) so skip
        // on older kernels. Note that we skip on <= version 3 to keep the
        // parsing simple
        if (os.equals("Linux")) {
            String osversion = System.getProperty("os.version");
            String[] vers = osversion.split("\\.", 0);
            int major = Integer.parseInt(vers[0]);
            if (major < 3) {
                System.out.format("Kernel version is %s, test skipped%n", osversion);
                return;
            }
        }

        // get local network configuration to use
        NetworkConfiguration config = NetworkConfiguration.probe();

        // multicast groups used for the test
        InetAddress ip4Group1 = InetAddress.getByName("225.4.5.6");
        InetAddress ip4Group2 = InetAddress.getByName("225.4.6.6");

        for (NetworkInterface nif: config.ip4Interfaces()) {
            InetAddress source = config.ip4Addresses(nif).iterator().next();
            test(INET, nif, ip4Group1, ip4Group2);

            // Solaris and Linux allow IPv6 sockets join IPv4 multicast groups
            if (os.equals("SunOS") || os.equals("Linux"))
                test(UNSPEC, nif, ip4Group1, ip4Group2);
        }
    }
}
