-
Notifications
You must be signed in to change notification settings - Fork 53
/
emulcomm.py
executable file
·2331 lines (1779 loc) · 67 KB
/
emulcomm.py
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Author: Justin Cappos, Armon Dadgar
Start Date: 27 June 2008
Description:
This is a collection of communications routines that provide a programmer
with a reasonable environment. This is used by repy.py to provide a
highly restricted (but usable) environment.
"""
import socket
# Armon: Used to check if a socket is ready
import select
# socket uses getattr and setattr. We need to make these available to it...
socket.getattr = getattr
socket.setattr = setattr
# needed to set threads for recvmess and waitforconn
import threading
# threading in python2.7 uses hasattr. It needs to be made available.
threading.hasattr = hasattr
# So I can exit all threads when an error occurs or do select
import harshexit
# Needed for finding out info about sockets, available interfaces, etc
import nonportable
# So I can print a clean traceback when an error happens
import tracebackrepy
# accounting
# id(sock) will be used to register and unregister sockets with nanny
import nanny
# give me uniqueIDs for the comminfo table
import idhelper
# for sleep
import time
# Armon: Used for decoding the error messages
import errno
# Armon: Used for getting the constant IP values for resolving our external IP
import repy_constants
# Get the exceptions
from exception_hierarchy import *
###### Module Data
# This is a dictionary of all currently bound sockets. Since multiple
# UDP bindings on a single port is hairy, we store bound sockets
# here, and use them for both sending and receiving if they are
# available. This feels slightly byzantine, but it allows us to
# avoid modifying the repy API. See also SeattleTestbed/repy_v2#9.
#
# Format of entries is as follows:
# Key - 3-tuple of ("UDP", IP, Port)
# Val - Bound socket object
_BOUND_SOCKETS = {}
# If we have a preference for an IP/Interface this flag is set to True
user_ip_interface_preferences = False
# Do we allow non-specified IPs
allow_nonspecified_ips = True
# Armon: Specified the list of allowed IP and Interfaces in order of their preference
# The basic structure is list of tuples (IP, Value), IP is True if its an IP, False if its an interface
user_specified_ip_interface_list = []
# This list caches the allowed IP's
# It is updated at the launch of repy, or by calls to getmyip and update_ip_cache
# NOTE: The loopback address 127.0.0.1 is always permitted. update_ip_cache will always add this
# if it is not specified explicitly by the user
allowediplist = []
cachelock = threading.Lock() # This allows only a single simultaneous cache update
# Trying to send a UDP datagram of more than MAX_ALLOWABLE_DGRAM_SIZE
# bytes will generate uncatchable exceptions on Mac OS X, see
# SeattleTestbed/repy_v2#113.
#
# On most platforms, UDP over IPv4 can possibly handle: 65,507 bytes =
# 65,535 bytes (per its 16 bit "length" field)
# - 8 bytes for the UDP header
# -20 bytes for the IPv4 header.
#
# The limit on OS X is instead 9216, which you can see by executing
# `sysctl net.inet.udp.maxdgram`
# on an OS X system. Because we value consistency across platforms
# and think that behavior should be indistinguishable across platforms,
# we apply this limitation to all platforms, so 9217 and above should
# fail via RepyArgumentError from sendmessage.
#
# IPv6 jumbograms allow for UDP datagrams larger than that, but we ignore
# this for now. For reference, see Section 4 of RFC 2675.
MAX_ALLOWABLE_DGRAM_SIZE = 9216
##### Internal Functions
# Determines if a specified IP address is allowed in the context of user settings
def _ip_is_allowed(ip):
"""
<Purpose>
Determines if a given IP is allowed, by checking against the cached allowed IP's.
<Arguments>
ip: The IP address to search for.
<Returns>
True, if allowed. False, otherwise.
"""
global allowediplist
global user_ip_interface_preferences
global allow_nonspecified_ips
# If there is no preference, anything goes
# same with allow_nonspecified_ips
if not user_ip_interface_preferences or allow_nonspecified_ips:
return True
# Check the list of allowed IP's
return (ip in allowediplist)
# Only appends the elem to lst if the elem is unique
def _unique_append(lst, elem):
if elem not in lst:
lst.append(elem)
# This function updates the allowed IP cache
# It iterates through all possible IP's and stores ones which are bindable as part of the allowediplist
def update_ip_cache():
global allowediplist
global user_ip_interface_preferences
global user_specified_ip_interface_list
global allow_nonspecified_ips
# If there is no preference, this is a no-op
if not user_ip_interface_preferences:
return
# Acquire the lock to update the cache
cachelock.acquire()
# If there is any exception release the cachelock
try:
# Stores the IP's
allowed_list = []
# Iterate through the allowed list, handle each element
for (is_ip_addr, value) in user_specified_ip_interface_list:
# Handle normal IP's
if is_ip_addr:
_unique_append(allowed_list, value)
# Handle interfaces
else:
try:
# Get the IP's associated with the NIC
interface_ips = nonportable.os_api.get_interface_ip_addresses(value)
for interface_ip in interface_ips:
_unique_append(allowed_list, interface_ip)
except:
# Catch exceptions if the NIC does not exist
pass
# This will store all the IP's that we are able to bind to
bindable_list = []
# Try binding to every ip
for ip in allowed_list:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.bind((ip,0))
except:
pass # Not a good ip, skip it
else:
bindable_list.append(ip) # This is a good ip, store it
finally:
sock.close()
# Add loopback
_unique_append(bindable_list, "127.0.0.1")
# Update the global cache
allowediplist = bindable_list
finally:
# Release the lock
cachelock.release()
############## General Purpose socket functions ##############
def _is_already_connected_exception(exceptionobj):
"""
<Purpose>
Determines if a given error number indicates that the socket
is already connected.
<Arguments>
An exception object from a network call.
<Returns>
True if already connected, false otherwise
"""
# Get the type
exception_type = type(exceptionobj)
# Only continue if the type is socket.error
if exception_type is not socket.error:
return False
# Get the error number
errnum = exceptionobj[0]
# Store a list of error messages meaning we are connected
connected_errors = ["EISCONN", "WSAEISCONN"]
# Convert the errno to and error string name
try:
errname = errno.errorcode[errnum]
except Exception,e:
# The error is unknown for some reason...
errname = None
# Return if the error name is in our white list
return (errname in connected_errors)
def _is_addr_in_use_exception(exceptionobj):
"""
<Purpose>
Determines if a given error number indicates that the provided
localip / localport are already bound and that the unique
tuple is already in use.
<Arguments>
An exception object from a network call.
<Returns>
True if already in use, false otherwise
"""
# Get the type
exception_type = type(exceptionobj)
# Only continue if the type is socket.error
if exception_type is not socket.error:
return False
# Get the error number
errnum = exceptionobj[0]
# Store a list of error messages meaning we are in use
in_use_errors = ["EADDRINUSE", "WSAEADDRINUSE"]
# Convert the errno to and error string name
try:
errname = errno.errorcode[errnum]
except Exception,e:
# The error is unknown for some reason...
errname = None
# Return if the error name is in our white list
return (errname in in_use_errors)
def _is_addr_unavailable_exception(exceptionobj):
"""
<Purpose>
Determines if a given error number indicates that the provided
localip is not available during a bind() call.
This indicates an AddressBindingError should be raised.
<Arguments>
An exception object from a network call.
<Returns>
True if already in use, false otherwise
"""
# Get the type
exception_type = type(exceptionobj)
# Only continue if the type is socket.error
if exception_type is not socket.error:
return False
# Get the error number
errnum = exceptionobj[0]
# Store a list of error messages meaning the address is not available
not_avail_errors = ["EADDRNOTAVAIL", "WSAEADDRNOTAVAIL"]
# Convert the errno to and error string name
try:
errname = errno.errorcode[errnum]
except Exception,e:
# The error is unknown for some reason...
errname = None
# Return if the error name is in our white list
return (errname in not_avail_errors)
def _is_conn_refused_exception(exceptionobj):
"""
<Purpose>
Determines if a given error number indicates that the remote
host has actively refused the connection. E.g.
ECONNREFUSED
<Arguments>
An exception object from a network call.
<Returns>
True if the error indicates the connection was refused, false otherwise
"""
# Get the type
exception_type = type(exceptionobj)
# Only continue if the type is socket.error
if exception_type is not socket.error:
return False
# Get the error number
errnum = exceptionobj[0]
# Store a list of error messages meaning the host refused
refused_errors = ["ECONNREFUSED", "WSAECONNREFUSED"]
# Convert the errno to and error string name
try:
errname = errno.errorcode[errnum]
except Exception,e:
# The error is unknown for some reason...
errname = None
# Return if the error name is in our white list
return (errname in refused_errors)
def _is_conn_aborted_exception(exceptionobj):
"""
<Purpose>
Determines if a given error number indicates that a
connection abort on the socket occurred.
<Arguments>
An exception object from a network call.
<Returns>
True if connection aborted, false otherwise
"""
# Get the type
exception_type = type(exceptionobj)
# Only continue if the type is socket.error
if exception_type is not socket.error:
return False
# Get the error number
errnum = exceptionobj[0]
# Store a list of error messages meaning we are connected
aborted_errors = ["ECONNABORTED", "WSAECONNABORTED"]
# Convert the errno to and error string name
try:
errname = errno.errorcode[errnum]
except Exception,e:
# The error is unknown for some reason...
errname = None
# Return if the error name is in our white list
return (errname in aborted_errors)
def _is_network_down_exception(exceptionobj):
"""
<Purpose>
Determines if a given error number indicates that the
network is down.
<Arguments>
An exception object from a network call.
<Returns>
True if the network is down, false otherwise
"""
# Get the type
exception_type = type(exceptionobj)
# Only continue if the type is socket.error
if exception_type is not socket.error:
return False
# Get the error number
errnum = exceptionobj[0]
# These error messages mean we are disconnected.
# (The "WSA" ones are the Windows Sockets API's renditions of
# the Linux/BSD errno.h preprocessor macros).
net_down_errors = ["ENETDOWN", "EHOSTUNREACH", "ENETUNREACH",
"WSAENETDOWN", "WSAEHOSTUNREACH", "WSAENETUNREACH"]
# Convert the errno to and error string name
try:
errname = errno.errorcode[errnum]
except Exception,e:
# The error is unknown for some reason...
errname = None
# Return if the error name is in our white list
return (errname in net_down_errors)
def _is_recoverable_network_exception(exceptionobj):
"""
<Purpose>
Determines if a given error number is recoverable or fatal.
<Arguments>
An exception object from a network call.
<Returns>
True if potentially recoverable, False if fatal.
"""
# Get the type
exception_type = type(exceptionobj)
# socket.timeout is recoverable always
if exception_type == socket.timeout:
return True
# Only continue if the type is socket.error or select.error
elif exception_type != socket.error and exception_type != select.error:
return False
# Get the error number
errnum = exceptionobj[0]
# Store a list of recoverable error numbers
recoverable_errors = ["EINTR","EAGAIN","EBUSY","EWOULDBLOCK","ETIMEDOUT","ERESTART",
"WSAEINTR","WSAEWOULDBLOCK","WSAETIMEDOUT","EALREADY","WSAEALREADY",
"EINPROGRESS","WSAEINPROGRESS"]
# Convert the errno to and error string name
try:
errname = errno.errorcode[errnum]
except Exception,e:
# The error is unknown for some reason...
errname = None
# Return if the error name is in our white list
return (errname in recoverable_errors)
# Determines based on exception if the connection has been terminated
def _is_terminated_connection_exception(exceptionobj):
"""
<Purpose>
Determines if the exception is indicated the connection is terminated.
<Arguments>
An exception object from a network call.
<Returns>
True if the connection is terminated, False otherwise.
False means we could not determine with certainty if the socket is closed.
"""
# Get the type
exception_type = type(exceptionobj)
# We only want to continue if it is socket.error or select.error
if exception_type != socket.error and exception_type != select.error:
return False
# Get the error number
errnum = exceptionobj[0]
# Store a list of errors which indicate connection closed
connection_closed_errors = ["EPIPE","EBADF","EBADR","ENOLINK","EBADFD","ENETRESET",
"ECONNRESET","WSAEBADF","WSAENOTSOCK","WSAECONNRESET",]
# Convert the errnum to an error string
try:
errname = errno.errorcode[errnum]
except:
# The error number is not defined...
errname = None
# Return whether the errname is in our pre-defined list
return (errname in connection_closed_errors)
# Armon: This is used for semantics, to determine if we have a valid IP.
def _is_valid_ip_address(ipaddr):
"""
<Purpose>
Determines if ipaddr is a valid IP address.
0.X and 224-255.X addresses are not allowed.
Additionally, 192.168.0.0 is not allowed.
<Arguments>
ipaddr: String to check for validity. (It will check that this is a string).
<Returns>
True if a valid IP, False otherwise.
"""
# Argument must be of the string type
if not type(ipaddr) == str:
return False
if ipaddr == '192.168.0.0':
return False
# A valid IP should have 4 segments, explode on the period
octets = ipaddr.split(".")
# Check that we have 4 parts
if len(octets) != 4:
return False
# Check that each segment is a number between 0 and 255 inclusively.
for octet in octets:
# Attempt to convert to an integer
try:
ipnumber = int(octet)
except ValueError:
# There was an error converting to an integer, not an IP
return False
# IP addresses octets must be between 0 and 255
if not (ipnumber >= 0 and ipnumber <= 255):
return False
# should not have a ValueError (I already checked)
firstipnumber = int(octets[0])
# IP addresses with the first octet 0 refer to all local IPs. These are
# not allowed
if firstipnumber == 0:
return False
# IP addresses with the first octet >=224 are either Multicast or reserved.
# These are not allowed
if firstipnumber >= 224:
return False
# At this point, assume the IP is valid
return True
# Armon: This is used for semantics, to determine if the given port is valid
def _is_valid_network_port(port):
"""
<Purpose>
Determines if a given network port is valid.
<Arguments>
port: A numeric type (this will be checked) port number.
<Returns>
True if valid, False otherwise.
"""
# Check the type is int or long
if not (type(port) == long or type(port) == int):
return False
if port >= 1 and port <= 65535:
return True
else:
return False
# Used to decide if an IP is the loopback IP or not. This is needed for
# accounting
def _is_loopback_ipaddr(host):
if not host.startswith('127.'):
return False
if len(host.split('.')) != 4:
return False
octets = host.split('.')
if len(octets) != 4:
return False
for octet in octets:
try:
if int(octet) > 255 or int(octet) < 0:
return False
except ValueError:
return False
return True
# Checks if binding to the local port is allowed
# type should be "TCP" or "UDP".
def _is_allowed_localport(type, localport):
# Switch to the proper resource
if type == "TCP":
resource = "connport"
elif type == "UDP":
resource = "messport"
else:
raise InternalRepyError("Bad type specified for _is_allowed_localport()")
# Check what is allowed by nanny
return nanny.is_item_allowed(resource, float(localport))
######################### Simple Public Functions ##########################
# Public interface
def gethostbyname(name):
"""
<Purpose>
Provides information about a hostname. Calls socket.gethostbyname().
Translate a host name to IPv4 address format. The IPv4 address is
returned as a string, such as '100.50.200.5'. If the host name is an
IPv4 address itself it is returned unchanged.
<Arguments>
name:
The host name to translate.
<Exceptions>
RepyArgumentError (descends from NetworkError) if the name is not a string
NetworkAddressError (descends from NetworkError) if the address cannot
be resolved.
<Side Effects>
None.
<Resource Consumption>
This operation consumes network bandwidth of 4K netrecv, 1K netsend.
(It's hard to tell how much was actually sent / received at this level.)
<Returns>
The IPv4 address as a string.
"""
if type(name) is not str:
raise RepyArgumentError("gethostbyname() takes a string as argument.")
# charge 4K for a look up... I don't know the right number, but we should
# charge something. We'll always charge to the netsend interface...
nanny.tattle_quantity('netsend', 1024)
nanny.tattle_quantity('netrecv', 4096)
try:
return socket.gethostbyname(name)
except socket.gaierror:
raise NetworkAddressError("The hostname '"+name+"' could not be resolved.")
# Public interface
def getmyip():
"""
<Purpose>
Provides the IP of this computer on its public facing interface.
Does some clever trickery.
<Arguments>
None
<Exceptions>
InternetConnectivityError is the host is not connected to the internet.
<Side Effects>
None.
<Resource Consumption>
This operations consumes 256 netsend and 128 netrecv.
<Returns>
The localhost's IP address
"""
# Charge for the resources
nanny.tattle_quantity("netsend", 256)
nanny.tattle_quantity("netrecv", 128)
# I got some of this from: http://groups.google.com/group/comp.lang.python/browse_thread/thread/d931cdc326d7032b?hl=en
# Update the cache and return the first allowed IP
# Only if a preference is set
if user_ip_interface_preferences:
update_ip_cache()
# Return the first allowed ip, there is always at least 1 element (loopback)
return allowediplist[0]
# Initialize these to None, so we can detect a failure
myip = None
# It's possible on some platforms (Windows Mobile) that the IP will be
# 0.0.0.0 even when I have a public IP and the external IP is up. However, if
# I get a real connection with SOCK_STREAM, then I should get the real
# answer.
# Try each stable IP
for ip_addr in repy_constants.STABLE_PUBLIC_IPS:
try:
# Try to resolve using the current connection type and
# stable IP, using port 80 since some platforms panic
# when given 0 (FreeBSD)
myip = _get_localIP_to_remoteIP(socket.SOCK_DGRAM, ip_addr, 80)
except (socket.error, socket.timeout):
# We can ignore any networking related errors, since we want to try
# the other connection types and IP addresses. If we fail,
# we will eventually raise an exception anyways.
pass
else:
# Return immediately if the IP address is good
if _is_valid_ip_address(myip):
return myip
# Since we haven't returned yet, we must have failed.
# Raise an exception, we must not be connected to the internet
raise InternetConnectivityError("Cannot detect a connection to the Internet.")
def _get_localIP_to_remoteIP(connection_type, external_ip, external_port=80):
"""
<Purpose>
Resolve the local ip used when connecting outbound to an external ip.
<Arguments>
connection_type:
The type of connection to attempt. See socket.socket().
external_ip:
The external IP to attempt to connect to.
external_port:
The port on the remote host to attempt to connect to.
<Exceptions>
As with socket.socket(), socketobj.connect(), etc.
<Returns>
The locally assigned IP for the connection.
"""
# Open a socket
sockobj = socket.socket(socket.AF_INET, connection_type)
# Make sure that the socket obj doesn't hang forever in
# case connect() is blocking. Fix to SeattleTestbed/repy_v2#7.
sockobj.settimeout(1.0)
try:
sockobj.connect((external_ip, external_port))
# Get the local connection information for this socket
(myip, localport) = sockobj.getsockname()
# Always close the socket
finally:
sockobj.close()
return myip
###################### Shared message / connection items ###################
# Armon: How frequently should we check for the availability of the socket?
RETRY_INTERVAL = 0.2 # In seconds
def _cleanup_socket(self):
"""
<Purpose>
Internal cleanup method for open sockets. The socket
lock for the socket should be acquired prior to
calling.
<Arguments>
None
<Side Effects>
The insocket/outsocket handle will be released.
<Exceptions>
InternalRepyError is raised if the socket lock is not held
prior to calling the function.
<Returns>
None
"""
sock = self.socketobj
socket_lock = self.sock_lock
# Make sure the lock is already acquired
# BUG: We don't know which thread exactly acquired the lock.
if socket_lock.acquire(False):
socket_lock.release()
raise InternalRepyError("Socket lock should be acquired before calling _cleanup_socket!")
if (sock == None):
# Already cleaned up
return
# Shutdown the socket for writing prior to close
# to unblock any threads that are writing
try:
sock.shutdown(socket.SHUT_WR)
except:
pass
# Close the socket
try:
sock.close()
except:
pass
# socket id is used to unregister socket with nanny
sockid = id(sock)
# Re-store resources
nanny.tattle_remove_item('insockets', sockid)
nanny.tattle_remove_item('outsockets', sockid)
####################### Message sending #############################
# Public interface!!!
def sendmessage(destip, destport, message, localip, localport):
"""
<Purpose>
Send a message to a host / port
<Arguments>
destip:
The IP to send the message to
destport:
The port to send the message to
message:
The message to send
localip:
The local IP to send the message from
localport:
The local port to send the message from
<Exceptions>
AddressBindingError (descends NetworkError) when the local IP isn't
a local IP.
ResourceForbiddenError (descends ResourceException?) when the local
port isn't allowed
RepyArgumentError when the IPs, ports, and message aren't valid types
or values, or the message is longer than 9,216 bytes
AlreadyListeningError if there is an existing listening UDP socket
on the same local IP and port.
DuplicateTupleError if there is another sendmessage on the same
local IP and port to the same remote host.
<Side Effects>
None.
<Resource Consumption>
This operation consumes 64 bytes + number of bytes of the message that
were transmitted. This requires that the localport is allowed.
<Returns>
The number of bytes sent on success
"""
# Check the input arguments (type)
if type(destip) is not str:
raise RepyArgumentError("Provided destip must be a string!")
if type(localip) is not str:
raise RepyArgumentError("Provided localip must be a string!")
if type(destport) is not int:
raise RepyArgumentError("Provided destport must be an int!")
if type(localport) is not int:
raise RepyArgumentError("Provided localport must be an int!")
if type(message) is not str:
raise RepyArgumentError("Provided message must be a string!")
# Check the input arguments (sanity)
if not _is_valid_ip_address(destip):
raise RepyArgumentError("Provided destip is not valid! IP: '"+destip+"'")
if not _is_valid_ip_address(localip):
raise RepyArgumentError("Provided localip is not valid! IP: '"+localip+"'")
if not _is_valid_network_port(destport):
raise RepyArgumentError("Provided destport is not valid! Port: "+str(destport))
if not _is_valid_network_port(localport):
raise RepyArgumentError("Provided localport is not valid! Port: "+str(localport))
# Check that if localip == destip, then localport != destport
if localip == destip and localport == destport:
raise RepyArgumentError("Local socket name cannot match destination socket name! Local/Dest IP and Port match.")
# Check the input arguments (permission)
update_ip_cache()
if not _ip_is_allowed(localip):
raise ResourceForbiddenError("Provided localip is not allowed! IP: "+localip)
if not _is_allowed_localport("UDP", localport):
raise ResourceForbiddenError("Provided localport is not allowed! Port: "+str(localport))
# Fix SeattleTestbed/repy_v2#113, large UDP datagrams crash the sandbox
if len(message) > MAX_ALLOWABLE_DGRAM_SIZE:
raise RepyArgumentError("Provided message is too long for UDP datagrams on"
" some platforms. Maximum length is 9216 bytes.")
# Wait for netsend
if _is_loopback_ipaddr(destip):
nanny.tattle_quantity('loopsend', 0)
else:
nanny.tattle_quantity('netsend', 0)
try:
sock = None
if ("UDP", localip, localport) in _BOUND_SOCKETS:
sock = _BOUND_SOCKETS[("UDP", localip, localport)]
else:
# Get the socket
sock = _get_udp_socket(localip, localport)
# Register this socket with nanny
nanny.tattle_add_item("outsockets", id(sock))
# Send the message
bytessent = sock.sendto(message, (destip, destport))
# Account for the resources
if _is_loopback_ipaddr(destip):
nanny.tattle_quantity('loopsend', bytessent + 64)
else:
nanny.tattle_quantity('netsend', bytessent + 64)
return bytessent
except Exception, e:
try:
# If we're borrowing the socket, closing is not appropriate.
if not ("UDP", localip, localport) in _BOUND_SOCKETS:
sock.close()
except:
pass
# Check if address is already in use
if _is_addr_in_use_exception(e):
raise DuplicateTupleError("Provided Local IP and Local Port is already in use!")
if _is_addr_unavailable_exception(e):
raise AddressBindingError("Cannot bind to the specified local ip, invalid!")
# Complain if the network is down for UDP, too
# (See SeattleTestbed/repy_v2#80 for details)
if _is_network_down_exception(e):
raise InternetConnectivityError("The network is down or cannot be reached from the local IP!")
# Unknown error...
else:
raise
# Public interface!!!
def listenformessage(localip, localport):
"""
<Purpose>
Sets up a UDPServerSocket to receive incoming UDP messages.
<Arguments>
localip:
The local IP to register the handler on.
localport:
The port to listen on.