net.pms.network.UPNPHelper.java Source code

Java tutorial

Introduction

Here is the source code for net.pms.network.UPNPHelper.java

Source

/*
 * PS3 Media Server, for streaming any medias to your PS3.
 * Copyright (C) 2008  A.Brochard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; version 2
 * of the License only.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */
package net.pms.network;

import java.io.IOException;
import java.net.BindException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import net.pms.PMS;
import net.pms.configuration.PmsConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper class to handle the UPnP traffic that makes PMS discoverable by
 * other clients.
 * See http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf
 * and http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1-AnnexA.pdf
 * for the specifications.
 */
public class UPNPHelper {
    /** Logger instance to write messages to the logs. */
    private static final Logger logger = LoggerFactory.getLogger(UPNPHelper.class);

    /** Carriage return and line feed. */
    private static final String CRLF = "\r\n";

    /** The Constant ALIVE. */
    private static final String ALIVE = "ssdp:alive";

    /**
     * IPv4 Multicast channel reserved for SSDP by Internet Assigned Numbers Authority (IANA).
     * MUST be 239.255.255.250.
     */
    private static final String IPV4_UPNP_HOST = "239.255.255.250";

    /**
     * IPv6 Multicast channel reserved for SSDP by Internet Assigned Numbers Authority (IANA).
     * MUST be [FF02::C].
     */
    private static final String IPV6_UPNP_HOST = "[FF02::C]";

    /**
     * Multicast channel reserved for SSDP by Internet Assigned Numbers Authority (IANA).
     * MUST be 1900.
     */
    private static final int UPNP_PORT = 1900;

    /** The Constant BYEBYE. */
    private static final String BYEBYE = "ssdp:byebye";

    /** The listener. */
    private static Thread listenerThread;

    /** The alive thread. */
    private static Thread aliveThread;

    private static final PmsConfiguration configuration = PMS.getConfiguration();

    /**
     * This utility class is not meant to be instantiated.
     */
    private UPNPHelper() {
    }

    /**
     * Send UPnP discovery search message to discover devices of interest on
     * the network.
     *
     * @param host The multicast channel
     * @param port The multicast port
     * @param st The search target string
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private static void sendDiscover(String host, int port, String st) throws IOException {
        String usn = PMS.get().usn();
        String serverHost = PMS.get().getServer().getHost();
        int serverPort = PMS.get().getServer().getPort();
        SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US);

        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));

        if (st.equals(usn)) {
            usn = "";
        } else {
            usn += "::";
        }

        StringBuilder discovery = new StringBuilder();

        discovery.append("HTTP/1.1 200 OK").append(CRLF);
        discovery.append("CACHE-CONTROL: max-age=1200").append(CRLF);
        discovery.append("DATE: ").append(sdf.format(new Date(System.currentTimeMillis()))).append(" GMT")
                .append(CRLF);
        discovery.append("LOCATION: http://").append(serverHost).append(":").append(serverPort)
                .append("/description/fetch").append(CRLF);
        discovery.append("SERVER: ").append(PMS.get().getServerName()).append(CRLF);
        discovery.append("ST: ").append(st).append(CRLF);
        discovery.append("EXT: ").append(CRLF);
        discovery.append("USN: ").append(usn).append(st).append(CRLF);
        discovery.append("Content-Length: 0").append(CRLF).append(CRLF);

        sendReply(host, port, discovery.toString());
    }

    /**
     * Send reply.
     *
     * @param host the host
     * @param port the port
     * @param msg the msg
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private static void sendReply(String host, int port, String msg) {
        DatagramSocket datagramSocket = null;

        try {
            datagramSocket = new DatagramSocket();
            InetAddress inetAddr = InetAddress.getByName(host);
            DatagramPacket dgmPacket = new DatagramPacket(msg.getBytes(), msg.length(), inetAddr, port);

            logger.trace(
                    "Sending this reply [" + host + ":" + port + "]: " + StringUtils.replace(msg, CRLF, "<CRLF>"));

            datagramSocket.send(dgmPacket);
        } catch (Exception e) {
            logger.info(e.getMessage());
            logger.debug("Error sending reply", e);
        } finally {
            if (datagramSocket != null) {
                datagramSocket.close();
            }
        }
    }

    /**
     * Send alive.
     */
    public static void sendAlive() {
        logger.debug("Sending ALIVE...");
        MulticastSocket multicastSocket = null;

        try {
            multicastSocket = getNewMulticastSocket();
            InetAddress upnpAddress = getUPNPAddress();
            multicastSocket.joinGroup(upnpAddress);

            sendMessage(multicastSocket, "upnp:rootdevice", ALIVE);
            sendMessage(multicastSocket, PMS.get().usn(), ALIVE);
            sendMessage(multicastSocket, "urn:schemas-upnp-org:device:MediaServer:1", ALIVE);
            sendMessage(multicastSocket, "urn:schemas-upnp-org:service:ContentDirectory:1", ALIVE);
            sendMessage(multicastSocket, "urn:schemas-upnp-org:service:ConnectionManager:1", ALIVE);
        } catch (IOException e) {
            logger.debug("Error sending ALIVE message", e);
        } finally {
            if (multicastSocket != null) {
                // Clean up the multicast socket nicely
                try {
                    InetAddress upnpAddress = getUPNPAddress();
                    multicastSocket.leaveGroup(upnpAddress);
                } catch (IOException e) {
                }

                multicastSocket.disconnect();
                multicastSocket.close();
            }
        }
    }

    /**
     * Gets the new multicast socket.
     *
     * @return the new multicast socket
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private static MulticastSocket getNewMulticastSocket() throws IOException {
        NetworkInterface networkInterface = NetworkConfiguration.getInstance().getNetworkInterfaceByServerName();

        if (networkInterface == null) {
            networkInterface = PMS.get().getServer().getNetworkInterface();
        }

        if (networkInterface == null) {
            throw new IOException("No usable network interface found for UPnP multicast");
        }

        List<InetAddress> usableAddresses = new ArrayList<InetAddress>();
        List<InetAddress> networkInterfaceAddresses = Collections.list(networkInterface.getInetAddresses());

        for (InetAddress inetAddress : networkInterfaceAddresses) {
            if (inetAddress != null && inetAddress instanceof Inet4Address && !inetAddress.isLoopbackAddress()) {
                usableAddresses.add(inetAddress);
            }
        }

        if (usableAddresses.isEmpty()) {
            throw new IOException("No usable addresses found for UPnP multicast");
        }

        InetSocketAddress localAddress = new InetSocketAddress(usableAddresses.get(0), 0);
        MulticastSocket ssdpSocket = new MulticastSocket(localAddress);
        ssdpSocket.setReuseAddress(true);

        logger.trace(
                "Sending message from multicast socket on network interface: " + ssdpSocket.getNetworkInterface());
        logger.trace("Multicast socket is on interface: " + ssdpSocket.getInterface());
        ssdpSocket.setTimeToLive(32);
        logger.trace("Socket Timeout: " + ssdpSocket.getSoTimeout());
        logger.trace("Socket TTL: " + ssdpSocket.getTimeToLive());
        return ssdpSocket;
    }

    /**
     * Send the UPnP BYEBYE message.
     */
    public static void sendByeBye() {
        logger.info("Sending BYEBYE...");

        MulticastSocket multicastSocket = null;

        try {
            multicastSocket = getNewMulticastSocket();
            InetAddress upnpAddress = getUPNPAddress();
            multicastSocket.joinGroup(upnpAddress);

            sendMessage(multicastSocket, "upnp:rootdevice", BYEBYE);
            sendMessage(multicastSocket, "urn:schemas-upnp-org:device:MediaServer:1", BYEBYE);
            sendMessage(multicastSocket, "urn:schemas-upnp-org:service:ContentDirectory:1", BYEBYE);
            sendMessage(multicastSocket, "urn:schemas-upnp-org:service:ConnectionManager:1", BYEBYE);
        } catch (IOException e) {
            logger.debug("Error sending BYEBYE message", e);
        } finally {
            if (multicastSocket != null) {
                // Clean up the multicast socket nicely
                try {
                    InetAddress upnpAddress = getUPNPAddress();
                    multicastSocket.leaveGroup(upnpAddress);
                } catch (IOException e) {
                }

                multicastSocket.disconnect();
                multicastSocket.close();
            }
        }
    }

    /**
     * Utility method to call {@link Thread#sleep(long)} without having to
     * catch the InterruptedException.
     *
     * @param delay the delay
     */
    private static void sleep(int delay) {
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
        }
    }

    /**
     * Send the provided message to the socket.
     *
     * @param socket the socket
     * @param nt the nt
     * @param message the message
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private static void sendMessage(DatagramSocket socket, String nt, String message) throws IOException {
        String msg = buildMsg(nt, message);
        //Random rand = new Random();

        // logger.trace( "Sending this SSDP packet: " + CRLF + StringUtils.replace(msg, CRLF, "<CRLF>")));

        InetAddress upnpAddress = getUPNPAddress();
        DatagramPacket ssdpPacket = new DatagramPacket(msg.getBytes(), msg.length(), upnpAddress, UPNP_PORT);
        socket.send(ssdpPacket);

        // XXX Why is it necessary to sleep for this random time? What would happen when random equals 0?
        //sleep(rand.nextInt(1800 / 2));

        // XXX Why send the same packet twice?
        //socket.send(ssdpPacket);

        // XXX Why is it necessary to sleep for this random time (again)?
        //sleep(rand.nextInt(1800 / 2));
    }

    /**
     * Starts up two threads: one to broadcast UPnP ALIVE messages and another
     * to listen for responses. 
     *
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public static void listen() throws IOException {
        Runnable rAlive = new Runnable() {
            @Override
            public void run() {
                int delay = 10000;

                while (true) {
                    sleep(delay);
                    sendAlive();

                    // The first delay for sending an ALIVE message is 10 seconds,
                    // the second delay is for 20 seconds. From then on, all other
                    // delays are for 180 seconds.
                    switch (delay) {
                    case 10000:
                        delay = 20000;
                        break;
                    case 20000:
                        delay = 180000;
                        break;
                    }
                }
            }
        };

        aliveThread = new Thread(rAlive, "UPNP-AliveMessageSender");
        aliveThread.start();

        Runnable r = new Runnable() {
            @Override
            public void run() {
                boolean bindErrorReported = false;

                while (true) {
                    MulticastSocket multicastSocket = null;

                    try {
                        // Use configurable source port as per http://code.google.com/p/ps3mediaserver/issues/detail?id=1166
                        multicastSocket = new MulticastSocket(configuration.getUpnpPort());

                        if (bindErrorReported) {
                            logger.warn(
                                    "Finally, acquiring port " + configuration.getUpnpPort() + " was successful!");
                        }

                        NetworkInterface ni = NetworkConfiguration.getInstance().getNetworkInterfaceByServerName();

                        try {
                            // Setting the network interface will throw a SocketException on Mac OSX
                            // with Java 1.6.0_45 or higher, but if we don't do it some Windows
                            // configurations will not listen at all.
                            if (ni != null) {
                                multicastSocket.setNetworkInterface(ni);
                            } else if (PMS.get().getServer().getNetworkInterface() != null) {
                                multicastSocket.setNetworkInterface(PMS.get().getServer().getNetworkInterface());
                                logger.trace("Setting multicast network interface: "
                                        + PMS.get().getServer().getNetworkInterface());
                            }
                        } catch (SocketException e) {
                            // Not setting the network interface will work just fine on Mac OSX.
                        }

                        multicastSocket.setTimeToLive(4);
                        multicastSocket.setReuseAddress(true);
                        InetAddress upnpAddress = getUPNPAddress();
                        multicastSocket.joinGroup(upnpAddress);

                        while (true) {
                            byte[] buf = new byte[1024];
                            DatagramPacket receivePacket = new DatagramPacket(buf, buf.length);
                            multicastSocket.receive(receivePacket);

                            String s = new String(receivePacket.getData());

                            InetAddress address = receivePacket.getAddress();

                            if (s.startsWith("M-SEARCH")) {
                                String remoteAddr = address.getHostAddress();
                                int remotePort = receivePacket.getPort();

                                if (configuration.getIpFiltering().allowed(address)) {
                                    logger.trace(
                                            "Receiving a M-SEARCH from [" + remoteAddr + ":" + remotePort + "]");

                                    if (StringUtils.indexOf(s,
                                            "urn:schemas-upnp-org:service:ContentDirectory:1") > 0) {
                                        sendDiscover(remoteAddr, remotePort,
                                                "urn:schemas-upnp-org:service:ContentDirectory:1");
                                    }

                                    if (StringUtils.indexOf(s, "upnp:rootdevice") > 0) {
                                        sendDiscover(remoteAddr, remotePort, "upnp:rootdevice");
                                    }

                                    if (StringUtils.indexOf(s, "urn:schemas-upnp-org:device:MediaServer:1") > 0) {
                                        sendDiscover(remoteAddr, remotePort,
                                                "urn:schemas-upnp-org:device:MediaServer:1");
                                    }

                                    if (StringUtils.indexOf(s, "ssdp:all") > 0) {
                                        sendDiscover(remoteAddr, remotePort,
                                                "urn:schemas-upnp-org:device:MediaServer:1");
                                    }

                                    if (StringUtils.indexOf(s, PMS.get().usn()) > 0) {
                                        sendDiscover(remoteAddr, remotePort, PMS.get().usn());
                                    }
                                }
                            } else if (s.startsWith("NOTIFY")) {
                                String remoteAddr = address.getHostAddress();
                                int remotePort = receivePacket.getPort();

                                logger.trace("Receiving a NOTIFY from [" + remoteAddr + ":" + remotePort + "]");
                            }
                        }
                    } catch (BindException e) {
                        if (!bindErrorReported) {
                            logger.error("Unable to bind to " + configuration.getUpnpPort()
                                    + ", which means that PMS will not automatically appear on your renderer! "
                                    + "This usually means that another program occupies the port. Please "
                                    + "stop the other program and free up the port. "
                                    + "PMS will keep trying to bind to it...[" + e.getMessage() + "]");
                        }

                        bindErrorReported = true;
                        sleep(5000);
                    } catch (IOException e) {
                        logger.error("UPNP network exception", e);
                        sleep(1000);
                    } finally {
                        if (multicastSocket != null) {
                            // Clean up the multicast socket nicely
                            try {
                                InetAddress upnpAddress = getUPNPAddress();
                                multicastSocket.leaveGroup(upnpAddress);
                            } catch (IOException e) {
                            }

                            multicastSocket.disconnect();
                            multicastSocket.close();
                        }
                    }
                }
            }
        };

        listenerThread = new Thread(r, "UPNPHelper");
        listenerThread.start();
    }

    /**
     * Shut down the threads that send ALIVE messages and listen to responses.
     */
    public static void shutDownListener() {
        listenerThread.interrupt();
        aliveThread.interrupt();
    }

    /**
     * Builds a UPnP message string based on a message.
     *
     * @param nt the nt
     * @param message the message
     * @return the string
     */
    private static String buildMsg(String nt, String message) {
        StringBuilder sb = new StringBuilder();

        sb.append("NOTIFY * HTTP/1.1").append(CRLF);
        sb.append("HOST: ").append(IPV4_UPNP_HOST).append(":").append(UPNP_PORT).append(CRLF);
        sb.append("NT: ").append(nt).append(CRLF);
        sb.append("NTS: ").append(message).append(CRLF);

        if (message.equals(ALIVE)) {
            sb.append("LOCATION: http://").append(PMS.get().getServer().getHost()).append(":")
                    .append(PMS.get().getServer().getPort()).append("/description/fetch").append(CRLF);
        }

        sb.append("USN: ").append(PMS.get().usn());

        if (!nt.equals(PMS.get().usn())) {
            sb.append("::").append(nt);
        }

        sb.append(CRLF);

        if (message.equals(ALIVE)) {
            sb.append("CACHE-CONTROL: max-age=1800").append(CRLF);
        }

        if (message.equals(ALIVE)) {
            sb.append("SERVER: ").append(PMS.get().getServerName()).append(CRLF);
        }

        sb.append(CRLF);
        return sb.toString();
    }

    /**
     * Gets the uPNP address.
     *
     * @return the uPNP address
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private static InetAddress getUPNPAddress() throws IOException {
        return InetAddress.getByName(IPV4_UPNP_HOST);
    }
}