de.unipassau.isl.evs.ssh.master.network.UDPDiscoveryServer.java Source code

Java tutorial

Introduction

Here is the source code for de.unipassau.isl.evs.ssh.master.network.UDPDiscoveryServer.java

Source

/*
 * MIT License
 *
 * Copyright (c) 2016.
 * Bucher Andreas, Fink Simon Dominik, Fraedrich Christoph, Popp Wolfgang,
 * Sell Leon, Werli Philemon
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package de.unipassau.isl.evs.ssh.master.network;

import android.content.Context;
import android.net.wifi.WifiManager;
import android.support.annotation.Nullable;
import android.util.Log;

import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;
import java.security.Signature;

import de.ncoder.typedmap.Key;
import de.unipassau.isl.evs.ssh.core.CoreConstants;
import de.unipassau.isl.evs.ssh.core.container.AbstractComponent;
import de.unipassau.isl.evs.ssh.core.container.Container;
import de.unipassau.isl.evs.ssh.core.container.ContainerService;
import de.unipassau.isl.evs.ssh.core.naming.DeviceID;
import de.unipassau.isl.evs.ssh.core.naming.NamingManager;
import de.unipassau.isl.evs.ssh.core.network.Client;
import de.unipassau.isl.evs.ssh.core.schedule.ExecutionServiceComponent;
import de.unipassau.isl.evs.ssh.core.sec.KeyStoreController;
import de.unipassau.isl.evs.ssh.master.database.SlaveController;
import de.unipassau.isl.evs.ssh.master.database.UserManagementController;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.util.ReferenceCountUtil;

import static de.unipassau.isl.evs.ssh.core.CoreConstants.NettyConstants.DISCOVERY_PAYLOAD_REQUEST;
import static de.unipassau.isl.evs.ssh.core.CoreConstants.NettyConstants.DISCOVERY_PAYLOAD_RESPONSE;

/**
 * This component is responsible for responding to UDP discovery packets and signalling the address and port back to
 * {@link Client}s searching for this Master.
 *
 * @author Niko Fink
 */
public class UDPDiscoveryServer extends AbstractComponent {
    public static final Key<UDPDiscoveryServer> KEY = new Key<>(UDPDiscoveryServer.class);

    private static final String TAG = UDPDiscoveryServer.class.getSimpleName();

    /**
     * The channel listening for incoming UDP connections on the port of the client.
     * Use {@link ChannelFuture#sync()} to wait for client startup.
     */
    private ChannelFuture channel;
    /**
     * The lock used to prevent android from hibernating the network Stack while UDP discovery is running.
     */
    private WifiManager.MulticastLock multicastLock;

    @Override
    public void init(Container container) {
        super.init(container);

        // Acquire lock
        final WifiManager wifi = (WifiManager) requireComponent(ContainerService.KEY_CONTEXT)
                .getSystemService(Context.WIFI_SERVICE);
        multicastLock = wifi.createMulticastLock(getClass().getSimpleName());
        multicastLock.acquire();

        // Setup UDP Channel
        Bootstrap b = new Bootstrap().channel(NioDatagramChannel.class)
                .group(requireComponent(ExecutionServiceComponent.KEY)).handler(new RequestHandler())
                .option(ChannelOption.SO_BROADCAST, true);
        channel = b.bind(CoreConstants.NettyConstants.DISCOVERY_SERVER_PORT);
    }

    @Override
    public void destroy() {
        if (multicastLock != null) {
            if (multicastLock.isHeld()) {
                multicastLock.release();
            }
            multicastLock = null;
        }
        if (channel.channel().isActive()) {
            channel.channel().close();
        }
        super.destroy();
    }

    /**
     * Send a response with the ConnectInformation of this Master to the requesting Client.
     *
     * @param request the request sent from a {@link Client}
     */
    private void sendDiscoveryResponse(DatagramPacket request) {
        final ByteBuf buffer = channel.channel().alloc().heapBuffer();

        // gather information
        final byte[] header = DISCOVERY_PAYLOAD_RESPONSE.getBytes();
        final String addressString = request.recipient().getAddress().getHostAddress();
        final byte[] address = addressString.getBytes();
        final InetSocketAddress serverAddress = requireComponent(Server.KEY).getAddress();
        if (serverAddress == null) {
            Log.w(TAG, "Could not respond to UDP discovery request as Server is not started yet");
            return;
        }
        final int port = serverAddress.getPort();
        Log.i(TAG, "sendDiscoveryResponse with connection data " + addressString + ":" + port + " to "
                + request.sender());

        // write it to the buffer
        buffer.writeInt(header.length);
        buffer.writeBytes(header);
        buffer.writeInt(address.length);
        buffer.writeBytes(address);
        buffer.writeInt(port);

        // and sign the data
        try {
            Signature signature = Signature.getInstance("ECDSA");
            signature.initSign(requireComponent(KeyStoreController.KEY).getOwnPrivateKey());
            signature.update(buffer.nioBuffer());
            final byte[] sign = signature.sign();
            buffer.writeInt(sign.length);
            buffer.writeBytes(sign);
        } catch (GeneralSecurityException e) {
            Log.w(TAG, "Could not send UDP discovery response", e);
        }

        final DatagramPacket response = new DatagramPacket(buffer, request.sender());
        channel.channel().writeAndFlush(response);
    }

    /**
     * The ChannelHandler that receives and parses incoming UDP requests and calls {@link #sendDiscoveryResponse(DatagramPacket)}
     * in order to respond to them.
     */
    private class RequestHandler extends ChannelHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            try {
                final DatagramPacket request = (DatagramPacket) msg;
                final ByteBuf buffer = request.content();
                buffer.markReaderIndex();
                final String messageType = readString(buffer);
                if (DISCOVERY_PAYLOAD_RESPONSE.equals(messageType)) {
                    Log.d(TAG, "UDP discovery Server can't handle UDP response from " + request.sender());
                    return;
                } else if (!DISCOVERY_PAYLOAD_REQUEST.equals(messageType)) {
                    Log.d(TAG, "Discarding UDP packet with illegal message type from " + request.sender() + ": "
                            + messageType);
                    return;
                }
                final DeviceID ownID = requireComponent(NamingManager.KEY).getOwnID();
                final DeviceID clientID = readDeviceID(buffer);
                if (clientID == null) {
                    Log.d(TAG, "Discarding UDP inquiry without client ID from " + request.sender());
                    return;
                }

                final boolean isMasterKnown = buffer.readBoolean();
                if (isMasterKnown) {
                    // if the master is known to the device, the ID that the device is searching must match with mine
                    final DeviceID masterID = readDeviceID(buffer);
                    if (!ownID.equals(masterID)) {
                        Log.d(TAG, "Discarding UDP inquiry from " + clientID + "(" + request.sender() + ") "
                                + "that is not looking for me (" + ownID + ") but " + masterID);
                        return;
                    }
                } else {
                    // if the device doesn't know his master, the master must know the device
                    if (!isDeviceRegistered(clientID)) {
                        Log.d(TAG, "Discarding UDP inquiry from " + clientID + "(" + request.sender() + ") "
                                + "that is looking for any master and is not registered");
                        return;
                    }
                }
                Log.d(TAG, "UDP inquiry received from " + clientID + "(" + request.sender()
                        + ") that is looking for " + (isMasterKnown ? "me" : "any master and is registered here"));
                sendDiscoveryResponse(request);
            } finally {
                ReferenceCountUtil.release(msg);
            }
        }

        private boolean isDeviceRegistered(DeviceID clientID) {
            return requireComponent(SlaveController.KEY).getSlave(clientID) != null
                    || requireComponent(UserManagementController.KEY).getUserDevice(clientID) != null;
        }

        /**
         * Read a string.
         */
        @Nullable
        private String readString(ByteBuf buffer) {
            final int length = buffer.readInt();
            if (length < 0 || length > 0xFFFF) {
                return null;
            }
            byte[] value = new byte[length];
            buffer.readBytes(value);
            return new String(value);
        }

        /**
         * Read a DeviceID.
         */
        @Nullable
        private DeviceID readDeviceID(ByteBuf buffer) {
            final int length = buffer.readInt();
            if (length != DeviceID.ID_LENGTH) {
                return null;
            }
            byte[] value = new byte[DeviceID.ID_LENGTH];
            buffer.readBytes(value);
            return new DeviceID(value);
        }
    }
}