/*
* This file is part of jNAT-PMPlib.
*
* jNAT-PMPlib 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 of the License, or
* (at your option) any later version.
*
* jNAT-PMPlib 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 jNAT-PMPlib. If not, see <http://www.gnu.org/licenses/>.
*/
package net.tomp2p.natpmp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.PortUnreachableException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
/**
* The message class manages a message that is being sent to a NAT-PMP gateway.
* The class respects the back-off time defined in the NAT-PMP specification.
* Consumers of this class need to ensure that only one message is being sent at
* a time to remain compliant with the NAT-PMP specification. This class is
* thread-safe.
*
* @see ExternalAddressRequestMessage
* @see MapRequestMessage
* @author flszen
*/
public abstract class Message {
private MessageType type;
private DatagramSocket socket;
// These are used to manage the response.
private byte[] response;
private NatPmpException responseException;
private MessageResponseInterface listener;
// These are global response values.
private ResultCode resultCode;
private Integer secondsSinceEpoch;
/**
* Constructs a new Message with the speficied type.
*
* @param type
* The {@link MessageType} of the message. This must not be null.
* @throws NullPointerException
* Thrown if MessageType is null.
*/
Message(MessageType type, MessageResponseInterface listener) {
// Localize the type. It is needed when a response is received.
this.type = type;
this.listener = listener;
// Reject if MessageType is null.
if (type == null) {
throw new NullPointerException("MessageType must not be null.");
}
}
/**
* Gets a byte array initialized with the request packet payload. The
* version and opcode fields are overwritten when the packet is sent,
* therefore it is not necessary to initialize them in this method.
*
* @return A byte arrray initialized with request packet payload.
*/
abstract byte[] getRequestPayload();
/**
* Gets the opcode of this message.
*
* @return The opcode.
*/
abstract byte getOpcode();
/**
* Parses the response. This method is automatically called once after a
* message is received.
*
* @throws Throwable
* Any exception may be thrown by the implementer.
*/
abstract void parseResponse(byte[] response) throws Throwable;
/**
* Gets the excpetion associated with the response. This exception is
* generally provided by an external entity.
*
* @return The {@link NatPmpException} associated with the response.
*/
public NatPmpException getResponseException() {
return responseException;
}
/**
* Gets the result code.
*
* @return The result code.
* @throws NatPmpException
* Thrown when no response has been received or the response
* parsing ran into some trouble.
*/
public ResultCode getResultCode() throws NatPmpException {
// Return the address.
return resultCode;
}
/**
* Gets the seconds since epoch as received from the NAT-PMP gateway.
*
* @return The seconds since the epoch on the gateway.
* @throws NatPmpException
* Thrown when no response has been received or the response
* parsing ran into some trouble.
*/
public Integer getSecondsSinceEpoch() throws NatPmpException {
// Return the address.
return secondsSinceEpoch;
}
/**
* Sets the exception associated with the response. This exception is
* generally provided by an external entity.
*
* @param responseException
* The {@link NatPmpException} to associate with the response.
*/
private void setResponseException(NatPmpException responseException) {
this.responseException = responseException;
}
/**
* Parses the response and notifies the listener of the outcome.
*/
void notifyListener() {
// If there is no response, make that notification.
if (response == null) {
if (listener != null) {
listener.noResponseReceived(this);
}
return;
}
// Run the internal method for this. It simplifies the logic in handling
// exception cases.
try {
internalNotifyListener();
} catch (NatPmpException ex) {
setResponseException(ex);
}
// If an exception is present, report it, otherwise it's a good
// response.
if (getResponseException() != null) {
if (listener != null) {
listener.exceptionGenerated(this, getResponseException());
}
} else {
// Make the notification that everyone hopes for.
if (listener != null) {
listener.responseReceived(this);
}
}
}
private void internalNotifyListener() throws NatPmpException {
// Parse the global response values.
secondsSinceEpoch = intFromByteArray(response, 4);
switch (shortFromByteArray(response, 2)) {
case 0:
resultCode = ResultCode.Success;
break;
case 1:
resultCode = ResultCode.UnsupportedVersion;
break;
case 2:
resultCode = ResultCode.NotAuthorizedRefused;
break;
case 3:
resultCode = ResultCode.NetworkFailure;
break;
case 4:
resultCode = ResultCode.OutOfResources;
break;
case 5:
resultCode = ResultCode.UnsupportedOpcode;
break;
default:
throw new NatPmpException("Unsupported Result Code: " + shortFromByteArray(response, 2));
}
// Throw an exception if the result code is not success.
if (resultCode != ResultCode.Success) {
throw new NatPmpException("Message was not successful. The returned message code was "
+ resultCode.toString() + ".");
}
// Verify the resultant opcode is correct.
if (response[1] != getOpcode() + (byte) 128) {
throw new NatPmpException("Incorrect opcode received from gateway.");
}
// Parse the response.
try {
parseResponse(response);
} catch (Throwable ex) {
if (ex instanceof NatPmpException) {
throw (NatPmpException) ex;
} else {
throw new NatPmpException("Exception encountered during parsing of response.", ex);
}
}
}
/**
* Gets the {@link MessageType} of this Message.
*
* @return The MessageType.
*/
MessageType getMessageType() {
return type;
}
/**
* Parses an unsigned integer (long) from a byte array in network byte
* order.
*
* @param src
* The source array.
* @param offset
* The offset in the array.
* @return The parsed unsigned integer. (Represented as a long.)
*/
static final int intFromByteArray(byte[] src, int offset) {
return (int) ((src[offset] & 0xFF) << 24) + ((src[offset + 1] & 0xFF) << 16) + ((src[offset + 2] & 0xFF) << 8)
+ (src[offset + 3] & 0xFF);
}
/**
* Writes a unsigned integer (long) to a byte array in network byte order.
*
* @param value
* The value to write.
* @param array
* The array to write to.
* @param offset
* The offset in the array.
*/
static final void intToByteArray(int value, byte[] array, int offset) {
array[offset] = (byte) (value >>> 24);
array[offset + 1] = (byte) (value >>> 16);
array[offset + 2] = (byte) (value >>> 8);
array[offset + 3] = (byte) value;
}
/**
* Parses an unsigned short (int) from a byte array in network byte order.
*
* @param src
* The source array.
* @param offset
* The offset in the array.
* @return The parsed unsigned short. (Represented as a long.)
*/
static final int shortFromByteArray(byte[] src, int offset) {
return (int) ((src[offset] & 0xFF) << 8) + (src[offset + 1] & 0xFF);
}
/**
* Writes a unsigned short (int) to a byte array in network byte order.
*
* @param value
* The value to write.
* @param array
* The array to write to.
* @param offset
* The offset in the array.
*/
static final void shortToByteArray(int value, byte[] array, int offset) {
array[offset] = (byte) (value >>> 8);
array[offset + 1] = (byte) value;
}
/**
* Sends the message. The sending process is synchronous. The NAT-PMP
* protocol specifies back-off times that are respected by this
* implementation. Each message object may only be sent once.
*
* @param destination
* The destination address.
* @throws NatPmpException
* Thrown when this message is already being sent or if there is
* a problem setting up the {@link DatagramSocket}.
*/
synchronized void sendMessage(InetAddress destination) {
try {
sendMessageInternal(destination);
} catch (NatPmpException ex) {
setResponseException(ex);
}
}
private void sendMessageInternal(InetAddress destination) throws NatPmpException {
// Reject if a send is ongoing.
if (this.socket != null) {
throw new NatPmpException("Message is already being sent.");
}
// Set up the socket.
try {
socket = new DatagramSocket();
socket.connect(destination, 5351);
socket.setSoTimeout(250);
} catch (IOException ex) {
// Try to clean up the socket.
if (socket != null) {
socket.close();
socket = null;
}
throw new NatPmpException("Exception during socket setup.", ex);
}
// Perform the sending and receiving process.
sendMessageInternal();
// Close the socket.
socket.close();
}
/**
* This handles the execution of the message sending and receiving process.
*/
private void sendMessageInternal() throws NatPmpException {
// Loop until all attempts have been made.
for (int attempts = 4; attempts > 0; attempts--) {
// Send the packet.
try {
byte[] payload = getRequestPayload();
payload[0] = 0;
payload[1] = getOpcode();
DatagramPacket packet = new DatagramPacket(payload, payload.length, socket.getRemoteSocketAddress());
socket.send(packet);
} catch (PortUnreachableException ex) {
throw new NatPmpException("The gateway is unreachable.");
} catch (IOException ex) {
throw new NatPmpException("Exception while sending packet.", ex);
}
// Wait for a response.
try {
byte[] localResponse = new byte[16];
DatagramPacket packet = new DatagramPacket(localResponse, 0, 16);
socket.receive(packet);
// Handle a response.
if (packet.getLength() > 0) {
this.response = localResponse;
return;
}
} catch (SocketTimeoutException ex) {
// Ignore this.
} catch (PortUnreachableException ex) {
throw new NatPmpException("The gateway is unreachable.");
} catch (IOException ex) {
throw new NatPmpException("Exception while waiting for packet to be received.", ex);
}
// Double the socket timeout for the next attempt.
try {
socket.setSoTimeout(socket.getSoTimeout() * 2);
} catch (SocketException ex) {
throw new NatPmpException("Exception while increasing socket timeout time.", ex);
}
}
}
}