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
87 if not url:
88 raise ValueError('not a valid URL')
89 p = urlparse.urlparse(url)
90 if not p[0] or not p[1]:
91 raise ValueError('not a valid URL')
92 if ':' in p[1]:
93 hostname, port = p[1].split(':')
94 port = int(port)
95 else:
96 hostname, port = p[1], 80
97 return hostname, port
98
106
108 """
109 :returns: ROS_IP/ROS_HOSTNAME override or None, ``str``
110 :raises: :exc:`ValueError` If ROS_IP/ROS_HOSTNAME/__ip/__hostname are invalidly specified
111 """
112
113
114 for arg in sys.argv:
115 if arg.startswith('__hostname:=') or arg.startswith('__ip:='):
116 try:
117 _, val = arg.split(':=')
118 return val
119 except:
120 raise ValueError("invalid ROS command-line remapping argument '%s'"%arg)
121
122
123
124 if ROS_HOSTNAME in os.environ:
125 hostname = os.environ[ROS_HOSTNAME]
126 if hostname == '':
127 msg = 'invalid ROS_HOSTNAME (an empty string)'
128 sys.stderr.write(msg + '\n')
129 logger.warn(msg)
130 else:
131 parts = urlparse.urlparse(hostname)
132 if parts.scheme:
133 msg = 'invalid ROS_HOSTNAME (protocol ' + ('and port ' if parts.port else '') + 'should not be included)'
134 sys.stderr.write(msg + '\n')
135 logger.warn(msg)
136 elif hostname.find(':') != -1:
137
138
139 msg = 'invalid ROS_HOSTNAME (port should not be included)'
140 sys.stderr.write(msg + '\n')
141 logger.warn(msg)
142 return hostname
143 elif ROS_IP in os.environ:
144 ip = os.environ[ROS_IP]
145 if ip == '':
146 msg = 'invalid ROS_IP (an empty string)'
147 sys.stderr.write(msg + '\n')
148 logger.warn(msg)
149 elif ip.find('://') != -1:
150 msg = 'invalid ROS_IP (protocol should not be included)'
151 sys.stderr.write(msg + '\n')
152 logger.warn(msg)
153 elif ip.find('.') != -1 and ip.rfind(':') > ip.rfind('.'):
154 msg = 'invalid ROS_IP (port should not be included)'
155 sys.stderr.write(msg + '\n')
156 logger.warn(msg)
157 elif ip.find('.') == -1 and ip.find(':') == -1:
158 msg = 'invalid ROS_IP (must be a valid IPv4 or IPv6 address)'
159 sys.stderr.write(msg + '\n')
160 logger.warn(msg)
161 return ip
162 return None
163
165 """
166 :param hostname: host name/address, ``str``
167 :returns True: if hostname maps to a local address, False otherwise. False conditions include invalid hostnames.
168 """
169 try:
170 if use_ipv6():
171 reverse_ips = [host[4][0] for host in socket.getaddrinfo(hostname, 0, 0, 0, socket.SOL_TCP)]
172 else:
173 reverse_ips = [host[4][0] for host in socket.getaddrinfo(hostname, 0, socket.AF_INET, 0, socket.SOL_TCP)]
174 except socket.error:
175 return False
176 local_addresses = ['localhost'] + get_local_addresses()
177
178 if ([ip for ip in reverse_ips if (ip.startswith('127.') or ip == '::1')] != []) or (set(reverse_ips) & set(local_addresses) != set()):
179 return True
180 return False
181
183 """
184 :returns: default local IP address (e.g. eth0). May be overriden by ROS_IP/ROS_HOSTNAME/__ip/__hostname, ``str``
185 """
186 override = get_address_override()
187 if override:
188 return override
189 addrs = get_local_addresses()
190 if len(addrs) == 1:
191 return addrs[0]
192 for addr in addrs:
193
194 if not addr.startswith('127.') and not addr == '::1':
195 return addr
196 else:
197 if use_ipv6():
198 return '::1'
199 else:
200 return '127.0.0.1'
201
202
203 _local_addrs = None
205 """
206 :returns: known local addresses. Not affected by ROS_IP/ROS_HOSTNAME, ``[str]``
207 """
208
209 global _local_addrs
210 if _local_addrs is not None:
211 return _local_addrs
212
213 local_addrs = None
214 if _is_unix_like_platform():
215
216 v4addrs = []
217 v6addrs = []
218 import netifaces
219 for iface in netifaces.interfaces():
220 try:
221 ifaddrs = netifaces.ifaddresses(iface)
222 except ValueError:
223
224
225
226 continue
227 if socket.AF_INET in ifaddrs:
228 v4addrs.extend([addr['addr'] for addr in ifaddrs[socket.AF_INET]])
229 if socket.AF_INET6 in ifaddrs:
230 v6addrs.extend([addr['addr'] for addr in ifaddrs[socket.AF_INET6]])
231 if use_ipv6():
232 local_addrs = v6addrs + v4addrs
233 else:
234 local_addrs = v4addrs
235 else:
236
237 if use_ipv6():
238 local_addrs = [host[4][0] for host in socket.getaddrinfo(socket.gethostname(), 0, 0, 0, socket.SOL_TCP)]
239 else:
240 local_addrs = [host[4][0] for host in socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, socket.SOL_TCP)]
241 _local_addrs = local_addrs
242 return local_addrs
243
246
248 """
249 :param address: (optional) address to compare against, ``str``
250 :returns: address TCP/IP sockets should use for binding. This is
251 generally 0.0.0.0, but if \a address or ROS_IP/ROS_HOSTNAME is set
252 to localhost it will return 127.0.0.1, ``str``
253 """
254 if address is None:
255 address = get_address_override()
256 if address and (address == 'localhost' or address.startswith('127.') or address == '::1' ):
257
258 if use_ipv6():
259 return '::1'
260 elif address.startswith('127.'):
261 return address
262 else:
263 return '127.0.0.1'
264 else:
265 if use_ipv6():
266 return '::'
267 else:
268 return '0.0.0.0'
269
270
272 """
273 Determine host-name for use in host-name-based addressing (e.g. XML-RPC URIs):
274 - if ROS_IP/ROS_HOSTNAME is set, use that address
275 - if the hostname returns a non-localhost value, use that
276 - use whatever L{get_local_address()} returns
277 """
278 hostname = get_address_override()
279 if not hostname:
280 try:
281 hostname = socket.gethostname()
282 except:
283 pass
284 if not hostname or hostname == 'localhost' or hostname.startswith('127.'):
285 hostname = get_local_address()
286 return hostname
287
289 """
290 Determine the XMLRPC URI for local servers. This handles the search
291 logic of checking ROS environment variables, the known hostname,
292 and local interface IP addresses to determine the best possible
293 URI.
294
295 :param port: port that server is running on, ``int``
296 :returns: XMLRPC URI, ``str``
297 """
298
299
300 return 'http://%s:%s/'%(get_host_name(), port)
301
302
303
304
306 """
307 Exception to represent errors decoding handshake
308 """
309 pass
310
312 """
313 Decode serialized ROS handshake header into a Python dictionary
314
315 header is a list of string key=value pairs, each prefixed by a
316 4-byte length field. It is preceeded by a 4-byte length field for
317 the entire header.
318
319 :param header_str: encoded header string. May contain extra data at the end, ``str``
320 :returns: key value pairs encoded in \a header_str, ``{str: str}``
321 """
322 (size, ) = struct.unpack('<I', header_str[0:4])
323 size += 4
324 header_len = len(header_str)
325 if size > header_len:
326 raise ROSHandshakeException("Incomplete header. Expected %s bytes but only have %s"%((size+4), header_len))
327
328 d = {}
329 start = 4
330 while start < size:
331 (field_size, ) = struct.unpack('<I', header_str[start:start+4])
332 if field_size == 0:
333 raise ROSHandshakeException("Invalid 0-length handshake header field")
334 start += field_size + 4
335 if start > size:
336 raise ROSHandshakeException("Invalid line length in handshake header: %s"%size)
337 line = header_str[start-field_size:start]
338
339
340 if python3 == 1:
341 line = line.decode()
342
343 idx = line.find("=")
344 if idx < 0:
345 raise ROSHandshakeException("Invalid line in handshake header: [%s]"%line)
346 key = line[:idx]
347 value = line[idx+1:]
348 d[key.strip()] = value
349 return d
350
352 """
353 Read in tcpros header off the socket \a sock using buffer \a b.
354
355 :param sock: socket must be in blocking mode, ``socket``
356 :param b: buffer to use, ``StringIO`` for Python2, ``BytesIO`` for Python 3
357 :param buff_size: incoming buffer size to use, ``int``
358 :returns: key value pairs encoded in handshake, ``{str: str}``
359 :raises: :exc:`ROSHandshakeException` If header format does not match expected
360 """
361 header_str = None
362 while not header_str:
363 d = sock.recv(buff_size)
364 if not d:
365 raise ROSHandshakeException("connection from sender terminated before handshake header received. %s bytes were received. Please check sender for additional details."%b.tell())
366 b.write(d)
367 btell = b.tell()
368 if btell > 4:
369
370
371 bval = b.getvalue()
372 (size,) = struct.unpack('<I', bval[0:4])
373 if btell - 4 >= size:
374 header_str = bval
375
376
377 leftovers = bval[size+4:]
378 b.truncate(len(leftovers))
379 b.seek(0)
380 b.write(leftovers)
381 header_recvd = True
382
383
384 return decode_ros_handshake_header(bval)
385
387 """
388 Encode ROS handshake header as a byte string. Each header
389 field is a string key value pair. The encoded header is
390 prefixed by a length field, as is each field key/value pair.
391 key/value pairs a separated by a '=' equals sign.
392
393 FORMAT: (4-byte length + [4-byte field length + field=value ]*)
394
395 :param header: header field keys/values, ``dict``
396 :returns: header encoded as byte string, ``bytes``
397 """
398 str_cls = str if python3 else unicode
399
400
401 encoded_header = {}
402 for k, v in header.items():
403 if isinstance(k, str_cls):
404 k = k.encode('utf-8')
405 if isinstance(v, str_cls):
406 v = v.encode('utf-8')
407 encoded_header[k] = v
408
409 fields = [k + b"=" + v for k, v in sorted(encoded_header.items())]
410 s = b''.join([struct.pack('<I', len(f)) + f for f in fields])
411
412 return struct.pack('<I', len(s)) + s
413
415 """
416 Write ROS handshake header header to socket sock
417
418 :param sock: socket to write to (must be in blocking mode), ``socket.socket``
419 :param header: header field keys/values, ``{str : str}``
420 :returns: Number of bytes sent (for statistics), ``int``
421 """
422 s = encode_ros_handshake_header(header)
423 sock.sendall(s)
424 return len(s)
425