instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / Core / Utilities / utilities.py
1 # Written by Jie Yang
2 # see LICENSE.txt for license information
3
4 import socket
5 from time import time, strftime, gmtime
6 from base64 import encodestring, decodestring
7 from BaseLib.Core.Utilities.Crypto import sha
8 import sys
9 import os
10 import copy
11 from types import UnicodeType, StringType, LongType, IntType, ListType, DictType
12 import urlparse
13 from traceback import print_exc,print_stack
14 import binascii
15
16 STRICT_CHECK = True
17 DEBUG = False
18
19 infohash_len = 20
20
21 def bin2str(bin):
22     # Full BASE64-encoded 
23     return encodestring(bin).replace("\n","")
24     
25 def str2bin(str):
26     return decodestring(str)
27
28 def validName(name):
29     if not isinstance(name, str) and len(name) == 0:
30         raise RuntimeError, "invalid name: " + name
31     return True
32
33 def validPort(port):
34     port = int(port)
35     if port < 0 or port > 65535:
36         raise RuntimeError, "invalid Port: " + str(port)
37     return True
38
39 def validIP(ip):
40     try:
41         try:
42             # Is IPv4 addr?
43             socket.inet_aton(ip)
44             return True
45         except socket.error:
46             # Is hostname / IPv6?
47             socket.getaddrinfo(ip, None)
48             return True
49     except:
50         print_exc()
51     raise RuntimeError, "invalid IP address: " + ip
52
53     
54 def validPermid(permid):
55     if not isinstance(permid, str):
56         raise RuntimeError, "invalid permid: " + permid
57     # Arno,2010-02-17: permid is ASN.1 encoded data that is NOT fixed length
58     return True
59
60 def validInfohash(infohash):
61     if not isinstance(infohash, str):
62         raise RuntimeError, "invalid infohash " + infohash
63     if STRICT_CHECK and len(infohash) != infohash_len:
64         raise RuntimeError, "invalid length infohash " + infohash
65     return True
66     
67 def isValidPermid(permid):
68     try:
69         return validPermid(permid)
70     except:
71         return False
72     
73 def isValidInfohash(infohash):
74     try:
75         return validInfohash(infohash)
76     except:
77         return False
78
79 def isValidPort(port):
80     try:
81         return validPort(port)
82     except:
83         return False
84     
85 def isValidIP(ip):
86     try:
87         return validIP(ip)
88     except:
89         return False
90
91 def isValidName(name):
92     try:
93         return validPort(name)
94     except:
95         return False
96     
97     
98 def validTorrentFile(metainfo):
99     # Jie: is this function too strict? Many torrents could not be downloaded
100     if type(metainfo) != DictType:
101         raise ValueError('metainfo not dict')
102     
103     
104     if 'info' not in metainfo:
105         raise ValueError('metainfo misses key info')
106     
107     if 'announce' in metainfo and not isValidURL(metainfo['announce']):
108         raise ValueError('announce URL bad')
109     
110     # http://www.bittorrent.org/DHT_protocol.html says both announce and nodes
111     # are not allowed, but some torrents (Azureus?) apparently violate this.
112
113     #if 'announce' in metainfo and 'nodes' in metainfo:
114     #    raise ValueError('both announce and nodes present')
115     
116     if 'nodes' in metainfo:
117         nodes = metainfo['nodes']
118         if type(nodes) != ListType:
119             raise ValueError('nodes not list, but '+`type(nodes)`)
120         for pair in nodes:
121             if type(pair) != ListType and len(pair) != 2:
122                 raise ValueError('node not 2-item list, but '+`type(pair)`)
123             host,port = pair
124             if type(host) != StringType:
125                 raise ValueError('node host not string, but '+`type(host)`)
126             if type(port) != IntType:
127                 raise ValueError('node port not int, but '+`type(port)`)
128
129     if not ('announce' in metainfo or 'nodes' in metainfo):
130         raise ValueError('announce and nodes missing')
131
132     # 04/05/10 boudewijn: with the introduction of magnet links we
133     # also allow for peer addresses to be (temporarily) stored in the
134     # metadata.  Typically these addresses are recently gathered.
135     if "initial peers" in metainfo:
136         if not isinstance(metainfo["initial peers"], list):
137             raise ValueError("initial peers not list, but %s" % type(metainfo["initial peers"]))
138         for address in metainfo["initial peers"]:
139             if not (isinstance(address, tuple) and len(address) == 2):
140                 raise ValueError("address not 2-item tuple, but %s" % type(address))
141             if not isinstance(address[0], str):
142                 raise ValueError("address host not string, but %s" % type(address[0]))
143             if not isinstance(address[1], int):
144                 raise ValueError("address port not int, but %s" % type(address[1]))
145     
146     info = metainfo['info']
147     if type(info) != DictType:
148         raise ValueError('info not dict')
149     
150     if 'root hash' in info:
151         infokeys = ['name','piece length', 'root hash']
152     elif 'live' in info:
153         infokeys = ['name','piece length', 'live']
154     else:
155         infokeys = ['name','piece length', 'pieces']
156     for key in infokeys:
157         if key not in info:
158             raise ValueError('info misses key '+key)
159     name = info['name']
160     if type(name) != StringType:
161         raise ValueError('info name is not string but '+`type(name)`)
162     pl = info['piece length']
163     if type(pl) != IntType and type(pl) != LongType:
164         raise ValueError('info piece size is not int, but '+`type(pl)`)
165     if 'root hash' in info:
166         rh = info['root hash']
167         if type(rh) != StringType or len(rh) != 20:
168             raise ValueError('info roothash is not 20-byte string')
169     elif 'live' in info:
170         live = info['live']
171         if type(live) != DictType:
172             raise ValueError('info live is not a dict')
173         else:
174             if 'authmethod' not in live:
175                 raise ValueError('info live misses key'+'authmethod')
176     else:
177         p = info['pieces']
178         if type(p) != StringType or len(p) % 20 != 0:
179             raise ValueError('info pieces is not multiple of 20 bytes')
180         
181     if 'length' in info:
182         # single-file torrent
183         if 'files' in info:
184             raise ValueError('info may not contain both files and length key')
185         
186         l = info['length']
187         if type(l) != IntType and type(l) != LongType:
188             raise ValueError('info length is not int, but '+`type(l)`)
189     else:
190         # multi-file torrent
191         if 'length' in info:
192             raise ValueError('info may not contain both files and length key')
193         
194         files = info['files']
195         if type(files) != ListType:
196             raise ValueError('info files not list, but '+`type(files)`)
197         
198         filekeys = ['path','length']
199         for file in files:
200             for key in filekeys:
201                 if key not in file:
202                     raise ValueError('info files missing path or length key')
203             
204             p = file['path']
205             if type(p) != ListType:
206                 raise ValueError('info files path is not list, but '+`type(p)`)
207             for dir in p:
208                 if type(dir) != StringType:
209                     raise ValueError('info files path is not string, but '+`type(dir)`)
210             
211             l = file['length']
212             if type(l) != IntType and type(l) != LongType:
213                 raise ValueError('info files length is not int, but '+`type(l)`)
214             
215     # common additional fields
216     if 'announce-list' in metainfo:
217         al = metainfo['announce-list']
218         if type(al) != ListType:
219             raise ValueError('announce-list is not list, but '+`type(al)`)
220         for tier in al:
221             if type(tier) != ListType:
222                 raise ValueError('announce-list tier is not list '+`tier`)
223         # Jie: this limitation is not necessary
224 #            for url in tier:
225 #                if not isValidURL(url):
226 #                    raise ValueError('announce-list url is not valid '+`url`)
227
228     if 'azureus_properties' in metainfo:
229         azprop = metainfo['azureus_properties']
230         if type(azprop) != DictType:
231             raise ValueError('azureus_properties is not dict, but '+`type(azprop)`)
232         if 'Content' in azprop:
233                 content = azprop['Content']
234                 if type(content) != DictType:
235                     raise ValueError('azureus_properties content is not dict, but '+`type(content)`)
236                 if 'thumbnail' in content:
237                     thumb = content['thumbnail']
238                     if type(content) != StringType:
239                         raise ValueError('azureus_properties content thumbnail is not string')
240
241     # Diego: perform check on httpseeds/url-list field
242     if 'url-list' in metainfo:
243         if 'files' in metainfo['info']:
244             # Diego: only single-file mode allowed for http seeding now
245             raise ValueError("Only single-file mode supported with HTTP seeding: remove url-list")
246         elif type( metainfo['url-list'] ) != ListType:
247             raise ValueError('url-list is not list, but '+`type(metainfo['url-list'])`)
248         else:
249             for url in metainfo['url-list']:
250                 if not isValidURL(url):
251                     raise ValueError("url-list url is not valid: "+`url`)
252
253     if 'httpseeds' in metainfo:
254         if 'files' in metainfo['info']:
255             # Diego: only single-file mode allowed for http seeding now
256             raise ValueError("Only single-file mode supported with HTTP seeding: remove httpseeds")
257         elif type( metainfo['httpseeds'] ) != ListType:
258             raise ValueError('httpseeds is not list, but '+`type(metainfo['httpseeds'])`)
259         else:
260             for url in metainfo['httpseeds']:
261                 if not isValidURL(url):
262                     raise ValueError("httpseeds url is not valid: "+`url`)
263
264
265 def isValidTorrentFile(metainfo):
266     try:
267         validTorrentFile(metainfo)
268         return True
269     except:
270         if DEBUG:
271             print_exc()
272         return False
273     
274     
275 def isValidURL(url):
276     if url.lower().startswith('udp'):    # exception for udp
277         url = url.lower().replace('udp','http',1)
278     r = urlparse.urlsplit(url)
279     # if DEBUG:
280     #     print >>sys.stderr,"isValidURL:",r
281     
282     if r[0] == '' or r[1] == '':
283         return False
284     return True
285     
286 def show_permid(permid):
287     # Full BASE64-encoded. Must not be abbreviated in any way. 
288     if not permid:
289         return 'None'
290     return encodestring(permid).replace("\n","")
291     # Short digest
292     ##return sha(permid).hexdigest()
293
294 def show_permid_short(permid):
295     if not permid:
296         return 'None'
297     s = encodestring(permid).replace("\n","")
298     return s[-10:]
299     #return encodestring(sha(s).digest()).replace("\n","")
300
301 def show_permid_shorter(permid):
302     if not permid:
303         return 'None'
304     s = encodestring(permid).replace("\n","")
305     return s[-5:]
306
307 def readableBuddyCastMsg(buddycast_data,selversion):
308     """ Convert msg to readable format.
309     As this copies the original dict, and just transforms it,
310     most added info is already present and therefore logged
311     correctly. Exception is the OLPROTO_VER_EIGHTH which
312     modified the preferences list. """
313     prefxchg_msg = copy.deepcopy(buddycast_data)
314     
315     if prefxchg_msg.has_key('permid'):
316         prefxchg_msg.pop('permid')
317     if prefxchg_msg.has_key('ip'):
318         prefxchg_msg.pop('ip')
319     if prefxchg_msg.has_key('port'):
320         prefxchg_msg.pop('port')
321         
322     name = repr(prefxchg_msg['name'])    # avoid coding error
323
324     if prefxchg_msg['preferences']:
325         prefs = []
326         if selversion < 8: # OLPROTO_VER_EIGHTH: Can't use constant due to recursive import
327             for pref in prefxchg_msg['preferences']:
328                 prefs.append(show_permid(pref))
329         else:
330             for preftuple in prefxchg_msg['preferences']:
331                 # Copy tuple and escape infohash
332                 newlist = []
333                 for i in range(0,len(preftuple)):
334                     if i == 0:
335                         val = show_permid(preftuple[i])
336                     else:
337                         val = preftuple[i]
338                     newlist.append(val)
339                 prefs.append(newlist)
340                     
341         prefxchg_msg['preferences'] = prefs
342
343         
344     if prefxchg_msg.get('taste buddies', []):
345         buddies = []
346         for buddy in prefxchg_msg['taste buddies']:
347             buddy['permid'] = show_permid(buddy['permid'])
348             if buddy.get('preferences', []):
349                 prefs = []
350                 for pref in buddy['preferences']:
351                     prefs.append(show_permid(pref))
352                 buddy['preferences'] = prefs
353             buddies.append(buddy)
354         prefxchg_msg['taste buddies'] = buddies
355         
356     if prefxchg_msg.get('random peers', []):
357         peers = []
358         for peer in prefxchg_msg['random peers']:
359             peer['permid'] = show_permid(peer['permid'])
360             peers.append(peer)
361         prefxchg_msg['random peers'] = peers
362         
363     return prefxchg_msg
364     
365 def print_prefxchg_msg(prefxchg_msg):
366     def show_permid(permid):
367         return permid
368     print "------- preference_exchange message ---------"
369     print prefxchg_msg
370     print "---------------------------------------------"
371     print "permid:", show_permid(prefxchg_msg['permid'])
372     print "name", prefxchg_msg['name']
373     print "ip:", prefxchg_msg['ip']
374     print "port:", prefxchg_msg['port']
375     print "preferences:"
376     if prefxchg_msg['preferences']:
377         for pref in prefxchg_msg['preferences']:
378             print "\t", pref#, prefxchg_msg['preferences'][pref]
379     print "taste buddies:"
380     if prefxchg_msg['taste buddies']:
381         for buddy in prefxchg_msg['taste buddies']:
382             print "\t permid:", show_permid(buddy['permid'])
383             #print "\t permid:", buddy['permid']
384             print "\t ip:", buddy['ip']
385             print "\t port:", buddy['port']
386             print "\t age:", buddy['age']
387             print "\t preferences:"
388             if buddy['preferences']:
389                 for pref in buddy['preferences']:
390                     print "\t\t", pref#, buddy['preferences'][pref]
391             print
392     print "random peers:"
393     if prefxchg_msg['random peers']:
394         for peer in prefxchg_msg['random peers']:
395             print "\t permid:", show_permid(peer['permid'])
396             #print "\t permid:", peer['permid']
397             print "\t ip:", peer['ip']
398             print "\t port:", peer['port']
399             print "\t age:", peer['age']
400             print    
401             
402 def print_dict(data, level=0):
403     if isinstance(data, dict):
404         print
405         for i in data:
406             print "  "*level, str(i) + ':',
407             print_dict(data[i], level+1)
408     elif isinstance(data, list):
409         if not data:
410             print "[]"
411         else:
412             print
413         for i in xrange(len(data)):
414             print "  "*level, '[' + str(i) + ']:',
415             print_dict(data[i], level+1)
416     else:
417         print data
418         
419 def friendly_time(old_time):
420     curr_time = time()
421     try:
422         old_time = int(old_time)
423         assert old_time > 0
424         diff = int(curr_time - old_time)
425     except:
426         if isinstance(old_time, str):
427             return old_time
428         else:
429             return '?'
430     if diff < 0:
431         return '?'
432     elif diff < 2:
433         return str(diff) + " sec. ago"
434     elif diff < 60:
435         return str(diff) + " secs. ago"
436     elif diff < 120:
437         return "1 min. ago"
438     elif diff < 3600:
439         return str(int(diff/60)) + " mins. ago"
440     elif diff < 7200:
441         return "1 hour ago"
442     elif diff < 86400:
443         return str(int(diff/3600)) + " hours ago"
444     elif diff < 172800:
445         return "Yesterday"
446     elif diff < 259200:
447         return str(int(diff/86400)) + " days ago"
448     else:
449         return strftime("%d-%m-%Y", gmtime(old_time))
450         
451 def sort_dictlist(dict_list, key, order='increase'):
452     
453     aux = []
454     for i in xrange(len(dict_list)):
455         #print >>sys.stderr,"sort_dictlist",key,"in",dict_list[i].keys(),"?"
456         if key in dict_list[i]:
457             aux.append((dict_list[i][key],i))
458     aux.sort()
459     if order == 'decrease' or order == 1:    # 0 - increase, 1 - decrease
460         aux.reverse()
461     return [dict_list[i] for x, i in aux]
462
463
464 def dict_compare(a, b, keys):
465     for key in keys:
466         order = 'increase'
467         if type(key) == tuple:
468             skey, order = key
469         else:
470             skey = key
471
472         if a.get(skey) > b.get(skey):
473             if order == 'decrease' or order == 1:
474                 return -1
475             else:
476                 return 1
477         elif a.get(skey) < b.get(skey):
478             if order == 'decrease' or order == 1:
479                 return 1
480             else:
481                 return -1
482
483     return 0
484
485
486 def multisort_dictlist(dict_list, keys):
487
488     listcopy = copy.copy(dict_list)
489     cmp = lambda a, b: dict_compare(a, b, keys)
490     listcopy.sort(cmp=cmp)
491     return listcopy
492
493
494 def find_content_in_dictlist(dict_list, content, key='infohash'):
495     title = content.get(key)
496     if not title:
497         print 'Error: content had no content_name'
498         return False
499     for i in xrange(len(dict_list)):
500         if title == dict_list[i].get(key):
501             return i
502     return -1
503
504 def remove_torrent_from_list(list, content, key = 'infohash'):
505     remove_data_from_list(list, content, key)
506
507 def remove_data_from_list(list, content, key = 'infohash'):
508     index = find_content_in_dictlist(list, content, key)
509     if index != -1:
510         del list[index]
511     
512 def sortList(list_to_sort, list_key, order='decrease'):
513         aux = zip(list_key, list_to_sort)
514         aux.sort()
515         if order == 'decrease':
516             aux.reverse()
517         return [i for k, i in aux]    
518
519 def getPlural( n):
520         if n == 1:
521             return ''
522         else:
523             return 's'
524
525
526 def find_prog_in_PATH(prog):
527     envpath = os.path.expandvars('${PATH}')
528     if sys.platform == 'win32':
529         splitchar = ';'
530     else:
531         splitchar = ':'
532     paths = envpath.split(splitchar)
533     foundat = None
534     for path in paths:
535         fullpath = os.path.join(path,prog)
536         if os.access(fullpath,os.R_OK|os.X_OK):
537             foundat = fullpath
538             break
539     return foundat
540     
541 def hostname_or_ip2ip(hostname_or_ip):
542     # Arno: don't DNS resolve always, grabs lock on most systems
543     ip = None
544     try:
545         # test that hostname_or_ip contains a xxx.xxx.xxx.xxx string
546         socket.inet_aton(hostname_or_ip)
547         ip = hostname_or_ip
548
549     except:
550         try:
551             # dns-lookup for hostname_or_ip into an ip address
552             ip = socket.gethostbyname(hostname_or_ip)
553             if not hostname_or_ip.startswith("superpeer"):
554                 print >>sys.stderr,"hostname_or_ip2ip: resolved ip from hostname, an ip should have been provided", hostname_or_ip
555
556         except:
557             print >>sys.stderr,"hostname_or_ip2ip: invalid hostname", hostname_or_ip
558             print_exc()
559
560     return ip
561
562
563 def get_collected_torrent_filename(infohash):
564     # Arno: Better would have been the infohash in hex.
565     filename = sha(infohash).hexdigest()+'.torrent'    # notice: it's sha1-hash of infohash
566     return filename
567     # exceptions will be handled by got_metadata()
568     
569
570 def uintToBinaryString(uint, length=4):
571     '''
572     Converts an unsigned integer into its binary representation.
573     
574     @type uint: int
575     @param uint: un unsigned intenger to convert into binary data.
576     
577     @type length: int
578     @param length: the number of bytes the the resulting binary
579                    string should have
580                    
581     @rtype: a binary string
582     @return: a binary string. Each element in the string is one byte 
583             of data.
584                    
585     @precondition: uint >= 0 and uint < 2**(length*8)
586     '''
587     assert 0 <= uint < 2**(length*8), "Cannot represent string"
588     hexlen = length*2
589     hexString =  "{0:0>{1}}".format(hex(uint)[2:], hexlen)
590     if hexString.endswith('L'):
591         hexString = hexString[:-1]
592     
593     binaryString = binascii.unhexlify(hexString)
594     return binaryString
595
596 def binaryStringToUint(bstring):
597     '''
598     Converts a binary string into an unsigned integer
599     
600     @param bstring: a string of binary data
601     
602     @return a non-negative integer representing the 
603             value of the binary data interpreted as an
604             unsigned integer 
605     '''
606     hexstr = binascii.hexlify(bstring)
607     intval = int(hexstr,16)
608     return intval
609
610
611     
612
613
614 if __name__=='__main__':
615
616     torrenta = {'name':'a', 'swarmsize' : 12}
617     torrentb = {'name':'b', 'swarmsize' : 24}
618     torrentc = {'name':'c', 'swarmsize' : 18, 'Web2' : True}
619     torrentd = {'name':'b', 'swarmsize' : 36, 'Web2' : True}
620
621     torrents = [torrenta, torrentb, torrentc, torrentd]
622     print multisort_dictlist(torrents, ["Web2", ("swarmsize", "decrease")])
623
624
625     #d = {'a':1,'b':[1,2,3],'c':{'c':2,'d':[3,4],'k':{'c':2,'d':[3,4]}}}
626     #print_dict(d)