/* dCache - http://www.dcache.org/
*
* Copyright (C) 2015 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.dcache.util;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Immutable class representing a port range.
*/
public class PortRange
{
/** Pattern matching <PORT>[:<PORT>] */
protected static final Pattern FORMAT =
Pattern.compile("(\\d+)(?:(?:,|:)(\\d+))?");
/**
* Random number generator used when binding sockets.
*/
private static final Random _random = new Random();
/**
* The port range to use.
*/
protected final int _lower;
protected final int _upper;
/**
* Creates a port range with the given bounds (both inclusive).
* Zero is excluded from non-empty port ranges.
*
* @throws IllegalArgumentException is either bound is not between
* 0 and 65535, or if <code>high</code> is lower than
* <code>low</code>.
*/
public PortRange(int low, int high)
{
/* Exclude zero from degenerate interval. Zero has a special
* meaning when binding a port.
*/
_lower = (low == 0 && high > 0) ? 1 : low;
_upper = high;
if (low < 0 || high < low || 65535 < high) {
throw new IllegalArgumentException("Invalid range");
}
}
/**
* Creates a port range containing a single port.
*/
public PortRange(int port)
{
this(port, port);
}
/**
* Parse a port range. A port range consists of either a single
* integer, or two integers separated by either a comma or a
* colon.
*
* The bounds must be between 0 and 65535, both inclusive.
*
* @return The port range represented by <code>s</code>. Returns
* the range [0,0] if <code>s</code> is null or empty.
*/
public static PortRange valueOf(String s)
throws IllegalArgumentException
{
try {
Matcher m = FORMAT.matcher(s);
if (!m.matches()) {
throw new IllegalArgumentException("Invalid range: " + s);
}
String lowString = m.group(1);
String highString = m.group(2);
int low = Integer.parseInt(lowString);
int high = (highString == null) ? low : Integer.parseInt(highString);
return new PortRange(low, high);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid range: " + s);
}
}
/**
* Returns the tcp port range.
* <p>
* It first checks the 'GLOBUS_TCP_PORT_RANGE' environment variable. If that
* system property is not set then 'org.globus.tcp.port.range' system
* property is checked. Returns an open range otherwise.
* <p>
* The port range is in the following form: minport, maxport
*/
public static PortRange getGlobusTcpPortRange()
{
String value = System.getenv("GLOBUS_TCP_PORT_RANGE");
if (value != null) {
return valueOf(value);
}
value = System.getProperty("org.globus.tcp.port.range");
if (value != null) {
return valueOf(value);
}
return new PortRange(0);
}
public int getLower()
{
return _lower;
}
public int getUpper()
{
return _upper;
}
/**
* Returns a random port within the range.
*/
public int random()
{
return _random.nextInt(_upper - _lower + 1) + _lower;
}
/**
* Returns the successor of a port within the range, wrapping
* around to the lowest port if necessary.
*/
public int succ(int port)
{
return (port < _upper ? port + 1 : _lower);
}
/**
* Binds <code>socket</socket> to <code>endpoint</code>. If the
* port in <code>endpoint</code> is zero, then a port is chosen
* from this port range. If the port range is [0,0], then a free
* port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(ServerSocket socket, InetSocketAddress endpoint)
throws IOException
{
int port = endpoint.getPort();
PortRange range = (port > 0) ? new PortRange(port) : this;
return range.bind(socket, endpoint.getAddress(), 0);
}
/**
* Binds <code>socket</socket> to <code>endpoint</code>. If the
* port in <code>endpoint</code> is zero, then a port is chosen
* from this port range. If the port range is [0,0], then a free
* port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(Socket socket, InetSocketAddress endpoint)
throws IOException
{
int port = endpoint.getPort();
PortRange range = (port > 0) ? new PortRange(port) : this;
return range.bind(socket, endpoint.getAddress());
}
/**
* Binds <code>socket</socket> to <code>address</code>. A port is
* chosen from this port range. If the port range is [0,0], then a
* free port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(ServerSocket socket, InetAddress address)
throws IOException
{
return bind(socket, address, 0);
}
/**
* Binds <code>socket</socket> to <code>address</code>. A port is
* chosen from this port range. If the port range is [0,0], then a
* free port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(ServerSocket socket, InetAddress address, int backlog)
throws IOException
{
int start = random();
int port = start;
do {
try {
socket.bind(new InetSocketAddress(address, port), backlog);
return port;
} catch (BindException e) {
}
port = succ(port);
} while (port != start);
throw new BindException("No free port within range");
}
/**
* Binds <code>socket</socket> to <code>address</code>. A port is
* chosen from this port range. If the port range is [0,0], then a
* free port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(Socket socket, InetAddress address)
throws IOException
{
int start = random();
int port = start;
do {
try {
socket.bind(new InetSocketAddress(address, port));
return port;
} catch (BindException e) {
}
port = succ(port);
} while (port != start);
throw new BindException("No free port within range");
}
/**
* Binds <code>socket</socket> to the wildcard
* <code>address</code>. A port is chosen from this port range. If
* the port range is [0,0], then a free port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(ServerSocket socket)
throws IOException
{
return bind(socket, (InetAddress) null);
}
/**
* Binds <code>socket</socket> to the wildcard
* <code>address</code>. A port is chosen from this port range. If
* the port range is [0,0], then a free port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(ServerSocket socket, int backlog)
throws IOException
{
return bind(socket, (InetAddress) null, backlog);
}
/**
* Binds <code>socket</socket> to the wildcard
* <code>address</code>. A port is chosen from this port range. If
* the port range is [0,0], then a free port is chosen by the OS.
*
* @throws IOException if the bind operation fails, or if the
* socket is already bound.
*/
public int bind(Socket socket)
throws IOException
{
return bind(socket, (InetAddress) null);
}
@Override
public String toString()
{
return String.format("%d:%d", _lower, _upper);
}
}