Open Fetion -- Implement Fetion Protocol with PHP
project @ sourceforge.net
Usage
Usage: fetion [options] user_mobile password
fetion [options] user_mobile password sendto_sid content
* if no sendto_sid specified, all available sid will be displayed
* you can not send sms to yourself
* use sid as 'sendto_sid' instead of mobile number or fetion number
-p Proxy
-d Debug output
<?php
/**
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*
* @author c.young[@]xicabin.com
* @license GPL
* @version 1.0
*/
define('FETION_URL', 'http://221.130.44.194/ht/sd.aspx');
define('FETION_LOGIN_URL', 'https://nav.fetion.com.cn/ssiportal/SSIAppSignIn.aspx');
define('FETION_CONFIG_URL', 'http://nav.fetion.com.cn/nav/getsystemconfig.aspx');
define('FETION_SIPP', 'SIPP');
static $fetion_proxy = null;
static $fetion_debug = false;
/**
* generate guid for windows / *nix
*/
function guid(){
if (function_exists('com_create_guid')){
return com_create_guid();
}else{
mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
$charid = strtoupper(md5(uniqid(rand(), true)));
$hyphen = chr(45);// "-"
$uuid = chr(123)// "{"
.substr($charid, 0, 8).$hyphen
.substr($charid, 8, 4).$hyphen
.substr($charid,12, 4).$hyphen
.substr($charid,16, 4).$hyphen
.substr($charid,20,12)
.chr(125);// "}"
return $uuid;
}
}
/**
* debug output
*
* @msg message
* @data addtional data
*/
function fetion_debug($msg, $data = null) {
global $fetion_debug;
if ($fetion_debug) {
print "[*] $msg\r\n";
if (!empty($data)) {
print_r($data);
}
}
}
/**
* create sip package
*
* @invite sip invite
* @fields array of fields
* @arg argument to send
*/
function fetion_sip_create($invite, $fields, $arg = '') {
$sip = $invite."\r\n";
foreach ($fields as $k=>$v) {
$sip .= "$k: $v\r\n";
}
$sip .= "L: ".strval(strlen($arg))."\r\n\r\n{$arg}";
return $sip;
}
/**
* create a curl handle with fetion option
*
* @url url
* @ssic user identification
* @post data to post
*/
function fetion_curl_init($url, $ssic = null, $post = null) {
// create a new guid, and keep it !
static $guid = null;
if ($guid == null) {
$guid = strtolower(trim(guid(), "{}"));
}
// set headers, e.g. pragma
$headers = array('Content-Type: application/oct-stream', 'Pragma: xz4BBcV'.$guid);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_USERAGENT, 'IIC2.0/PC 3.2.0540');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// ssic
if ($ssic != null) {
curl_setopt($ch, CURLOPT_COOKIE, "ssic=$ssic");
}
// post data
if ($post != null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
// proxy
global $fetion_proxy;
if ($fetion_proxy != null) {
curl_setopt($ch, CURLOPT_PROXY, $fetion_proxy);
}
return $ch;
}
/**
* run a curl query
*
* @see fetion_curl_init
*/
function fetion_curl_exec($url, $ssic = null, $post = null) {
$ch = fetion_curl_init($url, $ssic, $post);
$succeed = curl_exec($ch);
if (!$succeed) {
error_log(curl_error($ch));
}
curl_close($ch);
return $succeed;
}
/**
* login
*
* @mobileno mobile number
* @pwd password
*/
function fetion_login($mobileno, $pwd) {
$login_url = FETION_LOGIN_URL."?mobileno=$mobileno&pwd=$pwd";
$ssic_regex = '/ssic\s+(.*)/s';
$sid_regex = '/sip:(\d+)@(.+);/s';// sid@domain
$cookie_file = date('YmdHis').'_cookie.txt';// create a tmp file to save cookie
$return_val = false;
$ch = fetion_curl_init($login_url, null, null);
// do not verify host
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// save cookie for further process
file_put_contents($cookie_file, '');
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
$succeed = curl_exec($ch);
// close first, in order to make cookie file written
curl_close($ch);
fetion_debug("login to nav.fetion.com.cn");
if (!$succeed) {
error_log(curl_error($ch));
return false;
}
// get ssic from cookie
$ssic = false;
$matches = array();
if (!preg_match($ssic_regex, file_get_contents($cookie_file), $matches)) {
error_log("Fetion Error: No ssic found in cookie");
return false;
}
$ssic = trim($matches[1]);
fetion_debug("ssic: ".substr($ssic, 0, 10)."...");
// get other login info from output
$result_xml = simplexml_load_string($succeed);
$return_val = array(
'ssic' => $ssic,
'status-code' => strval($result_xml['status-code']),
'uri' => strval($result_xml->user['uri']),
'mobile-no' => strval($result_xml->user['mobile-no']),
'user-status' => strval($result_xml->user['user-status'])
);
// extract sid and domain for further use
if (preg_match($sid_regex, $return_val['uri'], $matches)) {
$return_val['sid'] = $matches[1];
$return_val['domain'] = $matches[2];
}
fetion_debug("sid: {$return_val['sid']}");
unlink($cookie_file);
return $return_val;
}
/**
* get fetion system config, not used
*/
function fetion_get_system_config() {
$post_fields = '<config><client type="PC" version="3.2.0540" platform="W5.1" /><client-config version="0" /></config>';
return fetion_curl_exec(FETION_CONFIG_URL, null, $post_fields);
}
/**
* hex to binary
*
* @hex string hex code
*/
function fetion_hex2bin($hex) {
$bin = '';
$len = strlen($hex);
for ($I = 0; $I < $len; $I += 2) {
$bin .= chr(hexdec(substr($hex, $I, 2)));
}
return $bin;
}
/**
* get hash password
*
* @password real password
*/
function fetion_hash_password($password) {
// in fact, salt is constant value
$salt = chr(0x77).chr(0x7A).chr(0x6D).chr(0x03);
$src = $salt.hash('sha1', $password, true);
return strtoupper(bin2hex($salt.sha1($src, true)));
}
/**
* create a random cnonce
*/
function fetion_calc_cnonce() {
return sprintf("%04X%04X%04X%04X%04X%04X%04X%04X",
rand() & 0xFFFF, rand() & 0xFFFF, rand() & 0xFFFF,
rand() & 0xFFFF, rand() & 0xFFFF, rand() & 0xFFFF,
rand() & 0xFFFF, rand() & 0xFFFF);
}
/**
* get salt from real password
*
* @password real password
*/
function fetion_calc_salt($password) {
return substr(fetion_hash_password($password), 0, 8);
}
/**
* calculate response
*
* @sid fetion id
* @domain domain
* @password real password
* @nonce nonce from server
* @cnonce cnonce
*/
function fetion_calc_response($sid, $domain, $password, $nonce, $cnonce) {
$password = fetion_hash_password($password);
$str = fetion_hex2bin(substr($password, 8));
$key = sha1("$sid:$domain:$str", true);
$h1 = strtoupper(md5("$key:$nonce:$cnonce"));
$h2 = strtoupper(md5("REGISTER:$sid"));
$res = strtoupper(md5("$h1:$nonce:$h2"));
return $res;
}
/**
* get url with next request number
*
* @t i don't known
*/
function fetion_next_url($t = 's') {
static $seq = 0;
++$seq;
return FETION_URL."?t=$t&i=$seq";
}
/**
* get next call id
*/
function fetion_next_call() {
static $call = 0;
++$call;
return $call;
}
/**
* register to server
*
* @ssic user identification
* @sid fetion id
* @domain domain
* @password real password
*/
function fetion_http_register($ssic, $sid, $domain, $password) {
$nonce_regex = '/nonce="(\w+)"/s';
$ok_regex = '/OK/s';
$arg = '<args><device type="PC" version="44" client-version="3.2.0540" />';
$arg .= '<caps value="simple-im;im-session;temp-group;personal-group" />';
$arg .= '<events value="contact;permission;system-message;personal-group" />';
$arg .= '<user-info attributes="all" /><presence><basic value="400" desc="" /></presence></args>';
fetion_debug("begin register");
$call = fetion_next_call();
fetion_curl_exec(fetion_next_url(), $ssic, FETION_SIPP);
$msg = fetion_sip_create('R fetion.com.cn SIP-C/2.0', array('F'=>$sid, 'I'=>$call, 'Q'=>'1 R'), $arg).FETION_SIPP;
fetion_curl_exec(fetion_next_url('i'), $ssic, $msg);
$msg = fetion_curl_exec(fetion_next_url(), $ssic, FETION_SIPP);
fetion_debug("recv nonce...");
$matches = array();
if (!preg_match($nonce_regex, $msg, $matches)) {
error_log('Fetion Error: no nonce found');
return false;
}
$nonce = $matches[1];
$salt = fetion_calc_salt($password);
$cnonce = fetion_calc_cnonce();
$response = fetion_calc_response($sid, $domain, $password, $nonce, $cnonce);
fetion_debug("nonce: $nonce");
fetion_debug("salt: $salt");
fetion_debug("cnonce: $cnonce");
fetion_debug("response: $response");
$msg = fetion_sip_create('R fetion.com.cn SIP-C/2.0', array('F'=>$sid, 'I'=>$call, 'Q'=>'2 R', 'A'=>"Digest algorithm=\"SHA1-sess\",response=\"$response\",cnonce=\"$cnonce\",salt=\"$salt\""), $arg).FETION_SIPP;
fetion_debug("send response...");
fetion_curl_exec(fetion_next_url(), $ssic, $msg);
$msg = fetion_curl_exec(fetion_next_url(), $ssic, FETION_SIPP);
return preg_match($ok_regex, $msg);
}
/**
* send sms use http
*
* @ssic user identification
* @sid fetion id
* @to receiver mobile number or sid
* @content sms content
*/
function fetion_http_send_sms($ssic, $sid, $to, $content) {
$ok_regex = '/Send SMS OK/s';
$msg = fetion_sip_create('M fetion.com.cn SIP-C/2.0', array('F'=>$sid, 'I'=>fetion_next_call(), 'Q'=>'1 M', 'T'=>$to, 'N'=>'SendSMS'), $content).FETION_SIPP;
fetion_debug("send sms...");
fetion_curl_exec(fetion_next_url(), $ssic, $msg);
$msg = fetion_curl_exec(fetion_next_url(), $ssic, FETION_SIPP);
return preg_match($ok_regex, $msg);
}
/**
* get buddy list
*
* @ssic user identification
* @sid fetion id
*/
function fetion_get_buddy_list($ssic, $sid) {
$buddy_regex = '/.*?\r\n\r\n(.*)'.FETION_SIPP.'\s*$/is';
$arg = '<args><contacts><buddy-lists /><buddies attributes="all" /><mobile-buddies attributes="all" /><chat-friends /><blacklist /></contacts></args>';
$msg = fetion_sip_create('S fetion.com.cn SIP-C/2.0', array('F'=>$sid, 'I'=>fetion_next_call(), 'Q'=>'1 S', 'N'=>'GetContactList'), $arg).FETION_SIPP;
fetion_curl_exec(fetion_next_url(), $ssic, $msg);
$msg = fetion_curl_exec(fetion_next_url(), $ssic, FETION_SIPP);
$matches = array();
if (!preg_match($buddy_regex, $msg, $matches)) {
error_log("Fetion Error: No buddy list found");
return false;
}
$buddy_list = simplexml_load_string($matches[1]);
$buddies = array();
foreach ($buddy_list->contacts->buddies->buddy as $buddy) {
$buddies[strval($buddy['uri'])] = strval($buddy['local-name']);
}
foreach ($buddy_list->contacts->{'mobile-buddies'}->{'mobile-buddy'} as $buddy) {
$buddies[strval($buddy['uri'])] = strval($buddy['local-name']);
}
return $buddies;
}
/**
* usage
*/
function usage() {
echo "Usage: fetion [options] user_mobile password\r\n";
echo " fetion [options] user_mobile password sendto_sid content\r\n";
echo "\r\n";
echo " * if no sendto_sid specified, all available sid will be displayed\r\n";
echo " * you can not send sms to yourself\r\n";
echo " * use sid as 'sendto_sid' instead of mobile number or fetion number\r\n";
echo " -p <proxy[:port]> Proxy\r\n";
echo " -d Debug output\r\n";
}
/**
* main
*
* @args command line args
*/
function main($argc, $argv) {
global $fetion_proxy;
global $fetion_debug;
$user_mobile = null;
$password = null;
$sendto_sid = null;
$content = null;
if ($argc < 2) {
usage();
return 1;
}
for ($I = 1; $I < $argc; ++$I) {
if ($argv[$I] == '-p') {
$fetion_proxy = $argv[++$I];
} else if ($argv[$I] == '-d') {
$fetion_debug = true;
} else {
$user_mobile = $argv[$I++];
$password = $argv[$I++];
if (isset($argv[$I])) {
$sendto_sid = $argv[$I++];
$content = $argv[$I];
}
break;
}
}
$login_info = fetion_login($user_mobile, $password);
if ($login_info === false) {
print "[*] login failed\r\n";
return 1;
}
$ssic = $login_info['ssic'];
$sid = $login_info['sid'];
$domain = $login_info['domain'];
print "[*] login successful\r\n";
$ok = fetion_http_register($ssic, $sid, $domain, $password);
if ($ok === false) {
print "[*] register failed\r\n";
return 1;
}
print "[*] register successful\r\n";
if (empty($sendto_sid) || empty($content)) {
$buddies = fetion_get_buddy_list($ssic, $sid);
if ($buddies === false) {
print "[*] get buddy list failed\r\n";
} else {
print " [sid] [name]\r\n";
foreach ($buddies as $sid=>$name) {
printf(" %35s => %s\r\n", $sid, $name);
}
}
} else {
$ok = fetion_http_send_sms($ssic, $sid, $sendto_sid, $content);
print "[*] send sms ".strval($ok ? 'successful' : 'failed')."\r\n";
}
}
main($argc, $argv);
?>