飞信发送API网上有很多,但没有多少是我自己满意的。很多网站提供基于Web的API调用方式向用户提供服务,但是作为使用者我心里还是没底。我总是担心自己的密码会被某些人记录,一直想写一个自己用的PHP版本飞信发送程序。
  因为本人没有任何逆向基础,同时飞信版本变化不同。从nathan在百度上发布《飞信协议分析》到现在也有3年了,且当时分析的是飞信2006版本。这中间变化太多,也使得我在写PHP版本飞信发送程序是走了很多弯路。
  我曾经拜读过superli_198的《让 PHP 程序利用飞信(Fetion)发免费短信》,但是该版本使用的通讯方式目前已经不被飞信支持,且superli_198也没有做新的更新。我也下载过c.young[@]xicabin.com的Openfetion,但是该版本存在明显bug,现在也不能正常使用。无奈只能硬着头皮修改一个C# 版本的飞信发送程序。
  在移植C#版本的飞信发送程序到PHP过程中,我遇到了一个关于MD5加密相关的问题,困了很多天。最后在CSDN论坛ycTIN的帮助下,问题得以解决。非常感谢ycTIN。 以下是我完成的PHP版飞信短信发送类,截止到2010年2月17日下午4点该程序一直能正常工作。技术上没有什么难度,发在这里和大家交流。
<?php
/**
 *@desc 飞信短信发送类(Encoded:UTF-8)
 *使用方法:$myNewFetion = new myFetion('1381111111', 'password','1382222222', '测试消息' );
 *非常感谢CSDN论坛ycTIN在MD5加密部分的帮助!
 *本程序未做容错处理,为防止诈骗短信乱发,程序不提供添加好友功能
 *测试URL:http://i.isclab.org/tools/fetion.php
 *
 *程序运行条件:
 *1.服务器能够访问飞信服务器nav.fetion.com.cn的443端口(https)
 *2.服务器端PHP程序能够创建socket访问221.176.31.4的8080端口
 *
 *关键技术:
 *1.CURL + SSL通讯
 *2.PHP Socket编程
 *3.PHP MD5函数的深入理解
 *4.PHP DOM处理XML
 *
 *@author shadu AT foxmail DOT com /CNOS(http://bbs.ouropen.org)
 *@version 2010-02-17
 *@copyright 任意拷贝和修改!
 **/
class myFetion{
    private $mobile_no   = '1381111111'; // 发送者手机号
    private $fetion_no   = '738713940' ; // 发送者飞信号,程序自动获取
    private $fetion_pwd  = 'mypassword' ; // 发送者飞信登录密码
    private $cookie_file = 'cookie.txt' ; // 临时存放的cookie文件
    
    public $SMS_RECEIVER = '1382222222' ; //  短信接收者手机号码 
    public $SMS_TEXT     = 'sms test' ; // 短信内容,支持中文
    
    private $NONCE       = 'AAB3238922BCC25A6F606EB525FFDC56' ; // SIPC服务器返回,每次不同
    private $C_NONCE     = 'AAB3238922BCC25A6F606EB525FFDC56' ; // 是随机的,但是固定值也没关系
    private $SSIC        = '' ; // cookie中提取的变量
    private $RESPONSE    = '' ; // 加密后的密钥串    
    private $url_nav            = 'https://nav.fetion.com.cn/nav/getsystemconfig.aspx' ; // 443端口获取导航信息
    private $domain_fetion      = 'fetion.com.cn' ; // 飞信服务器的域名
    
    private $SIPC_PROXY         = '221.176.31.4:8080';  //  8080端口飞信通讯占用 
    private $SSI_PROXY_SIGN_IN  = 'https://uid.fetion.com.cn/ssiportal/SSIAppSignIn.aspx' ; // 登录URL
    private $SSI_PROXY_SIGH_OUT = 'http://ssi.fetion.com.cn/ssiportal/SSIAppSignOut.aspx' ; // 登出URL
    
    private $proxy_http = 'proxy.example.com:8080' ; // HTTP代理服务器地址
    private $curl = NULL ;
    private $socket = NULL ;
    
    /**
     *从导航网站获取信息
     **/
    private $REQUEST_CONFIG = "<config><user mobile-no=\"%s\" /><client type=\"PC\" version=\"2.3.0230\" platform=\"W5.1\" /><servers version=\"0\" /><service-no version=\"12\" /><parameters version=\"15\" /><hints version=\"13\" /><http-applications version=\"14\" /><client-config version=\"17\" /></config>";
    
    /**
     *使用手机号码和密码向服务器获取对应的飞信号码信息
     **/
    private $REQUEST_SSI_SIGN  = "mobileno=%s&pwd=%s" ;
    
    /**
     *使用飞信号码向SIPC服务器注册,获取临时变量NONCE和SSIC的值
     **/
    private $REQUEST_SIPC_SIGN_NONCE      = "R %s SIP-C/2.0\r\nF: %s\r\nI: 1\r\nQ: 1 R\r\nL: %d\r\n\r\n%s" ;
    private $REQUEST_SIPC_SIGN_NONCE_BODY = "<args><device type=\"PC\" version=\"6\" client-version=\"2.3.0230\" /><caps value=\"simple-im;im-session;temp-group\" /><events value=\"contact;permission;system-message\" /><user-info attributes=\"all\" /><presence><basic value=\"400\" desc=\"\" /></presence></args>";
    
    /**
     *使用飞信号码和加密的密码登录飞信SIPC服务器
     **/
    private $REQUEST_SIPC_LOGIN           = "R %s SIP-C/2.0\r\nF: %s\r\nI: 1\r\nQ: 2 R\r\nA: Digest response=\"%s\",cnonce=\"%s\"\r\nL: %d\r\n\r\n%s";
    private $REQUEST_SIPC_LOGIN_BODY      = "<args><device type=\"PC\" version=\"6\" client-version=\"2.3.0230\" /><caps value=\"simple-im;im-session;temp-group\" /><events value=\"contact;permission;system-message\" /><user-info attributes=\"all\" /><presence><basic value=\"400\" desc=\"\" /></presence></args>";
    
    private $REQUEST_SIPC_SENDSMS         = "M %s SIP-C/2.0\r\nF: %s\r\nI: 2\r\nQ: 1 M\r\nT: tel:%s\r\nN: SendSMS\r\nL: %d\r\n\r\n%s";
    
    private $REQUEST_SIPC_LOGOUT          = "R %s SIP-C/2.0\r\nF: %s\r\nI: 1 \r\nQ: 3 R\r\nX: 0\r\n\r\n";
    
    /**
     *@param $sender 短信发送者手机号
     *@param $passwd 短信发送者密码
     *@param $receiver 短信接收者手机号
     *@param $msg 短信内容
     **/
    public function __construct($sender, $passwd, $receiver, $msg){
        $this->mobile_no    = $sender ;
        $this->fetion_pwd   = $passwd;
        $this->SMS_RECEIVER = $receiver;
        $this->SMS_TEXT     = $msg;
        $this->cookie_file  = $this->mobile_no . $this->cookie_file ;
        file_put_contents($this->cookie_file, '') ;
        
        $this->FetionGetConfig(); // 从导航网站443端口获取登录信息
        $this->FetionSocektInit(); // 初始化到SIPC的8080端口socket连接
        $this->FetionGetSIPCNonce(); // 向服务器注册飞信号,获取关键变量值
        if($this->FetionLogin()){    // 发送登录认证命令
            $this->FetionSendSMS(); // 发送短信发送命令
            $this->FetionLogout();
        }        
    }
    
    
    /**
     *从导航地址获取配置信息
     **/
    private function FetionGetConfig(){
        $this->REQUEST_CONFIG = sprintf($this->REQUEST_CONFIG,
                                        $this->mobile_no);
        $this->curl = curl_init();
        curl_setopt($this->curl, CURLOPT_URL, $this->url_nav);
        curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($this->curl, CURLOPT_COOKIEJAR, $this->cookie_file);
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($this->curl, CURLOPT_POST, 1);
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, $this->REQUEST_CONFIG);
        //curl_setopt($this->curl, CURLOPT_PROXY, $this->proxy_http); // 设置代理服务器
        $xml_config = curl_exec($this->curl);
        
        // 以下是从导航页面返回的XML里取相关信息
        file_put_contents("test3.xml", $xml_config) ;
        $xmlDom = new DOMDocument() ;
        $xmlDom->loadXML($xml_config);
        $fetion_server = $xmlDom->getElementsByTagName('servers');
        $fetion_server->item(0)->getElementsByTagName('sipc-proxy')->item(0)->nodeValue;
        $this->SSI_PROXY_SIGN_IN  = $fetion_server->item(0)->getElementsByTagName('ssi-app-sign-in')->item(0)->nodeValue;
        $this->SSI_PROXY_SIGH_OUT = $fetion_server->item(0)->getElementsByTagName('ssi-app-sign-out')->item(0)->nodeValue;
        $this->SSI_PROXY_SIGN_IN;
        
        // 以下获取手机号对应的飞信号
        sprintf($this->REQUEST_SSI_SIGN, $this->mobile_no, $this->fetion_pwd) ;
        curl_setopt($this->curl, CURLOPT_URL, $this->SSI_PROXY_SIGN_IN);
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, sprintf($this->REQUEST_SSI_SIGN, $this->mobile_no, $this->fetion_pwd));
        $Result = curl_exec($this->curl);
        curl_close($this->curl);
        file_put_contents("test4.xml", $Result) ;
        $xmlDom->loadXML($Result);
        $uri = $xmlDom->getElementsByTagName("user")->item(0)->getAttribute("uri");
        //"sip:[email protected];p=5914"
        if(preg_match('/^sip:(\d+)@(\S+);.*$/', $uri, $matches)){
            $this->fetion_no = $matches[1] ;
            $this->domain_fetion = $matches[2] ;
        }        
    }
    
    /**
     *初始化Fetion通讯Socket
     **/
    private function FetionSocektInit(){
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        list($ip_fetion, $port_fetion) = split(':', $this->SIPC_PROXY) ; // "221.176.31.4:8080"
        socket_connect($this->socket, $ip_fetion, $port_fetion) ;
    }
    
    /**
     *注册飞信号码并获取临时变量NONCE和SSIC
     **/
    private function FetionGetSIPCNonce(){
        $REQUEST_SIPC_SIGN_NONCE = sprintf($this->REQUEST_SIPC_SIGN_NONCE, $this->domain_fetion,
                                           $this->fetion_no, strlen($this->REQUEST_SIPC_SIGN_NONCE_BODY),
                                           $this->REQUEST_SIPC_SIGN_NONCE_BODY) ;
        $sock_data = socket_write($this->socket, $REQUEST_SIPC_SIGN_NONCE);
        $buf = '' ;
        if (false == ($buf = socket_read($this->socket, 1000))) {
            echo "Line:" . __LINE__ . "socket_read() failed; reason: " . socket_strerror(socket_last_error($this->socket)) . "\n";
        }
        $regex_ssic = '/.*nonce=\"(\\w+)\".*/s' ;
        if(!preg_match($regex_ssic, $buf, $matches)){
            echo "Fetion Error: No nonce found in socket\n";
        }
        $this->NONCE = strtoupper(trim($matches[1]));        $regex_ssic = '/ssic\s+(.*)/s';
        if (!preg_match($regex_ssic, file_get_contents($this->cookie_file), $matches)) {
            echo "Fetion Error: No ssic found in cookie\n";
        }
        $this->SSIC = trim($matches[1]);
    }
    

解决方案 »

  1.   


        /**
         *登录飞信服务器
         **/
        private function FetionLogin(){
            $this->RESPONSE = $this->FetionEncryptPassWD() ;
            $REQUEST_SIPC_LOGIN = sprintf($this->REQUEST_SIPC_LOGIN,
                                          $this->domain_fetion, $this->fetion_no,
                                          $this->RESPONSE, $this->C_NONCE,
                                          strlen($this->REQUEST_SIPC_LOGIN_BODY),
                                          $this->REQUEST_SIPC_LOGIN_BODY);
            $buf = '' ;
            $sock_data = socket_write($this->socket, $REQUEST_SIPC_LOGIN);
            if (false == ($buf = socket_read($this->socket, 1000))) {
                echo "Line:" . __LINE__ . "socket_read() failed; reason: " . socket_strerror(socket_last_error($this->socket)) . "\n";
            }
            if(preg_match_all('/200/s', $buf, $matches)){
                return True;
            }else{
                return False;
            }
        }
        
        /**
         *发短信
         **/
        public function FetionSendSMS(){
            //"M %s SIP-C/2.0\r\nF: %s\r\nI: 2\r\nQ: 1 M\r\nT: tel:%s\r\nN: SendSMS\r\nL: %d\r\n\r\n%s";
            $REQUEST_SENDSMS = sprintf($this->REQUEST_SIPC_SENDSMS,
                                       $this->domain_fetion, $this->fetion_no,
                                       $this->SMS_RECEIVER,
                                       strlen($this->SMS_TEXT),
                                       $this->SMS_TEXT) ;
            $buf = '' ;
            $sock_data = socket_write($this->socket, $REQUEST_SENDSMS);
            if (false == ($buf = socket_read($this->socket, 1000))) {
                echo "\nLine:" . __LINE__ . " socket_read() failed; reason: " . socket_strerror(socket_last_error($this->socket)) . "\n";
            }
            if(preg_match_all('/200/s', $buf, $matches)){
                return True;
            }else{
                return False;
            }
        }
        
        /**
         *登出飞信服务器
         **/
        private function FetionLogout(){
            //string Logout = String.Format(FETION_SIPC_LOGOUT, FETION_DOMAIN_URL, Fetion_Number);
            $FETION_SIPC_LOGOUT   = "R %s SIP-C/2.0\r\nF: %s\r\nI: 1 \r\nQ: 3 R\r\nX: 0\r\n\r\n";
            $REQUEST_SIPC_LOGOUT = sprintf($this->REQUEST_SIPC_LOGOUT,
                                           $this->domain_fetion,
                                           $this->fetion_no) ;
            socket_write($this->socket, $REQUEST_SIPC_LOGOUT);
            socket_close($this->socket) ;        
        }    /**
         *生成加密串,感谢CSND ycTIN的帮助!
         *@return string 加密的密码串     *
         **/
            
        private function FetionEncryptPassWD() {
            $key = md5($this->fetion_no . ':' . $this->domain_fetion . ':' . $this->fetion_pwd, true);
            $h1 = strtoupper(md5($key . ':' . $this->NONCE . ':' . $this->C_NONCE));
            
            $h2 = "REGISTER:" . $this->fetion_no ;
            $h2 = strtoupper(md5($h2));
            
            $response = "$h1:" . $this->NONCE . ":" . $h2;
            $response = strtoupper(md5($response));
            
            return $response ;
        }    /**
         *打印一下临时变量
         **/
        public function printVar(){
            print "\nCNONCE:" . $this->C_NONCE;
            print "\nDomain:" . $this->domain_fetion;
            print "\nNONCE:" . $this->NONCE;
            print "\nRESPONSE:" . $this->RESPONSE;
        }
        public function __destruct(){
            //$this->FetionLogout() ;
            @unlink($this->cookie_file); // 删除cookie文件
        }
    }//$myNewFetion = new myFetion('13811111111', 'password', '13822222222', '飞信测试');//$myNewFetion->printVar() ;
    ?>
    测试用的Web程序:fetion.php
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <title>飞信短信PHP类测试界面</title>
        </head>
        <body>
            <div style="margin-top:5px;margin-right:400px;">
                <?php
                error_reporting(0);
                @require("class.fetion.php");
                
                if(isset($_POST['sender'])){
                    $receiver = '' ;
                    $msg = '' ;
                    if (!is_numeric($_POST['sender'])) return ;
                    if (!isset($_POST['pass'])) return;
                    if (!isset($_POST['receiver'])) {
                        $receiver = $_POST['sender'] ;
                    }else{
                        $receiver = $_POST['receiver'];
                    }
                    if (!isset($_POST['msg'])) {
                        $msg = "短信测试--http://i.isclab.org/tools/fetion.php" ;
                    }else{
                        $msg = $_POST['msg'] ;
                    }
                    $myfetion = new myFetion($_POST['sender'], $_POST['pass'], $receiver, $msg);
                    echo "<div><font color=\"red\">测试短信已经发送</font></div>" ;
                }
                ?>
                <br/>
                <div id="sendsms">
                    <form method="POST" action="fetion.php">
                        发送者的手机号<input name="sender" type="text" size="12" /><br/>
                        登录飞信的密码<input name="pass" type="password" size="13" /><br/>
                        接收者的手机号<input name="receiver" type="text" size="12" /><br/>
                        短信发送的内容<input name="msg" type="text" size="50" /><br/><br/>
                        <input name="send" value="发送短信" type="submit" />
                    </form>
                </div>
                <div>
                    <font color="red">本人申明不会记录您的密码,请在测试前三思!<br>
                    同时,建议测试完毕后立即修改您的飞信登录密码
                    </font><br/><br/>
                    因网站服务商禁用了php的socket功能,本站代码暂时无法测试。
                    请<a href="fetion.zip">下载代码</a>本地测试。
                </div>
            </div>
            
        </body>
    </html>
      

  2.   

    非常感谢给与帮助的以下tx:
    ycTIN,wuyq11,Moxie,PHP菜鸟等
      

  3.   

    先收藏,以后用的时候慢慢学,祝ttm1984春节好!
      

  4.   

    嗯. 不错... 赞一个..有点小问题可以继续 优化.就是对于 curl 做POST 会有效率问题.
    另: 使用临时文件做POST xml 数据, 如果并发访问大的话,会存在冲突..
      

  5.   

    谢谢mrshellycurl的效率低我还真不知道,其实直接做成socket也是可以的。
    另外过程中的xml文件是调试使用的,可以把那两句注释掉。
    目前处理方式是每个手机号使用一个cookie文件,用完了就删除。
      

  6.   

    为什么只能给自己发,发给别人不行.提示ssic错误,
      

  7.   


    就是打算给好友发,才提示
    Fetion Error: No ssic found in cookie 
    Notice: Undefined offset: 1 in D:\PHPSERVER\htdocs\fetion.php on line 174
    CNONCE:AAB3238922BCC25A6F606EB525FFDC56 
    Domain:fetion.com.cn 
    NONCE:15CF056E5A612D915B18466277A6D414 
    RESPONSE:11D3621E2424E50C748F683751A9B869
    不能成功发送
    line 174的内容是:$this->SSIC = trim($matches[1]);
      

  8.   


    给常感谢你的测试和提醒,确实存在这个问题。
    我已经修改了这个bug,请参见这页面http://topic.csdn.net/u/20100303/22/2295337f-dc45-4108-813b-ffa688b11f76.html
      

  9.   

    我用的也是这个类 但是 总是在 socket_read的时候 出错
    Warning: socket_read() [function.socket-read]: unable to read from socket [0]: 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。 in 
      

  10.   

    干嘛不用wap飞信,几行代码就行了,写这么长代码有毛用呀