/* * Copyrigth (C) 2010 Henrik Baastrup. * * Licensed under the GNU Lesser General Public License version 3; * you may not use this file except in compliance with the License. * You should have received a copy of the license together with this * file but can obtain a copy of the License at: * * http://www.gnu.org/licenses/lgpl-3.0.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package javax.net.stun.services; import java.io.IOException; import java.lang.Thread.UncaughtExceptionHandler; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.stun.MessageAttribute; import javax.net.stun.MessageHeader; import javax.net.stun.Utils; /** * * @author Henrik Baastrup */ public class BindingService implements Runnable,UncaughtExceptionHandler { private DatagramSocket receiveSocket; private boolean running = false; private Thread thread = null; private SharedSecretService sharedSecretService = null; private InetAddress sharedSecretServiceAddress = null; private int sharedSecretServicePort = 3478; private InetAddress localAddress = null; private int localPort = 3478; private InetAddress publicAddress = null; private InetAddress alternateAddress = null; private int alternatePort = 0; private boolean debug = false; public BindingService(final InetAddress localIpAddress, final int localPort, final InetAddress alternateIpAddress, final int alternatePort) { localAddress = localIpAddress; publicAddress = localAddress; if (localPort!=0) this.localPort = localPort; alternateAddress = alternateIpAddress; this.alternatePort = alternatePort; } public BindingService(final InetAddress localIpAddress, final int localPort, final InetAddress alternateIpAddress, final int alternatePort, final SharedSecretService sharedSecretService) { this(localIpAddress, localPort, alternateIpAddress, alternatePort); this.sharedSecretService = sharedSecretService; } public BindingService(final InetAddress localIpAddress, final int localPort, final InetAddress alternateIpAddress, final int alternatePort, final InetAddress sharedSecretServiceAddress, final int sharedSecretServicePort) { this(localIpAddress, localPort, alternateIpAddress, alternatePort); this.sharedSecretServiceAddress = sharedSecretServiceAddress; this.sharedSecretServicePort = sharedSecretServicePort; } public boolean isRunning() {return running;} public void startThread() { if (running) return; thread = new Thread(this, "Binding Service Thread"); thread.setUncaughtExceptionHandler(this); try { receiveSocket = new DatagramSocket(localPort, localAddress); receiveSocket.setSoTimeout(1000); } catch (SocketException ex) { Logger.getLogger(BindingService.class.getName()).log(Level.SEVERE, null, ex); return; } thread.start(); } public void stopThread() { running = false; thread.interrupt(); synchronized (receiveSocket) { receiveSocket.notifyAll(); } } public void run() { if (debug) { Logger.getLogger(BindingService.class.getName()).log(Level.INFO, "Service thread start with following parameters:"); Logger.getLogger(BindingService.class.getName()).log(Level.INFO, " Servic address: "+localAddress+":"+localPort); Logger.getLogger(BindingService.class.getName()).log(Level.INFO, " Alternative servic address: "+alternateAddress+":"+alternatePort); if (sharedSecretService!=null) { Logger.getLogger(BindingService.class.getName()).log(Level.INFO, " Using Shared Secret by argument"); } if (sharedSecretServiceAddress!=null) { Logger.getLogger(BindingService.class.getName()).log(Level.INFO, " Using Shred Secret with address: "+sharedSecretServiceAddress); } } byte[] buf = new byte[0xffff+20]; DatagramPacket recDatagramPacket = new DatagramPacket(buf, buf.length); running = true; while (running) { try { try { receiveSocket.receive(recDatagramPacket); } catch (SocketTimeoutException ignore) { continue; } catch (IOException ex) { Logger.getLogger(BindingService.class.getName()).log(Level.SEVERE, null, ex); break; } if (!running) break; response(receiveSocket, recDatagramPacket); } catch (RuntimeException ex) { Logger.getLogger(BindingService.class.getName()).log(Level.SEVERE, null, ex); } } receiveSocket.close(); running = false; if (debug) { Logger.getLogger(BindingService.class.getName()).log(Level.INFO, "Service thread stopped"); } } private void response(DatagramSocket socket, DatagramPacket receivedDatagramPacket) { InetAddress clientAddr = receivedDatagramPacket.getAddress(); int clientPort = receivedDatagramPacket.getPort(); if (debug) { Logger.getLogger(BindingService.class.getName()).log(Level.INFO, "Recived request from: "+clientAddr+":"+clientPort); } DatagramSocket alternativePortSocket = null; try { MessageHeader receivedHeader = MessageHeader.create(receivedDatagramPacket.getData()); MessageAttribute changeRequest = receivedHeader.getMessageAttribute(MessageAttribute.MessageAttributeType.CHANGE_REQUEST); if (changeRequest==null) return; //If not a Change Request I will not response! //Create return header. MessageHeader returnHeader = createResponse(receivedHeader, clientAddr, clientPort); if (returnHeader==null) return; //We will not response the alternative server should do that! if (returnHeader.getChangeAddress()) { //TODO: We need to pass the response to an other server on an other address // alternateAddress and alternatePort } byte buffer[] = returnHeader.toBytes(); //Does the client want the response on a different address or port? InetAddress returnAddr = clientAddr; int returnPort = clientPort; MessageAttribute responseAddress = receivedHeader.getMessageAttribute(MessageAttribute.MessageAttributeType.RESPONSE_ADDRESS); if (responseAddress!=null) { returnAddr = responseAddress.getAddress(); returnPort = responseAddress.getPort(); } DatagramPacket out = new DatagramPacket(buffer, buffer.length, returnAddr, returnPort); if (returnHeader.getChangePort()) { //Create a new socket to change origin port alternativePortSocket = new DatagramSocket(); alternativePortSocket.send(out); } else { //Use the same port we recived on socket.send(out); } } catch (IOException ex) { Logger.getLogger(BindingService.class.getName()).log(Level.SEVERE, null, ex); } finally { if (alternativePortSocket!=null) alternativePortSocket.close(); } } private MessageHeader createResponse(MessageHeader receivedHeader, InetAddress clientAddr, int clientPort) { MessageHeader returnHeader = new MessageHeader(); byte password[] = controllMessageIntegrity(receivedHeader, returnHeader); if (returnHeader.getType()!=MessageHeader.HeaderType.NOT_KNOWN) return returnHeader; //Failed Message Integrity check adn contain an error response! returnHeader = new MessageHeader(MessageHeader.HeaderType.BINDING_RESPONSE); returnHeader.setTransactionId(receivedHeader.getTransactionId()); //Mapped Address attribute MessageAttribute attr = MessageAttribute.create(MessageAttribute.MessageAttributeType.MAPPED_ADDRESS, clientAddr, clientPort); returnHeader.addMessageAttribute(attr); if (receivedHeader.getMessageAttribute(MessageAttribute.MessageAttributeType.RESPONSE_ADDRESS)!=null) { //Reflected from attribute attr = MessageAttribute.create(MessageAttribute.MessageAttributeType.REFLECTED_FROM, clientAddr, clientPort); returnHeader.addMessageAttribute(attr); } //Change Address attribute if (alternateAddress!=null) { attr = MessageAttribute.create(MessageAttribute.MessageAttributeType.CHANGED_ADDRESS, alternateAddress, alternatePort); } else { attr = MessageAttribute.create(MessageAttribute.MessageAttributeType.CHANGED_ADDRESS, publicAddress, localPort); } returnHeader.addMessageAttribute(attr); //Source Address attribute MessageAttribute changeRequest = receivedHeader.getMessageAttribute(MessageAttribute.MessageAttributeType.CHANGE_REQUEST); InetAddress respAddr; int respPort; if (changeRequest!=null) { if (changeRequest.isAddressChanged()) { if (alternateAddress==null) respAddr = publicAddress; else respAddr = alternateAddress; returnHeader.setChangeAddress(true); } else respAddr = publicAddress; if (changeRequest.isPortChanged()) { if (alternatePort==0) respPort = localPort; else respPort = alternatePort; returnHeader.setChangePort(true); } else respPort = localPort; } else { respAddr = publicAddress; respPort = localPort; } attr = MessageAttribute.create(MessageAttribute.MessageAttributeType.SOURCE_ADDRESS, respAddr, respPort); returnHeader.addMessageAttribute(attr); if (password!=null) { try { MessageAttribute messageIntegrityAttr = MessageAttribute.create(MessageAttribute.MessageAttributeType.MESSAGE_INTEGRITY, password, returnHeader.toBytes()); returnHeader.addMessageAttribute(messageIntegrityAttr); } catch (IOException ex) { Logger.getLogger(BindingService.class.getName()).log(Level.SEVERE, null, ex); MessageAttribute errorCode = MessageAttribute.create(MessageAttribute.MessageAttributeType.ERROR_CODE, Utils.createErrorString(500), 500); returnHeader = new MessageHeader(MessageHeader.HeaderType.BINDING_ERROR_RESPONSE); returnHeader.addMessageAttribute(errorCode); returnHeader.setTransactionId(receivedHeader.getTransactionId()); } } return returnHeader; } /** * Will set the address use in a Change Request. The default value is the local address * set in the creator. This is usefull if the server is behind a NAT. * @param arg0 */ public void setPublicAddress(InetAddress arg0) {publicAddress = arg0;} public void setDebug(boolean arg0) {debug = arg0;} private byte[] controllMessageIntegrity(MessageHeader receivedHeader, MessageHeader returnHeader) { byte password[] = null; if (sharedSecretService!=null) { int errorInt = sharedSecretService.controllMessageIntegrity(receivedHeader); if (errorInt!=0) { returnHeader.setType(MessageHeader.HeaderType.BINDING_ERROR_RESPONSE); MessageAttribute errorCode = MessageAttribute.create(MessageAttribute.MessageAttributeType.ERROR_CODE, Utils.createErrorString(errorInt), errorInt); returnHeader.addMessageAttribute(errorCode); returnHeader.setTransactionId(receivedHeader.getTransactionId()); return null; } password = sharedSecretService.getPassword(receivedHeader); } else if (sharedSecretServiceAddress!=null) { SSLSocket sslSocket = null; try { SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); sslSocket = (SSLSocket)sslFactory.createSocket(sharedSecretServiceAddress, sharedSecretServicePort); sslSocket.startHandshake(); MessageHeader head = new MessageHeader(receivedHeader); head.setType(MessageHeader.HeaderType.SHARED_SECRET_VERIFY_REQUEST); head = Utils.socketSendRecive(sslSocket, head); MessageAttribute errorCode = head.getMessageAttribute(MessageAttribute.MessageAttributeType.ERROR_CODE); MessageAttribute passwordAttr = head.getMessageAttribute(MessageAttribute.MessageAttributeType.PASSWORD); if (passwordAttr==null && errorCode==null) { errorCode = MessageAttribute.create(MessageAttribute.MessageAttributeType.ERROR_CODE, Utils.createErrorString(600), 600); } if (errorCode!=null) { returnHeader.setType(MessageHeader.HeaderType.BINDING_ERROR_RESPONSE); returnHeader.addMessageAttribute(errorCode); returnHeader.setTransactionId(receivedHeader.getTransactionId()); return null; } password = passwordAttr.getPassword(); } catch (IOException ex) { Logger.getLogger(BindingService.class.getName()).log(Level.SEVERE, null, ex); } finally { if (sslSocket!=null) try{sslSocket.close();}catch(IOException ignore){} } } return password; } public void uncaughtException(Thread t, Throwable e) { System.err.println("Uncaught exception in thread: "+t.getName()+". The thread will die"); Logger.getLogger(BindingService.class.getName()).log(Level.SEVERE, null, e); } }