Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes #110, warns on some incorrect ROS_IP and ROS_HOSTNAME entries #134

Closed
wants to merge 8 commits into from
120 changes: 79 additions & 41 deletions tools/rosgraph/src/rosgraph/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@
import platform

try:
from cStringIO import StringIO #Python 2.x
from cStringIO import StringIO # Python 2.x
python3 = 0
except ImportError:
from io import BytesIO #Python 3.x
from io import BytesIO # Python 3.x
python3 = 1

try:
Expand Down Expand Up @@ -85,30 +85,32 @@

logger = logging.getLogger('rosgraph.network')


def parse_http_host_and_port(url):
"""
Convenience routine to handle parsing and validation of HTTP URL
port due to the fact that Python only provides easy accessors in
Python 2.5 and later. Validation checks that the protocol and host
are set.

:param url: URL to parse, ``str``
:returns: hostname and port number in URL or 80 (default), ``(str, int)``
:raises: :exc:`ValueError` If the url does not validate
"""
# can't use p.port because that's only available in Python 2.5
if not url:
raise ValueError('not a valid URL')
raise ValueError('not a valid URL')
p = urlparse.urlparse(url)
if not p[0] or not p[1]: #protocol and host
if not p[0] or not p[1]: # protocol and host
raise ValueError('not a valid URL')
if ':' in p[1]:
hostname, port = p[1].split(':')
port = int(port)
else:
else:
hostname, port = p[1], 80
return hostname, port



def _is_unix_like_platform():
"""
:returns: true if the platform conforms to UNIX/POSIX-style APIs
Expand All @@ -117,6 +119,7 @@ def _is_unix_like_platform():
#return platform.system() in ['Linux', 'Mac OS X', 'Darwin']
return platform.system() in ['Linux', 'FreeBSD']


def get_address_override():
"""
:returns: ROS_IP/ROS_HOSTNAME override or None, ``str``
Expand All @@ -129,8 +132,8 @@ def get_address_override():
try:
_, val = arg.split(':=')
return val
except: #split didn't unpack properly
raise ValueError("invalid ROS command-line remapping argument '%s'"%arg)
except: # split didn't unpack properly
raise ValueError("invalid ROS command-line remapping argument '%s'" % arg)

# check ROS_HOSTNAME and ROS_IP environment variables, which are
# aliases for each other
Expand All @@ -139,15 +142,39 @@ def get_address_override():
msg = 'invalid ROS_HOSTNAME (an empty string)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
elif os.environ[ROS_HOSTNAME].count(':') > 0:
msg = 'invalid ROS_HOSTNAME (protocols, ports and passwords should not be included)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
return os.environ[ROS_HOSTNAME]
elif ROS_IP in os.environ:
if os.environ[ROS_IP] == '':
msg = 'invalid ROS_IP (an empty string)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
return os.environ[ROS_IP]
elif os.environ[ROS_IP].count('.') == 0 and os.environ[ROS_IP].count(':') == 0:
msg = 'invalid ROS_IP (must be a valid IPv4 or IPv6 address)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
elif not set(os.environ[ROS_IP]).issubset(set('.:1234567890aAbBcCdDeEfF')):
msg = 'invalid ROS_IP (must be a valid IPv4 or IPv6 address)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
elif os.environ[ROS_IP].count('.') != 3 and os.environ[ROS_IP].count('.') > 0:
msg = 'invalid ROS_IP (valid IPv4 addresses have format 1.2.3.4)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
elif os.environ[ROS_IP].count(':') != 7 and os.environ[ROS_IP].count(':') > 0 and os.environ[ROS_IP].count('"::"') == 0:
msg = 'invalid ROS_IP (valid IPv6 addresses have format 1:2:3:4:5:6:7:8)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
elif os.environ[ROS_IP].count(':') > 7 and os.environ[ROS_IP].count('"::"') > 0:
msg = 'invalid ROS_IP (valid IPv6 addresses have format 1:2:3:4:5:6:7:8)'
sys.stderr.write(msg + '\n')
logger.warn(msg)
return None


def is_local_address(hostname):
"""
:param hostname: host name/address, ``str``
Expand All @@ -162,7 +189,8 @@ def is_local_address(hostname):
if ([ip for ip in reverse_ips if (ip.startswith('127.') or ip == '::1')] != []) or (set(reverse_ips) & set(local_addresses) != set()):
return True
return False



def get_local_address():
"""
:returns: default local IP address (e.g. eth0). May be overriden by ROS_IP/ROS_HOSTNAME/__ip/__hostname, ``str``
Expand All @@ -177,14 +205,16 @@ def get_local_address():
# pick first non 127/8 address
if not addr.startswith('127.') and not addr == '::1':
return addr
else: # loopback
else: # loopback
if use_ipv6():
return '::1'
else:
return '127.0.0.1'

# cache for performance reasons
_local_addrs = None


def get_local_addresses():
"""
:returns: known local addresses. Not affected by ROS_IP/ROS_HOSTNAME, ``[str]``
Expand All @@ -204,7 +234,8 @@ def get_local_addresses():
for i in netifaces.interfaces():
try:
local_addrs.extend([d['addr'] for d in netifaces.ifaddresses(i)[netifaces.AF_INET]])
except KeyError: pass
except KeyError:
pass
elif _is_unix_like_platform():
# unix-only branch
import ifaddrs
Expand All @@ -224,9 +255,11 @@ def get_local_addresses():
_local_addrs = local_addrs
return local_addrs


def use_ipv6():
return ROS_IPV6 in os.environ and os.environ[ROS_IPV6] == 'on'


def get_bind_address(address=None):
"""
:param address: (optional) address to compare against, ``str``
Expand All @@ -236,18 +269,19 @@ def get_bind_address(address=None):
"""
if address is None:
address = get_address_override()
if address and (address == 'localhost' or address.startswith('127.') or address == '::1' ):
if address and (address == 'localhost' or address.startswith('127.') or address == '::1'):
#localhost or 127/8
if use_ipv6():
return '::1'
else:
return '127.0.0.1' #loopback
return '127.0.0.1' # loopback
else:
if use_ipv6():
return '::'
else:
return '0.0.0.0'


# #528: semi-complicated logic for determining XML-RPC URI
def get_host_name():
"""
Expand All @@ -266,19 +300,20 @@ def get_host_name():
hostname = get_local_address()
return hostname


def create_local_xmlrpc_uri(port):
"""
Determine the XMLRPC URI for local servers. This handles the search
logic of checking ROS environment variables, the known hostname,
and local interface IP addresses to determine the best possible
URI.

:param port: port that server is running on, ``int``
:returns: XMLRPC URI, ``str``
"""
#TODO: merge logic in rosgraph.xmlrpc with this routine
# in the future we may not want to be locked to http protocol nor root path
return 'http://%s:%s/'%(get_host_name(), port)
return 'http://%s:%s/' % (get_host_name(), port)


## handshake utils ###########################################
Expand All @@ -289,50 +324,52 @@ class ROSHandshakeException(Exception):
"""
pass


def decode_ros_handshake_header(header_str):
"""
Decode serialized ROS handshake header into a Python dictionary

header is a list of string key=value pairs, each prefixed by a
4-byte length field. It is preceeded by a 4-byte length field for
the entire header.

:param header_str: encoded header string. May contain extra data at the end, ``str``
:returns: key value pairs encoded in \a header_str, ``{str: str}``
"""
(size, ) = struct.unpack('<I', header_str[0:4])
size += 4 # add in 4 to include size of size field
size += 4 # add in 4 to include size of size field
header_len = len(header_str)
if size > header_len:
raise ROSHandshakeException("Incomplete header. Expected %s bytes but only have %s"%((size+4), header_len))
raise ROSHandshakeException("Incomplete header. Expected %s bytes but only have %s" % ((size + 4), header_len))

d = {}
start = 4
while start < size:
(field_size, ) = struct.unpack('<I', header_str[start:start+4])
(field_size, ) = struct.unpack('<I', header_str[start:start + 4])
if field_size == 0:
raise ROSHandshakeException("Invalid 0-length handshake header field")
start += field_size + 4
if start > size:
raise ROSHandshakeException("Invalid line length in handshake header: %s"%size)
line = header_str[start-field_size:start]
raise ROSHandshakeException("Invalid line length in handshake header: %s" % size)
line = header_str[start - field_size:start]

#python3 compatibility
if python3 == 1:
line = line.decode()

idx = line.find("=")
if idx < 0:
raise ROSHandshakeException("Invalid line in handshake header: [%s]"%line)
raise ROSHandshakeException("Invalid line in handshake header: [%s]" % line)
key = line[:idx]
value = line[idx+1:]
value = line[idx + 1:]
d[key.strip()] = value
return d



def read_ros_handshake_header(sock, b, buff_size):
"""
Read in tcpros header off the socket \a sock using buffer \a b.

:param sock: socket must be in blocking mode, ``socket``
:param b: buffer to use, ``StringIO`` for Python2, ``BytesIO`` for Python 3
:param buff_size: incoming buffer size to use, ``int``
Expand All @@ -343,7 +380,7 @@ def read_ros_handshake_header(sock, b, buff_size):
while not header_str:
d = sock.recv(buff_size)
if not d:
raise ROSHandshakeException("connection from sender terminated before handshake header received. %s bytes were received. Please check sender for additional details."%b.tell())
raise ROSHandshakeException("connection from sender terminated before handshake header received. %s bytes were received. Please check sender for additional details." % b.tell())
b.write(d)
btell = b.tell()
if btell > 4:
Expand All @@ -353,17 +390,18 @@ def read_ros_handshake_header(sock, b, buff_size):
(size,) = struct.unpack('<I', bval[0:4])
if btell - 4 >= size:
header_str = bval

# memmove the remnants of the buffer back to the start
leftovers = bval[size+4:]
leftovers = bval[size + 4:]
b.truncate(len(leftovers))
b.seek(0)
b.write(leftovers)
header_recvd = True

# process the header
return decode_ros_handshake_header(bval)


def encode_ros_handshake_header(header):
"""
Encode ROS handshake header as a byte string. Each header
Expand All @@ -375,19 +413,20 @@ def encode_ros_handshake_header(header):

:param header: header field keys/values, ``dict``
:returns: header encoded as byte string, ``str``
"""
fields = ["%s=%s"%(k,v) for k,v in header.items()]
"""
fields = ["%s=%s" % (k, v) for k, v in header.items()]

# in the usual configuration, the error 'TypeError: can't concat bytes to str' appears:
if python3 == 0:
#python 2
s = ''.join(["%s%s"%(struct.pack('<I', len(f)), f) for f in fields])
s = ''.join(["%s%s" % (struct.pack('<I', len(f)), f) for f in fields])
return struct.pack('<I', len(s)) + s
else:
#python 3
#python 3
s = b''.join([(struct.pack('<I', len(f)) + f.encode("utf-8")) for f in fields])
return struct.pack('<I', len(s)) + s



def write_ros_handshake_header(sock, header):
"""
Write ROS handshake header header to socket sock
Expand All @@ -398,5 +437,4 @@ def write_ros_handshake_header(sock, header):
"""
s = encode_ros_handshake_header(header)
sock.sendall(s)
return len(s) #STATS

return len(s) # STATS
Loading