[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. |