[Piper-net-devel] SF.net SVN: piper-net:[14] trunk/jnatlib/src/net/jnatlib/stun
Status: Pre-Alpha
Brought to you by:
rdodgen
|
From: <rd...@us...> - 2009-03-15 04:16:07
|
Revision: 14
http://piper-net.svn.sourceforge.net/piper-net/?rev=14&view=rev
Author: rdodgen
Date: 2009-03-15 04:15:57 +0000 (Sun, 15 Mar 2009)
Log Message:
-----------
STUN client multithreaded, uses commons logging. Works with stun.ekiga.net so far
Modified Paths:
--------------
trunk/jnatlib/src/net/jnatlib/stun/StunClient.java
trunk/jnatlib/src/net/jnatlib/stun/StunMessage.java
trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java
Added Paths:
-----------
trunk/jnatlib/src/net/jnatlib/stun/StunResponseHandler.java
Modified: trunk/jnatlib/src/net/jnatlib/stun/StunClient.java
===================================================================
--- trunk/jnatlib/src/net/jnatlib/stun/StunClient.java 2009-01-10 06:19:22 UTC (rev 13)
+++ trunk/jnatlib/src/net/jnatlib/stun/StunClient.java 2009-03-15 04:15:57 UTC (rev 14)
@@ -11,97 +11,342 @@
import java.io.*;
import java.net.*;
+import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
+import java.util.Map;
+import java.util.concurrent.DelayQueue;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import net.jnatlib.stun.StunMessage.MessageType;
import net.jnatlib.stun.StunMessageAttribute.AttributeType;
public class StunClient {
-
- public static void main(String[] args) {
+ private static Log logger = LogFactory.getLog(StunClient.class);
+
+ private DatagramSocket sock = null;
+
+ private DelayQueue<StunTransmitAttempt> transmitQueue =
+ new DelayQueue<StunTransmitAttempt>();
+
+ private Thread retryThread = null;
+ private Thread readThread = null;
+
+ private volatile boolean stopping = false;
+
+ /**
+ * STUN client will be bound to the 'wildcard' address
+ * See the description for the default DatagramSocket constructor
+ * @throws SocketException if binding fails
+ */
+ public StunClient() throws SocketException {
+ this(new DatagramSocket());
+ }
+
+ /**
+ * STUN client will be bound to a given address
+ * While a client bound to the wildcard address works in most cases,
+ * a host can have any number of routes/interfaces available.
+ * Instead of letting the OS pick what interface to use to reach the
+ * STUN server, a number of STUN clients created with this constructor
+ * can be used to test various interfaces - each of which may give
+ * a different public IP.
+ *
+ * See documentation for NetworkInterface
+ * @param bind IP address to bind to
+ * @throws SocketException if binding fails
+ */
+ public StunClient(InetAddress bind) throws SocketException {
+ this(new DatagramSocket(0, bind));
+ }
+
+ public StunClient(DatagramSocket d) {
+ sock = d;
+ createReadThread();
+ createRetryThread();
+ logger.info("Started client on local port " + sock.getLocalPort());
+ }
+
+ private void createRetryThread() {
+ retryThread = new Thread(new Runnable() {
+ public void run() {
+ while (!stopping) {
+ try {
+ StunTransmitAttempt t = transmitQueue.take();
+ if (t.hasFailed()) {
+ logger.warn("Request has run out of transmit attempts: " +
+ t.getMessage());
+ //Trigger timeout handler
+ t.processFailure();
+ } else if (t.gotResponse()) {
+ logger.debug("Discarding a transmit attempt that had a response");
+ } else {
+ logger.debug("Sending message " + t.getMessage().toString());
+ t.transmit(sock);
+ //Requeue with the updated retransmit delay
+ transmitQueue.put(t);
+ }
+ } catch (InterruptedException e) {
+ //Stopping flag needs to be re-evaluated
+ }
+ }
+ logger.debug("Transmit thread is exiting");
+ }
+ });
+ retryThread.setName("StunClientTransmitter");
+ retryThread.setDaemon(true);
+ retryThread.start();
+ }
+
+ private void createReadThread() {
+ readThread = new Thread(new Runnable() {
+ public void run() {
+ byte[] buffer = new byte[2048];
+
+ while (!stopping) {
+ try {
+ DatagramPacket p = new DatagramPacket(buffer, buffer.length);
+ sock.receive(p);
+ logger.debug("Got packet of length " + p.getLength());
+
+ StunMessage m = StunMessage.parseByteMessage(buffer, p.getLength());
+
+ byte[] a = m.getTransactionID();
+
+ //TODO: supposedly iterating the delay queue is slow and naughty
+ StunTransmitAttempt request = null;
+ for (StunTransmitAttempt t : transmitQueue) {
+ byte[] b = t.getMessage().getTransactionID();
+ boolean match = true;
+ if (a.length != b.length)
+ continue;
+ for (int i = 0; i < a.length; i++) {
+ if (a[i] != b[i]) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ request = t;
+ break;
+ }
+ }
+ if (request == null) {
+ logger.debug("Unrecognized transaction ID in response: " +
+ m.toString());
+ } else {
+ logger.debug("Got a response for " +
+ request.getMessage().toString() +
+ " ; " + m.toString());
+ request.processResponse(m);
+ }
+
+ } catch (IOException e) {
+ logger.error("IO exception reading from STUN socket", e);
+ }
+
+ }
+ }
+ });
+ readThread.setName("StunClientReader");
+ readThread.setDaemon(true);
+ readThread.start();
+ }
+
+ public void sendRequest(StunMessage req, SocketAddress dst, StunResponseHandler callback) {
+ StunTransmitAttempt t = new StunTransmitAttempt(req, dst);
+ t.setResponseHandler(callback);
+ transmitQueue.put(t);
+ }
+
+ public static void main(String[] args) throws InterruptedException {
// Get server name, port in args
String host = args[0];
int port = Integer.parseInt(args[1]);
- StunMessage received = StunClient.getSTUNResponse(host, port);
- System.out.println(received.getMessageType());
- byte[] transactionID = received.getTransactionID();
- System.out.print("Transaction ID: ");
- for(int i = 0; i < transactionID.length; i++)
- System.out.print(transactionID[i] + " ");
- System.out.println("");
- Hashtable<AttributeType, StunMessageAttribute> attributes = received.getAttributes();
- Enumeration<AttributeType> keys = attributes.keys();
- while(keys.hasMoreElements()) {
- AttributeType key = keys.nextElement();
- System.out.println(key + ": ");
- Hashtable<String, Object> values = attributes.get(key).getValues();
- Enumeration<String> keysForValues = values.keys();
- while(keysForValues.hasMoreElements()) {
- String keyForValue = keysForValues.nextElement();
- if(keyForValue.equals("IP Address")) {
- System.out.print("\t " + keyForValue + ": ");
- int[] ip = (int[])values.get(keyForValue);
- for(int i = 0; i < ip.length-1; i++) {
- System.out.print(ip[i] + ".");
- }
- System.out.println(ip[ip.length-1]);
- } else {
- System.out.println("\t " + keyForValue + ": " + values.get(keyForValue));
- }
- }
+ final Object waiter = new Object();
+ synchronized (waiter) {
+ StunClient c = null;
+ try {
+ c = new StunClient();
+ } catch (SocketException e) {
+ System.err.println("Binding failed");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ StunMessage request = new StunMessage(MessageType.BIND_REQUEST);
+ c.sendRequest(request, new InetSocketAddress(host, port), new StunResponseHandler() {
+ public void handleResponse(StunMessage response) {
+ System.out.println("Got response: " + response);
+ Map<AttributeType, StunMessageAttribute> attr = response.getAttributes();
+ System.out.println("Mapped address: " +
+ attr.get(AttributeType.MAPPED_ADDRESS).getAsEndpoint().toString());
+
+ synchronized (waiter) {
+ waiter.notify();
+ }
+ }
+
+ public void handleError(StunMessage errorResponse) {
+ System.err.println("Error response! " + errorResponse);
+ synchronized (waiter) {
+ waiter.notify();
+ }
+ }
+
+ public void handleTimeout() {
+ System.err.println("Timed out awaiting binding response");
+ synchronized (waiter) {
+ waiter.notify();
+ }
+ }
+ });
+
+ System.out.println("Waiting on STUN response...");
+ waiter.wait();
}
}
+}
- public static StunMessage getSTUNResponse(String host, int port) {
- DatagramSocket NATServerConnection;
- StunMessage bindRequest = new StunMessage(MessageType.BIND_REQUEST);
- StunMessage bindResponse = null;
- int retransmissionTimeOutSeconds = 1;
- boolean retransmit = false;
- do {
- try {
- System.out.println("Opening socket");
- InetSocketAddress socketAddr =
- new InetSocketAddress(host, port);
- NATServerConnection = new DatagramSocket(54322);
- NATServerConnection.setSoTimeout(10000);
- NATServerConnection.connect(socketAddr);
- System.out.println("Writing to socket");
- byte[] message = bindRequest.getMessage();
- DatagramPacket packet = new DatagramPacket(message,
- message.length, socketAddr);
- byte[] buffer = new byte[2048];
- NATServerConnection.send(packet);
- System.out.println("Reading from socket");
- DatagramPacket p = new DatagramPacket(buffer, 2048);
- NATServerConnection.receive(p);
- bindResponse = StunMessage.parseByteMessage(buffer);
- NATServerConnection.close();
- retransmit = false;
- } catch (SocketTimeoutException e) {
- System.out.println(e);
- try {
- Thread.sleep(retransmissionTimeOutSeconds*1000);
- } catch (InterruptedException e1) {
- System.out.println(e1);
- }
- retransmit = true;
- } catch (IOException e) {
- System.out.println(e);
- }
- if(bindResponse == null) {
- try {
- Thread.sleep(retransmissionTimeOutSeconds*1000);
- } catch (InterruptedException e) {
- System.out.println(e);
- }
- retransmissionTimeOutSeconds *= 2;
- } else {
- break;
- }
- } while (retransmit);
- return bindResponse;
- }
+class StunTransmitAttempt implements Delayed {
+ private static Log logger = LogFactory.getLog(StunTransmitAttempt.class);
+
+ private static final int RETRANSMIT_MAX = 7;
+ private static final int RETRANSMIT_TIMEOUT = 500;
+ private static final int RETRANSMIT_BACKOFF = 2;
+ private static final int FINAL_WAIT_MULTIPLIER = 16;
+
+ private int transmitCount = 0;
+ private long lastTransmit = 0;
+
+ //Recv thread sets this, transmit thread reads it
+ private volatile boolean gotResponse = false;
+
+ private StunMessage msg = null;
+ private SocketAddress server = null;
+ private StunResponseHandler handler = null;
+
+ public StunTransmitAttempt(StunMessage msg, SocketAddress server) {
+ if (msg == null || server == null)
+ throw new IllegalArgumentException("Both arguments must be non-null");
+
+ this.msg = msg;
+ this.server = server;
+ }
+
+ public StunMessage getMessage() {
+ return msg;
+ }
+ public SocketAddress getDestination() {
+ return server;
+ }
+
+ public void setResponseHandler(StunResponseHandler h) {
+ //Grab a lock - also locks in processResponse
+ synchronized (this) {
+ this.handler = h;
+ }
+ }
+
+ //TODO: Consider making retransmitCount atomic
+ public void transmit(DatagramSocket transmitVia) {
+ if (hasFailed() || gotResponse())
+ throw new RuntimeException("Attempting to retransmit after failure/response");
+ transmitCount++;
+ lastTransmit = (new Date()).getTime();
+
+ byte[] b = getMessage().toByteArray();
+ try {
+ DatagramPacket dp = new DatagramPacket(b, b.length, getDestination());
+ transmitVia.send(dp);
+ } catch (IOException e) {
+ logger.warn("STUN send failed due to IO exception", e);
+ }
+ }
+
+ public void processResponse(StunMessage response) {
+ //Lock self since handler may be modified/unset
+ //Also lock since gotResponse is a condition and also changes
+ synchronized (this) {
+ //Multiple responses are possible in response to really
+ //funky retransmission scenarios. Log and drop.
+ if (gotResponse) {
+ logger.debug("Duplicate response: " + response.toString());
+ return;
+ }
+
+ gotResponse = true;
+
+ if (this.handler != null) {
+ if (response.getMessageType().isError()) {
+ this.handler.handleError(response);
+ } else {
+ this.handler.handleResponse(response);
+ }
+ }
+ }
+ }
+
+ public boolean gotResponse() {
+ return gotResponse;
+ }
+ public boolean hasFailed() {
+ return (transmitCount >= RETRANSMIT_MAX) && !gotResponse;
+ }
+
+ /**
+ * Should be called after hasFailed returns true
+ * and the final long wait has elapsed (hasFailed
+ * will be true during this final wait, but a response
+ * can still come in. This is instead to be called just
+ * as the attempt is removed from the queue)
+ *
+ * Triggers handleTimeout in handler
+ */
+ public void processFailure() {
+ if (hasFailed() && handler != null)
+ handler.handleTimeout();
+ }
+
+ public long getDelay(TimeUnit unit) {
+ long targetTime = lastTransmit;
+
+ if (transmitCount == 0 || hasFailed()) {
+ //This is the first attempt, or shouldn't attempt again
+ //In either case, no delay before processing
+ return 0;
+ } else if (transmitCount >= RETRANSMIT_MAX) {
+ //Waiting for a response from the last transmission
+ //hasFailed will
+ targetTime += (FINAL_WAIT_MULTIPLIER * RETRANSMIT_TIMEOUT);
+ } else {
+ //Typical case
+ //Backoff is exponential (RFC says to double after every attempt)
+ //Number of doubles is transmitCount - 1
+ //(on first retransmit, multiply by 1, not 2)
+ int mult = (int) Math.pow(RETRANSMIT_BACKOFF, transmitCount - 1);
+ targetTime += mult * RETRANSMIT_TIMEOUT;
+ }
+
+ long now = (new Date()).getTime();
+ long delayMs = targetTime - now;
+
+ logger.trace(String.format("Message %s being delayed for %d ms (already sent %d times)",
+ msg.toString(), delayMs, transmitCount));
+
+ //Performed calc in ms, so convert to requested unit
+ return unit.convert(delayMs, TimeUnit.MILLISECONDS);
+ }
+
+ public int compareTo(Delayed arg0) {
+ long diff = this.getDelay(TimeUnit.MILLISECONDS) -
+ arg0.getDelay(TimeUnit.MILLISECONDS);
+ if (diff == 0) return 0;
+ if (diff > 0) return 1;
+ return -1;
+ }
+
}
Modified: trunk/jnatlib/src/net/jnatlib/stun/StunMessage.java
===================================================================
--- trunk/jnatlib/src/net/jnatlib/stun/StunMessage.java 2009-01-10 06:19:22 UTC (rev 13)
+++ trunk/jnatlib/src/net/jnatlib/stun/StunMessage.java 2009-03-15 04:15:57 UTC (rev 14)
@@ -9,52 +9,81 @@
package net.jnatlib.stun;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Map;
import java.util.Random;
import java.util.Hashtable;
import net.jnatlib.stun.StunMessageAttribute.AttributeType;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.logging.Log;
+
public class StunMessage {
+ private static Log logger = LogFactory.getLog(StunMessage.class);
+
public enum MessageType {
- BIND_REQUEST(0x0001),
- BIND_RESPONSE(0x0101),
- BIND_ERROR_RESPONSE(0x0111),
- SHARED_SECRET_REQUEST(0x0002), //This is now reserved
- SHARED_SECRET_RESPONSE(0x0102), //This is now reserved
- SHARED_SECRET_ERROR_RESPONSE(0x0112); //This is now reserved
+ BIND_REQUEST(0x0001, false),
+ BIND_RESPONSE(0x0101, false),
+ BIND_ERROR_RESPONSE(0x0111, true),
+ SHARED_SECRET_REQUEST(0x0002, false), //This is now reserved
+ SHARED_SECRET_RESPONSE(0x0102, false), //This is now reserved
+ SHARED_SECRET_ERROR_RESPONSE(0x0112, true); //This is now reserved
- private final int hexRepresentation;
-
- MessageType(int hex) {
- this.hexRepresentation = hex;
+ private final short value;
+ private final boolean error;
+
+ MessageType(int value, boolean isError) {
+ this.value = (short)value;
+ this.error = isError;
}
- public int getHexRepresentation() {
- return hexRepresentation;
+ public boolean isError() {
+ return error;
}
+
+ public short getValue() {
+ return value;
+ }
+
+ public static MessageType getForValue(short value) {
+ //Pasted from StunMessageAttribute. Probably overkill.
+ for (MessageType a : MessageType.values()) {
+ if (a.getValue() == value) {
+ return a;
+ }
+ }
+ return null;
+ }
}
- private static final int MAGIC_COOKIE = 0x2112A442;
- private static final int TRANSACTION_ID_LENGTH = 12;
+ //The single byte cast is needed due to java only having signed bytes
+ //Public for use by StunMessageAttribute
+ public static final byte[] MAGIC_COOKIE = new byte[] { 0x21, 0x12, (byte)0xA4, 0x42 };
+
+ private static final int TRANSACTION_ID_LENGTH = 16;
private static final int HEADER_LENGTH = 20;
private MessageType messageType;
- private int length;
- private byte[] transactionID;
- private Hashtable<AttributeType, StunMessageAttribute> messageAttributes;
- private byte[] originalAttributes;
+ private byte[] transactionID = new byte[TRANSACTION_ID_LENGTH];
- public StunMessage(MessageType messageType, byte[] transactionID, int length) {
- this.messageType = messageType;
- this.length = length;
- this.transactionID = new byte[TRANSACTION_ID_LENGTH];
- System.arraycopy(transactionID, 0, this.transactionID, 0, TRANSACTION_ID_LENGTH);
- messageAttributes = new Hashtable<AttributeType, StunMessageAttribute>();
- }
+ //AttributeType has a very limited domain
+ private Map<AttributeType, StunMessageAttribute> messageAttributes =
+ new Hashtable<AttributeType, StunMessageAttribute>
+ (AttributeType.values().length);
public StunMessage(MessageType messageType, byte[] transactionID) {
- this(messageType, transactionID, 0);
+ if (messageType == null)
+ throw new IllegalArgumentException("messageType and transactionID must be non-null");
+ this.setTransactionID(transactionID);
+ this.messageType = messageType;
}
public StunMessage(MessageType messageType) {
@@ -63,184 +92,146 @@
/**
* This creates a randomly generated transaction ID
+ * All generated IDs contain the 'magic cookie' version indicator
*
* @return a byte array representing a new transaction ID
*/
+
public static byte[] createTransactionID() {
- String transactionID = "";
- char[] letters = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
- 'a', 'b', 'c', 'd', 'e', 'f' };
- Random rng = new Random();
- for (int i = 0; i < TRANSACTION_ID_LENGTH; i++)
- transactionID += letters[rng.nextInt(letters.length)];
- return transactionID.getBytes();
+ byte[] ret = new byte[TRANSACTION_ID_LENGTH];
+
+ //Not sure if Random is thread safe
+ synchronized (tidGen) {
+ //Fill the entire result array with random bytes...
+ tidGen.nextBytes(ret);
+ }
+ //...but we substitute the magic cookie to indicate version at the beginning
+ System.arraycopy(MAGIC_COOKIE, 0, ret, 0, MAGIC_COOKIE.length);
+
+ //TODO: Keep a list of recently used; loop if needed. Highly unlikely...
+
+ return ret;
}
+ //Used above. One global instance so that successive calls give random IDs
+ private static final Random tidGen = new Random();
public MessageType getMessageType() {
return messageType;
}
public byte[] getTransactionID() {
- // We make a copy so that a malicious user cannot
- // modify the transaction ID
- byte[] transactionIDCopy = new byte[TRANSACTION_ID_LENGTH];
- System.arraycopy(transactionID, 0, transactionIDCopy, 0, TRANSACTION_ID_LENGTH);
- return transactionIDCopy;
+ return transactionID;
}
- public Hashtable<AttributeType, StunMessageAttribute> getAttributes() {
- return messageAttributes;
+ /**
+ * Returns an unmodifiable mapping of attribute type -> attribute
+ */
+ public Map<AttributeType, StunMessageAttribute> getAttributes() {
+ return Collections.unmodifiableMap(messageAttributes);
}
- public void setMessageType(MessageType newMessageType) {
- messageType = newMessageType;
- }
-
- public void addAttribute(StunMessageAttribute newMessageAttribute) {
- // The RFC specifies if an attribute is given more then once, all
- // duplicates after the first are to be ignored
- if(!messageAttributes.containsKey(newMessageAttribute.getAttributeType()))
- messageAttributes.put(newMessageAttribute.getAttributeType(), newMessageAttribute);
+ /**
+ * Adds an attribute if one of the same type does not already exist
+ * The RFC specifies if an attribute is given more then once, all
+ * duplicates after the first are to be ignored.
+ * @return true if added, false if not due to duplicate
+ */
+ public boolean addAttribute(StunMessageAttribute newMessageAttribute) {
+ if(messageAttributes.containsKey(newMessageAttribute.getAttributeType()))
+ return false;
+ messageAttributes.put(newMessageAttribute.getAttributeType(), newMessageAttribute);
+ return true;
}
public void setTransactionID(byte[] newTransactionID) {
- // Check that this is a valid transaction ID
- if (newTransactionID.length == TRANSACTION_ID_LENGTH)
- System.arraycopy(transactionID, 0, this.transactionID, 0, TRANSACTION_ID_LENGTH);
+ if (newTransactionID == null)
+ throw new IllegalArgumentException("Transaction ID must be non-null");
+ if (newTransactionID.length != TRANSACTION_ID_LENGTH)
+ throw new IllegalArgumentException("Invalid transaction id length");
+ System.arraycopy(newTransactionID, 0, this.transactionID, 0, TRANSACTION_ID_LENGTH);
}
- public byte[] getMessage() {
- byte[] message = new byte[length + HEADER_LENGTH];
- message[0] = (byte) ((messageType.getHexRepresentation() >>> 8) & 0xFF);
- message[1] = (byte) (messageType.getHexRepresentation() & 0xFF);
- message[2] = (byte) ((length >>> 8) & 0xFF);
- message[3] = (byte) (length & 0xFF);
- message[4] = (byte) ((MAGIC_COOKIE >>> 24) & 0xFF);
- message[5] = (byte) ((MAGIC_COOKIE >>> 16) & 0xFF);
- message[6] = (byte) ((MAGIC_COOKIE >>> 8) & 0xFF);
- message[7] = (byte) (MAGIC_COOKIE & 0xFF);
- System.arraycopy(transactionID, 0, message, 8, TRANSACTION_ID_LENGTH);
- if(originalAttributes != null)
- System.arraycopy(originalAttributes, 0, message, 8+TRANSACTION_ID_LENGTH, originalAttributes.length);
- return message;
+ public String toString() {
+ StringBuilder b = new StringBuilder(transactionID.length);
+ for (int i = 0; i < transactionID.length; i++)
+ b.append(Integer.toHexString(transactionID[i] & 0x000000ff));
+ return "[" + this.messageType.toString() + ": " + b.toString() + "]";
}
- public void setOriginalAttributes(byte[] originalAttributes) {
- this.originalAttributes = new byte[originalAttributes.length];
- System.arraycopy(originalAttributes, 0, this.originalAttributes, 0, originalAttributes.length);
+ public byte[] toByteArray() {
+ try {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ DataOutputStream dout = new DataOutputStream(bytes);
+ //Data output stream specifies big endian (which we need)
+ dout.writeShort(messageType.getValue());
+ //Write a placeholder for length, to be updated at the end
+ dout.writeShort(0);
+
+ dout.write(transactionID);
+
+ for (StunMessageAttribute attr : getAttributes().values())
+ attr.writeToStream(dout);
+
+ //Update length
+ byte[] ret = bytes.toByteArray();
+ int length = ret.length - HEADER_LENGTH;
+ ret[2] = (byte)(length >>> 8);
+ ret[3] = (byte)(length);
+
+ return ret;
+
+ } catch (IOException ex) {
+ //This shouldn't happen since dout is wrapping a byte array
+ throw new RuntimeException(ex);
+ }
}
- public static StunMessage parseByteMessage(byte[] message) {
- MessageType messageType = null;
- // Find out the message type
- int typeHexRepresentation = ((message[0] & 0xFF) << 8) + (message[1] & 0xFF);
- for (MessageType m : MessageType.values())
- if (m.hexRepresentation == typeHexRepresentation) {
- messageType = m;
- break;
- }
- // See the length of the message
- int length = ((message[2] & 0xFF) << 8) + (message[3] & 0xFF);
- if (message[2] != 0)
- return null;
- // Make sure that the magic cookie is contained in the message
- int magicCookie = ((message[4] & 0xFF) << 24) + ((message[5] & 0xFF) << 16) + ((message[6] & 0xFF) << 8)
- + (message[7] & 0xFF);
- // If the magic cookie is not contained, we ignore this message
- //TODO: Is this proper?
- //if (magicCookie != MAGIC_COOKIE)
- // return null;
- // Find the transaction ID
- byte[] transactionID = new byte[16];
- System.arraycopy(message, 8, transactionID, 0, TRANSACTION_ID_LENGTH);
- byte[] originalAttributes = new byte[length];
- System.arraycopy(message, 8+TRANSACTION_ID_LENGTH, originalAttributes, 0, length);
- //Start looking for attributes after the header
- int i = HEADER_LENGTH;
- //Create the new message so that we can add attributes to it
- StunMessage stunMessage = new StunMessage(messageType, transactionID, length);
- stunMessage.setOriginalAttributes(originalAttributes);
- //Continue looking through the message while we have valid content left
- while (i < length) {
- StunMessageAttribute newMessageAttribute = null;
- // Find the type of this attribute
- int attrTypeHexRepresentation = ((message[i] & 0xFF) << 8) + (message[++i] & 0xFF);
- AttributeType type = null;
- for (AttributeType t : AttributeType.values()) {
- if (t.getHexRepresentation() == attrTypeHexRepresentation) {
- type = t;
- break;
- }
- }
- // Find the length of this attribute
- int attrLength = ((message[++i] & 0xFF) << 8) + (message[++i] & 0xFF);
- newMessageAttribute = new StunMessageAttribute(type, length);
- if(type != null && !stunMessage.getAttributes().containsKey(type)) {
- // I created a different case for each type of attribute
- switch (type) {
- case MAPPED_ADDRESS:
- {
- i++;
- int IPVersion = message[++i];
- int port = ((message[++i] & 0xFF) << 8) + (message[++i] & 0xFF);
- int[] IPAddress = null;
- if (IPVersion == 1) {
- IPAddress = new int[4];
- for (int j = 0; j < 4; j++)
- IPAddress[j] = message[++i] & 0xFF;
- } else {
- IPAddress = new int[8];
- for (int j = 0; j < 8; j++)
- IPAddress[j] = ((message[++i] & 0xFF) << 8) + (message[++i] & 0xFF);
- }
- newMessageAttribute.addValue("Port", port);
- newMessageAttribute.addValue("IP Address", IPAddress);
- break;
- }
- case XOR_MAPPED_ADDRESS:
- {
- i++;
- int IPVersion = message[++i];
- //Convert the magic cookie to an array since it is usseful for XORing the port and IP
- byte[] magicCookieAsArray = new byte[4];
- magicCookieAsArray[0] = (byte) ((MAGIC_COOKIE >>> 24) & 0xFF);
- magicCookieAsArray[1] = (byte) ((MAGIC_COOKIE >>> 16) & 0xFF);
- magicCookieAsArray[2] = (byte) ((MAGIC_COOKIE >>> 8) & 0xFF);
- magicCookieAsArray[3] = (byte) (MAGIC_COOKIE & 0xFF);
- int[] portArray = new int[2];
- for (int j = 0; j < 2; j++)
- portArray[j] = (message[++i] & 0xFF) ^ magicCookieAsArray[j];
- int port = (portArray[0] << 8) + portArray[1];
- int[] IPAddress = null;
- if (IPVersion == 1) {
- IPAddress = new int[4];
- for (int j = 0; j < 4; j++)
- IPAddress[j] = ((message[++i] & 0xFF) ^ magicCookieAsArray[j]) & 0xFF;
- } else {
- //NEED IPV6 SUPPORT
- }
- newMessageAttribute.addValue("Port", port);
- newMessageAttribute.addValue("IP Address", IPAddress);
- break;
- }
- case USERNAME:
- case MESSAGE_INTEGRITY:
- case ERROR_CODE:
- case UNKNOWN_ATTRIBUTES:
- case REALM:
- case NONCE:
- default:
- i += attrLength;
- break;
- }
- i++;
- stunMessage.addAttribute(newMessageAttribute);
- } else {
- i += attrLength;
- }
- //The RFC specifies each attribute must end on an index divisible by 4
- i += i % 4;
- }
- return stunMessage;
+ public static StunMessage parseByteMessage(byte[] message, int length) {
+ ByteArrayInputStream bytes = new ByteArrayInputStream(message, 0, length);
+ DataInputStream din = new DataInputStream(bytes);
+ try {
+ MessageType messageType = MessageType.getForValue(din.readShort());
+ if (messageType == null) {
+ logger.error("Dropping STUN message - unknown message type");
+ return null;
+ }
+
+ int reportedLength = din.readUnsignedShort();
+
+ if ((length - HEADER_LENGTH) != reportedLength) {
+ logger.error("Dropping STUN message - datagram length doesn't match reported length");
+ return null;
+ }
+
+ byte[] tid = new byte[TRANSACTION_ID_LENGTH];
+ din.readFully(tid);
+
+ StunMessage ret = new StunMessage(messageType, tid);
+
+ //Keep reading until no bytes are left
+ //Message length should match datagream size
+ //The last attribute should end on the last byte
+ //If not, the last attribute-read will choke and let us know
+ while (bytes.available() > 0) {
+ StunMessageAttribute attr = null;
+ try {
+ attr = StunMessageAttribute.readFromStream(din);
+ } catch (Exception attrEx) {
+ //TODO: Make the exception type more specific
+ //This handles malformed attributes / unknown comp-required attributes
+ logger.error("Dropping STUN message - failure reading attribute", attrEx);
+ return null;
+ }
+ //Can be null if comp-optional
+ if (attr != null)
+ ret.addAttribute(attr);
+
+ }
+
+ return ret;
+ } catch (IOException ex) {
+ logger.error("IO error in parseByteMessage", ex);
+ return null;
+ }
}
}
Modified: trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java
===================================================================
--- trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java 2009-01-10 06:19:22 UTC (rev 13)
+++ trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java 2009-03-15 04:15:57 UTC (rev 14)
@@ -9,10 +9,27 @@
package net.jnatlib.stun;
-import java.util.Hashtable;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.logging.Log;
+
public class StunMessageAttribute {
+ private static Log logger = LogFactory.getLog(StunMessageAttribute.class);
+
public enum AttributeType {
+ /*
+ * The reserved attribute types below may be
+ * "comprehension required' but can be ignored
+ * Made obsolete by recent RFC
+ */
+
MAPPED_ADDRESS (0x0001),
RESPONSE_ADDRESS (0x0002), //This is now reserved
CHANGE_REQUEST (0x0003), //This is now reserved
@@ -28,34 +45,253 @@
NONCE (0x0015),
XOR_MAPPED_ADDRESS (0x0020);
- private final int hexRepresentation;
+ private final short value;
- AttributeType(int hex) {
- this.hexRepresentation = hex;
+ AttributeType(int value) {
+ this.value = (short)value;
}
- public int getHexRepresentation() { return hexRepresentation; }
+ public short getValue() { return value; }
+
+ public static AttributeType getForValue(short value) {
+ /*TODO: Client code should be able to handle attributes that we don't know about
+ * There should be a means for client code to declare that it will handle
+ * specific comp-required attributes, and attributeType should be able to hold
+ * arbitrary values.
+ */
+ //How ugly. Thankfully there aren't many types...
+ for (AttributeType a : AttributeType.values()) {
+ if (a.getValue() == value) {
+ return a;
+ }
+ }
+ return null;
+ }
}
- AttributeType attributeType;
- int length;
- Hashtable<String, Object> values;
+ //Attribute types between 0 and this value must be understood.
+ //Error condition if not. Values greater than this can be ignored
+ private static final int COMPREHENSION_UPPER = 0x7FFF;
+ private AttributeType attributeType;
+ private byte[] rawValue;
+
public AttributeType getAttributeType() {
return attributeType;
}
- public Hashtable<String, Object> getValues() {
- return values;
+ /**
+ * Reads an attribute from a stream representing a stun message
+ * This method throws IO exceptions in many cases of bad input
+ * It is only useful when reading a stun message
+ * StunMessage.parseByteMessage drops messages on error
+ * and logs exceptions thrown.
+ * @param din
+ * @return The next attribute in the stream, or null
+ * @throws IOException
+ * @throws RuntimeException if an unknown comp-required attribute is encountered
+ */
+ protected static StunMessageAttribute readFromStream(DataInputStream din)
+ throws IOException {
+
+ StunMessageAttribute ret = null;
+
+ short typeVal = din.readShort();
+ AttributeType a = AttributeType.getForValue(typeVal);
+ if (a == null) {
+ //The attribute type is not understood by this implementation
+
+ //Java doesn't have unsigned types
+ //Casting to int will cause sign extension for "negative" shorts
+ //Lop off the extra 1s
+ int intTypeVal = typeVal & 0xFFFF;
+
+ //Use of integer here for type allows "unsigned" comparison
+ if (intTypeVal < COMPREHENSION_UPPER) {
+ //TODO: Proper type for unknown attr exception
+ String err = "Unknwon comp-required attribute: " +
+ Integer.toHexString(intTypeVal);
+ logger.error(err);
+ throw new RuntimeException(err);
+ } else {
+ //Not fatal, so ignore
+ logger.warn("Unknwon comprehension-optional attribute: "
+ + Integer.toHexString(intTypeVal));
+ }
+ } else {
+ //Attribute is understood (or at least 'reserved')
+ ret = new StunMessageAttribute(a);
+ }
+
+ int length = din.readUnsignedShort();
+
+ //Zero length arrays are okay
+ byte[] value = new byte[length];
+ if (length > 0) {
+ din.readFully(value);
+ }
+
+ //We may be ignoring this attribute, as stated above
+ //Read the value and padding anyway so that further attributes can be read
+ if (ret != null) ret.rawValue = value;
+
+ int pad = length % 4;
+ for (int i = 0; i < pad; i++)
+ din.read();
+
+ return ret;
}
- public void addValue(String key, Object value) {
- values.put(key, value);
+ protected void writeToStream(DataOutputStream dout)
+ throws IOException {
+
+ dout.writeShort(attributeType.getValue());
+ //Length field does not include padding
+ if (rawValue != null) {
+ dout.writeShort(rawValue.length);
+ dout.write(rawValue, 0, rawValue.length);
+ } else {
+ //Value was never set, write 0 for length but no value
+ dout.writeShort(0);
+ }
+
+ //Pad to multiple of 4
+ int pad = rawValue.length % 4;
+ for (int i = 0; i < pad; i++)
+ dout.writeByte(0);
+
}
- StunMessageAttribute(AttributeType type, int length) {
+ public StunMessageAttribute(AttributeType type) {
+ if (type == null)
+ throw new IllegalArgumentException("type cannot be null");
attributeType = type;
- this.length = length;
- values = new Hashtable<String, Object>();
}
+
+ /**
+ * Sets the value which will be included, unmodified, in this attribute
+ */
+ public void setRawValue(byte[] rawValue) {
+ this.rawValue = rawValue;
+ }
+
+ /**
+ * Returns the byte array as received, with no attempt to interpret it
+ * May be null if the raw value has not been set. Never null for
+ * an attribute that was received over the network.
+ */
+ public byte[] getRawValue() {
+ return rawValue;
+ }
+
+ //TODO: An xor impl
+ /**
+ * Attempts to interpret the attribute value as an IP endpoint
+ * (address + port) as seen in MAPPED-ADDRESS
+ * getAsXORedEndpoint correctly handles XOR-MAPPED-ADDRESS
+ * An exception will be thrown if the value length is incorrect
+ * @throws IllegalArgumentException if the value is not suitable
+ * @return IP + port encoded in the attribute
+ */
+ public InetSocketAddress getAsEndpoint() {
+ if (rawValue == null) throw new IllegalArgumentException("Null value");
+ //Two possible lengths - depends if IPv4 or IPv6
+ if (rawValue.length != (4 + 4) && rawValue.length != (4 + 16)) {
+ //TODO: Better fitting exception type
+ throw new IllegalArgumentException(
+ "Lenght does not correspond to an encoded IPv4 or IPv6 address");
+ }
+
+ try {
+ DataInputStream din = new DataInputStream(new ByteArrayInputStream(rawValue));
+ //First byte is ignored for alignment purposes
+ din.readByte();
+
+ byte family = din.readByte();
+ int port = din.readUnsignedShort();
+
+ InetAddress addr = null;
+
+ if (family == 0x01) {
+ //IPv4
+ if (rawValue.length != (4 + 4))
+ throw new IllegalArgumentException("Wrong length for IPv4");
+
+ byte[] buff = new byte[4];
+ din.readFully(buff);
+ addr = InetAddress.getByAddress(buff);
+
+ } else if (family == 0x02) {
+ //IPv6
+ if (rawValue.length != (4 + 16))
+ throw new IllegalArgumentException("Wrong length for IPv6");
+
+ byte[] buff = new byte[4];
+ din.readFully(buff);
+ addr = InetAddress.getByAddress(buff);
+
+ } else {
+ throw new IllegalArgumentException("Unknown address family in attribute");
+ }
+
+ InetSocketAddress ret = new InetSocketAddress(addr, port);
+ logger.debug("Parsed a socket address attribute: " + ret);
+ return ret;
+ } catch (IOException e) {
+ //It's on a byte array, and we do our own checking on addr length
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Encodes the socket endpoint and store it in the attribute value
+ * @param addr non-null address / port combo
+ * @param xor true to obfuscate the address (XOR-MAPPED-ADDRESS)
+ */
+ public void setAsEndpoint(InetSocketAddress addr, boolean xor) {
+ if (addr == null) throw new IllegalArgumentException("Addr can't be null");
+ //TODO: implement me!
+ }
+
+ /**
+ * Attempts to interpret the attribute value as a UTF8 string
+ * as needed for attribute types such as SOFTWARE
+ * Null value and empty value return an empty string
+ */
+ public String getAsString() {
+ //Assume that null or zero length means empty string
+ //Zero length is possible from readFromStream, but null is not
+ if (rawValue == null || rawValue.length == 0) return "";
+
+ try {
+ return new String(rawValue, "UTF8");
+ } catch (UnsupportedEncodingException e) {
+ // UTF8 is supported
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Encode a string in UTF8 and store it in the attribute value
+ * @param str non-null string to be encoded
+ */
+ public void setAsString(String str) {
+ if (str == null) throw new IllegalArgumentException("Str can't be null");
+ try {
+ this.setRawValue(str.getBytes("UTF8"));
+ } catch (UnsupportedEncodingException e) {
+ //UTF8 is known to be supported
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String toString() {
+ String ret = attributeType.toString();
+ if (rawValue != null) {
+ ret += " [" + rawValue.length + " bytes]";
+ } else {
+ ret += " [0 bytes]";
+ }
+ return ret;
+ }
}
Added: trunk/jnatlib/src/net/jnatlib/stun/StunResponseHandler.java
===================================================================
--- trunk/jnatlib/src/net/jnatlib/stun/StunResponseHandler.java (rev 0)
+++ trunk/jnatlib/src/net/jnatlib/stun/StunResponseHandler.java 2009-03-15 04:15:57 UTC (rev 14)
@@ -0,0 +1,9 @@
+package net.jnatlib.stun;
+
+public interface StunResponseHandler {
+
+ public void handleResponse(StunMessage response);
+ public void handleError(StunMessage errorResponse);
+ public void handleTimeout();
+
+}
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|