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