/*
* 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.util.HashMap;
import java.util.Iterator;
import java.util.logging.Logger;
import org.ice4j.Transport;
import org.ice4j.TransportAddress;
/**
* This class is an implementation of Allocations in TURN server.
*
* @author Aakash Garg
*
*/
public class Allocation
{
/**
* Our class logger.
*/
private static final Logger logger = Logger.getLogger(Allocation.class
.getName());
/**
* represents the relay address associated with this Allocation.
*/
private final TransportAddress relayAddress;
/**
* Represents the FiveTuple associated with this Allocation.
*/
private final FiveTuple fiveTuple;
/**
* Represents the username associated with this Allocation.
*/
private final String username;
/**
* represents the password associated with this Allocation.
*/
private final String password;
/**
* The time in milliseconds when the Allocation will expire.
*/
private long expirationTime = -1;
/**
* Determines whether or not the Allocation has expired.
*/
private boolean expired = false;
/**
* The default lifetime allowed for a Allocation.
*/
public static final long DEFAULT_LIFETIME = 10 * 60 * 1000;
/**
* The max lifetime allowed for a Allocation.
*/
public static final long MAX_LIFETIME = 60 * 60 * 1000;
/**
* The maximum no of Permissions per Allocation.
*/
public static final int MAX_PERMISSIONS = 10;
/**
* The Maximum no of ChannelBinds per Allocation.
*/
public static final int MAX_CHANNELBIND = 10;
/**
* The <tt>Thread</tt> which expires the <tt>Permission</tt>s of this
* <tt>Allocation</tt> and removes them from {@link #permissions}.
*/
private Thread permissionExpireThread;
/**
* Represents the permissions associated with peerAddress IP installed for
* this Allocation.
*/
private final HashMap<TransportAddress, Permission> permissions =
new HashMap<TransportAddress, Permission>();
/**
* The <tt>Thread</tt> which expires the <tt>ChannelBind</tt>s of this
* <tt>Allocation</tt> and removes them from {@link #channelBindings}.
*/
private Thread channelBindExpireThread;
/**
* Represents the Channel Bindings associated with this Allocation.
*/
private final HashMap<Character, ChannelBind> channelBindings =
new HashMap<Character, ChannelBind>();
/**
* Contains the mapping of peerAdress of ChannelBinds to Channelno. This is
* used to check the peerAddress while creating new Permissions and
* ChannelBinds.
*/
private final HashMap<TransportAddress, Character> peerToChannelMap =
new HashMap<TransportAddress, Character>();
/**
* Maps one-to-one from ConnecionID to Data Connection.
*/
private final HashMap<Integer,FiveTuple> connIdToDataConnMap
= new HashMap<Integer,FiveTuple>();
/**
* Maps one-to-one from ConnecionID to Peer TCP Connection.
*/
private final HashMap<Integer,FiveTuple> connIdToPeerConnMap
= new HashMap<Integer,FiveTuple>();
/**
* Constructor to instantiate an Allocation without a username and password.
*
* @param relayAddress the realyAddress associated with this Allocation.
* @param fiveTuple the fiveTuple associated with this Allocation.
*/
public Allocation( TransportAddress relayAddress,
FiveTuple fiveTuple)
{
this(relayAddress, fiveTuple, null, null);
}
/**
* Constructor to instantiate an Allocation without a username and password
* with the lifetime value.
*
* @param relayAddress the realyAddress associated with this Allocation.
* @param fiveTuple the fiveTuple associated with this Allocation.
* @param lifetime the lifetime for this Allocation.
*/
public Allocation( TransportAddress relayAddress,
FiveTuple fiveTuple,
long lifetime)
{
this(relayAddress, fiveTuple, null, null, lifetime);
}
/**
* Constructor to instantiate an Allocation with given relayAddress,
* fiveTuple, username, passowrd and with default lifetime value.
*
* @param relayAddress the realyAddress associated with this Allocation.
* @param fiveTuple the fiveTuple associated with this Allocation.
* @param username the username associated with this Allocation.
* @param password the password associated with this Allocation.
*/
public Allocation( TransportAddress relayAddress,
FiveTuple fiveTuple,
String username,
String password)
{
this(relayAddress, fiveTuple, username, password,
Allocation.DEFAULT_LIFETIME);
}
/**
* Constructor to instantiate an Allocation with given relayAddress,
* fiveTuple, username, passowrd and with default lifetime value.
*
* @param relayAddress the realyAddress associated with this Allocation.
* @param fiveTuple the fiveTuple associated with this Allocation.
* @param username the username associated with this Allocation.
* @param password the password associated with this Allocation.
* @param lifetime the lifetime for this allocation.
*/
public Allocation( TransportAddress relayAddress,
FiveTuple fiveTuple,
String username,
String password,
long lifetime)
{
this.relayAddress = relayAddress;
this.fiveTuple = fiveTuple;
this.username = username;
this.password = password;
this.setLifetime(lifetime);
}
/**
* returns the fiveTuple associated with this Allocation.
*/
public FiveTuple getFiveTuple()
{
return this.fiveTuple;
}
/**
* Returns the relayAddress associated with this Allocation.
*/
public TransportAddress getRelayAddress()
{
return this.relayAddress;
}
/**
* Returns the clientAddress associated with this Allocation.
* The client address who instianted this allocation.
*/
public TransportAddress getClientAddress()
{
return this.getFiveTuple().getClientTransportAddress();
}
/**
* Returns the serverAddress associated with this Allocation.
* The serverAddress on which this allocation request is received.
*/
public TransportAddress getServerAddress()
{
return this.getFiveTuple().getServerTransportAddress();
}
/**
* Returns the Client Data Connection corresponding to Connection Id for
* which ConnectionBind Request has been received.
*
* @param connectionId the ConnectionId for which Client Data Connection is
* to be returned.
* @return Client Data Connection if exists else null.
*/
public FiveTuple getDataConnection(int connectionId){
return this.connIdToDataConnMap.get(connectionId);
}
/**
* Returns the Peer TCP Data Connection corresponding to Connection Id for
* which ConnectionBind Request has been received.
*
* @param connectionId the ConnectionId for which Peer TCP Data Connection is
* to be returned.
* @return Peer TCP Data Connection if exists else null.
*/
public FiveTuple getPeerTCPConnection(int connectionId){
return this.connIdToPeerConnMap.get(connectionId);
}
/**
* Adds the Connection Id with corresponding Client Data Connection to for
* which ConnectionBind Request has been received.
*
* @param connectionId the ConnectionId.
* @param clientDataConn Client Data Connection to corresponding
* ConnectionId.
*/
public void addDataConnection(int connectionId, FiveTuple clientDataConn)
{
this.connIdToDataConnMap.put(
connectionId, clientDataConn);
}
/**
* Adds the Connection Id corresponding to Peer TCP Data Connection for
* which ConnectionBind Request has been received.
*
* @param connectionId the ConnectionId.
* @param peerDataConn Peer TCP Data Connection to corresponding
* ConnectionId.
*/
public void addPeerTCPConnection(int connectionId, FiveTuple peerDataConn)
{
this.connIdToPeerConnMap.put(connectionId,peerDataConn);
}
/**
* Removes the Client Data Connection with corresponding to Connection Id.
*
* @param connectionId the ConnectionId corresponding to Client Data
* Connection.
*/
public void removeDataConnection(int connectionId)
{
this.connIdToDataConnMap.remove(connectionId);
}
/**
* Removes the Peer TCP Data Connection corresponding to Connection Id.
*
* @param connectionId the ConnectionId corresponding to Client Data
* Connection.
*/
public void removePeerTCPConnection(int connectionId)
{
this.connIdToPeerConnMap.remove(connectionId);
}
/**
* Returns the lifetime associated with this Allocation. If the allocation
* is expired it returns 0.
*/
public long getLifetime()
{
if (!isExpired())
{
return (this.expirationTime - System.currentTimeMillis());
}
else
{
return 0;
}
}
/**
* Sets the time to expire in milli-seconds for this allocation. Max
* lifetime can be Allocation.MAX_LIFEIME.
*
* @param lifetime the lifetime for this Allocation.
*/
public void setLifetime(long lifetime)
{
synchronized (this)
{
this.expirationTime = System.currentTimeMillis()
+ Math.min(lifetime * 1000, Allocation.MAX_LIFETIME);
}
}
/**
* Refreshes the allocation with the DEFAULT_LIFETIME value.
*/
public void refresh()
{
this.setLifetime(Allocation.DEFAULT_LIFETIME);
}
/**
* refreshes the allocation with given lifetime value.
*
* @param lifetime the required lifetime of allocation.
*/
public void refresh(int lifetime)
{
this.setLifetime(lifetime);
}
/**
* Start the Allocation. This launches the countdown to the moment the
* Allocation would expire.
*/
public synchronized void start()
{
synchronized (this)
{
if (expirationTime == -1)
{
expired = false;
expirationTime = DEFAULT_LIFETIME + System.currentTimeMillis();
}
else
{
throw new IllegalStateException(
"Allocation has already been started!");
}
}
}
/**
* Determines whether this <tt>Allocation</tt> is expired now.
*
* @return <tt>true</tt> if this <tt>Allocation</tT> is expired now;
* otherwise, <tt>false</tt>
*/
public boolean isExpired()
{
return isExpired(System.currentTimeMillis());
}
/**
* Expires the Allocation. Once this method is called the Allocation is
* considered terminated.
*/
public synchronized void expire()
{
expired = true;
/*
* TurnStack has a background Thread running with the purpose of
* removing expired Allocations.
*/
}
/**
* Determines whether this <tt>Allocation</tt> will be expired at a specific
* point in time.
*
* @param now the time in milliseconds at which the <tt>expired</tt> state
* of this <tt>Allocation</tt> is to be returned
* @return <tt>true</tt> if this <tt>Allocation</tt> will be expired at the
* specified point in time; otherwise, <tt>false</tt>
*/
public synchronized boolean isExpired(long now)
{
if (expirationTime == -1)
return false;
else if (expirationTime < now)
return true;
else
return expired;
}
/**
* Adds a new Permission for this Allocation.
*
* @param peerIP the peer IP address foe which to create this permission to
* be added to this allocation.
*/
public void addNewPermission(TransportAddress peerIP)
{
TransportAddress peerIp =
new TransportAddress(peerIP.getAddress(), 0, Transport.UDP);
Permission permission = new Permission(peerIP);
this.addNewPermission(permission);
}
/**
* Adds a new Permission for this Allocation.
*
* @param permission the permission to be added to this allocation.
*/
public void addNewPermission(Permission permission)
{
TransportAddress peerAddr =
new TransportAddress(permission.getIpAddress().getAddress(), 0,
Transport.UDP);
if (this.permissions.containsKey(peerAddr))
{
this.permissions.get(permission.getIpAddress()).refresh();
}
else if (!this.canHaveMorePermisions())
{
return;
}
else
{
this.permissions.put(
permission.getIpAddress(), permission);
maybeStartPermissionExpireThread();
}
}
/**
* Binds a new Channel to this Allocation.
* If an existing ChannelBind is found it is refreshed
* else a new ChannelBind and permission is added.
*
* @param channelBind the channelBind to be added to this allocation.
* @throws IllegalArgumentException if the channelNo of the channelBind to
* be added is already occupied.
*/
public void addChannelBind(ChannelBind channelBind)
{
TransportAddress peerAddr =
new TransportAddress(
channelBind.getPeerAddress().getAddress(),
0,
Transport.UDP);
if (isBadChannelRequest(channelBind))
{
throw new IllegalArgumentException("400: BAD REQUEST");
}
else if(!channelBindings.containsKey(channelBind.getChannelNo())
&& !peerToChannelMap.containsKey(channelBind.getPeerAddress()))
{
synchronized(this.channelBindings)
{
this.channelBindings.put( channelBind.getChannelNo(),
channelBind);
}
synchronized(this.peerToChannelMap)
{
this.peerToChannelMap.put(
channelBind.getPeerAddress(), channelBind.getChannelNo());
}
}
else
{
synchronized(this.channelBindings)
{
this.channelBindings.get(channelBind.getChannelNo()).refresh();
}
}
this.addNewPermission(peerAddr);
maybeStartChannelBindExpireThread();
}
/**
* Determines whether the ChannelBind request is a BAD request or not.
* A request is BAD when the same client sends a ChannelBind Request and
* channel no or peerAddress coincides with existing channel bindings.
* A request is not bad if the channel no and peerAddress in the ChannelBind
* request are same as that in current mapping.
*
* @param channelBind the channelBind request to validate.
* @return true if request is a BAD request.
*/
public boolean isBadChannelRequest(ChannelBind channelBind)
{
boolean hasChannelNo =
this.channelBindings.containsKey(channelBind.getChannelNo());
boolean hasPeerAddr =
this.peerToChannelMap.containsKey(channelBind.getPeerAddress());
if(hasChannelNo && hasPeerAddr)
{
if (this.channelBindings.get(
channelBind.getChannelNo()).equals(
channelBind.getPeerAddress()))
{
return false;
}
}
else if(!hasChannelNo && !hasPeerAddr)
{
return false;
}
return true;
}
/**
* Removes the channelBind associated with this channlNo from this
* allocation.
*
* @param channelNo the channelNo for which the ChannelBind to delete.
* @return the ChannnelBindingf associated with this channelNo.
*/
public ChannelBind removeChannelBind(char channelNo)
{
ChannelBind channelBind = null;
synchronized (this.channelBindings)
{
channelBind = this.channelBindings.remove(channelNo);
}
return channelBind;
}
/**
* Checks if the Permission is installed for the peerAddress. The port value
* is ignored.
*
* @param peerAddress
* the peerAddress for which to check permission.
* @return true if permission is installed for peerAddress else false.
*/
public boolean isPermitted(TransportAddress peerAddress)
{
peerAddress =
new TransportAddress(peerAddress.getAddress(), 0,
peerAddress.getTransport());
if (this.permissions.containsKey(peerAddress))
{
return true;
}
return false;
}
/**
* Checks if the specified channel no is binded to this allocation.
*
* @param channelNo
* the channel number to check.
* @return true if the specified channel no. is installed for this
* allocation.
*/
public boolean containsChannel(char channelNo)
{
return this.channelBindings.containsKey(channelNo);
}
/**
* Gets the channelNO for the specified peerAddress.
* @param peerAddress the peerAddress for which to get the channel.
* @return channelNo is channelNo is found, else 0x1000.
*/
public char getChannel(TransportAddress peerAddress)
{
char val = 0x1000;
if(this.peerToChannelMap.containsKey(peerAddress))
{
return this.peerToChannelMap.get(peerAddress);
}
return val;
}
/**
* Gets the peerAddress associated with specified channelNo.
*
* @param channelNo
* the channel no for which to get the peerAddress.
* @return peerAddress the peerAddress associated with the channelNo in this
* allocation.
*/
public TransportAddress getPeerAddr(char channelNo)
{
ChannelBind cb = this.channelBindings.get(channelNo);
if(cb!=null)
{
return cb.getPeerAddress();
}
return null;
}
/**
* Determines if more permissions can be added to this allocation.
*
* @return true if no of permissions are less than maximum allowed
* permissions per Allocation.
*/
public boolean canHaveMorePermisions()
{
return (this.permissions.size() < MAX_PERMISSIONS);
}
/**
* Determines if more channels can be added to this allocation.
*
* @return true if no of channels are less than maximum allowed channels per
* Allocation.
*/
public boolean canHaveMoreChannels()
{
return (this.channelBindings.size() < MAX_CHANNELBIND);
}
/**
* Initialises and starts {@link #channelBindExpireThread} if necessary.
*/
public void maybeStartChannelBindExpireThread()
{
synchronized (channelBindings)
{
if (!channelBindings.isEmpty() && (channelBindExpireThread == null))
{
Thread t = new Thread()
{
@Override
public void run()
{
runInAllocationChannelBindExpireThread();
}
};
t.setDaemon(true);
t.setName(getClass().getName() + ".channelBindExpireThread");
boolean started = false;
channelBindExpireThread = t;
try
{
t.start();
started = true;
}
finally
{
if (!started && (channelBindExpireThread == t))
channelBindExpireThread = null;
}
}
}
}
/**
* Runs in {@link #channelBindExpireThread} and expires the
* <tt>ChannelBind</tt>s of this <tt>Allocation</tt> and removes them from
* {@link #channelBindingings}.
*/
private void runInAllocationChannelBindExpireThread()
{
try
{
long idleStartTime = -1;
do
{
synchronized (channelBindings)
{
try
{
channelBindings.wait(ChannelBind.MAX_LIFETIME);
}
catch (InterruptedException ie)
{
}
/*
* Is the current Thread still designated to expire the
* ChannelBinds of this Allocation?
*/
if (Thread.currentThread() != channelBindExpireThread)
break;
long now = System.currentTimeMillis();
/*
* Has the current Thread been idle long enough to merit
* disposing of it?
*/
if (channelBindings.isEmpty())
{
if (idleStartTime == -1)
idleStartTime = now;
else if (now - idleStartTime > 60 * 1000)
break;
}
else
{
// Expire the ChannelBinds of this Allocation.
idleStartTime = -1;
for (Iterator<ChannelBind> i =
channelBindings.values().iterator(); i.hasNext();)
{
ChannelBind channelBind = i.next();
if (channelBind == null)
{
i.remove();
}
else if (channelBind.isExpired(now))
{
logger.finer("ChannelBind " + channelBind
+ " expired");
i.remove();
this.peerToChannelMap.remove(
channelBind.getPeerAddress());
channelBind.expire();
}
}
}
}
}
while (true);
}
finally
{
synchronized (channelBindings)
{
if (channelBindExpireThread == Thread.currentThread())
channelBindExpireThread = null;
/*
* If channelBindExpireThread dies unexpectedly and yet it is
* still necessary, resurrect it.
*/
if (channelBindExpireThread == null)
maybeStartChannelBindExpireThread();
}
}
}
/**
* Initialises and starts {@link #permissionExpireThread} if necessary.
*/
public void maybeStartPermissionExpireThread()
{
synchronized (permissions)
{
if (!permissions.isEmpty() && (permissionExpireThread == null))
{
Thread t = new Thread()
{
@Override
public void run()
{
runInAllocationPermissionExpireThread();
}
};
t.setDaemon(true);
t.setName(getClass().getName() + ".permissionExpireThread");
boolean started = false;
permissionExpireThread = t;
try
{
t.start();
started = true;
}
finally
{
if (!started && (permissionExpireThread == t))
permissionExpireThread = null;
}
}
}
}
/**
* Runs in {@link #PermissionExpireThread} and expires the
* <tt>Permission</tt>s of this <tt>Allocation</tt> and removes them from
* {@link #permissions}.
*/
private void runInAllocationPermissionExpireThread()
{
try
{
long idleStartTime = -1;
do
{
synchronized (permissions)
{
try
{
permissions.wait(Permission.MAX_LIFETIME);
}
catch (InterruptedException ie)
{
}
/*
* Is the current Thread still designated to expire the
* Permissions of this Allocation?
*/
if (Thread.currentThread() != permissionExpireThread)
break;
long now = System.currentTimeMillis();
/*
* Has the current Thread been idle long enough to merit
* disposing of it?
*/
if (permissions.isEmpty())
{
if (idleStartTime == -1)
idleStartTime = now;
else if (now - idleStartTime > 60 * 1000)
break;
}
else
{
// Expire the Permissions of this Allocation.
idleStartTime = -1;
for (Iterator<Permission> i =
permissions.values().iterator(); i.hasNext();)
{
Permission permission = i.next();
if (permission == null)
{
i.remove();
}
else if (permission.isExpired(now))
{
logger.finer("Permission " + permission
+ " expired");
i.remove();
permission.expire();
}
}
}
}
}
while (true);
}
finally
{
synchronized (permissions)
{
if (permissionExpireThread == Thread.currentThread())
permissionExpireThread = null;
/*
* If permissionExpireThread dies unexpectedly and yet it is
* still necessary, resurrect it.
*/
if (permissionExpireThread == null)
maybeStartPermissionExpireThread();
}
}
}
@Override
public int hashCode()
{
return this.fiveTuple.hashCode();
}
/**
* Since an Allocation is uniquely identified by its relay address or five
* tuple hence we only compare these members.
*/
@Override
public boolean equals(Object o)
{
if (!(o instanceof Allocation))
{
return false;
}
Allocation allocation = (Allocation) o;
if (!this.fiveTuple.equals(allocation.fiveTuple))
{
return false;
}
if (!this.relayAddress.equals(allocation.relayAddress))
{
return false;
}
return true;
}
@Override
public String toString()
{
return this.getRelayAddress().toString();
}
}