piper-net-devel Mailing List for Piper
Status: Pre-Alpha
Brought to you by:
rdodgen
You can subscribe to this list here.
2008 |
Jan
|
Feb
|
Mar
|
Apr
|
May
|
Jun
|
Jul
|
Aug
|
Sep
|
Oct
|
Nov
|
Dec
(1) |
---|---|---|---|---|---|---|---|---|---|---|---|---|
2009 |
Jan
(4) |
Feb
|
Mar
(2) |
Apr
(1) |
May
(1) |
Jun
|
Jul
|
Aug
|
Sep
|
Oct
|
Nov
|
Dec
|
From: <rd...@us...> - 2009-05-20 06:04:09
|
Revision: 16 http://piper-net.svn.sourceforge.net/piper-net/?rev=16&view=rev Author: rdodgen Date: 2009-05-20 06:03:44 +0000 (Wed, 20 May 2009) Log Message: ----------- StunClient is pretty much done and working. Has a main method which runs a quick test; should be moved to a unit test. sendMessage now has a variant which returns a Future. Instead of specifying a callback, one can now block on responses. Added StunBindingHelper, just a convenience class for getting an InetSocketAddress and not worrying about STUN messages. Modified Paths: -------------- trunk/jnatlib/src/net/jnatlib/stun/StunClient.java trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java Added Paths: ----------- trunk/jnatlib/src/net/jnatlib/stun/StunBindingHelper.java Added: trunk/jnatlib/src/net/jnatlib/stun/StunBindingHelper.java =================================================================== --- trunk/jnatlib/src/net/jnatlib/stun/StunBindingHelper.java (rev 0) +++ trunk/jnatlib/src/net/jnatlib/stun/StunBindingHelper.java 2009-05-20 06:03:44 UTC (rev 16) @@ -0,0 +1,102 @@ +package net.jnatlib.stun; + +import java.net.InetSocketAddress; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import net.jnatlib.stun.StunMessage.MessageType; + +public class StunBindingHelper { + private final StunClient c; + private InetSocketAddress defaultServer = null; + + public StunBindingHelper(StunClient client) { + if (client == null) + throw new IllegalArgumentException("Client can't be null"); + c = client; + } + + public void setDefaultServer(InetSocketAddress defaultServer) { + this.defaultServer = defaultServer; + } + + /** + * @return Address contained in m's XOR_MAPPED_ADDRESS or MAPPED_ADDRESS, default null + */ + public static InetSocketAddress getBindingFromMessage(StunMessage m) { + StunMessageAttribute ma = + m.getAttributes().get(StunMessageAttribute.AttributeType.MAPPED_ADDRESS); + + if (ma != null) + return ma.getAsEndpoint(); + + ma = m.getAttributes().get(StunMessageAttribute.AttributeType.XOR_MAPPED_ADDRESS); + + if (ma != null) + return ma.getAsEndpoint(); + + return null; + } + + public Future<InetSocketAddress> getPublicAddress(InetSocketAddress server) { + StunMessage m = new StunMessage(MessageType.BIND_REQUEST); + final Future<StunMessage> futureResponse = c.sendRequest(m, server); + + return new Future<InetSocketAddress>() { + public boolean cancel(boolean mayInterruptIfRunning) { + return futureResponse.cancel(mayInterruptIfRunning); + } + + public InetSocketAddress get() throws InterruptedException, + ExecutionException { + + StunMessage response = futureResponse.get(); + return getResultForMessage(response); + + } + + public InetSocketAddress get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + StunMessage response = futureResponse.get(timeout, unit); + return getResultForMessage(response); + } + + //Shared logic for get methods + private InetSocketAddress getResultForMessage(StunMessage response) + throws ExecutionException { + + InetSocketAddress responseAddr = + StunBindingHelper.getBindingFromMessage(response); + + if (responseAddr == null) + throw new ExecutionException( + new Exception("Message didn't contain a mapped address")); + + return responseAddr; + } + + public boolean isCancelled() { + return futureResponse.isCancelled(); + } + + public boolean isDone() { + return futureResponse.isCancelled(); + } + + }; + + } + + public Future<InetSocketAddress> getPublicAddress() { + //TODO: Better exception type + if (this.defaultServer == null) + throw new RuntimeException("No default server set"); + return getPublicAddress(defaultServer); + } + +} + + Modified: trunk/jnatlib/src/net/jnatlib/stun/StunClient.java =================================================================== --- trunk/jnatlib/src/net/jnatlib/stun/StunClient.java 2009-03-15 19:52:04 UTC (rev 15) +++ trunk/jnatlib/src/net/jnatlib/stun/StunClient.java 2009-05-20 06:03:44 UTC (rev 16) @@ -12,12 +12,16 @@ 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.CancellationException; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -71,6 +75,19 @@ logger.info("Started client on local port " + sock.getLocalPort()); } + public void stop() { + if (stopping) return; + stopping = true; + retryThread.interrupt(); + readThread.interrupt(); + try { + retryThread.join(); + readThread.join(); + } catch (InterruptedException e) { + } + + } + private void createRetryThread() { retryThread = new Thread(new Runnable() { public void run() { @@ -160,9 +177,24 @@ public void sendRequest(StunMessage req, SocketAddress dst, StunResponseHandler callback) { StunTransmitAttempt t = new StunTransmitAttempt(req, dst); t.setResponseHandler(callback); + transmitQueue.put(t); + } + + public Future<StunMessage> sendRequest(StunMessage req, SocketAddress dst) { + StunTransmitAttempt t = new StunTransmitAttempt(req, dst); + StunRequestFuture f = new StunRequestFuture(t); transmitQueue.put(t); + return f; } + /** + * Cancels the given request if possible. Fails silently. + */ + public void cancelRequest(StunMessage req) { + transmitQueue.remove(req); + } + + //TODO: Move this to a unit test public static void main(String[] args) throws InterruptedException { // Get server name, port in args String host = args[0]; @@ -207,8 +239,121 @@ System.out.println("Waiting on STUN response..."); waiter.wait(); + + System.out.println("Trying with StunBindingHelper..."); + + StunBindingHelper helper = new StunBindingHelper(c); + helper.setDefaultServer(new InetSocketAddress(host, port)); + Future<InetSocketAddress> helperFuture = helper.getPublicAddress(); + try { + InetSocketAddress helperResult = helperFuture.get(); + System.out.println("Helper got response: " + helperResult.toString()); + } catch (ExecutionException e) { + System.err.println("ExecutionException in helper!"); + e.printStackTrace(); + } + + } } + + //TODO: Add an exception that can hold a StunMessage indicating error + private class StunRequestFuture implements Future<StunMessage> { + private StunTransmitAttempt _attempt = null; + private StunMessage _error = null; + private StunMessage _response = null; + private boolean _timeout = false; + private boolean _cancelled = false; + + //Callbacks occur in a worker thread. + //A thread may be waiting on the result. + //This is used to implement waiting. + private final Object _locker = new Object(); + + + public StunRequestFuture(StunTransmitAttempt t) { + _attempt = t; + _attempt.setResponseHandler(new StunResponseHandler() { + + public void handleError(StunMessage errorResponse) { + synchronized (_locker) { + _error = errorResponse; + _locker.notifyAll(); + } + } + + public void handleResponse(StunMessage response) { + synchronized (_locker) { + _response = response; + _locker.notifyAll(); + } + } + + public void handleTimeout() { + synchronized (_locker) { + _timeout = true; + _locker.notifyAll(); + } + } + }); + } + + public boolean cancel(boolean mayInterruptIfRunning) { + synchronized (_locker) { + cancelRequest(_attempt.getMessage()); + } + return true; + } + + public StunMessage get() throws InterruptedException, ExecutionException { + //Keep trying due to spurious wakeups + while (true) { + try { + return get(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + } + } + } + + public StunMessage get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + + synchronized (_locker) { + if (isDone()) + return tryGetResult(); + _locker.wait(unit.toMillis(timeout)); + if (isDone()) + return tryGetResult(); + throw new TimeoutException(); + } + + } + + //Helper for get. Throws exception or returns response. + //based on current state. + private StunMessage tryGetResult() throws ExecutionException { + if (_cancelled) + throw new CancellationException(); + if (_timeout) + throw new ExecutionException( + new Exception("STUN request timeout")); + if (_error != null) + throw new ExecutionException( + new Exception("STUN error response: " + _error.toString())); + if (_response != null) + return _response; + return null; + } + + public boolean isCancelled() { + return _cancelled; + } + + public boolean isDone() { + return _cancelled || _timeout || (_error != null) || (_response != null); + } + } + } class StunTransmitAttempt implements Delayed { @@ -348,5 +493,4 @@ if (diff > 0) return 1; return -1; } - -} +} \ No newline at end of file Modified: trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java =================================================================== --- trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java 2009-03-15 19:52:04 UTC (rev 15) +++ trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java 2009-05-20 06:03:44 UTC (rev 16) @@ -185,22 +185,33 @@ return rawValue; } + /** + * See getAsEndpoint(boolean xor) + * This variant infers the xor parameter based on attribute type + */ + public InetSocketAddress getAsEndpoint() { + return getAsEndpoint( + this.attributeType == StunMessageAttribute.AttributeType.XOR_MAPPED_ADDRESS); + } + //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 + * (address + port) as seen in MAPPED-ADDRESS / 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() { + public InetSocketAddress getAsEndpoint(boolean xor) { + if (xor) + throw new RuntimeException("XOR not implemented"); + 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"); + "Length does not correspond to an encoded IPv4 or IPv6 address"); } try { @@ -236,7 +247,7 @@ } InetSocketAddress ret = new InetSocketAddress(addr, port); - logger.debug("Parsed a socket address attribute: " + ret); + logger.trace("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 @@ -252,6 +263,7 @@ public void setAsEndpoint(InetSocketAddress addr, boolean xor) { if (addr == null) throw new IllegalArgumentException("Addr can't be null"); //TODO: implement me! + throw new RuntimeException("XOR not implemented"); } /** This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: Randy D. <rd...@ma...> - 2009-04-11 02:46:01
|
Now with useful content sort of. http://piper-net.sourceforge.net/wikka/wikka.php?wakka=HomePage http://piper-net.sourceforge.net/wikka/wikka.php?wakka=Node http://piper-net.sourceforge.net/wikka/wikka.php?wakka=Plugins http://piper-net.sourceforge.net/wikka/wikka.php?wakka=Connection |
From: <rd...@us...> - 2009-03-15 19:52:23
|
Revision: 15 http://piper-net.svn.sourceforge.net/piper-net/?rev=15&view=rev Author: rdodgen Date: 2009-03-15 19:52:04 +0000 (Sun, 15 Mar 2009) Log Message: ----------- Fixed a padding bug for Stun, added versions and auto-inc build numbers to build files Modified Paths: -------------- trunk/build-common.xml trunk/jnatlib/build.xml trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java trunk/piper/build.xml trunk/upnplib/build.xml Modified: trunk/build-common.xml =================================================================== --- trunk/build-common.xml 2009-03-15 04:15:57 UTC (rev 14) +++ trunk/build-common.xml 2009-03-15 19:52:04 UTC (rev 15) @@ -2,6 +2,7 @@ <!-- The following must be set by files including this one <property name="jar.name" value="change-me.jar" /> + <property name="version.number" value="version.number.here" /> <fileset dir="${external.lib.dir}" id="libraries"> <include name="commons-jxpath-1.1.jar"/> <include name="commons-logging.jar"/> @@ -61,6 +62,8 @@ <resources refid="libraries_merged" /> </copy> + <buildnumber file="build.num"/> + <manifestclasspath property="jar.classpath" jarfile="${dist.dir}/${jar.name}"> <classpath> @@ -75,6 +78,7 @@ <include name="**/*.class"/> <manifest> <attribute name="Class-Path" value="${jar.classpath}"/> + <attribute name="Implementation-Version" value="${version.number}-b${build.number}"/> </manifest> </jar> </target> Modified: trunk/jnatlib/build.xml =================================================================== --- trunk/jnatlib/build.xml 2009-03-15 04:15:57 UTC (rev 14) +++ trunk/jnatlib/build.xml 2009-03-15 19:52:04 UTC (rev 15) @@ -1,6 +1,7 @@ <project name="jnatlib" default="jar"> <import file="../build-common.xml" /> <property name="jar.name" value="jnatlib.jar" /> + <property name="version.number" value="0.1" /> <fileset dir="${external.lib.dir}" id="libraries"> <include name="commons-logging.jar"/> </fileset> Modified: trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java =================================================================== --- trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java 2009-03-15 04:15:57 UTC (rev 14) +++ trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java 2009-03-15 19:52:04 UTC (rev 15) @@ -43,7 +43,8 @@ REFLECTED_FROM (0x000B), //This is now reserved REALM (0x0014), NONCE (0x0015), - XOR_MAPPED_ADDRESS (0x0020); + XOR_MAPPED_ADDRESS (0x0020), + SOFTWARE (0x8022); private final short value; @@ -156,7 +157,7 @@ } //Pad to multiple of 4 - int pad = rawValue.length % 4; + int pad = 4 - (rawValue.length % 4); for (int i = 0; i < pad; i++) dout.writeByte(0); Modified: trunk/piper/build.xml =================================================================== --- trunk/piper/build.xml 2009-03-15 04:15:57 UTC (rev 14) +++ trunk/piper/build.xml 2009-03-15 19:52:04 UTC (rev 15) @@ -1,6 +1,7 @@ <project name="piper" default="jar"> <import file="../build-common.xml" /> <property name="jar.name" value="piper.jar" /> + <property name="version.number" value="0.1" /> <fileset dir="${external.lib.dir}" id="libraries"> <include name="log4j-1.2.15.jar"/> </fileset> Modified: trunk/upnplib/build.xml =================================================================== --- trunk/upnplib/build.xml 2009-03-15 04:15:57 UTC (rev 14) +++ trunk/upnplib/build.xml 2009-03-15 19:52:04 UTC (rev 15) @@ -1,6 +1,7 @@ <project name="upnplib" default="jar"> <import file="../build-common.xml" /> <property name="jar.name" value="upnplib.jar" /> + <property name="version.number" value="0.1" /> <fileset dir="${external.lib.dir}" id="libraries"> <include name="commons-jxpath-1.1.jar"/> <include name="commons-logging.jar"/> This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
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. |
From: Randy D. <rd...@ma...> - 2009-01-12 00:35:35
|
Due to time ticking away too damn fast we'll need to focus just on jnatlib from now on. I'll try to finally define the public interfaces for it in the next few days. |
From: <trh...@us...> - 2009-01-10 06:19:29
|
Revision: 13 http://piper-net.svn.sourceforge.net/piper-net/?rev=13&view=rev Author: trhodes1517 Date: 2009-01-10 06:19:22 +0000 (Sat, 10 Jan 2009) Log Message: ----------- Forgot to include the Shutdown plugin in the last check in. Added Paths: ----------- trunk/piper/src/net/piper/plugins/Shutdown/ trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPlugin.java trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginPanel.java trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettings.java trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettingsPanel.java Added: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPlugin.java =================================================================== --- trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPlugin.java (rev 0) +++ trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPlugin.java 2009-01-10 06:19:22 UTC (rev 13) @@ -0,0 +1,26 @@ +package net.piper.plugins.Shutdown; + +import net.piper.plugins.Plugin; +import net.piper.plugins.PluginSettings; + + +public class ShutdownPlugin extends Plugin{ + + private ShutdownPluginSettingsPanel settingsPanel; + + public ShutdownPlugin() { + initVisualComponents(); + settingsPanel = new ShutdownPluginSettingsPanel(); + } + + public ShutdownPluginSettingsPanel getSettingsPanel() { + return settingsPanel; + } + public boolean performAction() { + + + + return false; + } + +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPlugin.java ___________________________________________________________________ Added: svn:executable + * Added: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginPanel.java =================================================================== --- trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginPanel.java (rev 0) +++ trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginPanel.java 2009-01-10 06:19:22 UTC (rev 13) @@ -0,0 +1,7 @@ +package net.piper.plugins.Shutdown; + +import net.piper.plugins.PluginPanel; + +public class ShutdownPluginPanel extends PluginPanel{ + +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginPanel.java ___________________________________________________________________ Added: svn:executable + * Added: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettings.java =================================================================== --- trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettings.java (rev 0) +++ trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettings.java 2009-01-10 06:19:22 UTC (rev 13) @@ -0,0 +1,22 @@ +package net.piper.plugins.Shutdown; + +import net.piper.plugins.PluginSettings; + +public class ShutdownPluginSettings extends PluginSettings{ + private String username; + private String password; + + public void setUsername(String s ){ + username = s; + } + public String getUsername() { + return username; + } + + public void setPassword(String s) { + password = s; + } + public String getPassword() { + return password; + } +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettings.java ___________________________________________________________________ Added: svn:executable + * Added: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettingsPanel.java =================================================================== --- trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettingsPanel.java (rev 0) +++ trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettingsPanel.java 2009-01-10 06:19:22 UTC (rev 13) @@ -0,0 +1,63 @@ +package net.piper.plugins.Shutdown; + +import net.piper.plugins.PluginSettingsPanel; +import net.piper.plugins.PluginSettings; + +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.JPasswordField; +import javax.swing.JCheckBox; + +public class ShutdownPluginSettingsPanel extends PluginSettingsPanel{ + + private ShutdownPluginSettings settings; + private JCheckBox restartBox; + private JTextField userText; + private JPasswordField passText; + + public ShutdownPluginSettingsPanel() { + PluginSettings tempSettings = this.getSettingsFromFile(); + if( tempSettings != null + && tempSettings instanceof ShutdownPluginSettings ) + settings = (ShutdownPluginSettings)tempSettings; + else + settings = new ShutdownPluginSettings(); + + init(); + + } + + public void applySettings() { + // settings.setRestart( restartBox.getValue() ); + settings.setUsername( userText.getText() ); + settings.setPassword( passText.getText() ); + + } + public void saveSettings() { + applySettings(); + writeSettings(); + } + + public ShutdownPluginSettings getSettings() { + return settings; + } + + public void init() { + restartBox = new JCheckBox("Restart? ", true); + this.add( restartBox ); + + JLabel username = new JLabel("Username: " ); + userText = new JTextField(); + userText.setColumns(8); + this.add(username); + this.add(userText); + + JLabel password = new JLabel("Password: " ); + passText = new JPasswordField(); + passText.setColumns(8); + this.add(password); + this.add(passText); + } + +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/Shutdown/ShutdownPluginSettingsPanel.java ___________________________________________________________________ Added: svn:executable + * This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <trh...@us...> - 2009-01-10 06:07:41
|
Revision: 12 http://piper-net.svn.sourceforge.net/piper-net/?rev=12&view=rev Author: trhodes1517 Date: 2009-01-10 06:07:30 +0000 (Sat, 10 Jan 2009) Log Message: ----------- Changed the structure of plugins and added an example plugin. Note that this is only the rough beginnings of the base plugin structure. Added Paths: ----------- trunk/piper/src/net/piper/plugins/Plugin.java trunk/piper/src/net/piper/plugins/PluginPanel.java trunk/piper/src/net/piper/plugins/PluginSettings.java trunk/piper/src/net/piper/plugins/PluginSettingsPanel.java trunk/piper/test/TestPluginStuff.java Removed Paths: ------------- trunk/piper/src/net/piper/plugins/MyFirstPlugin.java trunk/piper/src/net/piper/plugins/Plugin.java trunk/piper/src/net/piper/plugins/PluginSettings.java trunk/piper/src/net/piper/plugins/PluginSettingsPersistenceDelegate.java trunk/piper/test/TestXMLSettings.java Deleted: trunk/piper/src/net/piper/plugins/MyFirstPlugin.java =================================================================== --- trunk/piper/src/net/piper/plugins/MyFirstPlugin.java 2009-01-04 01:27:17 UTC (rev 11) +++ trunk/piper/src/net/piper/plugins/MyFirstPlugin.java 2009-01-10 06:07:30 UTC (rev 12) @@ -1,16 +0,0 @@ -package net.piper.plugins; - -public class MyFirstPlugin extends Plugin { - - public boolean performAction() { - PluginSettings settings = getSettings(); - - settings.addSetting( "key", "abc" ); - - return true; - } - - public String getName() { - return "MyFirstPlugin"; - } -} \ No newline at end of file Deleted: trunk/piper/src/net/piper/plugins/Plugin.java =================================================================== --- trunk/piper/src/net/piper/plugins/Plugin.java 2009-01-04 01:27:17 UTC (rev 11) +++ trunk/piper/src/net/piper/plugins/Plugin.java 2009-01-10 06:07:30 UTC (rev 12) @@ -1,120 +0,0 @@ -package net.piper.plugins; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileInputStream; -import java.io.FileOutputStream; - -import java.beans.XMLDecoder; -import java.beans.XMLEncoder; - -import java.util.Map; -public abstract class Plugin { - - public static final String PLUGIN_DIR = "plugins"; - - private PluginSettings settings; - private File xmlFile; - - /** - * Main way to execute the action of a plugin. - **/ - public abstract boolean performAction(); - - /** - * This is used to determine where to save the file. - * Also it is helpful when creating the GUI representing - * the various plugins. - **/ - public abstract String getName(); - - // public abstract void initializeSettingsDialog(); - - /** - * This method will be called after the 'settings' dialog for the - * current plugin is closed. - **/ - public void modifySettings( Map<String, String> newSettings ) { - if( settings == null ) return; - - for( String key : newSettings.keySet() ) - settings.addSetting( key, newSettings.get(key) ); - } - - /** - * Completely erases all settings from memory and disk. - **/ - public boolean eraseSettings() { - //returns true, because no settings have been added - if( xmlFile == null ) return true; - - boolean b = xmlFile.delete(); - settings = null; - - return b; - } - - /** - * Gets all settings. If the internal PluginSettings has not been set, - * getSettings looks for a settings file to read from. - **/ - public PluginSettings getSettings() { - - if( settings == null ) { - xmlFile = new File(PLUGIN_DIR + File.separator - + this.getName()+"Settings" + ".xml"); - - //makes sure that the parent path is valid. - xmlFile.getParentFile().mkdirs(); - - if( xmlFile.exists() ) { - try{ - FileInputStream in = new FileInputStream( xmlFile ); - XMLDecoder decode = new XMLDecoder( in ); - settings = (PluginSettings)decode.readObject(); - } catch( FileNotFoundException e ) { - System.err.println( "Cannot Find file: "+xmlFile.toString() - +"\nThis should never happen" ); - } - } else { - settings = new PluginSettings(); - } - } - - return settings; - } - - /** - * Writes all of the settings in the contained pluginSettings class - **/ - public void writeSettings() { - - if( settings == null || xmlFile == null ) - return; - - try{ - FileOutputStream out = new FileOutputStream( xmlFile ); - XMLEncoder encode = new XMLEncoder( out ); - - //Uses the custom made PersistenceDelegator - encode.setPersistenceDelegate( PluginSettings.class, - new PluginSettingsPersistenceDelegate() ); - - encode.writeObject( settings ); - encode.close(); - } catch( FileNotFoundException e ) { - System.err.println("Cannot write to file: "+xmlFile.toString() ); - } - } - - // public void displaySettingsDialog() {} - - /** - * Should be called when the plugin is no longer used. This saves the - * plugins current settings. - **/ - public void close() { - writeSettings(); - } - -} \ No newline at end of file Added: trunk/piper/src/net/piper/plugins/Plugin.java =================================================================== --- trunk/piper/src/net/piper/plugins/Plugin.java (rev 0) +++ trunk/piper/src/net/piper/plugins/Plugin.java 2009-01-10 06:07:30 UTC (rev 12) @@ -0,0 +1,91 @@ +package net.piper.plugins; + +import javax.swing.JPanel; +import javax.swing.JDialog; +import javax.swing.JButton; + +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.Dimension; + +public abstract class Plugin extends JPanel{ + + private final String DEFAULT_NAME = this.getClass().getSimpleName(); + + /** + * Need to do send/recieveAction. Check metadata. + **/ + + /** + * Main way to execute the action of a plugin. + **/ + public abstract boolean performAction(); + + public abstract PluginSettingsPanel getSettingsPanel(); + + public void initVisualComponents() { + + JButton go = new JButton("Go: "+DEFAULT_NAME) ; + go.addActionListener( new ActionListener() { + public void actionPerformed( ActionEvent e ) { + String message = "Action succesful!"; + if( !performAction() ) { + message = "Action Unsuccesful. Printing to log"; + + } + + } + }); + this.add( go ); + + JButton settings = new JButton("Settings") ; + settings.addActionListener( new ActionListener() { + public void actionPerformed( ActionEvent e ) { + JDialog settingsDialog = new JDialog( Frame.getFrames()[0], DEFAULT_NAME + " Settings", true ); + + JPanel myPanel = new JPanel(); + myPanel.setPreferredSize( new Dimension( 600, 400 ) ); + + settingsDialog.setContentPane(myPanel); + + settingsDialog.getContentPane().add( getSettingsPanel() ); + + JButton apply = new JButton("Apply Settings"); + apply.addActionListener( new ActionListener() { + public void actionPerformed( ActionEvent e ) { + getSettingsPanel().applySettings(); + // settingsDialog.dispose(); + } + }); + + settingsDialog.getContentPane().add( apply ); + + JButton save = new JButton("Save Settings"); + save.addActionListener( new ActionListener() { + public void actionPerformed( ActionEvent e ) { + getSettingsPanel().saveSettings(); + // settingsDialog.dispose(); + } + }); + + settingsDialog.getContentPane().add( save ); + + settingsDialog.setResizable( false ); + settingsDialog.pack(); + + settingsDialog.setVisible( true ); + } + }); + this.add( settings ); + } + + /** + * Should be called when the plugin is no longer used. This saves the + * plugins current settings. + **/ + public void close() { + getSettingsPanel().writeSettings(); + } + +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/Plugin.java ___________________________________________________________________ Added: svn:executable + * Added: trunk/piper/src/net/piper/plugins/PluginPanel.java =================================================================== --- trunk/piper/src/net/piper/plugins/PluginPanel.java (rev 0) +++ trunk/piper/src/net/piper/plugins/PluginPanel.java 2009-01-10 06:07:30 UTC (rev 12) @@ -0,0 +1,16 @@ +package net.piper.plugins; + +import javax.swing.JPanel; +import javax.swing.JButton; + + +public class PluginPanel extends JPanel{ + + public PluginPanel() { + initComponents(); + } + + public void initComponents() { + + } +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/PluginPanel.java ___________________________________________________________________ Added: svn:executable + * Deleted: trunk/piper/src/net/piper/plugins/PluginSettings.java =================================================================== --- trunk/piper/src/net/piper/plugins/PluginSettings.java 2009-01-04 01:27:17 UTC (rev 11) +++ trunk/piper/src/net/piper/plugins/PluginSettings.java 2009-01-10 06:07:30 UTC (rev 12) @@ -1,30 +0,0 @@ -package net.piper.plugins; - -import java.util.TreeMap; -import java.util.Collection; - -public class PluginSettings { - - private TreeMap<String, String> settings; - - public PluginSettings() { - settings = new TreeMap<String, String> (); - } - - public void addSetting( String key, String value ) { - settings.put( key, value ); - } - - public String removeSetting( String key ) { - return settings.remove( key ); - } - - public Collection<String> getAllKeys() { - return settings.keySet(); - } - - public String getSetting( String key ) { - return settings.get( key ); - } - -} \ No newline at end of file Added: trunk/piper/src/net/piper/plugins/PluginSettings.java =================================================================== --- trunk/piper/src/net/piper/plugins/PluginSettings.java (rev 0) +++ trunk/piper/src/net/piper/plugins/PluginSettings.java 2009-01-10 06:07:30 UTC (rev 12) @@ -0,0 +1,5 @@ +package net.piper.plugins; + +public abstract class PluginSettings { + +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/PluginSettings.java ___________________________________________________________________ Added: svn:executable + * Added: trunk/piper/src/net/piper/plugins/PluginSettingsPanel.java =================================================================== --- trunk/piper/src/net/piper/plugins/PluginSettingsPanel.java (rev 0) +++ trunk/piper/src/net/piper/plugins/PluginSettingsPanel.java 2009-01-10 06:07:30 UTC (rev 12) @@ -0,0 +1,83 @@ +package net.piper.plugins; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileInputStream; +import java.io.FileOutputStream; + +import java.beans.XMLDecoder; +import java.beans.XMLEncoder; + +import javax.swing.JPanel; + +public abstract class PluginSettingsPanel extends JPanel{ + public abstract void saveSettings(); + public abstract void applySettings(); + + public abstract PluginSettings getSettings(); + + private final String DEFAULT_NAME = this.getClass().getSimpleName(); + private File xmlFile; + + public static final String PLUGIN_DIR = "plugins"; + + + /** + * Gets all settings. If the internal PluginSettings has not been set, + * getSettings looks for a settings file to read from. + **/ + public PluginSettings getSettingsFromFile() { + + if( xmlFile == null ) + setXMLFile( DEFAULT_NAME ); + + PluginSettings settings = null; + + if( xmlFile.exists() ) { + try{ + FileInputStream in = new FileInputStream( xmlFile ); + XMLDecoder decode = new XMLDecoder( in ); + settings = (PluginSettings)decode.readObject(); + } catch( FileNotFoundException e ) { + System.err.println( "Cannot Find file: "+xmlFile.toString() + +"\nThis should never happen" ); + } + } + + return settings; + } + + /** + * Writes all of the settings in the contained pluginSettings class + **/ + public void writeSettings() { + + if( xmlFile == null ) + setXMLFile( DEFAULT_NAME ); + + + if( xmlFile.exists() ) + xmlFile.delete(); + else + xmlFile.getParentFile().mkdirs(); + + PluginSettings mySettings = getSettings(); + if( mySettings == null ) + return; + + try{ + FileOutputStream out = new FileOutputStream( xmlFile ); + XMLEncoder encode = new XMLEncoder( out ); + + encode.writeObject( mySettings ); + encode.close(); + } catch( FileNotFoundException e ) { + System.err.println("Cannot write to file: "+xmlFile.toString() ); + } + } + private void setXMLFile(String name) { + xmlFile = new File(PLUGIN_DIR + File.separator + + name + ".xml"); + } + +} \ No newline at end of file Property changes on: trunk/piper/src/net/piper/plugins/PluginSettingsPanel.java ___________________________________________________________________ Added: svn:executable + * Deleted: trunk/piper/src/net/piper/plugins/PluginSettingsPersistenceDelegate.java =================================================================== --- trunk/piper/src/net/piper/plugins/PluginSettingsPersistenceDelegate.java 2009-01-04 01:27:17 UTC (rev 11) +++ trunk/piper/src/net/piper/plugins/PluginSettingsPersistenceDelegate.java 2009-01-10 06:07:30 UTC (rev 12) @@ -1,29 +0,0 @@ -package net.piper.plugins; - -import java.beans.DefaultPersistenceDelegate; -import java.beans.Statement; -import java.beans.Encoder; - -import java.util.Map; -import java.util.TreeMap; - -public class PluginSettingsPersistenceDelegate - extends DefaultPersistenceDelegate { - - protected void initialize(Class type, Object oldInstance, - Object newInstance, Encoder out) { - // Note, the "size" property will be set here. - super.initialize(type, oldInstance, newInstance, out); - - PluginSettings m = - (PluginSettings)oldInstance; - for ( String s : m.getAllKeys() ) { - out.writeStatement( - new Statement(oldInstance, - "addSetting", // Could also use "addElement" here. - new String[]{ s, m.getSetting(s)}) ); - } - } - - -} \ No newline at end of file Added: trunk/piper/test/TestPluginStuff.java =================================================================== --- trunk/piper/test/TestPluginStuff.java (rev 0) +++ trunk/piper/test/TestPluginStuff.java 2009-01-10 06:07:30 UTC (rev 12) @@ -0,0 +1,23 @@ +package test; + +import javax.swing.JFrame; + +import net.piper.plugins.*; +import net.piper.plugins.Shutdown.*; + +public class TestPluginStuff extends JFrame{ + + public static void main(String[] args ) { + TestPluginStuff test = new TestPluginStuff(); + } + + public TestPluginStuff () { + super("MyPluginTest"); + ShutdownPlugin sp = new ShutdownPlugin(); + + this.add(sp); + this.pack(); + this.setVisible(true); + } + +} \ No newline at end of file Deleted: trunk/piper/test/TestXMLSettings.java =================================================================== --- trunk/piper/test/TestXMLSettings.java 2009-01-04 01:27:17 UTC (rev 11) +++ trunk/piper/test/TestXMLSettings.java 2009-01-10 06:07:30 UTC (rev 12) @@ -1,75 +0,0 @@ -package net.piper.plugins.test; - -import junit.framework.*; - -import net.piper.plugins.*; - -import java.util.TreeMap; -import java.util.Map; - -import java.beans.XMLEncoder; -import java.beans.XMLDecoder; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileInputStream; - -public class TestXMLSettings extends TestCase { - - public void testSimplePlugin() { - MyFirstPlugin test = new MyFirstPlugin(); - - test.performAction(); - - test.close(); - } - - public void testReadAndWrite( ) throws Exception { - PluginSettings test = new PluginSettings(); - PluginSettings readTest; - TreeMap<String, String> data = generateSettings(50); - for( String s : data.keySet() ) - test.addSetting( s, data.get( s ) ); - - File f = new File( "testSettings.xml" ); - FileOutputStream out = new FileOutputStream( f ); - - XMLEncoder xmlEncode = new XMLEncoder( out ); - xmlEncode.setPersistenceDelegate(PluginSettings.class, - new PluginSettingsPersistenceDelegate() ); - - xmlEncode.writeObject( test ); - xmlEncode.close(); - out.close(); - - FileInputStream in = new FileInputStream( f ); - - XMLDecoder xmlDecode = new XMLDecoder( in ); - readTest = (PluginSettings)xmlDecode.readObject(); - - for( String s : data.keySet() ) - assertEquals( data.get( s ), readTest.getSetting( s ) ); - } - public static TreeMap<String, String> generateSettings(int count) { - - TreeMap<String, String> out = new TreeMap<String, String>(); - for( int i = 0; i < count; i++ ) { - String key = randomString(); - String value = randomString(); - out.put( key, value ); - } - - return out; - } - public static String randomString() { - - int len = (int) (Math.random()*20)+1; - String out = ""; - - for( int i = 0; i < len; i ++ ) - out += (char) ((int) (Math.random()*26)+ (int)'A'); - - return out; - } - -} This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <ben...@us...> - 2009-01-04 01:27:22
|
Revision: 11 http://piper-net.svn.sourceforge.net/piper-net/?rev=11&view=rev Author: benhiller Date: 2009-01-04 01:27:17 +0000 (Sun, 04 Jan 2009) Log Message: ----------- Initial check-in of STUN library Added Paths: ----------- trunk/jnatlib/src/net/ trunk/jnatlib/src/net/jnatlib/ trunk/jnatlib/src/net/jnatlib/stun/ 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: trunk/jnatlib/src/net/jnatlib/stun/StunClient.java =================================================================== --- trunk/jnatlib/src/net/jnatlib/stun/StunClient.java (rev 0) +++ trunk/jnatlib/src/net/jnatlib/stun/StunClient.java 2009-01-04 01:27:17 UTC (rev 11) @@ -0,0 +1,107 @@ +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. + +// This program 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 General Public License for more details. + +package net.jnatlib.stun; + +import java.io.*; +import java.net.*; +import java.util.Enumeration; +import java.util.Hashtable; + +import net.jnatlib.stun.StunMessage.MessageType; +import net.jnatlib.stun.StunMessageAttribute.AttributeType; + + +public class StunClient { + + public static void main(String[] args) { + // 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)); + } + } + } + } + + 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; + } +} Added: trunk/jnatlib/src/net/jnatlib/stun/StunMessage.java =================================================================== --- trunk/jnatlib/src/net/jnatlib/stun/StunMessage.java (rev 0) +++ trunk/jnatlib/src/net/jnatlib/stun/StunMessage.java 2009-01-04 01:27:17 UTC (rev 11) @@ -0,0 +1,246 @@ +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program 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 General Public License for more details. + +package net.jnatlib.stun; + +import java.util.Random; +import java.util.Hashtable; + +import net.jnatlib.stun.StunMessageAttribute.AttributeType; + + +public class StunMessage { + 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 + + private final int hexRepresentation; + + MessageType(int hex) { + this.hexRepresentation = hex; + } + + public int getHexRepresentation() { + return hexRepresentation; + } + } + + private static final int MAGIC_COOKIE = 0x2112A442; + private static final int TRANSACTION_ID_LENGTH = 12; + private static final int HEADER_LENGTH = 20; + + private MessageType messageType; + private int length; + private byte[] transactionID; + private Hashtable<AttributeType, StunMessageAttribute> messageAttributes; + private byte[] originalAttributes; + + 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>(); + } + + public StunMessage(MessageType messageType, byte[] transactionID) { + this(messageType, transactionID, 0); + } + + public StunMessage(MessageType messageType) { + this(messageType, StunMessage.createTransactionID()); + } + + /** + * This creates a randomly generated transaction ID + * + * @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(); + } + + 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; + } + + public Hashtable<AttributeType, StunMessageAttribute> getAttributes() { + return 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); + } + + 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); + } + + 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 void setOriginalAttributes(byte[] originalAttributes) { + this.originalAttributes = new byte[originalAttributes.length]; + System.arraycopy(originalAttributes, 0, this.originalAttributes, 0, originalAttributes.length); + } + + 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; + } +} Added: trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java =================================================================== --- trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java (rev 0) +++ trunk/jnatlib/src/net/jnatlib/stun/StunMessageAttribute.java 2009-01-04 01:27:17 UTC (rev 11) @@ -0,0 +1,61 @@ +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program 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 General Public License for more details. + +package net.jnatlib.stun; + +import java.util.Hashtable; + +public class StunMessageAttribute { + public enum AttributeType { + MAPPED_ADDRESS (0x0001), + RESPONSE_ADDRESS (0x0002), //This is now reserved + CHANGE_REQUEST (0x0003), //This is now reserved + SOURCE_ADDRESS (0x0004), //This is now reserved + CHANGED_ADDRESS(0x0005), //This is now reserved + USERNAME (0x0006), + PASSWORD (0x0007), //This is now reserved + MESSAGE_INTEGRITY (0x0008), + ERROR_CODE (0x0009), + UNKNOWN_ATTRIBUTES (0x000A), + REFLECTED_FROM (0x000B), //This is now reserved + REALM (0x0014), + NONCE (0x0015), + XOR_MAPPED_ADDRESS (0x0020); + + private final int hexRepresentation; + + AttributeType(int hex) { + this.hexRepresentation = hex; + } + + public int getHexRepresentation() { return hexRepresentation; } + } + + AttributeType attributeType; + int length; + Hashtable<String, Object> values; + + public AttributeType getAttributeType() { + return attributeType; + } + + public Hashtable<String, Object> getValues() { + return values; + } + + public void addValue(String key, Object value) { + values.put(key, value); + } + + StunMessageAttribute(AttributeType type, int length) { + attributeType = type; + this.length = length; + values = new Hashtable<String, Object>(); + } +} This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: Randy D. <rd...@ma...> - 2008-12-27 01:24:01
|
Stun: Woo! That much closer to the "fun part." I'm going to experiment with setting up two VMs in VirtualBox or similar with NAT used for networking - it's kind of a funky setup in that the two VMs have the same public IP but have to holepunch to connect to each other, but as long as the NAT implementation behaves, its an easier way to test than setting up a spare router. We might also consider setting up a way to use our personal machines for testing. I think all three of us are probably behind NAT devices. We can create a shell account on at least one machine and do the mediation stuff over ssh. Not sure how update versions would be pushed out though. Upnp: I haven't found a way to specify "use source address" when adding a port mapping. I think I'm going to write a small patch. The discovery process enumerates all available interfaces and sends a broadcast on each. It should just be a matter of recording for each discovered device which interface the broadcast was sent on. That should be solid even on machines with multiple interfaces with valid IPs (like my desktop). Wiki: I haven't been very productive lately in part to being sick, but in addition to the upnp patch I really need to get started on filling up the wiki ( http://apps.sourceforge.net/mediawiki/piper-net/index.php?title=Main_Page ) Right now we're pretty much doing foundation work, but the entire connection process etc. will be documented in the near future. Should make contributions much easier and less guess-y (unlike Andrew's pdfs) |