com.offbynull.portmapper.natpmp.NatPmpController.java Source code

Java tutorial

Introduction

Here is the source code for com.offbynull.portmapper.natpmp.NatPmpController.java

Source

/*
 * Copyright (c) 2013-2014, Kasra Faghihi, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.offbynull.portmapper.natpmp;

import com.offbynull.portmapper.common.CommunicationType;
import com.offbynull.portmapper.common.NetworkUtils;
import com.offbynull.portmapper.common.ResponseException;
import com.offbynull.portmapper.common.UdpCommunicator;
import com.offbynull.portmapper.common.UdpCommunicatorListener;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;

/**
 * Accesses NAT-PMP features of a gateway/router.
 * @author Kasra Faghihi
 */
public final class NatPmpController implements Closeable {

    private InetSocketAddress gateway;
    private UdpCommunicator communicator;

    // closed by udpcomm
    private DatagramChannel unicastChannel = null;
    private DatagramChannel ipv4MulticastChannel = null;
    private DatagramChannel ipv6MulticastChannel = null;

    /**
     * Constructs a {@link NatPmpController} object.
     * @param gatewayAddress address of router/gateway
     * @param listener a listener to listen for all NAT-PMP packets from this router
     * @throws NullPointerException if any argument is {@code null}
     * @throws IOException if problems initializing UDP channels
     */
    public NatPmpController(InetAddress gatewayAddress, final NatPmpControllerListener listener)
            throws IOException {
        Validate.notNull(gatewayAddress);

        this.gateway = new InetSocketAddress(gatewayAddress, 5351);

        List<DatagramChannel> channels = new ArrayList<>(3);

        try {
            unicastChannel = DatagramChannel.open();
            unicastChannel.configureBlocking(false);
            unicastChannel.socket().bind(new InetSocketAddress(0));

            ipv4MulticastChannel = DatagramChannel.open(StandardProtocolFamily.INET);
            ipv4MulticastChannel.configureBlocking(false);
            ipv4MulticastChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
            ipv4MulticastChannel.socket().bind(new InetSocketAddress(5350));
            NetworkUtils.multicastListenOnAllIpv4InterfaceAddresses(ipv4MulticastChannel);

            ipv6MulticastChannel = DatagramChannel.open(StandardProtocolFamily.INET6);
            ipv6MulticastChannel.configureBlocking(false);
            ipv6MulticastChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
            ipv6MulticastChannel.socket().bind(new InetSocketAddress(5350));
            NetworkUtils.multicastListenOnAllIpv6InterfaceAddresses(ipv6MulticastChannel);
        } catch (IOException ioe) {
            IOUtils.closeQuietly(unicastChannel);
            IOUtils.closeQuietly(ipv4MulticastChannel);
            IOUtils.closeQuietly(ipv6MulticastChannel);
            throw ioe;
        }

        channels.add(unicastChannel);
        channels.add(ipv4MulticastChannel);
        channels.add(ipv6MulticastChannel);

        this.communicator = new UdpCommunicator(channels);
        this.communicator.startAsync().awaitRunning();

        if (listener != null) {
            this.communicator.addListener(new UdpCommunicatorListener() {

                @Override
                public void incomingPacket(InetSocketAddress sourceAddress, DatagramChannel channel,
                        ByteBuffer packet) {
                    CommunicationType type;
                    if (channel == unicastChannel) {
                        type = CommunicationType.UNICAST;
                    } else if (channel == ipv4MulticastChannel || channel == ipv6MulticastChannel) {
                        type = CommunicationType.MULTICAST;
                    } else {
                        return; // unknown, do nothing
                    }

                    try {
                        packet.mark();
                        listener.incomingResponse(type, new ExternalAddressNatPmpResponse(packet));
                    } catch (BufferUnderflowException | IllegalArgumentException e) { // NOPMD
                        // ignore
                    } finally {
                        packet.reset();
                    }

                    try {
                        packet.mark();
                        listener.incomingResponse(type, new UdpMappingNatPmpResponse(packet));
                    } catch (BufferUnderflowException | IllegalArgumentException e) { // NOPMD
                        // ignore
                    } finally {
                        packet.reset();
                    }

                    try {
                        packet.mark();
                        listener.incomingResponse(type, new TcpMappingNatPmpResponse(packet));
                    } catch (BufferUnderflowException | IllegalArgumentException e) { // NOPMD
                        // ignore
                    } finally {
                        packet.reset();
                    }
                }
            });
        }
    }

    // CHECKSTYLE:OFF custom exception in javadoc not being recognized
    /**
     * Send an external address request to the gateway.
     * @param sendAttempts number of times to try to submit each request
     * @return external address response
     * @throws BufferUnderflowException if the message is too big to be written in to the buffer
     * @throws ResponseException no response available
     * @throws InterruptedException if thread was interrupted while waiting
     * @throws IllegalArgumentException if {@code sendAttempts < 1 || > 9}
     */
    public ExternalAddressNatPmpResponse requestExternalAddress(int sendAttempts) throws InterruptedException {
        // CHECKSTYLE:ON
        ExternalAddressNatPmpRequest req = new ExternalAddressNatPmpRequest();

        ExternalAddressNatPmpResponseCreator creator = new ExternalAddressNatPmpResponseCreator();
        return performRequest(sendAttempts, req, creator);
    }

    // CHECKSTYLE:OFF custom exception in javadoc not being recognized
    /**
     * Send a UDP map request to the gateway.
     * @param sendAttempts number of times to try to submit each request
     * @param internalPort internal port
     * @param suggestedExternalPort suggested external port ({@code 0} for no preference)
     * @param lifetime requested lifetime in seconds
     * @return MAP response
     * @throws NullPointerException if any argument is {@code null} or contains {@code null}
     * @throws BufferUnderflowException if the message is too big to be written in to the buffer
     * @throws IllegalArgumentException if any numeric argument is negative, or if {@code internalPort < 1 || > 65535}, or if
     * {@code suggestedExternalPort > 65535}, or if {@code sendAttempts < 1 || > 9}
     * @throws ResponseException the expected response never came in
     * @throws InterruptedException if thread was interrupted while waiting
     */
    public UdpMappingNatPmpResponse requestUdpMappingOperation(int sendAttempts, int internalPort,
            int suggestedExternalPort, long lifetime) throws InterruptedException {
        // CHECKSTYLE:ON
        UdpMappingNatPmpRequest req = new UdpMappingNatPmpRequest(internalPort, suggestedExternalPort, lifetime);

        RequestUdpMappingNatPmpResponseCreator creator = new RequestUdpMappingNatPmpResponseCreator(req);
        return performRequest(sendAttempts, req, creator);
    }

    // CHECKSTYLE:OFF custom exception in javadoc not being recognized
    /**
     * Send a TCP map request to the gateway.
     * @param sendAttempts number of times to try to submit each request
     * @param internalPort internal port
     * @param suggestedExternalPort suggested external port ({@code 0} for no preference)
     * @param lifetime requested lifetime in seconds
     * @return MAP response
     * @throws NullPointerException if any argument is {@code null} or contains {@code null}
     * @throws BufferUnderflowException if the message is too big to be written in to the buffer
     * @throws IllegalArgumentException if any numeric argument is negative, or if {@code internalPort < 1 || > 65535}, or if
     * {@code suggestedExternalPort > 65535}, or if {@code sendAttempts < 1 || > 9}
     * @throws ResponseException the expected response never came in
     * @throws InterruptedException if thread was interrupted while waiting
     */
    public TcpMappingNatPmpResponse requestTcpMappingOperation(int sendAttempts, int internalPort,
            int suggestedExternalPort, long lifetime) throws InterruptedException {
        // CHECKSTYLE:ON
        TcpMappingNatPmpRequest req = new TcpMappingNatPmpRequest(internalPort, suggestedExternalPort, lifetime);

        RequestTcpMappingNatPmpResponseCreator creator = new RequestTcpMappingNatPmpResponseCreator(req);
        return performRequest(sendAttempts, req, creator);
    }

    private <T extends NatPmpResponse> T performRequest(int sendAttempts, NatPmpRequest request, Creator<T> creator)
            throws InterruptedException {
        Validate.inclusiveBetween(1, 9, sendAttempts);

        ByteBuffer sendBuffer = ByteBuffer.allocate(12);

        request.dump(sendBuffer);
        sendBuffer.flip();

        for (int i = 1; i <= sendAttempts; i++) {
            T response = attemptRequest(sendBuffer, i, creator);
            if (response != null) {
                return response;
            }
        }

        throw new ResponseException();
    }

    private <T extends NatPmpResponse> T attemptRequest(ByteBuffer sendBuffer, int attempt, Creator<T> creator)
            throws InterruptedException {

        final LinkedBlockingQueue<ByteBuffer> recvBufferQueue = new LinkedBlockingQueue<>();

        UdpCommunicatorListener listener = new UdpCommunicatorListener() {

            @Override
            public void incomingPacket(InetSocketAddress sourceAddress, DatagramChannel channel,
                    ByteBuffer packet) {
                if (channel != unicastChannel) {
                    return;
                }

                recvBufferQueue.add(packet);
            }
        };

        // timeout duration should double each iteration, starting from 250 according to spec
        // i = 1, maxWaitTime = (1 << (1-1)) * 250 = (1 << 0) * 250 = 1 * 250 = 250
        // i = 2, maxWaitTime = (1 << (2-1)) * 250 = (1 << 1) * 250 = 2 * 250 = 500
        // i = 3, maxWaitTime = (1 << (3-1)) * 250 = (1 << 2) * 250 = 4 * 250 = 1000
        // i = 4, maxWaitTime = (1 << (4-1)) * 250 = (1 << 3) * 250 = 8 * 250 = 2000
        // ...
        try {
            communicator.addListener(listener);
            communicator.send(unicastChannel, gateway, sendBuffer);

            int maxWaitTime = (1 << (attempt - 1)) * 250; // NOPMD

            T pcpResponse = null;

            long endTime = System.currentTimeMillis() + maxWaitTime;
            long waitTime;
            while ((waitTime = endTime - System.currentTimeMillis()) > 0L) {
                waitTime = Math.max(waitTime, 0L); // must be at least 0, probably should never happen

                ByteBuffer recvBuffer = recvBufferQueue.poll(waitTime, TimeUnit.MILLISECONDS);

                if (recvBuffer != null) {
                    pcpResponse = creator.create(recvBuffer);
                    if (pcpResponse != null) {
                        break;
                    }
                }
            }

            return pcpResponse;
        } finally {
            communicator.removeListener(listener);
        }
    }

    @Override
    public void close() throws IOException {
        try {
            communicator.stopAsync().awaitTerminated();
        } catch (IllegalStateException iae) {
            throw new IOException(iae);
        }
    }

    private interface Creator<T extends NatPmpResponse> {
        T create(ByteBuffer response);
    }

    private static final class ExternalAddressNatPmpResponseCreator
            implements Creator<ExternalAddressNatPmpResponse> {
        public ExternalAddressNatPmpResponseCreator() {
        }

        @Override
        public ExternalAddressNatPmpResponse create(ByteBuffer recvBuffer) {
            ExternalAddressNatPmpResponse response;
            try {
                response = new ExternalAddressNatPmpResponse(recvBuffer);
            } catch (BufferUnderflowException | BufferOverflowException | IllegalArgumentException e) {
                //throw new BadResponseException(e);
                return null;
            }

            return response;
        }
    }

    private static final class RequestUdpMappingNatPmpResponseCreator implements Creator<UdpMappingNatPmpResponse> {
        private UdpMappingNatPmpRequest request;

        public RequestUdpMappingNatPmpResponseCreator(UdpMappingNatPmpRequest request) {
            Validate.notNull(request);
            this.request = request;
        }

        @Override
        public UdpMappingNatPmpResponse create(ByteBuffer recvBuffer) {
            UdpMappingNatPmpResponse response;
            try {
                response = new UdpMappingNatPmpResponse(recvBuffer);
            } catch (BufferUnderflowException | BufferOverflowException | IllegalArgumentException e) {
                //throw new BadResponseException(e);
                return null;
            }

            if (response.getInternalPort() == request.getInternalPort()) {
                return response;
            }

            return null;
        }
    }

    private static final class RequestTcpMappingNatPmpResponseCreator implements Creator<TcpMappingNatPmpResponse> {
        private TcpMappingNatPmpRequest request;

        public RequestTcpMappingNatPmpResponseCreator(TcpMappingNatPmpRequest request) {
            Validate.notNull(request);
            this.request = request;
        }

        @Override
        public TcpMappingNatPmpResponse create(ByteBuffer recvBuffer) {
            TcpMappingNatPmpResponse response;
            try {
                response = new TcpMappingNatPmpResponse(recvBuffer);
            } catch (BufferUnderflowException | BufferOverflowException | IllegalArgumentException e) {
                //throw new BadResponseException(e);
                return null;
            }

            if (response.getInternalPort() == request.getInternalPort()) {
                return response;
            }

            return null;
        }
    }
}