PageRenderTime 65ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/tools/rosgraph/src/rosgraph/network.py

https://gitlab.com/F34140r/ros_comm
Python | 416 lines | 340 code | 11 blank | 65 comment | 20 complexity | 38fad0318be5bc0814d8f9a6f4fab19e MD5 | raw file
Possible License(s): LGPL-2.1
  1. # Software License Agreement (BSD License)
  2. #
  3. # Copyright (c) 2008, Willow Garage, Inc.
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. #
  10. # * Redistributions of source code must retain the above copyright
  11. # notice, this list of conditions and the following disclaimer.
  12. # * Redistributions in binary form must reproduce the above
  13. # copyright notice, this list of conditions and the following
  14. # disclaimer in the documentation and/or other materials provided
  15. # with the distribution.
  16. # * Neither the name of Willow Garage, Inc. nor the names of its
  17. # contributors may be used to endorse or promote products derived
  18. # from this software without specific prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  23. # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  24. # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  25. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  26. # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  27. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  28. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  29. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  30. # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. # POSSIBILITY OF SUCH DAMAGE.
  32. #
  33. # Revision $Id: network.py 15125 2011-10-06 02:51:15Z kwc $
  34. """
  35. Network APIs for ROS-based systems, including IP address and ROS
  36. TCP header libraries. Because ROS-based runtimes must respect the
  37. ROS_IP and ROS_HOSTNAME environment variables, ROS-specific APIs
  38. are necessary for correctly retrieving local IP address
  39. information.
  40. """
  41. import logging
  42. import os
  43. import socket
  44. import struct
  45. import sys
  46. import platform
  47. try:
  48. from cStringIO import StringIO #Python 2.x
  49. python3 = 0
  50. except ImportError:
  51. from io import BytesIO #Python 3.x
  52. python3 = 1
  53. try:
  54. import urllib.parse as urlparse
  55. except ImportError:
  56. import urlparse
  57. from .rosenv import ROS_IP, ROS_HOSTNAME, ROS_IPV6
  58. SIOCGIFCONF = 0x8912
  59. SIOCGIFADDR = 0x8915
  60. if platform.system() == 'FreeBSD':
  61. SIOCGIFADDR = 0xc0206921
  62. if platform.architecture()[0] == '64bit':
  63. SIOCGIFCONF = 0xc0106924
  64. else:
  65. SIOCGIFCONF = 0xc0086924
  66. logger = logging.getLogger('rosgraph.network')
  67. def parse_http_host_and_port(url):
  68. """
  69. Convenience routine to handle parsing and validation of HTTP URL
  70. port due to the fact that Python only provides easy accessors in
  71. Python 2.5 and later. Validation checks that the protocol and host
  72. are set.
  73. :param url: URL to parse, ``str``
  74. :returns: hostname and port number in URL or 80 (default), ``(str, int)``
  75. :raises: :exc:`ValueError` If the url does not validate
  76. """
  77. # can't use p.port because that's only available in Python 2.5
  78. if not url:
  79. raise ValueError('not a valid URL')
  80. p = urlparse.urlparse(url)
  81. if not p[0] or not p[1]: #protocol and host
  82. raise ValueError('not a valid URL')
  83. if ':' in p[1]:
  84. hostname, port = p[1].split(':')
  85. port = int(port)
  86. else:
  87. hostname, port = p[1], 80
  88. return hostname, port
  89. def _is_unix_like_platform():
  90. """
  91. :returns: true if the platform conforms to UNIX/POSIX-style APIs
  92. @rtype: bool
  93. """
  94. #return platform.system() in ['Linux', 'Mac OS X', 'Darwin']
  95. return platform.system() in ['Linux', 'FreeBSD']
  96. def get_address_override():
  97. """
  98. :returns: ROS_IP/ROS_HOSTNAME override or None, ``str``
  99. :raises: :exc:`ValueError` If ROS_IP/ROS_HOSTNAME/__ip/__hostname are invalidly specified
  100. """
  101. # #998: check for command-line remappings first
  102. # TODO IPV6: check for compatibility
  103. for arg in sys.argv:
  104. if arg.startswith('__hostname:=') or arg.startswith('__ip:='):
  105. try:
  106. _, val = arg.split(':=')
  107. return val
  108. except: #split didn't unpack properly
  109. raise ValueError("invalid ROS command-line remapping argument '%s'"%arg)
  110. # check ROS_HOSTNAME and ROS_IP environment variables, which are
  111. # aliases for each other
  112. if ROS_HOSTNAME in os.environ:
  113. hostname = os.environ[ROS_HOSTNAME]
  114. if hostname == '':
  115. msg = 'invalid ROS_HOSTNAME (an empty string)'
  116. sys.stderr.write(msg + '\n')
  117. logger.warn(msg)
  118. else:
  119. parts = urlparse.urlparse(hostname)
  120. if parts.scheme:
  121. msg = 'invalid ROS_HOSTNAME (protocol ' + ('and port ' if parts.port else '') + 'should not be included)'
  122. sys.stderr.write(msg + '\n')
  123. logger.warn(msg)
  124. elif hostname.find(':') != -1:
  125. # this can not be checked with urlparse()
  126. # since it does not extract the port for a hostname like "foo:1234"
  127. msg = 'invalid ROS_HOSTNAME (port should not be included)'
  128. sys.stderr.write(msg + '\n')
  129. logger.warn(msg)
  130. return hostname
  131. elif ROS_IP in os.environ:
  132. ip = os.environ[ROS_IP]
  133. if ip == '':
  134. msg = 'invalid ROS_IP (an empty string)'
  135. sys.stderr.write(msg + '\n')
  136. logger.warn(msg)
  137. elif ip.find('://') != -1:
  138. msg = 'invalid ROS_IP (protocol should not be included)'
  139. sys.stderr.write(msg + '\n')
  140. logger.warn(msg)
  141. elif ip.find('.') != -1 and ip.rfind(':') > ip.rfind('.'):
  142. msg = 'invalid ROS_IP (port should not be included)'
  143. sys.stderr.write(msg + '\n')
  144. logger.warn(msg)
  145. elif ip.find('.') == -1 and ip.find(':') == -1:
  146. msg = 'invalid ROS_IP (must be a valid IPv4 or IPv6 address)'
  147. sys.stderr.write(msg + '\n')
  148. logger.warn(msg)
  149. return ip
  150. return None
  151. def is_local_address(hostname):
  152. """
  153. :param hostname: host name/address, ``str``
  154. :returns True: if hostname maps to a local address, False otherwise. False conditions include invalid hostnames.
  155. """
  156. try:
  157. reverse_ips = [host[4][0] for host in socket.getaddrinfo(hostname, 0, 0, 0, socket.SOL_TCP)]
  158. except socket.error:
  159. return False
  160. local_addresses = ['localhost'] + get_local_addresses()
  161. # 127. check is due to #1260
  162. if ([ip for ip in reverse_ips if (ip.startswith('127.') or ip == '::1')] != []) or (set(reverse_ips) & set(local_addresses) != set()):
  163. return True
  164. return False
  165. def get_local_address():
  166. """
  167. :returns: default local IP address (e.g. eth0). May be overriden by ROS_IP/ROS_HOSTNAME/__ip/__hostname, ``str``
  168. """
  169. override = get_address_override()
  170. if override:
  171. return override
  172. addrs = get_local_addresses()
  173. if len(addrs) == 1:
  174. return addrs[0]
  175. for addr in addrs:
  176. # pick first non 127/8 address
  177. if not addr.startswith('127.') and not addr == '::1':
  178. return addr
  179. else: # loopback
  180. if use_ipv6():
  181. return '::1'
  182. else:
  183. return '127.0.0.1'
  184. # cache for performance reasons
  185. _local_addrs = None
  186. def get_local_addresses():
  187. """
  188. :returns: known local addresses. Not affected by ROS_IP/ROS_HOSTNAME, ``[str]``
  189. """
  190. # cache address data as it can be slow to calculate
  191. global _local_addrs
  192. if _local_addrs is not None:
  193. return _local_addrs
  194. local_addrs = None
  195. if _is_unix_like_platform():
  196. # unix-only branch
  197. v4addrs = []
  198. v6addrs = []
  199. import netifaces
  200. for iface in netifaces.interfaces():
  201. try:
  202. ifaddrs = netifaces.ifaddresses(iface)
  203. except ValueError:
  204. # even if interfaces() returns an interface name
  205. # ifaddresses() might raise a ValueError
  206. # https://bugs.launchpad.net/ubuntu/+source/netifaces/+bug/753009
  207. continue
  208. if socket.AF_INET in ifaddrs:
  209. v4addrs.extend([addr['addr'] for addr in ifaddrs[socket.AF_INET]])
  210. if socket.AF_INET6 in ifaddrs:
  211. v6addrs.extend([addr['addr'] for addr in ifaddrs[socket.AF_INET6]])
  212. if use_ipv6():
  213. local_addrs = v6addrs + v4addrs
  214. else:
  215. local_addrs = v4addrs
  216. else:
  217. # cross-platform branch, can only resolve one address
  218. if use_ipv6():
  219. local_addrs = [host[4][0] for host in socket.getaddrinfo(socket.gethostname(), 0, 0, 0, socket.SOL_TCP)]
  220. else:
  221. local_addrs = [host[4][0] for host in socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, socket.SOL_TCP)]
  222. _local_addrs = local_addrs
  223. return local_addrs
  224. def use_ipv6():
  225. return ROS_IPV6 in os.environ and os.environ[ROS_IPV6] == 'on'
  226. def get_bind_address(address=None):
  227. """
  228. :param address: (optional) address to compare against, ``str``
  229. :returns: address TCP/IP sockets should use for binding. This is
  230. generally 0.0.0.0, but if \a address or ROS_IP/ROS_HOSTNAME is set
  231. to localhost it will return 127.0.0.1, ``str``
  232. """
  233. if address is None:
  234. address = get_address_override()
  235. if address and (address == 'localhost' or address.startswith('127.') or address == '::1' ):
  236. #localhost or 127/8
  237. if use_ipv6():
  238. return '::1'
  239. else:
  240. return '127.0.0.1' #loopback
  241. else:
  242. if use_ipv6():
  243. return '::'
  244. else:
  245. return '0.0.0.0'
  246. # #528: semi-complicated logic for determining XML-RPC URI
  247. def get_host_name():
  248. """
  249. Determine host-name for use in host-name-based addressing (e.g. XML-RPC URIs):
  250. - if ROS_IP/ROS_HOSTNAME is set, use that address
  251. - if the hostname returns a non-localhost value, use that
  252. - use whatever L{get_local_address()} returns
  253. """
  254. hostname = get_address_override()
  255. if not hostname:
  256. try:
  257. hostname = socket.gethostname()
  258. except:
  259. pass
  260. if not hostname or hostname == 'localhost' or hostname.startswith('127.'):
  261. hostname = get_local_address()
  262. return hostname
  263. def create_local_xmlrpc_uri(port):
  264. """
  265. Determine the XMLRPC URI for local servers. This handles the search
  266. logic of checking ROS environment variables, the known hostname,
  267. and local interface IP addresses to determine the best possible
  268. URI.
  269. :param port: port that server is running on, ``int``
  270. :returns: XMLRPC URI, ``str``
  271. """
  272. #TODO: merge logic in rosgraph.xmlrpc with this routine
  273. # in the future we may not want to be locked to http protocol nor root path
  274. return 'http://%s:%s/'%(get_host_name(), port)
  275. ## handshake utils ###########################################
  276. class ROSHandshakeException(Exception):
  277. """
  278. Exception to represent errors decoding handshake
  279. """
  280. pass
  281. def decode_ros_handshake_header(header_str):
  282. """
  283. Decode serialized ROS handshake header into a Python dictionary
  284. header is a list of string key=value pairs, each prefixed by a
  285. 4-byte length field. It is preceeded by a 4-byte length field for
  286. the entire header.
  287. :param header_str: encoded header string. May contain extra data at the end, ``str``
  288. :returns: key value pairs encoded in \a header_str, ``{str: str}``
  289. """
  290. (size, ) = struct.unpack('<I', header_str[0:4])
  291. size += 4 # add in 4 to include size of size field
  292. header_len = len(header_str)
  293. if size > header_len:
  294. raise ROSHandshakeException("Incomplete header. Expected %s bytes but only have %s"%((size+4), header_len))
  295. d = {}
  296. start = 4
  297. while start < size:
  298. (field_size, ) = struct.unpack('<I', header_str[start:start+4])
  299. if field_size == 0:
  300. raise ROSHandshakeException("Invalid 0-length handshake header field")
  301. start += field_size + 4
  302. if start > size:
  303. raise ROSHandshakeException("Invalid line length in handshake header: %s"%size)
  304. line = header_str[start-field_size:start]
  305. #python3 compatibility
  306. if python3 == 1:
  307. line = line.decode()
  308. idx = line.find("=")
  309. if idx < 0:
  310. raise ROSHandshakeException("Invalid line in handshake header: [%s]"%line)
  311. key = line[:idx]
  312. value = line[idx+1:]
  313. d[key.strip()] = value
  314. return d
  315. def read_ros_handshake_header(sock, b, buff_size):
  316. """
  317. Read in tcpros header off the socket \a sock using buffer \a b.
  318. :param sock: socket must be in blocking mode, ``socket``
  319. :param b: buffer to use, ``StringIO`` for Python2, ``BytesIO`` for Python 3
  320. :param buff_size: incoming buffer size to use, ``int``
  321. :returns: key value pairs encoded in handshake, ``{str: str}``
  322. :raises: :exc:`ROSHandshakeException` If header format does not match expected
  323. """
  324. header_str = None
  325. while not header_str:
  326. d = sock.recv(buff_size)
  327. if not d:
  328. raise ROSHandshakeException("connection from sender terminated before handshake header received. %s bytes were received. Please check sender for additional details."%b.tell())
  329. b.write(d)
  330. btell = b.tell()
  331. if btell > 4:
  332. # most likely we will get the full header in the first recv, so
  333. # not worth tiny optimizations possible here
  334. bval = b.getvalue()
  335. (size,) = struct.unpack('<I', bval[0:4])
  336. if btell - 4 >= size:
  337. header_str = bval
  338. # memmove the remnants of the buffer back to the start
  339. leftovers = bval[size+4:]
  340. b.truncate(len(leftovers))
  341. b.seek(0)
  342. b.write(leftovers)
  343. header_recvd = True
  344. # process the header
  345. return decode_ros_handshake_header(bval)
  346. def encode_ros_handshake_header(header):
  347. """
  348. Encode ROS handshake header as a byte string. Each header
  349. field is a string key value pair. The encoded header is
  350. prefixed by a length field, as is each field key/value pair.
  351. key/value pairs a separated by a '=' equals sign.
  352. FORMAT: (4-byte length + [4-byte field length + field=value ]*)
  353. :param header: header field keys/values, ``dict``
  354. :returns: header encoded as byte string, ``str``
  355. """
  356. fields = ["%s=%s"%(k,v) for k,v in header.items()]
  357. # in the usual configuration, the error 'TypeError: can't concat bytes to str' appears:
  358. if python3 == 0:
  359. #python 2
  360. s = ''.join(["%s%s"%(struct.pack('<I', len(f)), f) for f in fields])
  361. return struct.pack('<I', len(s)) + s
  362. else:
  363. #python 3
  364. s = b''.join([(struct.pack('<I', len(f)) + f.encode("utf-8")) for f in fields])
  365. return struct.pack('<I', len(s)) + s
  366. def write_ros_handshake_header(sock, header):
  367. """
  368. Write ROS handshake header header to socket sock
  369. :param sock: socket to write to (must be in blocking mode), ``socket.socket``
  370. :param header: header field keys/values, ``{str : str}``
  371. :returns: Number of bytes sent (for statistics), ``int``
  372. """
  373. s = encode_ros_handshake_header(header)
  374. sock.sendall(s)
  375. return len(s) #STATS