2 # see LICENSE.txt for license information
5 from time import time, strftime, gmtime
6 from base64 import encodestring, decodestring
7 from BaseLib.Core.Utilities.Crypto import sha
11 from types import UnicodeType, StringType, LongType, IntType, ListType, DictType
13 from traceback import print_exc,print_stack
23 return encodestring(bin).replace("\n","")
26 return decodestring(str)
29 if not isinstance(name, str) and len(name) == 0:
30 raise RuntimeError, "invalid name: " + name
35 if port < 0 or port > 65535:
36 raise RuntimeError, "invalid Port: " + str(port)
47 socket.getaddrinfo(ip, None)
51 raise RuntimeError, "invalid IP address: " + ip
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
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
67 def isValidPermid(permid):
69 return validPermid(permid)
73 def isValidInfohash(infohash):
75 return validInfohash(infohash)
79 def isValidPort(port):
81 return validPort(port)
91 def isValidName(name):
93 return validPort(name)
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')
104 if 'info' not in metainfo:
105 raise ValueError('metainfo misses key info')
107 if 'announce' in metainfo and not isValidURL(metainfo['announce']):
108 raise ValueError('announce URL bad')
110 # http://www.bittorrent.org/DHT_protocol.html says both announce and nodes
111 # are not allowed, but some torrents (Azureus?) apparently violate this.
113 #if 'announce' in metainfo and 'nodes' in metainfo:
114 # raise ValueError('both announce and nodes present')
116 if 'nodes' in metainfo:
117 nodes = metainfo['nodes']
118 if type(nodes) != ListType:
119 raise ValueError('nodes not list, but '+`type(nodes)`)
121 if type(pair) != ListType and len(pair) != 2:
122 raise ValueError('node not 2-item list, but '+`type(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)`)
129 if not ('announce' in metainfo or 'nodes' in metainfo):
130 raise ValueError('announce and nodes missing')
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]))
146 info = metainfo['info']
147 if type(info) != DictType:
148 raise ValueError('info not dict')
150 if 'root hash' in info:
151 infokeys = ['name','piece length', 'root hash']
153 infokeys = ['name','piece length', 'live']
155 infokeys = ['name','piece length', 'pieces']
158 raise ValueError('info misses key '+key)
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')
171 if type(live) != DictType:
172 raise ValueError('info live is not a dict')
174 if 'authmethod' not in live:
175 raise ValueError('info live misses key'+'authmethod')
178 if type(p) != StringType or len(p) % 20 != 0:
179 raise ValueError('info pieces is not multiple of 20 bytes')
182 # single-file torrent
184 raise ValueError('info may not contain both files and length key')
187 if type(l) != IntType and type(l) != LongType:
188 raise ValueError('info length is not int, but '+`type(l)`)
192 raise ValueError('info may not contain both files and length key')
194 files = info['files']
195 if type(files) != ListType:
196 raise ValueError('info files not list, but '+`type(files)`)
198 filekeys = ['path','length']
202 raise ValueError('info files missing path or length key')
205 if type(p) != ListType:
206 raise ValueError('info files path is not list, but '+`type(p)`)
208 if type(dir) != StringType:
209 raise ValueError('info files path is not string, but '+`type(dir)`)
212 if type(l) != IntType and type(l) != LongType:
213 raise ValueError('info files length is not int, but '+`type(l)`)
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)`)
221 if type(tier) != ListType:
222 raise ValueError('announce-list tier is not list '+`tier`)
223 # Jie: this limitation is not necessary
225 # if not isValidURL(url):
226 # raise ValueError('announce-list url is not valid '+`url`)
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')
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'])`)
249 for url in metainfo['url-list']:
250 if not isValidURL(url):
251 raise ValueError("url-list url is not valid: "+`url`)
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'])`)
260 for url in metainfo['httpseeds']:
261 if not isValidURL(url):
262 raise ValueError("httpseeds url is not valid: "+`url`)
265 def isValidTorrentFile(metainfo):
267 validTorrentFile(metainfo)
276 if url.lower().startswith('udp'): # exception for udp
277 url = url.lower().replace('udp','http',1)
278 r = urlparse.urlsplit(url)
280 # print >>sys.stderr,"isValidURL:",r
282 if r[0] == '' or r[1] == '':
286 def show_permid(permid):
287 # Full BASE64-encoded. Must not be abbreviated in any way.
290 return encodestring(permid).replace("\n","")
292 ##return sha(permid).hexdigest()
294 def show_permid_short(permid):
297 s = encodestring(permid).replace("\n","")
299 #return encodestring(sha(s).digest()).replace("\n","")
301 def show_permid_shorter(permid):
304 s = encodestring(permid).replace("\n","")
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)
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')
322 name = repr(prefxchg_msg['name']) # avoid coding error
324 if prefxchg_msg['preferences']:
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))
330 for preftuple in prefxchg_msg['preferences']:
331 # Copy tuple and escape infohash
333 for i in range(0,len(preftuple)):
335 val = show_permid(preftuple[i])
339 prefs.append(newlist)
341 prefxchg_msg['preferences'] = prefs
344 if prefxchg_msg.get('taste buddies', []):
346 for buddy in prefxchg_msg['taste buddies']:
347 buddy['permid'] = show_permid(buddy['permid'])
348 if buddy.get('preferences', []):
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
356 if prefxchg_msg.get('random peers', []):
358 for peer in prefxchg_msg['random peers']:
359 peer['permid'] = show_permid(peer['permid'])
361 prefxchg_msg['random peers'] = peers
365 def print_prefxchg_msg(prefxchg_msg):
366 def show_permid(permid):
368 print "------- preference_exchange message ---------"
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']
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]
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']
402 def print_dict(data, level=0):
403 if isinstance(data, dict):
406 print " "*level, str(i) + ':',
407 print_dict(data[i], level+1)
408 elif isinstance(data, list):
413 for i in xrange(len(data)):
414 print " "*level, '[' + str(i) + ']:',
415 print_dict(data[i], level+1)
419 def friendly_time(old_time):
422 old_time = int(old_time)
424 diff = int(curr_time - old_time)
426 if isinstance(old_time, str):
433 return str(diff) + " sec. ago"
435 return str(diff) + " secs. ago"
439 return str(int(diff/60)) + " mins. ago"
443 return str(int(diff/3600)) + " hours ago"
447 return str(int(diff/86400)) + " days ago"
449 return strftime("%d-%m-%Y", gmtime(old_time))
451 def sort_dictlist(dict_list, key, order='increase'):
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))
459 if order == 'decrease' or order == 1: # 0 - increase, 1 - decrease
461 return [dict_list[i] for x, i in aux]
464 def dict_compare(a, b, keys):
467 if type(key) == tuple:
472 if a.get(skey) > b.get(skey):
473 if order == 'decrease' or order == 1:
477 elif a.get(skey) < b.get(skey):
478 if order == 'decrease' or order == 1:
486 def multisort_dictlist(dict_list, keys):
488 listcopy = copy.copy(dict_list)
489 cmp = lambda a, b: dict_compare(a, b, keys)
490 listcopy.sort(cmp=cmp)
494 def find_content_in_dictlist(dict_list, content, key='infohash'):
495 title = content.get(key)
497 print 'Error: content had no content_name'
499 for i in xrange(len(dict_list)):
500 if title == dict_list[i].get(key):
504 def remove_torrent_from_list(list, content, key = 'infohash'):
505 remove_data_from_list(list, content, key)
507 def remove_data_from_list(list, content, key = 'infohash'):
508 index = find_content_in_dictlist(list, content, key)
512 def sortList(list_to_sort, list_key, order='decrease'):
513 aux = zip(list_key, list_to_sort)
515 if order == 'decrease':
517 return [i for k, i in aux]
526 def find_prog_in_PATH(prog):
527 envpath = os.path.expandvars('${PATH}')
528 if sys.platform == 'win32':
532 paths = envpath.split(splitchar)
535 fullpath = os.path.join(path,prog)
536 if os.access(fullpath,os.R_OK|os.X_OK):
541 def hostname_or_ip2ip(hostname_or_ip):
542 # Arno: don't DNS resolve always, grabs lock on most systems
545 # test that hostname_or_ip contains a xxx.xxx.xxx.xxx string
546 socket.inet_aton(hostname_or_ip)
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
557 print >>sys.stderr,"hostname_or_ip2ip: invalid hostname", hostname_or_ip
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
567 # exceptions will be handled by got_metadata()
570 def uintToBinaryString(uint, length=4):
572 Converts an unsigned integer into its binary representation.
575 @param uint: un unsigned intenger to convert into binary data.
578 @param length: the number of bytes the the resulting binary
581 @rtype: a binary string
582 @return: a binary string. Each element in the string is one byte
585 @precondition: uint >= 0 and uint < 2**(length*8)
587 assert 0 <= uint < 2**(length*8), "Cannot represent string"
589 hexString = "{0:0>{1}}".format(hex(uint)[2:], hexlen)
590 if hexString.endswith('L'):
591 hexString = hexString[:-1]
593 binaryString = binascii.unhexlify(hexString)
596 def binaryStringToUint(bstring):
598 Converts a binary string into an unsigned integer
600 @param bstring: a string of binary data
602 @return a non-negative integer representing the
603 value of the binary data interpreted as an
606 hexstr = binascii.hexlify(bstring)
607 intval = int(hexstr,16)
614 if __name__=='__main__':
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}
621 torrents = [torrenta, torrentb, torrentc, torrentd]
622 print multisort_dictlist(torrents, ["Web2", ("swarmsize", "decrease")])
625 #d = {'a':1,'b':[1,2,3],'c':{'c':2,'d':[3,4],'k':{'c':2,'d':[3,4]}}}