Cocobear用纯Python实现飞信协议

 

Linux下使用飞信有很多方式,可以安装pidgin的插件,也可以安装其他客户端。

pidgin的飞信插件最新是v0.98,可以从sourceforge.net上下载到源代码

($ cvs -d:pserver:[email protected]:/cvsroot/fetion co fetion)。

不过作者从10月6日好后好像再没有更新过,他最近在开发一个python的独立飞信客户端,等后期再技术回流给插件版本。

还有一个由邓东东主持开发的飞信应用程序开发库LibFetion,其大部分代码使用C编写,支持所有POSIX兼容的操作系统,现在貌似移植到了很多平台。基于这个库,开发了一个linux的客户端,还有其他平台的客户端,最新版本v0.9.1。

下载地址:http://www.libfetion.cn/demoapp_download.html

不过不是开源的 :< 作者封装了一个 .a 的二进制文件,公开接口函数。

------------------------------------------

俺们的可可熊(http://cocobear.info),最近用纯Python实现了飞信协议,OPEN SOURCE :)

了解更多: http://cocobear.info/blog/?s=fetion

  1. ------------------------------------------
  2. #!/usr/bin/env python
  3. # -*- coding: utf-8 -*-
  4. #Using GPL v2
  5. #Author: [email protected]
  6. import urllib2
  7. import urllib
  8. import cookielib
  9. import sys,re
  10. import binascii
  11. import hashlib
  12. import socket
  13. from hashlib import md5
  14. from hashlib import sha1
  15. from uuid import uuid1
  16. FetionVer = "2008"
  17. #"SIPP" USED IN HTTP CONNECTION
  18. FetionSIPP= "SIPP"
  19. FetionNavURL = "nav.fetion.com.cn"
  20. FetionConfigURL = "http://nav.fetion.com.cn/nav/getsystemconfig.aspx"
  21. FetionConfigXML = """<config><user mobile-no="%s" /><client type="PC" version="3.2.0540" platform="W5.1" /><servers version="0" /><service-no version="0" /><parameters version="0" /><hints version="0" /><http-applications version="0" /><client-config version="0" /></config>"""
  22. FetionLoginXML = """<args><device type="PC" version="0" client-version="3.2.0540" /><caps value="simple-im;im-session;temp-group;personal-group" /><events value="contact;permission;system-message;personal-group" /><user-info attributes="all" /><presence><basic value="400" desc="" /></presence></args>"""
  23. debug = True
  24. class PyFetionException(Exception):
  25.     """Base class for all exceptions raised by this module."""
  26. class PyFetionInfoError(PyFetionException):
  27.     """Phone number or password incomplete"""
  28. class PyFetionResponseException(PyFetionException):
  29.     """Base class for all exceptions that include SIPC/HTTP error code.
  30.     """
  31.     def __init__(self, code, msg):
  32.         self.PyFetion_code = code
  33.         self.PyFetion_error = msg
  34.         self.args = (code, msg)
  35. class PyFetionAuthError(PyFetionResponseException):
  36.     """Authentication error.
  37.     Your password error, or your mobile NO. don't support fetion
  38.     """
  39. class PyFetionRegisterError(PyFetionResponseException):
  40.     """RegisterError.
  41.     """
  42. class PyFetionSendError(PyFetionResponseException):
  43.     """Send SMS error
  44.     """
  45. class PyFetion():
  46.     __config_data = ""
  47.     __sipc_url    = ""
  48.     __sipc_proxy  = ""
  49.     __sid = ""
  50.     
  51.     mobile_no = ""
  52.     passwd = ""
  53.     login_type = ""
  54.     def __init__(self,mobile_no,passwd,login_type="HTTP"):
  55.         if not passwd or len(mobile_no) != 11:
  56.             raise PyFetionInfoError(mobile_no,passwd)
  57.         self.mobile_no = mobile_no
  58.         self.passwd = passwd
  59.         self.login_type = login_type
  60.         self.__get_system_config()
  61.         self.__set_system_config()
  62.     def login(self):
  63.         (self.__ssic,self.__domain) = self.__get_uri()
  64.         try:
  65.             self.__register(self.__ssic,self.__domain)
  66.         except PyFetionRegisterError,e:
  67.             print "Register Failed!"
  68.             #这里使用一个status变量作为类的成员,每一种失败后都改变一下这个
  69.             pass
  70.     def get_offline_msg(self):
  71.         self.__SIPC.get("")
  72.     def add(self,who):
  73.         self.__SIPC.get("INFO","AddBuddy",who)
  74.         response = self.__SIPC.send()
  75.         code = self.__SIPC.get_code(response)
  76.         if code == 521:
  77.             d_print("Aleady added.")
  78.         elif code == 522:
  79.             d_print("Mobile NO. Don't Have Fetion")
  80.             self.__SIPC.get("INFO","AddMobileBuddy",who)
  81.             response = self.__SIPC.send()
  82.     def get_personal_info(self):
  83.         self.__SIPC.get("INFO","GetPersonalInfo")
  84.         self.__SIPC.send()
  85.     def get_info(self,who):
  86.         self.__SIPC.get("INFO","GetContactsInfo",who)
  87.         response = self.__SIPC.send()
  88.         return response
  89.     def get_contact_list(self):
  90.         self.__SIPC.get("INFO","GetContactList")
  91.         response = self.__SIPC.send()
  92.         return response
  93.     def get_uri(self,who):
  94.         if who == self.mobile_no:
  95.             who = self.__uri
  96.         if not who.startswith("sip"):
  97.             l = self.get_contact_list()
  98.             all = re.findall('uri="(.+?)" ',l)
  99.             #Get uri from contact list, compare one by one
  100.             #I can't get other more effect way
  101.             for uri in all:
  102.                 ret = self.get_info(uri)
  103.                 no = re.findall('mobile-no="(.+?)" ',ret)
  104.                 if no:
  105.                     if no[0] == who:
  106.                         d_print(('who',),locals())
  107.                         who = uri
  108.                         break
  109.         return who
  110.     def send_msg(self,to,msg,flag="SENDMSG"):
  111.         self.__SIPC.get(flag,to,msg)
  112.         response = self.__SIPC.send()
  113.         code = self.__SIPC.get_code(response)
  114.         if code == 280:
  115.             d_print("Send sms/msg OK!")
  116.         else:
  117.             d_print(('code',),locals())
  118.     def send_sms(self,msg,to=None,long=False):
  119.         if not to:
  120.             to = self.__uri
  121.         else:
  122.             to = self.get_uri(to)
  123.         if long:
  124.             self.send_msg(to,msg,"SENDCatSMS")
  125.         else:
  126.             self.send_msg(to,msg,"SENDSMS")
  127.     def send_schedule_sms(self,msg,time,to=None):
  128.         if not to:
  129.             to = self.__uri
  130.         else:
  131.             to = self.get_uri(to)
  132.         self.__SIPC.get("SSSetScheduleSms",msg,time,to)
  133.         response = self.__SIPC.send()
  134.         code = self.__SIPC.get_code(response)
  135.         if code == 486:
  136.             d_print("Busy Here")
  137.             return None
  138.         if code == 200:
  139.             id = re.search('id="(/d+)"',response).group(1)
  140.             d_print(('id',),locals(),"schedule_sms id")
  141.             return id
  142.     def __register(self,ssic,domain):
  143.         self.__SIPC = SIPC(self.__sid,self.__domain,self.passwd,self.login_type,self.__http_tunnel,self.__ssic,self.__sipc_proxy)
  144.         response = ""
  145.         for step in range(1,3):
  146.                 self.__SIPC.get("REG",step,response)
  147.                 response = self.__SIPC.send()
  148.         code = self.__SIPC.get_code(response)
  149.         if code == 200:
  150.             d_print("register successful.")
  151.         else:
  152.             raise PyFetionRegisterError(code,response)
  153.     def __http_send(self,url,body="",exheaders="",login=False):
  154.         headers = {
  155.                    'User-Agent':'IIC2.0/PC 3.2.0540',
  156.                   }
  157.         headers.update(exheaders)
  158.         request = urllib2.Request(url,headers=headers,data=body)
  159.         try:
  160.             conn = urllib2.urlopen(request)
  161.         except urllib2.URLError, e:
  162.             code = e.code
  163.             msg = e.read()
  164.             if code == 401 or code == 404:
  165.                 if login:
  166.                     d_print(('code','text'),locals())
  167.                     raise PyFetionAuthError(code,msg)
  168.             return -1
  169.         return conn
  170.     def __get_system_config(self):
  171.         global FetionConfigURL
  172.         global FetionConfigXML
  173.         url = FetionConfigURL
  174.         body = FetionConfigXML % self.mobile_no
  175.         d_print(('url','body'),locals())
  176.         self.__config_data = self.__http_send(url,body).read()
  177.             
  178.     def __set_system_config(self):
  179.         sipc_url = re.search("<ssi-app-sign-in>(.*)</ssi-app-sign-in>",self.__config_data).group(1)
  180.         sipc_proxy = re.search("<sipc-proxy>(.*)</sipc-proxy>",self.__config_data).group(1)
  181.         http_tunnel = re.search("<http-tunnel>(.*)</http-tunnel>",self.__config_data).group(1)
  182.         d_print(('sipc_url','sipc_proxy','http_tunnel'),locals())
  183.         self.__sipc_url   = sipc_url
  184.         self.__sipc_proxy = sipc_proxy
  185.         self.__http_tunnel= http_tunnel
  186.     def __get_uri(self):
  187.         url = self.__sipc_url+"?mobileno="+self.mobile_no+"&pwd="+self.passwd
  188.         d_print(('url',),locals())
  189.         try:
  190.             ret = self.__http_send(url,login=True)
  191.         except PyFetionAuthError,e:
  192.             d_print(('e',),locals())
  193.             print "Your password error, or your mobile NO. don't support fetion"
  194.             sys.exit(-1)
  195.         header = str(ret.info())
  196.         body   = ret.read()
  197.         ssic = re.search("ssic=(.*);",header).group(1)
  198.         sid  = re.search("sip:(.*)@",body).group(1)
  199.         uri  = re.search('uri="(.*)" mobile-no',body).group(1)
  200.         status = re.search('user-status="(/d+)"',body).group(1)
  201.         domain = "fetion.com.cn"
  202.         d_print(('ssic','sid','uri','status','domain'),locals(),"Get SID OK")
  203.         self.__sid = sid
  204.         self.__uri = uri
  205.         return (ssic,domain)
  206. class SIPC():
  207.     global FetionVer
  208.     global FetionSIPP
  209.     global FetionLoginXML
  210.     header = ""
  211.     body = ""
  212.     content = ""
  213.     code = ''
  214.     ver  = "SIP-C/2.0"
  215.     ID   = 1
  216.     sid  = ""
  217.     domain = ""
  218.     passwd = ""
  219.     __http_tunnel = ""
  220.     def __init__(self,sid,domain,passwd,login_type,http_tunnel,ssic,sipc_proxy):
  221.         self.sid = sid
  222.         self.domain = domain
  223.         self.passwd = passwd
  224.         self.login_type = login_type
  225.         self.domain = domain
  226.         self.sid = sid
  227.         self.__seq = 1
  228.         self.__sipc_proxy = sipc_proxy
  229.         if self.login_type == "HTTP":
  230.             self.__http_tunnel = http_tunnel
  231.             self.__ssic = ssic
  232.             guid = str(uuid1())
  233.             self.__exheaders = {
  234.                  'Cookie':'ssic=%s' % self.__ssic,
  235.                  'Content-Type':'application/oct-stream',
  236.                  'Pragma':'xz4BBcV%s' % guid,
  237.                  }
  238.      
  239.     def init(self,type):
  240.         self.content = '%s %s %s/r/n' % (type,self.domain,self.ver)
  241.         self.header = [('F',self.sid),
  242.                        ('I',self.ID),
  243.                        ('Q','1 %s' % type),
  244.                       ]
  245.     def send(self):
  246.         content = self.content
  247.         d_print(('content',),locals())
  248.         if self.login_type == "HTTP":
  249.             #First time t SHOULD SET AS 'i'
  250.             #Otherwise 405 code get
  251.             if self.__seq == 1:
  252.                 t = 'i'
  253.             else:
  254.                 t = 's'
  255.             url = self.__http_tunnel+"?t=%s&i=%s" % (t,self.__seq)
  256.             response = self.__http_send(url,content,self.__exheaders).read()
  257.             self.__seq+=1
  258.             response = self.__sendSIPP()
  259.             #This line will enhance the probablity of success.
  260.             #Sometimes it will return FetionSIPP twice.
  261.             #Probably you need add more
  262.             if response == FetionSIPP:
  263.                 response = self.__sendSIPP()
  264.         else:
  265.             if self.__seq == 1:
  266.                 self.__tcp_init()
  267.             self.__tcp_send(content)
  268.             response = self.__tcp_recv()
  269.             d_print(('response',),locals())
  270.             self.__seq+=1
  271.         code = self.get_code(response)
  272.         d_print(('code',),locals())
  273.         return response
  274.  
  275.     def get_code(self,response):
  276.         try:
  277.             self.code =int(re.search("%s (/d{3})" % self.ver,response).group(1))
  278.             self.msg  =re.search("%s /d{3} (.*)/r" % self.ver,response).group(1)
  279.             d_print(('self.code','self.msg',),locals())
  280.             return self.code
  281.         except AttributeError,e:
  282.             return None
  283.  
  284.     def get(self,cmd,arg,ret="",extra=""):
  285.         body = ret
  286.         if cmd == "REG":
  287.             body = FetionLoginXML
  288.             self.init('R')
  289.             if arg == 1:
  290.                 pass
  291.             if arg == 2:
  292.                 nonce = re.search('nonce="(.*)"',ret).group(1)
  293.                 cnonce = self.__get_cnonce()
  294.                 if FetionVer == "2008":
  295.                     response=self.__get_response_sha1(nonce,cnonce)
  296.                 elif FetionVer == "2006":
  297.                     response=self.__get_response_md5(nonce,cnonce)
  298.                 salt = self.__get_salt()
  299.                 d_print(('nonce','cnonce','response','salt'),locals())
  300.                 #If this step failed try to uncomment this lines
  301.                 #del self.header[2]
  302.                 #self.header.insert(2,('Q','2 R'))
  303.                 if FetionVer == "2008":
  304.                     self.header.insert(3,('A','Digest algorithm="SHA1-sess",response="%s",cnonce="%s",salt="%s"' % (response,cnonce,salt)))
  305.                 elif FetionVer == "2006":
  306.                     self.header.insert(3,('A','Digest response="%s",cnonce="%s"' % (response,cnonce)))
  307.             #If register successful 200 code get
  308.             if arg == 3:
  309.                 return self.code
  310.         if cmd == "SENDMSG":
  311.             self.init('M')
  312.             self.header.insert(3,('T',arg))
  313.             self.header.insert(4,('C','text/plain'))
  314.             self.header.insert(5,('K','SaveHistory'))
  315.         
  316.         if cmd == "SENDSMS":
  317.             self.init('M')
  318.             self.header.append(('T',arg))
  319.             self.header.append(('N','SendSMS'))
  320.         if cmd == "SENDCatSMS":
  321.             self.init('M')
  322.             self.header.append(('T',arg))
  323.             self.header.append(('N','SendCatSMS'))
  324.         if cmd == "SSSetScheduleSms":
  325.             self.init('S')
  326.             self.header.insert(3,('N',cmd))
  327.             body = '<args><schedule-sms send-time="%s"><message>%s</message><receivers><receiver uri="%s" /></receivers></schedule-sms></args>' % (ret,arg,extra)
  328.         if cmd == "INFO":
  329.             self.init('S')
  330.             self.header.insert(3,('N',arg))
  331.             if arg == "GetPersonalInfo":
  332.                 body = '<args><personal attributes="all" /><services version="" attributes="all" /><config version="33" attributes="all" /><mobile-device attributes="all" /></args>'
  333.             elif arg == "GetContactList":
  334.                 body = '<args><contacts attributes="all"><buddies attributes="all" /></contacts></args>'
  335.             elif arg == "GetContactsInfo":
  336.                 body = '<args><contacts attributes="all"><contact uri="%s" /></contacts></args>' % ret
  337.             elif arg == "AddBuddy":
  338.                 body = '<args><contacts><buddies><buddy uri="tel:%s" buddy-lists="1" desc="This message is send by PyFetion" expose-mobile-no="1" expose-name="1" /></buddies></contacts></args>' % ret
  339.             elif arg == "AddMobileBuddy":
  340.                 body = '<args><contacts><mobile-buddies><mobile-buddy uri="tel:%s" buddy-lists="1" desc="THis message is send by PyFetion" invite="0" /></mobile-buddies></contacts></args>' % ret
  341.         
  342.         #general SIPC info
  343.         self.header.append(('L',len(body)))
  344.         for k in self.header:
  345.             self.content = self.content + k[0] + ": " + str(k[1]) + "/r/n"
  346.         self.content+="/r/n"
  347.         self.content+= body
  348.         if self.login_type == "HTTP":
  349.             #IN TCP CONNECTION "SIPP" SHOULD NOT BEEN SEND
  350.             self.content+= FetionSIPP
  351.         return self.content
  352.     def __sendSIPP(self):
  353.         body = FetionSIPP
  354.         url = self.__http_tunnel+"?t=s&i=%s" % self.__seq
  355.         response = self.__http_send(url,body,self.__exheaders).read()
  356.         d_print(('response',),locals())
  357.         self.__seq+=1
  358.         return response
  359.     def __http_send(self,url,body="",exheaders="",login=False):
  360.         headers = {
  361.                    'User-Agent':'IIC2.0/PC 3.2.0540',
  362.                   }
  363.         headers.update(exheaders)
  364.         request = urllib2.Request(url,headers=headers,data=body)
  365.         try:
  366.             conn = urllib2.urlopen(request)
  367.         except urllib2.URLError, e:
  368.             code = e.code
  369.             msg = e.read()
  370.             d_print(('code','text'),locals())
  371.             if code == 401 or code == 404:
  372.                 if login:
  373.                     raise PyFetionAuthError(code,msg)
  374.             return -1
  375.         return conn
  376.     def __tcp_init(self):
  377.         try:
  378.             self.__sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  379.         except socket.error,e:
  380.             s = None
  381.             print e.read()
  382.             #Should return -1 NOT just exit
  383.             sys.exit(-1)
  384.         (host,port) = tuple(self.__sipc_proxy.split(":"))
  385.         port = int(port)
  386.         try:
  387.             self.__sock.connect((host,port))
  388.         except socket.error,e:
  389.             self.__sock.close()
  390.             self.__sock = None
  391.             print e.read()
  392.             sys.exit(-1)
  393.     def __tcp_send(self,msg):
  394.         try:
  395.             self.__sock.send(msg)
  396.         except socket.error,e:
  397.             self.__sock.close()
  398.             print e.read()
  399.             sys.exit(-1)
  400.     def __tcp_recv(self):
  401.         try:
  402.             data = self.__sock.recv(4096)
  403.         except socket.error,e:
  404.             self.__sock.close()
  405.             print e.read()
  406.             sys.exit(-1)
  407.         return data
  408.     def __get_salt(self):
  409.         return self.__hash_passwd()[:8]
  410.     def __get_cnonce(self):
  411.         return md5(str(uuid1())).hexdigest().upper()
  412.     def __get_response_md5(self,nonce,cnonce):
  413.         #nonce = "3D8348924962579418512B8B3966294E"
  414.         #cnonce= "9E169DCA9CBD85F1D1A89A893E00917E"
  415.         key = md5("%s:%s:%s" % (self.sid,self.domain,self.passwd)).digest()
  416.         h1  = md5("%s:%s:%s" % (key,nonce,cnonce)).hexdigest().upper()
  417.         h2  = md5("REGISTER:%s" % self.sid).hexdigest().upper()
  418.         response  = md5("%s:%s:%s" % (h1,nonce,h2)).hexdigest().upper()
  419.         #d_print(('nonce','cnonce','key','h1','h2','response'),locals())
  420.         return response
  421.     def __get_response_sha1(self,nonce,cnonce):
  422.         #nonce = "3D8348924962579418512B8B3966294E"
  423.         #cnonce= "9E169DCA9CBD85F1D1A89A893E00917E"
  424.         hash_passwd = self.__hash_passwd()
  425.         hash_passwd_str = binascii.unhexlify(hash_passwd[8:])
  426.         key = sha1("%s:%s:%s" % (self.sid,self.domain,hash_passwd_str)).digest()
  427.         h1  = md5("%s:%s:%s" % (key,nonce,cnonce)).hexdigest().upper()
  428.         h2  = md5("REGISTER:%s" % self.sid).hexdigest().upper()
  429.         response = md5("%s:%s:%s" % (h1,nonce,h2)).hexdigest().upper()
  430.         return response
  431.     def __hash_passwd(self):
  432.         #salt = '%s%s%s%s' % (chr(0x77), chr(0x7A), chr(0x6D), chr(0x03))
  433.         salt = 'wzm/x03'
  434.         src  = salt+sha1(self.passwd).digest()
  435.         return "777A6D03"+sha1(src).hexdigest().upper()
  436. def d_print(vars=(),namespace=[],msg=""):
  437.     """if only sigle variable use like this ('var',)"""
  438.     global debug
  439.     if vars and not namespace and not msg:
  440.         msg = vars
  441.     if debug and vars and namespace:
  442.         for var in vars:
  443.             if var in namespace:
  444.                 print "[PyFetion]:/033[0;31;48m%s/033[0m" % var,
  445.                 print namespace[var]
  446.     if debug and msg:
  447.         print "[PyFetion]:/033[0;31;48m%s/033[0m" % msg
  448. def main(argv=None):
  449.     try:
  450.         phone = PyFetion("138888888","888888","TCP")
  451.     except PyFetionInfoError,e:
  452.         print "corrent your mobile NO. and password"
  453.         return -1
  454.     phone.login()
  455.     #phone.get_offline_msg()
  456.     #phone.add("138888888")
  457.     #phone.get_info()
  458.     #phone.get_contact_list()
  459.     #phone.send_sms("Hello, ",long=True)
  460.     s = "2008-12-31 02:39:00."
  461.     for i in range(100,500):
  462.         time = s + str(i)
  463.         phone.send_schedule_sms("请注意,这个是定时短信",time)
  464.     #time_format = "%Y-%m-%d %H:%M:%S"
  465.     #time.strftime(time_format,time.gmtime())
  466.     
  467. if __name__ == "__main__":
  468.     sys.exit(main())
  469. --
  470. Jianjun Kong|Happy Hacking
  471. Homepage:http://kongove.cn
  472. kongjianjun (at) gmail.com

你可能感兴趣的:(python,url,domain,login,sms,attributes)