1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 """
36 Network APIs for ROS-based systems, including IP address and ROS
37 TCP header libraries. Because ROS-based runtimes must respect the
38 ROS_IP and ROS_HOSTNAME environment variables, ROS-specific APIs
39 are necessary for correctly retrieving local IP address
40 information.
41 """
42
43 import logging
44 import os
45 import socket
46 import struct
47 import sys
48 import platform
49
50 try:
51 from cStringIO import StringIO
52 python3 = 0
53 except ImportError:
54 from io import BytesIO
55 python3 = 1
56
57 try:
58 import urllib.parse as urlparse
59 except ImportError:
60 import urlparse
61
62 from .rosenv import ROS_IP, ROS_HOSTNAME, ROS_IPV6
63
64 SIOCGIFCONF = 0x8912
65 SIOCGIFADDR = 0x8915
66 if platform.system() == 'FreeBSD':
67 SIOCGIFADDR = 0xc0206921
68 if platform.architecture()[0] == '64bit':
69 SIOCGIFCONF = 0xc0106924
70 else:
71 SIOCGIFCONF = 0xc0086924
72
73 logger = logging.getLogger('rosgraph.network')
74
76 """
77 Convenience routine to handle parsing and validation of HTTP URL
78 port due to the fact that Python only provides easy accessors in
79 Python 2.5 and later. Validation checks that the protocol and host
80 are set.
81
82 :param url: URL to parse, ``str``
83 :returns: hostname and port number in URL or 80 (default), ``(str, int)``
84 :raises: :exc:`ValueError` If the url does not validate
85 """
86 if not url:
87 raise ValueError('not a valid URL')
88 p = urlparse.urlparse(url)
89 if not p.scheme or not p.hostname:
90 raise ValueError('not a valid URL')
91 port = p.port if p.port else 80
92 return p.hostname, port
93
100
102 """
103 :returns: ROS_IP/ROS_HOSTNAME override or None, ``str``
104 :raises: :exc:`ValueError` If ROS_IP/ROS_HOSTNAME/__ip/__hostname are invalidly specified
105 """
106
107
108 for arg in sys.argv:
109 if arg.startswith('__hostname:=') or arg.startswith('__ip:='):
110 try:
111 _, val = arg.split(':=')
112 return val
113 except:
114 raise ValueError("invalid ROS command-line remapping argument '%s'"%arg)
115
116
117
118 if ROS_HOSTNAME in os.environ:
119 hostname = os.environ[ROS_HOSTNAME]
120 if hostname == '':
121 msg = 'invalid ROS_HOSTNAME (an empty string)'
122 sys.stderr.write(msg + '\n')
123 logger.warn(msg)
124 else:
125 parts = urlparse.urlparse(hostname)
126 if parts.scheme:
127 msg = 'invalid ROS_HOSTNAME (protocol ' + ('and port ' if parts.port else '') + 'should not be included)'
128 sys.stderr.write(msg + '\n')
129 logger.warn(msg)
130 elif hostname.find(':') != -1:
131
132
133 msg = 'invalid ROS_HOSTNAME (port should not be included)'
134 sys.stderr.write(msg + '\n')
135 logger.warn(msg)
136 return hostname
137 elif ROS_IP in os.environ:
138 ip = os.environ[ROS_IP]
139 if ip == '':
140 msg = 'invalid ROS_IP (an empty string)'
141 sys.stderr.write(msg + '\n')
142 logger.warn(msg)
143 elif ip.find('://') != -1:
144 msg = 'invalid ROS_IP (protocol should not be included)'
145 sys.stderr.write(msg + '\n')
146 logger.warn(msg)
147 elif ip.find('.') != -1 and ip.rfind(':') > ip.rfind('.'):
148 msg = 'invalid ROS_IP (port should not be included)'
149 sys.stderr.write(msg + '\n')
150 logger.warn(msg)
151 elif ip.find('.') == -1 and ip.find(':') == -1:
152 msg = 'invalid ROS_IP (must be a valid IPv4 or IPv6 address)'
153 sys.stderr.write(msg + '\n')
154 logger.warn(msg)
155 return ip
156 return None
157
159 """
160 :param hostname: host name/address, ``str``
161 :returns True: if hostname maps to a local address, False otherwise. False conditions include invalid hostnames.
162 """
163 try:
164 if use_ipv6():
165 reverse_ips = [host[4][0] for host in socket.getaddrinfo(hostname, 0, 0, 0, socket.SOL_TCP)]
166 else:
167 reverse_ips = [host[4][0] for host in socket.getaddrinfo(hostname, 0, socket.AF_INET, 0, socket.SOL_TCP)]
168 except socket.error:
169 return False
170 local_addresses = ['localhost'] + get_local_addresses()
171
172 if ([ip for ip in reverse_ips if (ip.startswith('127.') or ip == '::1')] != []) or (set(reverse_ips) & set(local_addresses) != set()):
173 return True
174 return False
175
177 """
178 :returns: default local IP address (e.g. eth0). May be overridden by ROS_IP/ROS_HOSTNAME/__ip/__hostname, ``str``
179 """
180 override = get_address_override()
181 if override:
182 return override
183 addrs = get_local_addresses()
184 if len(addrs) == 1:
185 return addrs[0]
186 for addr in addrs:
187
188 if not addr.startswith('127.') and not addr == '::1':
189 return addr
190 else:
191 if use_ipv6():
192 return '::1'
193 else:
194 return '127.0.0.1'
195
196
197 _local_addrs = None
199 """
200 :returns: known local addresses. Not affected by ROS_IP/ROS_HOSTNAME, ``[str]``
201 """
202
203 global _local_addrs
204 if _local_addrs is not None:
205 return _local_addrs
206
207 local_addrs = None
208 if _is_unix_like_platform():
209
210 v4addrs = []
211 v6addrs = []
212 import netifaces
213 for iface in netifaces.interfaces():
214 try:
215 ifaddrs = netifaces.ifaddresses(iface)
216 except ValueError:
217
218
219
220 continue
221 if socket.AF_INET in ifaddrs:
222 v4addrs.extend([addr['addr'] for addr in ifaddrs[socket.AF_INET]])
223 if socket.AF_INET6 in ifaddrs:
224 v6addrs.extend([addr['addr'] for addr in ifaddrs[socket.AF_INET6]])
225 if use_ipv6():
226 local_addrs = v6addrs + v4addrs
227 else:
228 local_addrs = v4addrs
229 else:
230
231 if use_ipv6():
232 local_addrs = [host[4][0] for host in socket.getaddrinfo(socket.gethostname(), 0, 0, 0, socket.SOL_TCP)]
233 else:
234 local_addrs = [host[4][0] for host in socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, socket.SOL_TCP)]
235 _local_addrs = local_addrs
236 return local_addrs
237
240
242 """
243 :param address: (optional) address to compare against, ``str``
244 :returns: address TCP/IP sockets should use for binding. This is
245 generally 0.0.0.0, but if \a address or ROS_IP/ROS_HOSTNAME is set
246 to localhost it will return 127.0.0.1, ``str``
247 """
248 if address is None:
249 address = get_address_override()
250 if address and (address == 'localhost' or address.startswith('127.') or address == '::1' ):
251
252 if use_ipv6():
253 return '::1'
254 elif address.startswith('127.'):
255 return address
256 else:
257 return '127.0.0.1'
258 else:
259 if use_ipv6():
260 return '::'
261 else:
262 return '0.0.0.0'
263
264
266 """
267 Determine host-name for use in host-name-based addressing (e.g. XML-RPC URIs):
268 - if ROS_IP/ROS_HOSTNAME is set, use that address
269 - if the hostname returns a non-localhost value, use that
270 - use whatever L{get_local_address()} returns
271 """
272 hostname = get_address_override()
273 if not hostname:
274 try:
275 hostname = socket.gethostname()
276 except:
277 pass
278 if not hostname or hostname == 'localhost' or hostname.startswith('127.'):
279 hostname = get_local_address()
280 return hostname
281
283 """
284 Determine the XMLRPC URI for local servers. This handles the search
285 logic of checking ROS environment variables, the known hostname,
286 and local interface IP addresses to determine the best possible
287 URI.
288
289 :param port: port that server is running on, ``int``
290 :returns: XMLRPC URI, ``str``
291 """
292
293
294 return 'http://%s:%s/'%(get_host_name(), port)
295
296
297
298
300 """
301 Exception to represent errors decoding handshake
302 """
303 pass
304
306 """
307 Decode serialized ROS handshake header into a Python dictionary
308
309 header is a list of string key=value pairs, each prefixed by a
310 4-byte length field. It is preceded by a 4-byte length field for
311 the entire header.
312
313 :param header_str: encoded header string. May contain extra data at the end, ``str``
314 :returns: key value pairs encoded in \a header_str, ``{str: str}``
315 """
316 (size, ) = struct.unpack('<I', header_str[0:4])
317 size += 4
318 header_len = len(header_str)
319 if size > header_len:
320 raise ROSHandshakeException("Incomplete header. Expected %s bytes but only have %s"%((size+4), header_len))
321
322 d = {}
323 start = 4
324 while start < size:
325 (field_size, ) = struct.unpack('<I', header_str[start:start+4])
326 if field_size == 0:
327 raise ROSHandshakeException("Invalid 0-length handshake header field")
328 start += field_size + 4
329 if start > size:
330 raise ROSHandshakeException("Invalid line length in handshake header: %s"%size)
331 line = header_str[start-field_size:start]
332
333
334 if python3 == 1:
335 line = line.decode()
336
337 idx = line.find("=")
338 if idx < 0:
339 raise ROSHandshakeException("Invalid line in handshake header: [%s]"%line)
340 key = line[:idx]
341 value = line[idx+1:]
342 d[key.strip()] = value
343 return d
344
346 """
347 Read in tcpros header off the socket \a sock using buffer \a b.
348
349 :param sock: socket must be in blocking mode, ``socket``
350 :param b: buffer to use, ``StringIO`` for Python2, ``BytesIO`` for Python 3
351 :param buff_size: incoming buffer size to use, ``int``
352 :returns: key value pairs encoded in handshake, ``{str: str}``
353 :raises: :exc:`ROSHandshakeException` If header format does not match expected
354 """
355 header_str = None
356 while not header_str:
357 d = sock.recv(buff_size)
358 if not d:
359 raise ROSHandshakeException("connection from sender terminated before handshake header received. %s bytes were received. Please check sender for additional details."%b.tell())
360 b.write(d)
361 btell = b.tell()
362 if btell > 4:
363
364
365 bval = b.getvalue()
366 (size,) = struct.unpack('<I', bval[0:4])
367 if btell - 4 >= size:
368 header_str = bval
369
370
371 leftovers = bval[size+4:]
372 b.truncate(len(leftovers))
373 b.seek(0)
374 b.write(leftovers)
375 header_recvd = True
376
377
378 return decode_ros_handshake_header(bval)
379
381 """
382 Encode ROS handshake header as a byte string. Each header
383 field is a string key value pair. The encoded header is
384 prefixed by a length field, as is each field key/value pair.
385 key/value pairs a separated by a '=' equals sign.
386
387 FORMAT: (4-byte length + [4-byte field length + field=value ]*)
388
389 :param header: header field keys/values, ``dict``
390 :returns: header encoded as byte string, ``bytes``
391 """
392 str_cls = str if python3 else unicode
393
394
395 encoded_header = {}
396 for k, v in header.items():
397 if isinstance(k, str_cls):
398 k = k.encode('utf-8')
399 if isinstance(v, str_cls):
400 v = v.encode('utf-8')
401 encoded_header[k] = v
402
403 fields = [k + b"=" + v for k, v in sorted(encoded_header.items())]
404 s = b''.join([struct.pack('<I', len(f)) + f for f in fields])
405
406 return struct.pack('<I', len(s)) + s
407
409 """
410 Write ROS handshake header header to socket sock
411
412 :param sock: socket to write to (must be in blocking mode), ``socket.socket``
413 :param header: header field keys/values, ``{str : str}``
414 :returns: Number of bytes sent (for statistics), ``int``
415 """
416 s = encode_ros_handshake_header(header)
417 sock.sendall(s)
418 return len(s)
419