diff --git a/.gitignore b/.gitignore index b25c15b..2f836aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *~ +*.pyc diff --git a/brathuhn.py b/brathuhn.py index cbf5688..537a102 100644 --- a/brathuhn.py +++ b/brathuhn.py @@ -1,4 +1,4 @@ -import xmpp, os, sys, time, traceback +import xmpp, os, sys, time, traceback, stream_ft import threading from config import * @@ -20,6 +20,7 @@ join_time = time.time()+60*60*24*365 #1 year in the future users = dict() STATUS_JOIN, STATUS_ONLINE, STATUS_OFFLINE, STATUS_RENAME, STATUS_XAWAY, STATUS_AWAY, STATUS_UNKNOWN = range(7) +SI, JINGLE = range(2) def getUsers(): """Return a dictionary of every user in the group, the values are status codes.""" @@ -232,6 +233,7 @@ def connect_and_join(): cl.RegisterHandler('message',__ignore_first) cl.RegisterHandler('presence',__presenceCB) + cl.RegisterHandler('iq',stream_ft.ft_listener) #authenticate auth=cl.auth(NAME,PWD,resource=jid.getResource()) @@ -312,3 +314,35 @@ def cancelCall(cid): if cid in call_ats: del call_ats[cid] +def sendFile(to,path,method=SI): + """Send a file using method specified + + @param to -- Reciever of the file + @param path -- Path to the file + @param method -- Initiation method, currently only stream initiation is supported + """ + + size = os.path.getsize(path) + name = os.path.basename(path) + fd = open(path,'rb') + if method==SI: + cim = stream_ft.create_init_message(room_nick,to,name,size,fd) + cl.send(cim) + else: + fd.close() + +def sendFileStream(to,size,name,fd,method=SI): + """Send a file using method specified + + @param to -- Reciever of the file + @param size -- Number of bytes to send + @param name -- The name of the file the user will see + @param fd -- A file like object + @param method -- Initiation method, currently only stream initiation is supported + """ + + if method==SI: + cim = stream_ft.create_init_message(room_nick,to,name,size,fd) + cl.send(cim) + else: + fd.close() diff --git a/config.py b/config.py index e77722a..f730a05 100644 --- a/config.py +++ b/config.py @@ -11,6 +11,10 @@ try: RESOURCE = cp.get("Account","RESOURCE") except: RESOURCE = 'bot' +try: + S5B_PROXY = cp.get("General","RESOURCE") +except: + S5B_PROXY = 'proxy.jabbim.cz' ROOM = cp.get("Conference","ROOM") PWD_ROOM = cp.get("Conference","PWD_ROOM") diff --git a/jingle_ft.py b/jingle_ft.py new file mode 100755 index 0000000..d9e55e5 --- /dev/null +++ b/jingle_ft.py @@ -0,0 +1,67 @@ +#!/usr/bin/python + +import xmpp, os, hashlib, time +from datetime import datetime + +ID=0 + +JINGLE_NS = 'urn:xmpp:jingle:1' +JINGLE_FT_NS = 'urn:xmpp:jingle:apps:file-transfer:3' +JINGLE_TRANSPORT_S5B_NS = 'urn:xmpp:jingle:transports:s5b:1' +HASHES_NS = 'urn:xmpp:hashes:1' + + +def create_init_message(mfrom,to,path,desc,host,port): + global ID + ID+=1 + iq=xmpp.Iq(xmlns=None, + attrs={'to':to,'id':"jin_ft_iq_%d" % ID,'type':'set'}) + jingle=__create_jingle_node(mfrom,'session-initiate',"jin_ft_%d" % ID) + iq.getChildren().append(jingle) + + cont=__create_content_node('initiator',time.time(),path,desc) + jingle.getChildren().append(cont) + tr = __create_transport_tcp_direct(mfrom,host,port,126<<8+127,ID) + cont.getChildren().append(tr) + return iq + +def __create_jingle_node(initiator,action,sid): + return xmpp.Protocol(name="jingle",xmlns=JINGLE_NS, + attrs={'initiator':initiator,'action':action,'sid':sid}) + + +def __create_content_node(creator,date,path,txt): + mfile=open(path,"r") + datefmt = datetime.fromtimestamp(date).strftime("%Y-%m-%dT%H:%M:%SZ") + name=os.path.basename(path) + size=os.path.getsize(path) + mhash=hashlib.sha1() + mhash.update(mfile.read()) + mfile.close() + + cont= xmpp.Protocol(name="content",attrs={'creator':creator,'name':name}) + desc = cont.addChild(name="description",namespace=JINGLE_FT_NS) + offer = desc.addChild(name="offer") + mfile = offer.addChild(name="file") + + mfile.addChild(name="date",payload=[datefmt]) + mfile.addChild(name="desc",payload=[txt]) + mfile.addChild(name="name",payload=[name]) + mfile.addChild(name="size",payload=[size]) + mfile.addChild(name="hash",payload=[mhash.hexdigest()], + attrs={'algo':'sha-1'},namespace=HASHES_NS) + + return cont + +def __create_transport_tcp_direct(mfrom,host,port,priority,id): + tr = xmpp.Protocol(name="transport", + attrs={'mode':'tcp','sid':"jin_ft_tr_%d" % id}) + tr.addChild(name="candidate",attrs={ + 'cid':"jin_ft_cd_%d" % id, + 'host':host, + 'jid':mfrom, + 'port':port, + 'priority':priority, + 'type':'direct'}) + return tr + diff --git a/main.py b/main.py index aadd7b1..e18cef8 100755 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import brathuhn +import brathuhn, os import stream_ft,xmpp @@ -13,16 +13,9 @@ import choose import tantrum import tell import last_active +import pen -def foo(msg,args): - cim = stream_ft.create_init_message(brathuhn.room_nick,msg.getFrom(),"brathuhn.py") - brathuhn.cl.send(cim) - -brathuhn.addPrivateCommand("foo","asdasdasd",foo) brathuhn.connect_and_join() - -brathuhn.cl.RegisterHandler('iq',stream_ft.ft_listener) - brathuhn.loop() diff --git a/s5b.py b/s5b.py new file mode 100644 index 0000000..f29ca18 --- /dev/null +++ b/s5b.py @@ -0,0 +1,52 @@ +import socket, random + +def s5b_prepare(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + port = None + while True: + port = random.randint(10000,2**16-1) + try: + s.bind(('', port)) + s.listen(1) + break + except socket.error: + pass + + return (s,port) + +def s5b_write_file(s,fd,size): + s.settimeout(20) + conn, addr = s.accept() + data = conn.recv(3) + + assert data[0]=='\x05' #SOCKS version not 5 + assert data[1]=='\x01' #not a connect command + assert data[2]=='\x00' #does not support other connection method + + #send VER=5 METHOD=NO_AUTH + conn.sendall('\x05\x00') + + data = conn.recv(47) + + assert data[0]=='\x05' # SOCKS version not 5 + assert data[1]=='\x01' # not a connect command + # data[2] is reserved + assert data[3]=='\x03' # addr type is name + assert data[4]=='(' + name = data[5:45] + assert data[45]=='\x00' + assert data[46]=='\x00' + + #ACK + conn.sendall('\x05\x00\x00'+'\x03('+name+'\x00\x00') + #send file + data = fd.read(min(size,1024)) + size -= len(data) + while data!='': + conn.sendall(data) + data = fd.read(min(size,1024)) + size -= len(data) + + conn.close() + fd.close() diff --git a/stream_ft.py b/stream_ft.py new file mode 100644 index 0000000..7777779 --- /dev/null +++ b/stream_ft.py @@ -0,0 +1,164 @@ + +import xmpp, os, hashlib, time, brathuhn +import socket, subprocess, random, s5b, util +import config +from threading import Thread +from datetime import datetime + +GET_IP_CMD = ["dig", "myip.opendns.com", "@resolver1.opendns.com", "+short"] +PROXY = config.S5B_PROXY + +ID=random.randint(0,1000000) + +BSTREAM_NS = "http://jabber.org/protocol/bytestreams" +SI_NS = 'http://jabber.org/protocol/si' +FILE_NS = 'http://jabber.org/protocol/si/profile/file-transfer' +FEATURE_NS = 'http://jabber.org/protocol/feature-neg' +X_DATA_NS = 'jabber:x:data' +JABBER_NS = 'jabber:client' + +BYTESTREAM = 'http://jabber.org/protocol/bytestreams' +IBB = 'http://jabber.org/protocol/ibb' + +transfer_ids = dict() +queries = dict() + +LOCAL_IP = util.get_local_ip() + +def create_init_message(mfrom,to,name,size,fd): + global ID + global transfer_ids + ID+=1 + + transfer_ids[unicode("si_%d"%ID)]=[fd,ID,size] + + iq=xmpp.Iq(xmlns=JABBER_NS, + attrs={'from':unicode(mfrom),'to':to,'id':"si_%d" % ID,'type':'set'}) + si=iq.addChild(name="si",namespace=SI_NS, attrs={'id':"si_%d" % ID, 'profile':FILE_NS}) + si.addChild(name="file", namespace=FILE_NS, attrs={'name':name,'size':size}) + fr=si.addChild(name="feature", namespace=FEATURE_NS) + x=fr.addChild(name="x", namespace=X_DATA_NS, attrs={'type':'form'}) + fd=x.addChild(name="field",attrs={'type':'list-single','var':'stream-method'}) + + opt=fd.addChild(name="option") + val=opt.addChild(name="value") + val.addData(BYTESTREAM) + + opt=fd.addChild(name="option") + val=opt.addChild(name="value") + val.addData(IBB) + + return iq + +def create_bytestream_query_message(mfrom,to,port,cid): + global queries + + sid = "si_%d" % cid + queries[sid] = cid + + iq=xmpp.Iq(xmlns=JABBER_NS, + attrs={'from':unicode(mfrom),'to':to,'id':sid,'type':'set'}) + + q = iq.addChild(name="query",namespace=BSTREAM_NS, attrs={'sid':sid}) + + hosts = [] + + p=subprocess.Popen(GET_IP_CMD,stdout=subprocess.PIPE) + p.wait() + hosts.append(p.stdout.read()) + hosts.append(LOCAL_IP) + + for host in hosts: + q.addChild(name="streamhost",attrs={'jid':unicode(mfrom),'port':str(port),'host':host}) + + #print "\n",q + return iq + +def start_ibb(cid,mfrom,fd): + print "IBB" + stream = xmpp.filetransfer.IBB() + stream.PlugIn(brathuhn.cl) + stream.OpenStream("si_%d" % cid, mfrom, fd, blocksize=4096) + +def ft_query(conn,msg): + global queries + global transfer_ids + + sid = msg.attrs['id'] + cid = queries[sid] + + assert sid in transfer_ids + + fd = transfer_ids[ sid ][0] + + if msg.attrs['type']=='error': + start_ibb(cid, unicode(msg.getFrom()), fd) + del transfer_ids[ sid ] + del queries[ sid ] + +def ft_init(conn,msg): + global ID + sid = msg.attrs['id'] + if msg.attrs['type']=='result': + fd = transfer_ids[ sid ][0] + size = transfer_ids[ sid ][2] + current_id = transfer_ids[ sid ][1] + else: + del transfer_ids[ sid ] + return + + si=msg.getChildren()[0] + # check if cancel message + if si.name!='si': + return + + fr=si.getChildren()[0] + # some clients send a profile name + # we will ignore it i.e take the next child + if fr.name=='file': + fr=si.getChildren()[1] + if fr.name!='feature': + return + x=fr.getChildren()[0] + if x.name!='x' and x.attrs['type']!='submit': + return + fi=x.getChildren()[0] + if fi.name!='field': + return + val=fi.getChildren()[0] + if val.name!='value': + return + + if val.data[0]==BYTESTREAM: + print "BYTESTREAM" + + s,port=s5b.s5b_prepare() + + q = create_bytestream_query_message(brathuhn.room_nick,msg.getFrom(),port,current_id) + brathuhn.cl.send(q) + + def __run(): + s5b.s5b_write_file(s,fd,size) + Thread(target=__run()).start() + + ID += 1 + + elif val.data[0]==IBB: + start_ibb(current_id, unicode(msg.getFrom()), fd) + + +def ft_listener(conn,msg): + global transfer_ids + global queries + + assert msg.name=='iq' + sid = msg.attrs['id'] + if sid in transfer_ids and not sid in queries: + print "init" + ft_init(conn,msg) + return + if sid in queries: + ft_query(conn,msg) + return + + diff --git a/util.py b/util.py new file mode 100644 index 0000000..d254fe9 --- /dev/null +++ b/util.py @@ -0,0 +1,39 @@ +import select +import socket +import threading +import random +from Queue import Queue, Empty + +MAGIC_PACKET = "" +for i in xrange(50): + MAGIC_PACKET += chr(random.randrange(ord('a'),ord('z'))) + +def get_local_ip(): + def udp_listening_server(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.bind(('', 8888)) + s.setblocking(0) + while True: + result = select.select([s],[],[]) + msg, address = result[0][0].recvfrom(1024) + msg = unicode(msg) + if msg == MAGIC_PACKET: + break + queue.put(address) + + queue = Queue() + thread = threading.Thread(target=udp_listening_server) + thread.queue = queue + thread.start() + s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s2.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + waiting = True + while waiting: + s2.sendto(bytes(MAGIC_PACKET), ('', 8888)) + try: + address = queue.get(False) + except Empty: + pass + else: + waiting = False + return address[0]