/*
* TurnServer, the OpenSource Java Solution for TURN protocol. Maintained by the
* Jitsi community (http://jitsi.org).
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 org.jitsi.turnserver.stack;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.logging.*;
import org.ice4j.*;
import org.ice4j.attribute.*;
import org.ice4j.ice.*;
import org.ice4j.message.*;
import org.ice4j.security.*;
import org.ice4j.socket.*;
import org.ice4j.stack.*;
import org.jitsi.turnserver.*;
import org.jitsi.turnserver.listeners.*;
import org.jitsi.turnserver.socket.*;
/**
* The entry point to the TurnServer stack. The class is used to start, stop and
* configure the stack.
*
* @author Aakash Garg
*/
public class TurnStack
extends StunStack
{
/**
* The <tt>Logger</tt> used by the <tt>turnStack</tt> class and its
* instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(TurnStack.class.getName());
/**
* The maximum no of Allocations per TurnStack.
*/
public static final int MAX_ALLOCATIONS = 500;
/**
* To track the portNo used.
*/
// private static int nextPortNo = 49152;
private static int nextPortNo = 8080;
/**
* Represents the Allocations stored for Server Side.
*/
private final HashMap<FiveTuple,Allocation> serverAllocations
= new HashMap<FiveTuple,Allocation>();
/**
* Contains the mapping of relayAddress to Allocation.
*/
private final HashMap<TransportAddress,Allocation> serverRelayAllocationMap
= new HashMap<TransportAddress,Allocation>();
/**
* RelayAddress reserved by server.
*/
private final HashSet<TransportAddress> reservedAddress
= new HashSet<TransportAddress>();
/**
* Represents the Allocations stored for Client Side.
*/
private final HashMap<FiveTuple,Allocation> clientAllocations
= new HashMap<FiveTuple,Allocation>();
/**
* Maps one-to-one from Data Connection to Connection Id.
*/
private final HashMap<FiveTuple, Integer> dataConnToConnIdMap =
new HashMap<FiveTuple, Integer>();
/**
* Maps one-to-one from Peer TCP Connection to Connection Id.
*/
private final HashMap<FiveTuple, Integer> peerConnToConnIdMap =
new HashMap<FiveTuple, Integer>();
/**
* Maps many-to-one from Connection Id to Allocation for where
* ConnectionBind Request has been received for Connection ID .
*/
private final HashMap<Integer, Allocation> connIdToAllocMap =
new HashMap<Integer, Allocation>();
/**
* Contains unAcknowledged Connection Id. Every element will expire after
* min of 30 sec.
*/
private final HashSet<Integer> unAcknowledgedConnId =
new HashSet<Integer>();
/**
* The <tt>Thread</tt> which expires the <tt>TurnServerAllocation</tt>s of
* this <tt>TurnStack</tt> and removes them from {@link #serverAllocations}
* .
*/
private Thread serverAllocationExpireThread;
/**
* Indicates that if the don't fragment is support or not.
*/
private static final boolean dontFragmentSupported = false;
/**
* Component variable.
*/
private Component component;
/**
* Boolean to allow or disallow TCP messages. Default is allowed.
*/
private boolean tcpAllowed = true;
/**
* Boolean to allow or disallow UDP messages. Default is allowed.
*/
private boolean udpAllowed = true;
/**
* Default Constructor. Initializes the NetAccessManager and
*/
public TurnStack()
{
super();
initCredentials();
}
/**
* Parameterized constructor for TurnStack.
*
* @param peerUdpMessageEventHandler
* the PeerUdpMessageEventHandler for this turnStack.
* @param channelDataEventHandler
* the ChannelDataEventHandler for this turnStack.
*/
public TurnStack(PeerUdpMessageEventHandler peerUdpMessageEventHandler,
ChannelDataEventHandler channelDataEventHandler) {
super(peerUdpMessageEventHandler,channelDataEventHandler);
initCredentials();
}
/**
* Called to notify this provider for an incoming message. method overridden
* to modify the logic of the Turn Stack.
*
* @param ev the event object that contains the new message.
*/
@Override
public void handleMessageEvent(StunMessageEvent ev)
{
Message msg = ev.getMessage();
logger.finest("Received an Event."+ev.getTransactionID());
if (!TurnStack.isTurnMessage(msg))
{
logger.finest("Ignored a non-TURN message!");
return;
}
else
{
removeUsernameIntegrityFromBinding(ev.getMessage());
super.handleMessageEvent(ev);
return;
}
/* logger.setLevel(Level.FINEST);
if (logger.isLoggable(Level.FINEST))
{
logger.finest("Received a message on " + ev.getLocalAddress()
+ " of type:" + (int) msg.getMessageType());
}
// request
if (msg instanceof Request)
{
TransactionID serverTid = ev.getTransactionID();
logger.finer("parsing request : "+serverTid);
TurnServerTransaction sTran =
(TurnServerTransaction) getServerTransaction(serverTid);
if (sTran != null)
{
// requests from this transaction have already been seen
// retransmit the response if there was any
logger.finest("found an existing transaction");
try
{
sTran.retransmitResponse();
logger.finest("Response retransmitted");
}
catch (Exception ex)
{
// we couldn't really do anything here .. apart from logging
logger.log(
Level.WARNING, "Failed to retransmit a Turn response",
ex);
}
if (!Boolean
.getBoolean(StackProperties.PROPAGATE_RECEIVED_RETRANSMISSIONS))
{
return;
}
}
else
{
logger.finest("existing transaction not found");
sTran =
new TurnServerTransaction(this, serverTid,
ev.getLocalAddress(), ev.getRemoteAddress());
// if there is an OOM error here, it will lead to
// NetAccessManager.handleFatalError that will stop the
// MessageProcessor thread and restart it that will lead again
// to an OOM error and so on... So stop here right now
try
{
sTran.start();
}
catch (OutOfMemoryError t)
{
logger.info("Turn transaction thread start failed:" + t);
return;
}
startNewServerTransactionThread(
serverTid, sTran);
}
// validate attributes that need validation.
try
{
// validateRequestAttributes(ev);
}
catch (Exception exc)
{
// validation failed. log get lost.
logger.log(
Level.FINE, "Failed to validate msg: " + ev, exc);
return;
}
try
{
fireMessageEventFormEventDispatcher(ev);
}
catch (Throwable t)
{
Response error;
logger.log(
Level.INFO, "Received an invalid request.", t);
Throwable cause = t.getCause();
if (((t instanceof StunException) && ((StunException) t)
.getID() == StunException.TRANSACTION_ALREADY_ANSWERED)
|| ((cause instanceof StunException) && ((StunException) cause)
.getID() == StunException.TRANSACTION_ALREADY_ANSWERED))
{
// do not try to send an error response since we will
// get another TRANSACTION_ALREADY_ANSWERED
return;
}
if (t instanceof IllegalArgumentException)
{
error = MessageFactory.createBindingErrorResponse(
ErrorCodeAttribute.BAD_REQUEST, t.getMessage());
}
else
{
error =
MessageFactory.createBindingErrorResponse(
ErrorCodeAttribute.SERVER_ERROR,
"Oops! Something went wrong on our side :(");
}
try
{
sendResponse(
serverTid.getBytes(), error, ev.getLocalAddress(),
ev.getRemoteAddress());
}
catch (Exception exc)
{
logger.log(
Level.FINE, "Couldn't send a server error response",
exc);
}
}
}
// response
else if (msg instanceof Response)
{
logger.finer("Parsing response");
TransactionID tid = ev.getTransactionID();
StunClientTransaction tran =
removeTransactionFromClientTransactions(tid);
if (tran != null)
{
tran.handleResponse(ev);
}
else
{
// do nothing - just drop the phantom response.
logger
.fine("Dropped response - no matching client tran found for"
+ " tid " + tid + "\n");
}
}
// indication
else if (msg instanceof Indication)
{
logger.finer("Dispatching a Indication.");
fireMessageEventFormEventDispatcher(ev);
}
*/ }
/**
* Method to know if the Don't fragment is supported.
*
* @return true if supported else false.
*/
public static boolean isDontfragmentsupported()
{
return dontFragmentSupported;
}
/**
* Returns the Allocation with the specified <tt>fiveTuple</tt> or
* <tt>null</tt> if no such Allocation exists.
*
* @param fiveTuple the fiveTuple of the Allocation we are looking for.
*
* @return the {@link Allocation} we are looking for.
*/
public Allocation getServerAllocation(FiveTuple fiveTuple)
{
Allocation allocation = null;
synchronized (this.serverAllocations)
{
allocation = this.serverAllocations.get(fiveTuple);
}
/*
* If a Allocation is expired, do not return it. It will be
* removed from serverAllocations soon.
*/
if ((allocation != null) && allocation.isExpired())
allocation = null;
return allocation;
}
/**
* Returns the Allocation with the specified <tt>fiveTuple</tt> or
* <tt>null</tt> if no such Allocation exists.
*
* @param fiveTuple the fiveTuple of the Allocation we are looking for.
*
* @return the {@link Allocation} we are looking for.
*/
public Allocation getClientAllocation(FiveTuple fiveTuple)
{
Allocation allocation;
synchronized (clientAllocations)
{
allocation = clientAllocations.get(fiveTuple);
}
/*
* If a Allocation is expired, do not return it. It will be
* removed from serverAllocations soon.
*/
if ((allocation != null) && allocation.isExpired())
allocation = null;
return allocation;
}
/**
* Determines if more allocations can be added to this TurnStack.
*
* @return true if no of allocations are less than maximum allowed
* allocations per TurnStack.
*/
public boolean canHaveMoreAllocations()
{
return (this.serverAllocations.size() < MAX_ALLOCATIONS);
}
/**
* Adds a new server allocation to this TurnStack.
*
* @param allocation the allocation to be added to this TurnStack.
*/
public synchronized void addNewServerAllocation(Allocation allocation)
{
synchronized(this.serverAllocations)
{
this.serverAllocations.put(allocation.getFiveTuple(), allocation);
IceSocketWrapper sock;
if(true)
{ // check if meanwhile other thread has put the same allocation.
try
{
logger.finer("Adding a new Socket for : "
+ allocation.getRelayAddress());
if(allocation.getRelayAddress().getTransport()==Transport.UDP)
{
sock = new IceUdpSocketWrapper(
new SafeCloseDatagramSocket(
allocation.getRelayAddress()));
}
else
{
IceTcpEventizedServerSockerWrapper mySock2 =
new IceTcpEventizedServerSockerWrapper(
new ServerSocket(allocation.getRelayAddress()
.getPort()), this.getComponent());
PeerTcpConnectEventListner listener =
new PeerTcpConnectEventListner(this);
mySock2.setEventListener(listener);
sock = mySock2;
/*
sock =
new IceTcpServerSocketWrapper(new ServerSocket(allocation
.getRelayAddress().getPort()),this.getComponent());
*/ }
this.addSocket(sock);
logger.finer("Added a new Socket for : "
+ allocation.getRelayAddress());
try
{
allocation.start();
}
catch(Exception e)
{
}
}
catch (SocketException e)
{
logger.finer("Error obtained : "+e.getMessage());
logger.log(Level.FINEST,
"Error! Cannot add new socket from TurnStack at "
+"addNewServerAllocation ");
logger.log(Level.FINEST, e.getMessage());
// allocation.expire();
}
catch (UnknownHostException e)
{
System.err.println("Unable to add TCP relay Address for : "
+ allocation.getRelayAddress());
}
catch (IOException e)
{
e.printStackTrace();
}
}
this.serverRelayAllocationMap.put(
allocation.getRelayAddress(),
allocation);
maybeStartServerAllocationExpireThread();
}
}
/**
* Gets the allocation corresponding to the relay address.
* @param relayAddress the relayAddress for which to find allocation.
* @return the Allocation corresponding to relayAddress.
*/
public Allocation getServerAllocation(TransportAddress relayAddress)
{
return this.serverRelayAllocationMap.get(relayAddress);
}
/**
* Function to check if given IP is allowed for peer address.s
* @param peerAddr
* @return
*/
public static boolean isIPAllowed(TransportAddress peerAddr)
{
String ip = peerAddr.getHostAddress();
int portNo = peerAddr.getPort();
//TODO : logic for validating the invalid IP address.
return true;
}
/**
* Reserves a port for future use for Reservation-token.
*
* @param reserAddr the address to be reserved.
* @return false if it is already reserved, else true.
*/
public boolean reservePort(TransportAddress reserAddr)
{
if(this.reservedAddress.contains(reserAddr))
{
return false;
}
else
{
this.reservedAddress.add(reserAddr);
return true;
}
}
/**
* Function to get new Relay address.
* TODO : It has to be replaced with jitsi api.
*
* @param evenCompulsary
* @return a new RelayAddress
*/
public TransportAddress getNewRelayAddress(boolean evenCompulsary,
Transport transport)
{
InetAddress ipAddress = null;
try
{
ipAddress = InetAddress.getLocalHost();
}
catch (UnknownHostException e)
{
e.printStackTrace();
}
TransportAddress possibleAddr =
new TransportAddress(ipAddress, nextPortNo++, transport);
int diff = evenCompulsary ? 2 : 1;
nextPortNo += (evenCompulsary && (nextPortNo%2)==0) ? 0 : 1;
while(this.reservedAddress.contains(possibleAddr) && nextPortNo < 65535)
{
nextPortNo += diff;
possibleAddr =
new TransportAddress(ipAddress, nextPortNo++, Transport.UDP);
}
return possibleAddr;
}
/**
* Adds a new ConnectionId for the specified peerAddress and for the
* specified allocation.
*
* @param connectionId the connectionId created.
* @param peerAddress the peerAddress who initiated the TCP connection on
* the relay address of the Allocation.
* @param allocation the allocation corresponding to the relay address on
* which the connect request is received.
*/
public void addUnAcknowlededConnectionId(int connectionId,
TransportAddress peerAddress, Allocation allocation)
{
FiveTuple peerTuple =
new FiveTuple(peerAddress,allocation.getRelayAddress(),
Transport.TCP);
this.unAcknowledgedConnId.add(connectionId);
this.peerConnToConnIdMap.put(
peerTuple, connectionId);
this.connIdToAllocMap.put(
connectionId, allocation);
allocation.addPeerTCPConnection(
connectionId, peerTuple);
logger.finest("Adding connectionId-" + connectionId + " for peerTuple-"
+ peerTuple + " at allocation-" + allocation);
}
/**
* Acknowledges the ConnectionID associated with the specified client data
* connection.
*
* @param connectionId the connectionId associated with the data connection.
* @param clientDataConnectionTuple the fiveTuple of the data connection.
*/
public void acknowledgeConnectionId(int connectionId,
FiveTuple clientDataConnectionTuple)
{
if (!this.unAcknowledgedConnId.contains(connectionId))
{
throw new IllegalArgumentException("No such connectionId:"
+ connectionId + " exists");
}
else
{
this.unAcknowledgedConnId.remove(connectionId);
this.dataConnToConnIdMap.put(
clientDataConnectionTuple, connectionId);
Allocation allocation = this.connIdToAllocMap.get(connectionId);
allocation.addDataConnection(
connectionId, clientDataConnectionTuple);
logger.finest("Acknowledging connectiodId-" + connectionId
+ " for client data conn-" + clientDataConnectionTuple);
}
}
/**
* Determines if the given ConnectionId is acknowledged or not.
* @param connectionID the connectionId to check.
* @return true if the specified connectionID is acknowledged, else false.
*/
public boolean isUnacknowledged(int connectionID){
return this.unAcknowledgedConnId.contains(connectionID);
}
/**
* Returns the Connection associated with the specified peerFiveTuple.
*
* @param peerFiveTuple the peerFiveTuple for which to get the ConnectionID.
* @return connectionID associated with the specified peerFiveTuple.
*/
public int getConnectionIdForPeer(FiveTuple peerFiveTuple)
{
return this.peerConnToConnIdMap.get(peerFiveTuple);
}
/**
* Returns the ConnectionID associated with the specified
*
* @param dataConnTuple the five tuple of the data connection.
* @return the connectionId associated with the given data connection if
* exists.
*/
public int getConnectionIdForDataConn(FiveTuple dataConnTuple)
{
return this.dataConnToConnIdMap.get(dataConnTuple);
}
/**
* Initialises and starts {@link #serverAllocationExpireThread} if
* necessary.
*/
public void maybeStartServerAllocationExpireThread()
{
synchronized (serverAllocations)
{
if (!serverAllocations.isEmpty()
&& (serverAllocationExpireThread == null))
{
Thread t = new Thread()
{
@Override
public void run()
{
runInServerAllocationExpireThread();
}
};
t.setDaemon(true);
t.setName(getClass().getName()
+ ".serverAllocationExpireThread");
boolean started = false;
serverAllocationExpireThread = t;
try
{
t.start();
started = true;
}
finally
{
if (!started && (serverAllocationExpireThread == t))
serverAllocationExpireThread = null;
}
}
}
}
/**
* Runs in {@link #serverAllocationExpireThread} and expires the
* <tt>Allocation</tt>s of this <tt>TurnStack</tt> and removes
* them from {@link #serverAllocations}.
*/
private void runInServerAllocationExpireThread()
{
try
{
long idleStartTime = -1;
do
{
synchronized (serverAllocations)
{
try
{
serverAllocations.wait(Allocation.DEFAULT_LIFETIME);
}
catch (InterruptedException ie)
{
}
/*
* Is the current Thread still designated to expire the
* Allocations of this TurnStack?
*/
if (Thread.currentThread() != serverAllocationExpireThread)
break;
long now = System.currentTimeMillis();
/*
* Has the current Thread been idle long enough to merit
* disposing of it?
*/
if (serverAllocations.isEmpty())
{
if (idleStartTime == -1)
idleStartTime = now;
else if (now - idleStartTime > 60 * 1000)
break;
}
else
{
// Expire the Allocations of this TurnStack.
idleStartTime = -1;
for (Iterator<Allocation> i =
serverAllocations.values().iterator(); i.hasNext();)
{
Allocation allocation = i.next();
if (allocation == null)
{
i.remove();
}
else if (allocation.isExpired(now))
{
logger.finer("allocation "+allocation+" expired");
i.remove();
allocation.expire();
}
}
}
}
}
while (true);
}
finally
{
synchronized (serverAllocations)
{
if (serverAllocationExpireThread == Thread.currentThread())
serverAllocationExpireThread = null;
/*
* If serverAllocationExpireThread dies unexpectedly and yet it
* is still necessary, resurrect it.
*/
if (serverAllocationExpireThread == null)
maybeStartServerAllocationExpireThread();
}
}
}
/**
* Method to check if the given message method is of Turn method.
*
* @param message
* @return true if message is of Turn method else false.
*/
public static boolean isTurnMessage(Message message)
{
char method = message.getMessageType();
method = (char) (method & 0xfeef); // ignore the class
logger.finest("method extracted from "
+ (int) message.getMessageType() + " is : " + (int) method);
boolean isTurnMessage = false;
switch (method)
{
// Turn Specific Methods
case Message.TURN_METHOD_ALLOCATE:
case Message.TURN_METHOD_CHANNELBIND:
case Message.TURN_METHOD_CREATEPERMISSION:
case Message.TURN_METHOD_DATA:
case Message.TURN_METHOD_REFRESH:
case Message.TURN_METHOD_SEND:
// Turn TCP support Methods
case Message.TURN_METHOD_CONNECT:
case Message.TURN_METHOD_CONNECTION_BIND:
case Message.TURN_METHOD_CONNECTION_ATTEMPT:
case Message.STUN_METHOD_BINDING:
isTurnMessage = true;
break;
default:
isTurnMessage = false;
}
return isTurnMessage;
}
/**
* Removes the username and Message Integrity attribute form Binding
* messages only.
*
* @param msg the Binding message from which the attribute is to be removed.
*/
private void removeUsernameIntegrityFromBinding(Message msg)
{
if((msg.getMessageType() & 0xfeef) != Message.STUN_METHOD_BINDING)
{
return;
}
if(msg.containsAttribute(Attribute.USERNAME))
{
msg.removeAttribute(Attribute.USERNAME);
}
if(msg.containsAttribute(Attribute.MESSAGE_INTEGRITY))
{
msg.removeAttribute(Attribute.MESSAGE_INTEGRITY);
}
}
/**
* Initializes the turnstack with the registered users with username and their
* corresponding key.
*/
public void initCredentials()
{
String fileName = TurnStackProperties.DEFAULT_ACCOUNTS_FILE;
FileReader fr;
try {
fr = new FileReader(fileName);
BufferedReader br = new BufferedReader(fr);
CredentialsManager cm = this.getCredentialsManager();
String line = null;
while((line = br.readLine())!=null)
{
String[] tok = line.split(":");
LongTermCredential ltc = new LongTermCredential(
tok[0].getBytes("UTF-8"), tok[1].getBytes("UTF-8"));
System.out.println("Adding - " + new String(ltc.getUsername())
+ ":" + new String(ltc.getPassword()));
// TODO replace with REALM instead of DEFAULT_REALM.
LongTermCredentialSession ltcs = new LongTermCredentialSession(
ltc,
TurnStackProperties.DEFAULT_REALM.getBytes("UTF-8"));
cm.registerAuthority(ltcs);
}
fr.close();
br.close();
} catch (FileNotFoundException fnfe) {
logger.finest("File not found.");
}catch(IOException ioe){
logger.finest("Unable to read file.");
}
}
/**
* Gets the component as RTP with TCP as transport also agent's stunStack as
* this TurnStack.
*
* @return component.
*/
public Component getComponent()
{
if (this.component == null)
{
Agent agent = new Agent();
agent.setStunStack(this);
IceMediaStream stream = IceMediaStream.build(agent, "Turn Server");
this.component = Component.build(Component.RTP, stream);
}
return this.component;
}
/**
* Determines if the UDP messages are allowed in TURN server.
*
* @return true if UDP is allowed else false.
*/
public boolean isUDPAllowed()
{
return this.udpAllowed;
}
/**
* Sets the udpAllowed variable to enable disable UDP messages.
*
* @param udpAllowed the boolean value to allow or disallow UDP messages.
*/
public void setUDPAllowed(boolean udpAllowed)
{
this.udpAllowed = udpAllowed;
}
/**
* Determines if the TCP messages are allowed in TURN server.
*
* @return true if TCP is allowed else false.
*/
public boolean isTCPAllowed()
{
return this.tcpAllowed;
}
/**
* Sets the tcpAllowed variable to enable disable TCP messages.
*
* @param tcpAllowed the boolean value to allow or disallow TCP messages.
*/
public void setTCPAllowed(boolean tcpAllowed)
{
this.tcpAllowed = tcpAllowed;
}
/**
* Gets the allocation for the specified connectionID no.
*
* @param connectionId the connectionID for which to get the allocation.
* @return Allocation corresponding to specified connectionID or nul if not
* found.
*/
public Allocation getAllocationFromConnectionId(int connectionId)
{
return this.connIdToAllocMap.get(connectionId);
}
}