From: Luke Fitzgerald Date: Wed, 16 Jun 2010 00:22:52 +0000 (+0100) Subject: Reordered methods and changed properties to constants as needed X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=2d883eed893f4c7178030c032b518444b43eeabe;p=quix0rs-gnu-social.git Reordered methods and changed properties to constants as needed --- diff --git a/plugins/Msn/extlib/phpmsnclass/msn.class.php b/plugins/Msn/extlib/phpmsnclass/msn.class.php index 6146bd1c5a..1e8d7e0f1f 100644 --- a/plugins/Msn/extlib/phpmsnclass/msn.class.php +++ b/plugins/Msn/extlib/phpmsnclass/msn.class.php @@ -1,49 +1,66 @@ user = $Configs['user']; $this->password = $Configs['password']; $this->alias = isset($Configs['alias']) ? $Configs['alias'] : ''; @@ -179,1271 +175,1496 @@ class MSN { = 0x7000800C; */ $this->clientid = $client_id; - $this->ABService=new SoapClient(realpath(dirname(__FILE__)).'/soap/msnab_sharingservice.wsdl',array('trace' => 1)); - } - - private function Array2SoapVar($Array, $ReturnSoapVarObj = true, $TypeName = null, $TypeNameSpace = null) { - $ArrayString = ''; - foreach($Array as $Key => $Val) { - if ($Key{0} == ':') continue; - $Attrib = ''; - if (is_array($Val[':'])) { - foreach ($Val[':'] as $AttribName => $AttribVal) - $Attrib .= " $AttribName='$AttribVal'"; - } - if ($Key{0} == '!') { - //List Type Define - $Key = substr($Key,1); - foreach ($Val as $ListKey => $ListVal) { - if ($ListKey{0} == ':') continue; - if (is_array($ListVal)) $ListVal = $this->Array2SoapVar($ListVal, false); - elseif (is_bool($ListVal)) $ListVal = $ListVal ? 'true' : 'false'; - $ArrayString .= "<$Key$Attrib>$ListVal"; - } - continue; - } - if (is_array($Val)) $Val = $this->Array2SoapVar($Val, false); - elseif (is_bool($Val)) $Val = $Val ? 'true' : 'false'; - $ArrayString .= "<$Key$Attrib>$Val"; - } - if ($ReturnSoapVarObj) return new SoapVar($ArrayString, XSD_ANYXML, $TypeName, $TypeNameSpace); - return $ArrayString; + $this->ABService = new SoapClient(realpath(dirname(__FILE__)).'/soap/msnab_sharingservice.wsdl', array('trace' => 1)); } /** - * Get Passport ticket - * - * @param string $url URL string (Optional) - * @return mixed Array of tickets or false on failure - */ - private function get_passport_ticket($url = '') { - $user = $this->user; - $password = htmlspecialchars($this->password); - - if ($url === '') - $passport_url = $this->passport_url; - else - $passport_url = $url; - - $XML = ' - -
- - {7108E71A-9926-4FCB-BCC9-9A9D3F32E423} - 4 - 1 - - AQAAAAIAAABsYwQAAAAxMDMz - - - - '.$user.' - '.$password.' - - -
- - - - http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue - - - http://Passport.NET/tb - - - - - http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue - - - messengerclear.live.com - - - - - - http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue - - - messenger.msn.com - - - - - - http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue - - - contacts.msn.com - - - - - - http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue - - - messengersecure.live.com - - - - - - http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue - - - spaces.live.com - - - - - - http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue - - - storage.msn.com - - - - - - -
'; - - //$this->debug_message("*** URL: $passport_url"); - //$this->debug_message("*** Sending SOAP:\n$XML"); - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $passport_url); - if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); - $data = curl_exec($curl); - $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - //$this->debug_message("*** Get Result:\n$data"); - - if ($http_code != 200) { - // sometimes, redirect to another URL - // MSNP15 - //psf:Redirect - //https://msnia.login.live.com/pp450/RST.srf - //Authentication Failure - if (strpos($data, 'psf:Redirect') === false) { - $this->debug_message("*** Could not get passport ticket! http code = $http_code"); - return false; - } - preg_match("#(.*)#", $data, $matches); - if (count($matches) == 0) { - $this->debug_message('*** Redirected, but could not get redirect URL!'); + * Signon methods + */ + + /** + * Connect to the NS server + * + * @param String $user Username + * @param String $password Password + * @param String $redirect_server Redirect server + * @param Integer $redirect_port Redirect port + * @return Boolean Returns true if successful + */ + private function connect($user, $password, $redirect_server = '', $redirect_port = 1863) { + $this->id = 1; + if ($redirect_server === '') { + $this->NSfp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout); + if (!$this->NSfp) { + $this->error = "!!! Could not connect to $this->server:$this->port, error => $errno, $errstr"; return false; } - $redirect_url = $matches[1]; - if ($redirect_url == $passport_url) { - $this->debug_message('*** Redirected, but to same URL!'); + } + else { + $this->NSfp = @fsockopen($redirect_server, $redirect_port, $errno, $errstr, $this->timeout); + if (!$this->NSfp) { + $this->error = "!!! Could not connect to $redirect_server:$redirect_port, error => $errno, $errstr"; return false; } - $this->debug_message("*** Redirected to $redirect_url"); - return $this->get_passport_ticket($redirect_url); } - - // sometimes, redirect to another URL, also return 200 + $this->authed = false; + // MSNP9 + // NS: >> VER {id} MSNP9 CVR0 // MSNP15 - //psf:Redirect - //https://msnia.login.live.com/pp450/RST.srf - //Authentication Failure - if (strpos($data, 'psf:Redirect') !== false) { - preg_match("#(.*)#", $data, $matches); - if (count($matches) != 0) { - $redirect_url = $matches[1]; - if ($redirect_url == $passport_url) { - $this->debug_message('*** Redirected, but to same URL!'); - return false; - } - $this->debug_message("*** Redirected to $redirect_url"); - return $this->get_passport_ticket($redirect_url); - } - } + // NS: >>> VER {id} MSNP15 CVR0 + $this->ns_writeln("VER $this->id ".PROTOCOL.' CVR0'); - // no Redurect faultcode or URL - // we should get the ticket here + $start_tm = time(); + while (!socketcheck($this->NSfp)) { + $data = $this->ns_readln(); + // no data? + if ($data === false) { + // logout now + // NS: >>> OUT + $this->ns_writeln("OUT"); + @fclose($this->NSfp); + $this->error = 'Timeout, maybe protocol changed!'; + return false; + } - // we need ticket and secret code - // RST1: messengerclear.live.com - // t=tick&p= - // binary secret - // RST2: messenger.msn.com - // t=tick - // RST3: contacts.msn.com - // t=tick&p= - // RST4: messengersecure.live.com - // t=tick&p= - // RST5: spaces.live.com - // t=tick&p= - // RST6: storage.msn.com - // t=tick&p= - preg_match("#". - "(.*)(.*)". - "(.*)(.*)". - "(.*)(.*)". - "(.*)(.*)". - "(.*)(.*)". - "(.*)(.*)". - "(.*)(.*)". - "#", - $data, $matches); + $code = substr($data, 0, 3); + $start_tm = time(); - // no ticket found! - if (count($matches) == 0) { - $this->debug_message('*** Could not get passport ticket!'); - return false; - } + switch ($code) { + case 'VER': + // MSNP9 + // NS: <<< VER {id} MSNP9 CVR0 + // NS: >>> CVR {id} 0x0409 winnt 5.1 i386 MSMSGS 6.0.0602 msmsgs {user} + // MSNP15 + // NS: <<< VER {id} MSNP15 CVR0 + // NS: >>> CVR {id} 0x0409 winnt 5.1 i386 MSMSGS 8.1.0178 msmsgs {user} + $this->ns_writeln("CVR $this->id 0x0409 winnt 5.1 i386 MSMSGS ".BUILDVER." msmsgs $user"); + break; - //$this->debug_message(var_export($matches, true)); - // matches[0]: all data - // matches[1]: RST1 (messengerclear.live.com) ticket - // matches[2]: ... - // matches[3]: RST1 (messengerclear.live.com) binary secret - // matches[4]: ... - // matches[5]: RST2 (messenger.msn.com) ticket - // matches[6]: ... - // matches[7]: RST3 (contacts.msn.com) ticket - // matches[8]: ... - // matches[9]: RST4 (messengersecure.live.com) ticket - // matches[10]: ... - // matches[11]: RST5 (spaces.live.com) ticket - // matches[12]: ... - // matches[13]: RST6 (storage.live.com) ticket - // matches[14]: ... + case 'CVR': + // MSNP9 + // NS: <<< CVR {id} {ver_list} {download_serve} .... + // NS: >>> USR {id} TWN I {user} + // MSNP15 + // NS: <<< CVR {id} {ver_list} {download_serve} .... + // NS: >>> USR {id} SSO I {user} + $this->ns_writeln("USR $this->id ".LOGIN_METHOD." I $user"); + break; - // so - // ticket => $matches[1] - // secret => $matches[3] - // web_ticket => $matches[5] - // contact_ticket => $matches[7] - // oim_ticket => $matches[9] - // space_ticket => $matches[11] - // storage_ticket => $matches[13] + case 'USR': + // already login for passport site, finish the login process now. + // NS: <<< USR {id} OK {user} {verify} 0 + if ($this->authed) return true; + // max. 16 digits for password + if (strlen($password) > 16) + $password = substr($password, 0, 16); - // yes, we get ticket - $aTickets = array( - 'ticket' => html_entity_decode($matches[1]), - 'secret' => html_entity_decode($matches[3]), - 'web_ticket' => html_entity_decode($matches[5]), - 'contact_ticket' => html_entity_decode($matches[7]), - 'oim_ticket' => html_entity_decode($matches[9]), - 'space_ticket' => html_entity_decode($matches[11]), - 'storage_ticket' => html_entity_decode($matches[13]) - ); - $this->ticket = $aTickets; - //$this->debug_message(var_export($aTickets, true)); - $ABAuthHeaderArray = array( - 'ABAuthHeader' => array( - ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), - 'ManagedGroupRequest' => false, - 'TicketToken' => htmlspecialchars($this->ticket['contact_ticket']), - ) - ); - $this->ABAuthHeader = new SoapHeader('http://www.msn.com/webservices/AddressBook', 'ABAuthHeader', $this->Array2SoapVar($ABAuthHeaderArray)); - return $aTickets; - } + $this->user = $user; + $this->password = $password; + // NS: <<< USR {id} SSO S {policy} {nonce} + @list(/* USR */, /* id */, /* SSO */, /* S */, $policy, $nonce,) = @explode(' ', $data); - /** - * Fetch contact list - * - * @return boolean true on success - */ - private function UpdateContacts() { - $ABApplicationHeaderArray = array( - 'ABApplicationHeader' => array( - ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), - 'ApplicationId' => 'CFE80F9D-180F-4399-82AB-413F33A1FA11', - 'IsMigration' => false, - 'PartnerScenario' => 'ContactSave' - ) - ); + $this->passport_policy = $policy; + $aTickets = $this->get_passport_ticket(); + if (!$aTickets || !is_array($aTickets)) { + // logout now + // NS: >>> OUT + $this->ns_writeln("OUT"); + @fclose($this->NSfp); + $this->error = 'Passport authenticated fail!'; + return false; + } - $ABApplicationHeader = new SoapHeader('http://www.msn.com/webservices/AddressBook', 'ABApplicationHeader', $this->Array2SoapVar($ABApplicationHeaderArray)); - $ABFindAllArray = array( - 'ABFindAll' => array( - ':' => array('xmlns'=>'http://www.msn.com/webservices/AddressBook'), - 'abId' => '00000000-0000-0000-0000-000000000000', - 'abView' => 'Full', - 'lastChange' => '0001-01-01T00:00:00.0000000-08:00', - ) - ); - $ABFindAll = new SoapParam($this->Array2SoapVar($ABFindAllArray), 'ABFindAll'); - $this->ABService->__setSoapHeaders(array($ABApplicationHeader, $this->ABAuthHeader)); - $this->Contacts = array(); - try { - $this->debug_message('*** Updating Contacts...'); - $Result = $this->ABService->ABFindAll($ABFindAll); - $this->debug_message("*** Result:\n".print_r($Result, true)."\n".$this->ABService->__getLastResponse()); - foreach($Result->ABFindAllResult->contacts->Contact as $Contact) - $this->Contacts[$Contact->contactInfo->passportName] = $Contact; - } catch(Exception $e) { - $this->debug_message("*** Update Contacts Error \nRequest:".$this->ABService->__getLastRequest()."\nError:".$e->getMessage()); - return false; - } - return true; - } + $ticket = $aTickets['ticket']; + $secret = $aTickets['secret']; + $this->ticket = $aTickets; + $login_code = $this->generateLoginBLOB($secret, $nonce); - /** - * Add contact - * - * @param string $email - * @param integer $network - * @param string $display - * @param boolean $sendADL - * @return boolean true on success - */ - private function addContact($email, $network, $display = '', $sendADL = false) { - if ($network != 1) return true; - if (isset($this->Contacts[$email])) return true; + // NS: >>> USR {id} SSO S {ticket} {login_code} + $this->ns_writeln("USR $this->id ".LOGIN_METHOD." S $ticket $login_code"); + $this->authed = true; + break; - $ABContactAddArray = array( - 'ABContactAdd' => array( - ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), - 'abId' => '00000000-0000-0000-0000-000000000000', - 'contacts' => array( - 'Contact' => array( - ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), - 'contactInfo' => array( - 'contactType' => 'LivePending', - 'passportName' => $email, - 'isMessengerUser' => true, - 'MessengerMemberInfo' => array( - 'DisplayName' => $email - ) - ) - ) - ), - 'options' => array( - 'EnableAllowListManagement' => true - ) - ) - ); - $ABContactAdd = new SoapParam($this->Array2SoapVar($ABContactAddArray), 'ABContactAdd'); - try { - $this->debug_message("*** Adding Contact $email..."); - $this->ABService->ABContactAdd($ABContactAdd); - } catch(Exception $e) { - $this->debug_message("*** Add Contact Error \nRequest:".$this->ABService->__getLastRequest()."\nError:".$e->getMessage()); - return false; - } - if ($sendADL && !feof($this->NSfp)) { - @list($u_name, $u_domain) = @explode('@', $email); - foreach (array('1', '2') as $l) { - $str = ''; - $len = strlen($str); - // NS: >>> ADL {id} {size} - //TODO introduce error checking - $this->ns_writeln("ADL $this->id $len"); - $this->ns_writedata($str); + case 'XFR': + // main login server will redirect to anther NS after USR command + // MSNP9 + // NS: <<< XFR {id} NS {server} 0 {server} + // MSNP15 + // NS: <<< XFR {id} NS {server} U D + @list(/* XFR */, /* id */, $Type, $server, /* ... */) = @explode(' ', $data); + if ($Type!='NS') break; + @list($ip, $port) = @explode(':', $server); + // this connection will close after XFR + @fclose($this->NSfp); + + $this->NSfp = @fsockopen($ip, $port, $errno, $errstr, $this->timeout); + if (!$this->NSfp) { + $this->error = "Can't connect to $ip:$port, error => $errno, $errstr"; + return false; + } + + // MSNP9 + // NS: >> VER {id} MSNP9 CVR0 + // MSNP15 + // NS: >>> VER {id} MSNP15 CVR0 + $this->ns_writeln("VER $this->id ".PROTOCOL.' CVR0'); + break; + + case 'GCF': + // return some policy data after 'USR {id} SSO I {user}' command + // NS: <<< GCF 0 {size} + @list(/* GCF */, /* 0 */, $size,) = @explode(' ', $data); + // we don't need the data, just read it and drop + if (is_numeric($size) && $size > 0) + $this->ns_readdata($size); + break; + + default: + // we'll quit if got any error + if (is_numeric($code)) { + // logout now + // NS: >>> OUT + $this->ns_writeln("OUT"); + @fclose($this->NSfp); + $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List"; + return false; + } + // unknown response from server, just ignore it + break; } } - $this->UpdateContacts(); - return true; + // never goto here } /** - * Remove contact from list - * - * @param integer $memberID - * @param string $email - * @param integer $network - * @param string $list - */ - function delMemberFromList($memberID, $email, $network, $list) { - if ($network != 1 && $network != 32) return true; - if ($memberID === false) return true; - $user = $email; - $ticket = htmlspecialchars($this->ticket['contact_ticket']); - if ($network == 1) - $XML = ' - - - - 996CDE1E-AA53-4477-B943-2BE802EA6166 - false - ContactMsgrAPI - - - false - '.$ticket.' - - - - - - 0 - Messenger - - - - - '.$list.' - - - Passport - '.$memberID.' - Accepted - - - - - - -'; - else - $XML = ' - - - - 996CDE1E-AA53-4477-B943-2BE802EA6166 - false - ContactMsgrAPI - - - false - '.$ticket.' - - - - - - 0 - Messenger - - - - - '.$list.' - - - Email - '.$memberID.' - Accepted - - - - - - -'; + * Sign onto the NS server and retrieve the address book + * + * @return void + */ + public function signon() { + /* FIXME Don't implement the signon as a loop or we could hang + * the queue handler! */ + $this->debug_message('*** Trying to connect to MSN network'); - $header_array = array( - 'SOAPAction: '.$this->delmember_soap, - 'Content-Type: text/xml; charset=utf-8', - 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)' - ); + while (true) { + // Connect + if (!$this->connect($this->user, $this->password)) { + $this->signonFailure("!!! Could not connect to server: $this->error"); + continue; + } - //$this->debug_message("*** URL: $this->delmember_url"); - //$this->debug_message("*** Sending SOAP:\n$XML"); - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->delmember_url); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); - $data = curl_exec($curl); - $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - //$this->debug_message("*** Get Result:\n$data"); + // Update contacts + if ($this->UpdateContacts() === false) continue; - if ($http_code != 200) { - preg_match('#(.*)(.*)#', $data, $matches); - if (count($matches) == 0) { - $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list"); - return false; + // Get membership lists + if (($this->aContactList = $this->getMembershipList()) === false) { + $this->signonFailure('!!! Get membership list failed'); + continue; } - $faultcode = trim($matches[1]); - $faultstring = trim($matches[2]); - if (strcasecmp($faultcode, 'soap:Client') || stripos($faultstring, 'Member does not exist') === false) { - $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list, error code: $faultcode, $faultstring"); - return false; + + if ($this->update_pending) { + if (is_array($this->aContactList)) { + $pending = 'Pending'; + foreach ($this->aContactList as $u_domain => $aUserList) { + foreach ($aUserList as $u_name => $aNetworks) { + foreach ($aNetworks as $network => $aData) { + if (isset($aData[$pending])) { + // pending list + $cnt = 0; + foreach (array('Allow', 'Reverse') as $list) { + if (isset($aData[$list])) + $cnt++; + else { + if ($this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) { + $this->aContactList[$u_domain][$u_name][$network][$list] = false; + $cnt++; + } + } + } + if ($cnt >= 2) { + $id = $aData[$pending]; + // we can delete it from pending now + if ($this->delMemberFromList($id, $u_name.'@'.$u_domain, $network, $pending)) + unset($this->aContactList[$u_domain][$u_name][$network][$pending]); + } + } + else { + // sync list + foreach (array('Allow', 'Reverse') as $list) { + if (!isset($aData[$list])) { + if ($this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) + $this->aContactList[$u_domain][$u_name][$network][$list] = false; + } + } + } + } + } + } + } + } + $n = 0; + $sList = ''; + $len = 0; + if (is_array($this->aContactList)) { + foreach ($this->aContactList as $u_domain => $aUserList) { + $str = ''; + $len += strlen($str); + if ($len > 7400) { + $this->aADL[$n] = ''.$sList.''; + $n++; + $sList = ''; + $len = strlen($str); + } + $sList .= $str; + foreach ($aUserList as $u_name => $aNetworks) { + foreach ($aNetworks as $network => $status) { + $str = ''; + $len += strlen($str); + // max: 7500, but is 19, + // so we use 7475 + if ($len > 7475) { + $sList .= ''; + $this->aADL[$n] = ''.$sList.''; + $n++; + $sList = ''.$str; + $len = strlen($sList); + } + else + $sList .= $str; + } + } + $sList .= ''; + } + } + $this->aADL[$n] = ''.$sList.''; + // NS: >>> BLP {id} BL + $this->ns_writeln("BLP $this->id BL"); + foreach ($this->aADL as $str) { + $len = strlen($str); + // NS: >>> ADL {id} {size} + $this->ns_writeln("ADL $this->id $len"); + $this->ns_writedata($str); + } + // NS: >>> PRP {id} MFN name + if ($this->alias == '') $this->alias = $user; + $aliasname = rawurlencode($this->alias); + $this->ns_writeln("PRP $this->id MFN $aliasname"); + //設定個人大頭貼 + //$MsnObj=$this->PhotoStckObj(); + // NS: >>> CHG {id} {status} {clientid} {msnobj} + $this->ns_writeln("CHG $this->id NLN $this->clientid"); + if ($this->PhotoStickerFile !== false) + $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile))); + // NS: >>> UUX {id} length + $str = ''.htmlspecialchars($this->psm).''; + $len = strlen($str); + $this->ns_writeln("UUX $this->id $len"); + $this->ns_writedata($str); + if (!socketcheck($this->NSfp)) { + $this->debug_message('*** Connected, waiting for commands'); + break; + } else { + $this->NSRetryWait($this->retry_wait); } - $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list, not present in list"); - return true; } - $this->debug_message("*** Member successfully deleted (network: $network) $email ($memberID) from $list list"); - return true; } /** - * Add contact to list + * Called if there is an error during signon * - * @param string $email - * @param integer $network - * @param string $list + * @param string $message Error message to log + * @return void */ - function addMemberToList($email, $network, $list) { - if ($network != 1 && $network != 32) return true; - $ticket = htmlspecialchars($this->ticket['contact_ticket']); - $user = $email; - - if ($network == 1) - $XML = ' - - - - 996CDE1E-AA53-4477-B943-2BE802EA6166 - false - ContactMsgrAPI - - - false - '.$ticket.' - - - - - - 0 - Messenger - - - - - '.$list.' - - - Passport - Accepted - '.$user.' - - - - - - -'; - else - $XML = ' - - - - 996CDE1E-AA53-4477-B943-2BE802EA6166 - false - ContactMsgrAPI - - - false - '.$ticket.' - - - - - - 0 - Messenger - - - - - '.$list.' - - - Email - Accepted - '.$user.' - - - MSN.IM.BuddyType - 32:YAHOO - - - - - - - - -'; - $header_array = array( - 'SOAPAction: '.$this->addmember_soap, - 'Content-Type: text/xml; charset=utf-8', - 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)' - ); - - //$this->debug_message("*** URL: $this->addmember_url"); - //$this->debug_message("*** Sending SOAP:\n$XML"); - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->addmember_url); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); - $data = curl_exec($curl); - $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - //$this->debug_message("*** Get Result:\n$data"); - - if ($http_code != 200) { - preg_match('#(.*)(.*)#', $data, $matches); - if (count($matches) == 0) { - $this->debug_message("*** Could not add member (network: $network) $email to $list list"); - return false; - } - $faultcode = trim($matches[1]); - $faultstring = trim($matches[2]); - if (strcasecmp($faultcode, 'soap:Client') || stripos($faultstring, 'Member already exists') === false) { - $this->debug_message("*** Could not add member (network: $network) $email to $list list, error code: $faultcode, $faultstring"); - return false; - } - $this->debug_message("*** Could not add member (network: $network) $email to $list list, already present"); - return true; - } - $this->debug_message("*** Member successfully added (network: $network) $email to $list list"); - return true; - } + private function signonFailure($message) { + $this->debug_message($message); + $this->callHandler('ConnectFailed'); + $this->NSRetryWait($this->retry_wait); + } /** - * Get membership lists + * Log out and close the NS connection * - * @param mixed $returnData Membership list or false on failure + * @return void */ - function getMembershipList($returnData = false) { - $ticket = htmlspecialchars($this->ticket['contact_ticket']); - $XML = ' - - - - 996CDE1E-AA53-4477-B943-2BE802EA6166 - false - Initial - - - false - '.$ticket.' - - - - - - - Messenger - Invitation - SocialNetwork - Space - Profile - - - - -'; - $header_array = array( - 'SOAPAction: '.$this->membership_soap, - 'Content-Type: text/xml; charset=utf-8', - 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)' - ); - //$this->debug_message("*** URL: $this->membership_url"); - //$this->debug_message("*** Sending SOAP:\n$XML"); - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->membership_url); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); - $data = curl_exec($curl); - $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - //$this->debug_message("*** Get Result:\n$data"); - if ($http_code != 200) return false; - $p = $data; - $aMemberships = array(); - while (1) { - //$this->debug_message("search p = $p"); - $start = strpos($p, ''); - $end = strpos($p, ''); - if ($start === false || $end === false || $start > $end) break; - //$this->debug_message("start = $start, end = $end"); - $end += 13; - $sMembership = substr($p, $start, $end - $start); - $aMemberships[] = $sMembership; - //$this->debug_message("add sMembership = $sMembership"); - $p = substr($p, $end); - } - //$this->debug_message("aMemberships = ".var_export($aMemberships, true)); - - $aContactList = array(); - foreach ($aMemberships as $sMembership) { - //$this->debug_message("sMembership = $sMembership"); - if (isset($matches)) unset($matches); - preg_match('#(.*)#', $sMembership, $matches); - if (count($matches) == 0) continue; - $sMemberRole = $matches[1]; - //$this->debug_message("MemberRole = $sMemberRole"); - if ($sMemberRole != 'Allow' && $sMemberRole != 'Reverse' && $sMemberRole != 'Pending') continue; - $p = $sMembership; - if (isset($aMembers)) unset($aMembers); - $aMembers = array(); - while (1) { - //$this->debug_message("search p = $p"); - $start = strpos($p, 'debug_message("add sMember = $sMember"); - $p = substr($p, $end); - } - //$this->debug_message("aMembers = ".var_export($aMembers, true)); - foreach ($aMembers as $sMember) { - //$this->debug_message("sMember = $sMember"); - if (isset($matches)) unset($matches); - preg_match('##', $sMember, $matches); - if (count($matches) == 0) continue; - $sMemberType = $matches[1]; - //$this->debug_message("MemberType = $sMemberType"); - $network = -1; - preg_match('#(.*)#', $sMember, $matches); - if (count($matches) == 0) continue; - $id = $matches[1]; - if ($sMemberType == 'PassportMember') { - if (strpos($sMember, 'Passport') === false) continue; - $network = 1; - preg_match('#(.*)#', $sMember, $matches); - } - else if ($sMemberType == 'EmailMember') { - if (strpos($sMember, 'Email') === false) continue; - // Value is 32: or 32:YAHOO - preg_match('#MSN.IM.BuddyType(.*):(.*)#', $sMember, $matches); - if (count($matches) == 0) continue; - if ($matches[1] != 32) continue; - $network = 32; - preg_match('#(.*)#', $sMember, $matches); - } - if ($network == -1) continue; - if (count($matches) > 0) { - $email = $matches[1]; - @list($u_name, $u_domain) = @explode('@', $email); - if ($u_domain == NULL) continue; - $aContactList[$u_domain][$u_name][$network][$sMemberRole] = $id; - $this->debug_message("*** Adding new contact (network: $network, status: $sMemberRole): $u_name@$u_domain ($id)"); - } - } + private function nsLogout() { + if (is_resource($this->NSfp) && !feof($this->NSfp)) { + // logout now + // NS: >>> OUT + $this->ns_writeln("OUT"); + fclose($this->NSfp); + $this->NSfp = false; + $this->debug_message("*** Logged out"); } - return $aContactList; } /** - * Connect to the NS server + * NS and SB command handling methods + */ + + /** + * Read and handle incoming command from NS * - * @param String $user Username - * @param String $password Password - * @param String $redirect_server Redirect server - * @param Integer $redirect_port Redirect port - * @return Boolean Returns true if successful + * @return void */ - private function connect($user, $password, $redirect_server = '', $redirect_port = 1863) { - $this->id = 1; - if ($redirect_server === '') { - $this->NSfp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout); - if (!$this->NSfp) { - $this->error = "!!! Could not connect to $this->server:$this->port, error => $errno, $errstr"; - return false; - } + private function nsReceive() { + // Sign in again if not signed in or socket failed + if (!is_resource($this->NSfp) || socketcheck($this->NSfp)) { + $this->callHandler('Reconnect'); + $this->NSRetryWait($this->retry_wait); + $this->signon(); + return; } - else { - $this->NSfp = @fsockopen($redirect_server, $redirect_port, $errno, $errstr, $this->timeout); - if (!$this->NSfp) { - $this->error = "!!! Could not connect to $redirect_server:$redirect_port, error => $errno, $errstr"; - return false; - } - } - $this->authed = false; - // MSNP9 - // NS: >> VER {id} MSNP9 CVR0 - // MSNP15 - // NS: >>> VER {id} MSNP15 CVR0 - $this->ns_writeln("VER $this->id $this->protocol CVR0"); - - $start_tm = time(); - while (!self::socketcheck($this->NSfp)) { - $data = $this->ns_readln(); - // no data? - if ($data === false) { - // logout now - // NS: >>> OUT - $this->ns_writeln("OUT"); - @fclose($this->NSfp); - $this->error = 'Timeout, maybe protocol changed!'; - return false; - } - - $code = substr($data, 0, 3); - $start_tm = time(); - - switch ($code) { - case 'VER': - // MSNP9 - // NS: <<< VER {id} MSNP9 CVR0 - // NS: >>> CVR {id} 0x0409 winnt 5.1 i386 MSMSGS 6.0.0602 msmsgs {user} - // MSNP15 - // NS: <<< VER {id} MSNP15 CVR0 - // NS: >>> CVR {id} 0x0409 winnt 5.1 i386 MSMSGS 8.1.0178 msmsgs {user} - $this->ns_writeln("CVR $this->id 0x0409 winnt 5.1 i386 MSMSGS $this->buildver msmsgs $user"); - break; - case 'CVR': - // MSNP9 - // NS: <<< CVR {id} {ver_list} {download_serve} .... - // NS: >>> USR {id} TWN I {user} - // MSNP15 - // NS: <<< CVR {id} {ver_list} {download_serve} .... - // NS: >>> USR {id} SSO I {user} - $this->ns_writeln("USR $this->id $this->login_method I $user"); + $data = $this->ns_readln(); + if ($data === false) { + // There was no data / an error when reading from the socket so reconnect + $this->callHandler('Reconnect'); + $this->NSRetryWait($this->retry_wait); + $this->signon(); + return; + } else { + switch (substr($data, 0, 3)) { + case 'SBS': + // after 'USR {id} OK {user} {verify} 0' response, the server will send SBS and profile to us + // NS: <<< SBS 0 null break; - case 'USR': - // already login for passport site, finish the login process now. - // NS: <<< USR {id} OK {user} {verify} 0 - if ($this->authed) return true; - // max. 16 digits for password - if (strlen($password) > 16) - $password = substr($password, 0, 16); - - $this->user = $user; - $this->password = $password; - // NS: <<< USR {id} SSO S {policy} {nonce} - @list(/* USR */, /* id */, /* SSO */, /* S */, $policy, $nonce,) = @explode(' ', $data); - - $this->passport_policy = $policy; - $aTickets = $this->get_passport_ticket(); - if (!$aTickets || !is_array($aTickets)) { - // logout now - // NS: >>> OUT - $this->ns_writeln("OUT"); - @fclose($this->NSfp); - $this->error = 'Passport authenticated fail!'; - return false; + case 'RFS': + // FIXME: + // NS: <<< RFS ??? + // refresh ADL, so we re-send it again + if (is_array($this->aADL)) { + foreach ($this->aADL as $str) { + $len = strlen($str); + // NS: >>> ADL {id} {size} + $this->ns_writeln("ADL $this->id $len"); + $this->ns_writedata($str); + } } - - $ticket = $aTickets['ticket']; - $secret = $aTickets['secret']; - $this->ticket = $aTickets; - $login_code = $this->generateLoginBLOB($secret, $nonce); - - // NS: >>> USR {id} SSO S {ticket} {login_code} - $this->ns_writeln("USR $this->id $this->login_method S $ticket $login_code"); - $this->authed = true; break; - case 'XFR': - // main login server will redirect to anther NS after USR command - // MSNP9 - // NS: <<< XFR {id} NS {server} 0 {server} - // MSNP15 - // NS: <<< XFR {id} NS {server} U D - @list(/* XFR */, /* id */, $Type, $server, /* ... */) = @explode(' ', $data); - if ($Type!='NS') break; - @list($ip, $port) = @explode(':', $server); - // this connection will close after XFR - @fclose($this->NSfp); - - $this->NSfp = @fsockopen($ip, $port, $errno, $errstr, $this->timeout); - if (!$this->NSfp) { - $this->error = "Can't connect to $ip:$port, error => $errno, $errstr"; - return false; + case 'LST': + // NS: <<< LST {email} {alias} 11 0 + @list(/* LST */, $email, /* alias */,) = @explode(' ', $data); + @list($u_name, $u_domain) = @explode('@', $email); + if (!isset($this->aContactList[$u_domain][$u_name][1])) { + $this->aContactList[$u_domain][$u_name][1]['Allow'] = 'Allow'; + $this->debug_message("*** Added to contact list: $u_name@$u_domain"); } - - // MSNP9 - // NS: >> VER {id} MSNP9 CVR0 - // MSNP15 - // NS: >>> VER {id} MSNP15 CVR0 - $this->ns_writeln("VER $this->id $this->protocol CVR0"); break; - case 'GCF': - // return some policy data after 'USR {id} SSO I {user}' command - // NS: <<< GCF 0 {size} - @list(/* GCF */, /* 0 */, $size,) = @explode(' ', $data); - // we don't need the data, just read it and drop - if (is_numeric($size) && $size > 0) - $this->ns_readdata($size); - break; + case 'ADL': + // randomly, we get ADL command, someone add us to their contact list for MSNP15 + // NS: <<< ADL 0 {size} + @list(/* ADL */, /* 0 */, $size,) = @explode(' ', $data); + if (is_numeric($size) && $size > 0) { + $data = $this->ns_readdata($size); + preg_match('##', $data, $matches); + if (is_array($matches) && count($matches) > 0) { + $u_domain = $matches[1]; + $u_name = $matches[2]; + $network = $matches[4]; + if (isset($this->aContactList[$u_domain][$u_name][$network])) + $this->debug_message("*** Someone (network: $network) added us to their list (but already in our list): $u_name@$u_domain"); + else { + $re_login = false; + $cnt = 0; + foreach (array('Allow', 'Reverse') as $list) { + if (!$this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) { + if ($re_login) { + $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list"); + continue; + } + $aTickets = $this->get_passport_ticket(); + if (!$aTickets || !is_array($aTickets)) { + // failed to login? ignore it + $this->debug_message("*** Could not re-login, something wrong here"); + $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list"); + continue; + } + $re_login = true; + $this->ticket = $aTickets; + $this->debug_message("**** Got new ticket, trying again"); + if (!$this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) { + $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list"); + continue; + } + } + $this->aContactList[$u_domain][$u_name][$network][$list] = false; + $cnt++; + } + $this->debug_message("*** Someone (network: $network) added us to their list: $u_name@$u_domain"); + } + $str = ''; + $len = strlen($str); - default: - // we'll quit if got any error - if (is_numeric($code)) { - // logout now - // NS: >>> OUT - $this->ns_writeln("OUT"); - @fclose($this->NSfp); - $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List"; - return false; + $this->callHandler('AddedToList', array('screenname' => $u_name.'@'.$u_domain, 'network' => $network)); + } + else + $this->debug_message("*** Someone added us to their list: $data"); } - // unknown response from server, just ignore it break; - } - } - // never goto here - } - /** - * Sign onto the NS server and retrieve the address book - * - * @return void - */ - public function signon() { - /* FIXME Don't implement the signon as a loop or we could hang - * the queue handler! */ - $this->debug_message('*** Trying to connect to MSN network'); + case 'RML': + // randomly, we get RML command, someome remove us to their contact list for MSNP15 + // NS: <<< RML 0 {size} + @list(/* RML */, /* 0 */, $size,) = @explode(' ', $data); + if (is_numeric($size) && $size > 0) { + $data = $this->ns_readdata($size); + preg_match('##', $data, $matches); + if (is_array($matches) && count($matches) > 0) { + $u_domain = $matches[1]; + $u_name = $matches[2]; + $network = $matches[4]; + if (isset($this->aContactList[$u_domain][$u_name][$network])) { + $aData = $this->aContactList[$u_domain][$u_name][$network]; - while (true) { - // Connect - if (!$this->connect($this->user, $this->password)) { - $this->signonFailure("!!! Could not connect to server: $this->error"); - continue; - } + foreach ($aData as $list => $id) + $this->delMemberFromList($id, $u_name.'@'.$u_domain, $network, $list); - // Update contacts - if ($this->UpdateContacts() === false) continue; + unset($this->aContactList[$u_domain][$u_name][$network]); + $this->debug_message("*** Someone (network: $network) removed us from their list: $u_name@$u_domain"); + } + else + $this->debug_message("*** Someone (network: $network) removed us from their list (but not in our list): $u_name@$u_domain"); - // Get membership lists - if (($this->aContactList = $this->getMembershipList()) === false) { - $this->signonFailure('!!! Get membership list failed'); - continue; - } + $this->callHandler('RemovedFromList', array('screenname' => $u_name.'@'.$u_domain, 'network' => $network)); + } + else + $this->debug_message("*** Someone removed us from their list: $data"); + } + break; - if ($this->update_pending) { - if (is_array($this->aContactList)) { - $pending = 'Pending'; - foreach ($this->aContactList as $u_domain => $aUserList) { - foreach ($aUserList as $u_name => $aNetworks) { - foreach ($aNetworks as $network => $aData) { - if (isset($aData[$pending])) { - // pending list - $cnt = 0; - foreach (array('Allow', 'Reverse') as $list) { - if (isset($aData[$list])) - $cnt++; - else { - if ($this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) { - $this->aContactList[$u_domain][$u_name][$network][$list] = false; - $cnt++; - } - } - } - if ($cnt >= 2) { - $id = $aData[$pending]; - // we can delete it from pending now - if ($this->delMemberFromList($id, $u_name.'@'.$u_domain, $network, $pending)) - unset($this->aContactList[$u_domain][$u_name][$network][$pending]); - } + case 'MSG': + // randomly, we get MSG notification from server + // NS: <<< MSG Hotmail Hotmail {size} + @list(/* MSG */, /* Hotmail */, /* Hotmail */, $size,) = @explode(' ', $data); + if (is_numeric($size) && $size > 0) { + $data = $this->ns_readdata($size); + $aLines = @explode("\n", $data); + $header = true; + $ignore = false; + $maildata = ''; + foreach ($aLines as $line) { + $line = rtrim($line); + if ($header) { + if ($line === '') { + $header = false; + continue; } - else { - // sync list - foreach (array('Allow', 'Reverse') as $list) { - if (!isset($aData[$list])) { - if ($this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) - $this->aContactList[$u_domain][$u_name][$network][$list] = false; - } + if (strncasecmp($line, 'Content-Type:', 13) == 0) { + if (strpos($line, 'text/x-msmsgsinitialmdatanotification') === false && strpos($line, 'text/x-msmsgsoimnotification') === false) { + // we just need text/x-msmsgsinitialmdatanotification + // or text/x-msmsgsoimnotification + $ignore = true; + break; } } + continue; + } + if (strncasecmp($line, 'Mail-Data:', 10) == 0) { + $maildata = trim(substr($line, 10)); + break; } } - } - } - } - $n = 0; - $sList = ''; - $len = 0; - if (is_array($this->aContactList)) { - foreach ($this->aContactList as $u_domain => $aUserList) { - $str = ''; - $len += strlen($str); - if ($len > 7400) { - $this->aADL[$n] = ''.$sList.''; - $n++; - $sList = ''; - $len = strlen($str); - } - $sList .= $str; - foreach ($aUserList as $u_name => $aNetworks) { - foreach ($aNetworks as $network => $status) { - $str = ''; - $len += strlen($str); - // max: 7500, but is 19, - // so we use 7475 - if ($len > 7475) { - $sList .= ''; - $this->aADL[$n] = ''.$sList.''; - $n++; - $sList = ''.$str; - $len = strlen($sList); + if ($ignore) { + $this->debug_message("*** Ignoring MSG for: $line"); + break; + } + if ($maildata == '') { + $this->debug_message("*** Ignoring MSG not for OIM"); + break; + } + $re_login = false; + if (strcasecmp($maildata, 'too-large') == 0) { + $this->debug_message("*** Large mail-data, need to get the data via SOAP"); + $maildata = $this->getOIM_maildata(); + if ($maildata === false) { + $this->debug_message("*** Could not get mail-data via SOAP"); + + // maybe we need to re-login again + $aTickets = $this->get_passport_ticket(); + if (!$aTickets || !is_array($aTickets)) { + // failed to login? ignore it + $this->debug_message("*** Could not re-login, something wrong here, ignoring this OIM"); + break; + } + $re_login = true; + $this->ticket = $aTickets; + $this->debug_message("*** Got new ticket, trying again"); + $maildata = $this->getOIM_maildata(); + if ($maildata === false) { + $this->debug_message("*** Could not get mail-data via SOAP, and re-login already attempted, ignoring this OIM"); + break; + } + } + } + // could be a lots of ..., so we can't use preg_match here + $p = $maildata; + $aOIMs = array(); + while (1) { + $start = strpos($p, ''); + $end = strpos($p, ''); + if ($start === false || $end === false || $start > $end) break; + $end += 4; + $sOIM = substr($p, $start, $end - $start); + $aOIMs[] = $sOIM; + $p = substr($p, $end); + } + if (count($aOIMs) == 0) { + $this->debug_message("*** Ignoring empty OIM"); + break; + } + foreach ($aOIMs as $maildata) { + // T: 11 for MSN, 13 for Yahoo + // S: 6 for MSN, 7 for Yahoo + // RT: the datetime received by server + // RS: already read or not + // SZ: size of message + // E: sender + // I: msgid + // F: always 00000000-0000-0000-0000-000000000009 + // N: sender alias + preg_match('#(.*)#', $maildata, $matches); + if (count($matches) == 0) { + $this->debug_message("*** Ignoring OIM maildata without type"); + continue; } + $oim_type = $matches[1]; + if ($oim_type = 13) + $network = 32; else - $sList .= $str; + $network = 1; + preg_match('#(.*)#', $maildata, $matches); + if (count($matches) == 0) { + $this->debug_message("*** Ignoring OIM maildata without sender"); + continue; + } + $oim_sender = $matches[1]; + preg_match('#(.*)#', $maildata, $matches); + if (count($matches) == 0) { + $this->debug_message("*** Ignoring OIM maildata without msgid"); + continue; + } + $oim_msgid = $matches[1]; + preg_match('#(.*)#', $maildata, $matches); + $oim_size = (count($matches) == 0) ? 0 : $matches[1]; + preg_match('#(.*)#', $maildata, $matches); + $oim_time = (count($matches) == 0) ? 0 : $matches[1]; + $this->debug_message("*** OIM received from $oim_sender, Time: $oim_time, MSGID: $oim_msgid, size: $oim_size"); + $sMsg = $this->getOIM_message($oim_msgid); + if ($sMsg === false) { + $this->debug_message("*** Could not get OIM, msgid = $oim_msgid"); + if ($re_login) { + $this->debug_message("*** Could not get OIM via SOAP, and re-login already attempted, ignoring this OIM"); + continue; + } + $aTickets = $this->get_passport_ticket(); + if (!$aTickets || !is_array($aTickets)) { + // failed to login? ignore it + $this->debug_message("*** Could not re-login, something wrong here, ignoring this OIM"); + continue; + } + $re_login = true; + $this->ticket = $aTickets; + $this->debug_message("*** get new ticket, try it again"); + $sMsg = $this->getOIM_message($oim_msgid); + if ($sMsg === false) { + $this->debug_message("*** Could not get OIM via SOAP, and re-login already attempted, ignoring this OIM"); + continue; + } + } + $this->debug_message("*** MSG (Offline) from $oim_sender (network: $network): $sMsg"); + $this->callHandler('IMin', array('sender' => $oim_sender, 'message' => $sMsg, 'network' => $network, 'offline' => true)); } } - $sList .= ''; - } - } - $this->aADL[$n] = ''.$sList.''; - // NS: >>> BLP {id} BL - $this->ns_writeln("BLP $this->id BL"); - foreach ($this->aADL as $str) { - $len = strlen($str); - // NS: >>> ADL {id} {size} - $this->ns_writeln("ADL $this->id $len"); - $this->ns_writedata($str); - } - // NS: >>> PRP {id} MFN name - if ($this->alias == '') $this->alias = $user; - $aliasname = rawurlencode($this->alias); - $this->ns_writeln("PRP $this->id MFN $aliasname"); - //設定個人大頭貼 - //$MsnObj=$this->PhotoStckObj(); - // NS: >>> CHG {id} {status} {clientid} {msnobj} - $this->ns_writeln("CHG $this->id NLN $this->clientid"); - if ($this->PhotoStickerFile !== false) - $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile))); - // NS: >>> UUX {id} length - $str = ''.htmlspecialchars($this->psm).''; - $len = strlen($str); - $this->ns_writeln("UUX $this->id $len"); - $this->ns_writedata($str); - if (!self::socketcheck($this->NSfp)) { - $this->debug_message('*** Connected, waiting for commands'); - break; - } else { - $this->NSRetryWait($this->retry_wait); - } - } - } - - /** - * Called if there is an error during signon - * - * @param string $message Error message to log - */ - private function signonFailure($message) { - $this->debug_message($message); - $this->callHandler('ConnectFailed'); - $this->NSRetryWait($this->retry_wait); - } - - function derive_key($key, $magic) { - $hash1 = mhash(MHASH_SHA1, $magic, $key); - $hash2 = mhash(MHASH_SHA1, $hash1.$magic, $key); - $hash3 = mhash(MHASH_SHA1, $hash1, $key); - $hash4 = mhash(MHASH_SHA1, $hash3.$magic, $key); - return $hash2.substr($hash4, 0, 4); - } - - function generateLoginBLOB($key, $challenge) { - $key1 = base64_decode($key); - $key2 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY HASH'); - $key3 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY ENCRYPTION'); - - // get hash of challenge using key2 - $hash = mhash(MHASH_SHA1, $challenge, $key2); - - // get 8 bytes random data - $iv = substr(base64_encode(rand(1000,9999).rand(1000,9999)), 2, 8); - - $cipher = mcrypt_cbc(MCRYPT_3DES, $key3, $challenge."\x08\x08\x08\x08\x08\x08\x08\x08", MCRYPT_ENCRYPT, $iv); - - $blob = pack('LLLLLLL', 28, 1, 0x6603, 0x8004, 8, 20, 72); - $blob .= $iv; - $blob .= $hash; - $blob .= $cipher; - - return base64_encode($blob); - } + break; - /** - * Get OIM mail data - * - * @return string mail data or false on failure - */ - function getOIM_maildata() { - preg_match('#t=(.*)&p=(.*)#', $this->ticket['web_ticket'], $matches); - if (count($matches) == 0) { - $this->debug_message('*** No web ticket?'); - return false; - } - $t = htmlspecialchars($matches[1]); - $p = htmlspecialchars($matches[2]); - $XML = ' - - - - '.$t.' -

'.$p.'

-
-
- - - -
'; + case 'UBM': + // randomly, we get UBM, this is the message from other network, like Yahoo! + // NS: <<< UBM {email} $network $type {size} + @list(/* UBM */, $from_email, $network, $type, $size,) = @explode(' ', $data); + if (is_numeric($size) && $size > 0) { + $data = $this->ns_readdata($size); + $aLines = @explode("\n", $data); + $header = true; + $ignore = false; + $sMsg = ''; + foreach ($aLines as $line) { + $line = rtrim($line); + if ($header) { + if ($line === '') { + $header = false; + continue; + } + if (strncasecmp($line, 'TypingUser:', 11) == 0) { + $ignore = true; + break; + } + continue; + } + $aSubLines = @explode("\r", $line); + foreach ($aSubLines as $str) { + if ($sMsg !== '') + $sMsg .= "\n"; + $sMsg .= $str; + } + } + if ($ignore) { + $this->debug_message("*** Ignoring message from $from_email: $line"); + break; + } + $this->debug_message("*** MSG from $from_email (network: $network): $sMsg"); + $this->callHandler('IMin', array('sender' => $from_email, 'message' => $sMsg, 'network' => $network, 'offline' => false)); + } + break; - $header_array = array( - 'SOAPAction: '.$this->oim_maildata_soap, - 'Content-Type: text/xml; charset=utf-8', - 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.$this->buildver.')' - ); + case 'UBX': + // randomly, we get UBX notification from server + // NS: <<< UBX email {network} {size} + @list(/* UBX */, /* email */, /* network */, $size,) = @explode(' ', $data); + // we don't need the notification data, so just ignore it + if (is_numeric($size) && $size > 0) + $this->ns_readdata($size); + break; - //$this->debug_message("*** URL: $this->oim_maildata_url"); - //$this->debug_message("*** Sending SOAP:\n$XML"); - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->oim_maildata_url); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); - $data = curl_exec($curl); - $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - //$this->debug_message("*** Get Result:\n$data"); + case 'CHL': + // randomly, we'll get challenge from server + // NS: <<< CHL 0 {code} + @list(/* CHL */, /* 0 */, $chl_code,) = @explode(' ', $data); + $fingerprint = $this->getChallenge($chl_code); + // NS: >>> QRY {id} {product_id} 32 + // NS: >>> fingerprint + $this->ns_writeln("QRY $this->id ".PROD_ID.' 32'); + $this->ns_writedata($fingerprint); + $this->ns_writeln("CHG $this->id NLN $this->clientid"); + if ($this->PhotoStickerFile !== false) + $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile))); + break; + case 'CHG': + // NS: <<< CHG {id} {status} {code} + // ignore it + // change our status to online first + break; - if ($http_code != 200) { - $this->debug_message("*** Could not get OIM maildata! http code: $http_code"); - return false; - } + case 'XFR': + // sometimes, NS will redirect to another NS + // MSNP9 + // NS: <<< XFR {id} NS {server} 0 {server} + // MSNP15 + // NS: <<< XFR {id} NS {server} U D + // for normal switchboard XFR + // NS: <<< XFR {id} SB {server} CKI {cki} U messenger.msn.com 0 + @list(/* XFR */, /* {id} */, $server_type, $server, /* CKI */, $cki_code, /* ... */) = @explode(' ', $data); + @list($ip, $port) = @explode(':', $server); + if ($server_type != 'SB') { + // maybe exit? + // this connection will close after XFR + $this->nsLogout(); + continue; + } - // See #XML_Data - preg_match('#]*)>(.*)#', $data, $matches); - if (count($matches) == 0) { - $this->debug_message('*** Could not get OIM maildata'); - return false; + $this->debug_message("NS: <<< XFR SB"); + $user = array_shift($this->waitingForXFR); + $bSBresult = $this->switchboard_control($ip, $port, $cki_code, $User, $Message); + /* + $bSBresult = $this->switchboard_control($ip, $port, $cki_code, $aMSNUsers[$nCurrentUser], $sMessage); + if ($bSBresult === false) { + // error for switchboard + $this->debug_message("!!! error for sending message to ".$aMSNUsers[$nCurrentUser]); + $aOfflineUsers[] = $aMSNUsers[$nCurrentUser]; + }*/ + break; + case 'QNG': + // NS: <<< QNG {time} + @list(/* QNG */, $ping_wait) = @explode(' ', $data); + $this->callHandler('Pong', $ping_wait); + break; + + case 'RNG': + if ($this->PhotoStickerFile !== false) + $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile))); + else + $this->ns_writeln("CHG $this->id NLN $this->clientid"); + // someone is trying to talk to us + // NS: <<< RNG {session_id} {server} {auth_type} {ticket} {email} {alias} U {client} 0 + $this->debug_message("NS: <<< RNG $data"); + @list(/* RNG */, $sid, $server, /* auth_type */, $ticket, $email, $name, ) = @explode(' ', $data); + @list($sb_ip, $sb_port) = @explode(':', $server); + $this->debug_message("*** RING from $email, $sb_ip:$sb_port"); + $this->addContact($email, 1, $email, true); + $this->connectToSBSession('Passive', $sb_ip, $sb_port, $email, array('sid' => $sid, 'ticket' => $ticket)); + break; + case 'OUT': + // force logout from NS + // NS: <<< OUT xxx + $this->debug_message("*** LOGOUT from NS"); + return $this->nsLogout(); + + default: + $code = substr($data,0,3); + if (is_numeric($code)) { + $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List"; + $this->debug_message("*** NS: $this->error"); + + return $this->nsLogout(); + } + break; + } } - return $matches[2]; } /** - * Fetch OIM message with given id - * - * @param string $msgid - * @return string Message or false on failure - */ - function getOIM_message($msgid) { - preg_match('#t=(.*)&p=(.*)#', $this->ticket['web_ticket'], $matches); - if (count($matches) == 0) { - $this->debug_message('*** No web ticket?'); - return false; - } - $t = htmlspecialchars($matches[1]); - $p = htmlspecialchars($matches[2]); - - // read OIM - $XML = ' - - - - '.$t.' -

'.$p.'

-
-
- - - '.$msgid.' - false - - -
'; - - $header_array = array( - 'SOAPAction: '.$this->oim_read_soap, - 'Content-Type: text/xml; charset=utf-8', - 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.$this->buildver.')' - ); + * Read and handle incoming command/message from + * a switchboard session socket + */ + private function sbReceive($socket) { + $intsocket = (int) $socket; + $session = &$this->switchBoardSessions[$intsocket]; - //$this->debug_message("*** URL: $this->oim_read_url"); - //$this->debug_message("*** Sending SOAP:\n$XML"); - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->oim_read_url); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); - $data = curl_exec($curl); - $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - curl_close($curl); - //$this->debug_message("*** Get Result:\n$data"); + if (feof($socket)) { + // Unset session lookup value + unset($this->switchBoardSessionLookup[$session['to']]); - if ($http_code != 200) { - $this->debug_message("*** Can't get OIM: $msgid, http code = $http_code"); - return false; + // Unset session itself + unset($this->switchBoardSessions[$intsocket]); + return; } - // why can't use preg_match('#(.*)#', $data, $matches)? - // multi-lines? - $start = strpos($data, ''); - $end = strpos($data, ''); - if ($start === false || $end === false || $start > $end) { - $this->debug_message("*** Can't get OIM: $msgid"); - return false; - } - $lines = substr($data, $start + 18, $end - $start); - $aLines = @explode("\n", $lines); - $header = true; - $ignore = false; - $sOIM = ''; - foreach ($aLines as $line) { - $line = rtrim($line); - if ($header) { - if ($line === '') { - $header = false; - continue; - } - continue; - } - // stop at empty lines - if ($line === '') break; - $sOIM .= $line; + $id = &$session['id']; + + $data = $this->sb_readln($socket); + $code = substr($data, 0, 3); + switch($code) { + case 'IRO': + // SB: <<< IRO {id} {rooster} {roostercount} {email} {alias} {clientid} + @list(/* IRO */, /* id */, $cur_num, $total, $email, $alias, $clientid) = @explode(' ', $data); + $this->debug_message("*** $email joined session"); + $session['joined'] = true; + break; + case 'BYE': + $this->debug_message("*** Quit for BYE"); + $this->endSBSession(); + break; + case 'USR': + // SB: <<< USR {id} OK {user} {alias} + // we don't need the data, just ignore it + // request user to join this switchboard + // SB: >>> CAL {id} {user} + $this->sb_writeln($socket, $id, "CAL $this->id $user"); + break; + case 'CAL': + // SB: <<< CAL {id} RINGING {?} + // we don't need this, just ignore, and wait for other response + $session['id']++; + break; + case 'JOI': + // SB: <<< JOI {user} {alias} {clientid?} + // someone join us + // we don't need the data, just ignore it + // no more user here + $session['joined'] = true; + break; + case 'MSG': + // SB: <<< MSG {email} {alias} {len} + @list(/* MSG */, $from_email, /* alias */, $len, ) = @explode(' ', $data); + $len = trim($len); + $data = $this->sb_readdata($socket, $len); + $aLines = @explode("\n", $data); + $header = true; + $ignore = false; + $is_p2p = false; + $sMsg = ''; + foreach ($aLines as $line) { + $line = rtrim($line); + if ($header) { + if ($line === '') { + $header = false; + continue; + } + if (strncasecmp($line, 'TypingUser:', 11) == 0) { + // typing notification, just ignore + $ignore = true; + break; + } + if (strncasecmp($line, 'Chunk:', 6) == 0) { + // we don't handle any split message, just ignore + $ignore = true; + break; + } + if (strncasecmp($line, 'Content-Type: application/x-msnmsgrp2p', 38) == 0) { + // p2p message, ignore it, but we need to send acknowledgement for it... + $is_p2p = true; + $p = strstr($data, "\n\n"); + $sMsg = ''; + if ($p === false) { + $p = strstr($data, "\r\n\r\n"); + if ($p !== false) + $sMsg = substr($p, 4); + } + else + $sMsg = substr($p, 2); + break; + } + if (strncasecmp($line, 'Content-Type: application/x-', 28) == 0) { + // ignore all application/x-... message + // for example: + // application/x-ms-ink => ink message + $ignore = true; + break; + } + if (strncasecmp($line, 'Content-Type: text/x-', 21) == 0) { + // ignore all text/x-... message + // for example: + // text/x-msnmsgr-datacast => nudge, voice clip.... + // text/x-mms-animemoticon => customized animemotion word + $ignore = true; + break; + } + continue; + } + if ($sMsg !== '') + $sMsg .= "\n"; + $sMsg .= $line; + } + if ($ignore) { + $this->debug_message("*** Ignoring SB data from $from_email: $line"); + break; + } + if ($is_p2p) { + // we will ignore any p2p message after sending acknowledgement + $ignore = true; + $len = strlen($sMsg); + $this->debug_message("*** p2p message from $from_email, size $len"); + // header = 48 bytes + // content >= 0 bytes + // footer = 4 bytes + // so it need to >= 52 bytes + /*if ($len < 52) { + $this->debug_message("*** p2p: size error, less than 52!"); + break; + }*/ + $aDwords = @unpack("V12dword", $sMsg); + if (!is_array($aDwords)) { + $this->debug_message("*** p2p: header unpack error!"); + break; + } + $this->debug_message("*** p2p: dump received message:\n".$this->dump_binary($sMsg)); + $hdr_SessionID = $aDwords['dword1']; + $hdr_Identifier = $aDwords['dword2']; + $hdr_DataOffsetLow = $aDwords['dword3']; + $hdr_DataOffsetHigh = $aDwords['dword4']; + $hdr_TotalDataSizeLow = $aDwords['dword5']; + $hdr_TotalDataSizeHigh = $aDwords['dword6']; + $hdr_MessageLength = $aDwords['dword7']; + $hdr_Flag = $aDwords['dword8']; + $hdr_AckID = $aDwords['dword9']; + $hdr_AckUID = $aDwords['dword10']; + $hdr_AckSizeLow = $aDwords['dword11']; + $hdr_AckSizeHigh = $aDwords['dword12']; + $this->debug_message("*** p2p: header SessionID = $hdr_SessionID"); + $this->debug_message("*** p2p: header Inentifier = $hdr_Identifier"); + $this->debug_message("*** p2p: header Data Offset Low = $hdr_DataOffsetLow"); + $this->debug_message("*** p2p: header Data Offset High = $hdr_DataOffsetHigh"); + $this->debug_message("*** p2p: header Total Data Size Low = $hdr_TotalDataSizeLow"); + $this->debug_message("*** p2p: header Total Data Size High = $hdr_TotalDataSizeHigh"); + $this->debug_message("*** p2p: header MessageLength = $hdr_MessageLength"); + $this->debug_message("*** p2p: header Flag = $hdr_Flag"); + $this->debug_message("*** p2p: header AckID = $hdr_AckID"); + $this->debug_message("*** p2p: header AckUID = $hdr_AckUID"); + $this->debug_message("*** p2p: header AckSize Low = $hdr_AckSizeLow"); + $this->debug_message("*** p2p: header AckSize High = $hdr_AckSizeHigh"); + if ($hdr_Flag == 2) { + //This is an ACK from SB ignore.... + $this->debug_message("*** p2p: //This is an ACK from SB ignore....:\n"); + break; + } + $MsgBody = $this->linetoArray(substr($sMsg, 48, -4)); + $this->debug_message("*** p2p: body".print_r($MsgBody, true)); + if (($MsgBody['EUF-GUID']=='{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}')&&($PictureFilePath=$this->GetPictureFilePath($MsgBody['Context']))) { + while (true) { + if ($this->sb_readln($socket) === false) break; + } + $this->debug_message("*** p2p: Inv hdr:\n".$this->dump_binary(substr($sMsg, 0, 48))); + preg_match('/{([0-9A-F\-]*)}/i', $MsgBody['Via'], $Matches); + $BranchGUID = $Matches[1]; + //it's an invite to send a display picture. + $new_id = ~$hdr_Identifier; + $hdr = pack( + "LLLLLLLLLLLL", $hdr_SessionID, + $new_id, + 0, 0, + $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, + 0, + 2, + $hdr_Identifier, + $hdr_AckID, + $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh + ); + $footer = pack("L", 0); + $message = "MIME-Version: 1.0\r\nContent-Type: application/x-msnmsgrp2p\r\nP2P-Dest: $from_email\r\n\r\n$hdr$footer"; + $len = strlen($message); + $this->sb_writeln($socket, $id, "MSG $this->id D $len"); + $this->sb_writedata($socket, $message); + $this->debug_message("*** p2p: send display picture acknowledgement for $hdr_SessionID"); + $this->debug_message("*** p2p: Invite ACK message:\n".$this->dump_binary($message)); + $this->sb_readln($socket); // Read ACK; + $this->debug_message("*** p2p: Invite ACK Hdr:\n".$this->dump_binary($hdr)); + $new_id -= 3; + //Send 200 OK message + $MessageContent="SessionID: ".$MsgBody['SessionID']."\r\n\r\n".pack("C", 0); + $MessagePayload= + "MSNSLP/1.0 200 OK\r\n". + "To: \r\n". + "From: user.">\r\n". + "Via: ".$MsgBody['Via']."\r\n". + "CSeq: ".($MsgBody['CSeq']+1)."\r\n". + "Call-ID: ".$MsgBody['Call-ID']."\r\n". + "Max-Forwards: 0\r\n". + "Content-Type: application/x-msnmsgr-sessionreqbody\r\n". + "Content-Length: ".strlen($MessageContent)."\r\n\r\n". + $MessageContent; + $hdr_TotalDataSizeLow=strlen($MessagePayload); + $hdr_TotalDataSizeHigh=0; + $hdr = pack( + "LLLLLLLLLLLL", $hdr_SessionID, + $new_id, + 0, 0, + $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, + strlen($MessagePayload), + 0, + rand(), + 0, + 0, 0 + ); + + $message = + "MIME-Version: 1.0\r\n". + "Content-Type: application/x-msnmsgrp2p\r\n". + "P2P-Dest: $from_email\r\n\r\n$hdr$MessagePayload$footer"; + $this->sb_writeln($socket, $id, "MSG $this->id D ".strlen($message)); + $this->sb_writedata($socket, $message); + $this->debug_message("*** p2p: dump 200 ok message:\n".$this->dump_binary($message)); + $this->sb_readln($socket); // Read ACK; + + $this->debug_message("*** p2p: 200 ok:\n".$this->dump_binary($hdr)); + // send data preparation message + // send 4 null bytes as data + $hdr_TotalDataSizeLow = 4; + $hdr_TotalDataSizeHigh = 0 ; + $new_id++; + $hdr = pack( + "LLLLLLLLLLLL", + $MsgBody['SessionID'], + $new_id, + 0, 0, + $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, + $hdr_TotalDataSizeLow, + 0, + rand(), + 0, + 0, 0 + ); + $message = + "MIME-Version: 1.0\r\n". + "Content-Type: application/x-msnmsgrp2p\r\n". + "P2P-Dest: $from_email\r\n\r\n$hdr".pack('L', 0)."$footer"; + $this->sb_writeln($socket, $id, "MSG $this->id D ".strlen($message)); + $this->sb_writedata($socket, $message); + $this->debug_message("*** p2p: dump send Data preparation message:\n".$this->dump_binary($message)); + $this->debug_message("*** p2p: Data Prepare Hdr:\n".$this->dump_binary($hdr)); + $this->sb_readln($socket); // Read ACK; + + // send Data Content.. + $footer=pack('N',1); + $new_id++; + $FileSize=filesize($PictureFilePath); + if ($hTitle=fopen($PictureFilePath,'rb')) { + $Offset = 0; + //$new_id++; + while (!feof($hTitle)) { + $FileContent = fread($hTitle, 1024); + $FileContentSize = strlen($FileContent); + $hdr = pack( + "LLLLLLLLLLLL", + $MsgBody['SessionID'], + $new_id, + $Offset, 0, + $FileSize, 0, + $FileContentSize, + 0x20, + rand(), + 0, + 0, 0 + ); + $message = + "MIME-Version: 1.0\r\n". + "Content-Type: application/x-msnmsgrp2p\r\n". + "P2P-Dest: $from_email\r\n\r\n$hdr$FileContent$footer"; + $this->sb_writeln($socket, $id, "MSG $this->id D ".strlen($message)); + $this->sb_writedata($socket, $message); + $this->debug_message("*** p2p: dump send Data Content message $Offset / $FileSize :\n".$this->dump_binary($message)); + $this->debug_message("*** p2p: Data Content Hdr:\n".$this->dump_binary($hdr)); + //$this->SB_readln($socket);//Read ACK; + $Offset += $FileContentSize; + } + } + //Send Bye + /* + $MessageContent="\r\n".pack("C", 0); + $MessagePayload= + "BYE MSNMSGR:MSNSLP/1.0\r\n". + "To: \r\n". + "From: user.">\r\n". + "Via: MSNSLP/1.0/TLP ;branch={".$BranchGUID."}\r\n". + "CSeq: 0\r\n". + "Call-ID: ".$MsgBody['Call-ID']."\r\n". + "Max-Forwards: 0\r\n". + "Content-Type: application/x-msnmsgr-sessionclosebody\r\n". + "Content-Length: ".strlen($MessageContent)."\r\n\r\n".$MessageContent; + $footer=pack('N',0); + $hdr_TotalDataSizeLow=strlen($MessagePayload); + $hdr_TotalDataSizeHigh=0; + $new_id++; + $hdr = pack("LLLLLLLLLLLL", + 0, + $new_id, + 0, 0, + $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, + 0, + 0, + rand(), + 0, + 0,0); + $message = + "MIME-Version: 1.0\r\n". + "Content-Type: application/x-msnmsgrp2p\r\n". + "P2P-Dest: $from_email\r\n\r\n$hdr$MessagePayload$footer"; + $this->sb_writeln($socket, $id, "MSG $id D ".strlen($message)); + $id++; + $this->sb_writedata($socket, $message); + $this->debug_message("*** p2p: dump send BYE message :\n".$this->dump_binary($message)); + */ + break; + } + //TODO: + //if ($hdr_Flag == 2) { + // just send ACK... + // $this->sb_writeln($socket, $id, "ACK $id"); + // break; + //} + if ($hdr_SessionID == 4) { + // ignore? + $this->debug_message("*** p2p: ignore flag 4"); + break; + } + $finished = false; + if ($hdr_TotalDataSizeHigh == 0) { + // only 32 bites size + if (($hdr_MessageLength + $hdr_DataOffsetLow) == $hdr_TotalDataSizeLow) + $finished = true; + } + else { + // we won't accept any file transfer + // so I think we won't get any message size need to use 64 bits + // 64 bits size here, can't count directly... + $totalsize = base_convert(sprintf("%X%08X", $hdr_TotalDataSizeHigh, $hdr_TotalDataSizeLow), 16, 10); + $dataoffset = base_convert(sprintf("%X%08X", $hdr_DataOffsetHigh, $hdr_DataOffsetLow), 16, 10); + $messagelength = base_convert(sprintf("%X", $hdr_MessageLength), 16, 10); + $now_size = bcadd($dataoffset, $messagelength); + if (bccomp($now_size, $totalsize) >= 0) + $finished = true; + } + if (!$finished) { + // ignore not finished split packet + $this->debug_message("*** p2p: ignore split packet, not finished"); + break; + } + //$new_id = ~$hdr_Identifier; + /* + $new_id++; + $hdr = pack("LLLLLLLLLLLL", $hdr_SessionID, + $new_id, + 0, 0, + $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, + 0, + 2, + $hdr_Identifier, + $hdr_AckID, + $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh); + $footer = pack("L", 0); + $message = "MIME-Version: 1.0\r\nContent-Type: application/x-msnmsgrp2p\r\nP2P-Dest: $from_email\r\n\r\n$hdr$footer"; + $len = strlen($message); + $this->sb_writeln($socket, $id, "MSG $id D $len"); + $id++; + $this->sb_writedata($socket, $message); + $this->debug_message("*** p2p: send acknowledgement for $hdr_SessionID"); + $this->debug_message("*** p2p: dump sent message:\n".$this->dump_binary($hdr.$footer)); + */ + break; + } + $this->debug_message("*** MSG from $from_email: $sMsg"); + $this->callHandler('IMin', array('sender' => $from_email, 'message' => $sMsg, 'network' => $network, 'offline' => false)); + break; + case '217': + $this->debug_message("*** User $user is offline. Trying OIM."); + $session['offline'] = true; + break; + default: + if (is_numeric($code)) { + $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List"; + $this->debug_message("*** SB: $this->error"); + $sessionEnd=true; + } + break; + } + } + + /** + * Checks for new data and calls appropriate methods + * + * This method is usually called in an infinite loop to keep checking for new data + * + * @return void + */ + public function receive() { + // First, get an array of sockets that have data that is ready to be read + $ready = array(); + $ready = $this->getSockets(); + $numrdy = stream_select($ready, $w = NULL, $x = NULL, NULL); + + // Now that we've waited for something, go through the $ready + // array and read appropriately + + foreach ($ready as $socket) { + if ($socket == $this->NSfp) { + $this->nsReceive(); + } else { + $this->sbReceive($socket); + } + } + } + + /** + * Switchboard related methods + */ + + /** + * Send a request for a switchboard session + * + * @param string $to Target email for switchboard session + */ + private function reqSBSession($to) { + $this->debug_message("*** Request SB for $to"); + $this->ns_writeln("XFR $this->id SB"); + + // Add to the queue of those waiting for a switchboard session reponse + $this->switchBoardSessions[$to] = array( + 'to' => $to, + 'socket' => NULL, + 'id' => 1, + 'joined' => false, + 'offline' => false, + 'XFRReqTime' => time() + ); + $this->waitingForXFR[] = &$this->switchBoardSessions[$to]; + } + + /** + * Following an XFR or RNG, connect to the switchboard session + * + * @param string $mode Mode, either 'Active' (in the case of XFR) or 'Passive' (in the case of RNG) + * @param string $ip IP of Switchboard + * @param integer $port Port of Switchboard + * @param string $to User on other end of Switchboard + * @param array $param Array of parameters - 'cki', 'ticket', 'sid' + * @return boolean true if successful + */ + private function connectToSBSession($mode, $ip, $port, $to, $param) { + $this->debug_message("*** SB: Trying to connect to switchboard server $ip:$port"); + + $socket = @fsockopen($ip, $port, $errno, $errstr, $this->timeout); + if (!$socket) { + $this->debug_message("*** SB: Can't connect to $ip:$port, error => $errno, $errstr"); + return false; + } + + // Store the socket in the lookup array + $this->switchBoardSessionLookup[$to] = $socket; + + // Store the socket in the sessions array + $intsocket = (int) $socket; + $this->switchBoardSessions[$to] = array( + 'to' => $to, + 'socket' => $socket, + 'id' => 1, + 'joined' => false, + 'offline' => false, + 'XFRReqTime' => time() + ); + + // Change the index of the session to the socket + $this->switchBoardSessions[$intsocket] = $this->switchBoardSessions[$to]; + unset($this->switchBoardSessions[$to]); + + $id = &$this->switchBoardSessions[$intsocket]['id']; + + if ($mode == 'Active') { + $cki_code = $param['cki']; + + // SB: >>> USR {id} {user} {cki} + $this->sb_writeln($socket, $id, "USR $id $this->user $cki_code"); + } else { + // Passive + $ticket = $param['ticket']; + $sid = $param['sid']; + + // SB: >>> ANS {id} {user} {ticket} {session_id} + $this->sb_writeln($socket, $id, "ANS $id $this->user $ticket $sid"); + } + } + + /** + * Called when we want to end a switchboard session + * or a switchboard session ends + * + * @param resource $socket Socket + * @param boolean $killsession Whether to delete the session + * @return void + */ + private function endSBSession($socket, $killsession = false) { + if (!socketcheck($socket)) { + $this->sb_writeln($socket, $fake = 0, 'OUT'); + } + @fclose($socket); + + // Unset session lookup value + $intsocket = (int) $socket; + unset($this->switchBoardSessionLookup[$this->switchBoardSessions[$intsocket]['to']]); + + // Unset session itself + unset($this->switchBoardSessions[$intsocket]); + } + + /** + * Send a message via an existing SB session + * + * @param string $to Recipient for message + * @param string $message Message + * @return boolean true on success + */ + private function sendMessageViaSB($to, $message) { + $socket = $this->switchBoardSessionLookup[$to]; + if (socketcheck($socket)) { + return false; + } + + if (!$this->switchBoardSessions[$to]['joined']) { + // If our participant has not joined the session yet we can't message them! + return false; + } + + $intsocket = (int) $socket; + $id = &$this->switchBoardSessions[$intsocket]['id']; + + $aMessage = $this->getMessage($Message); + //CheckEmotion... + $MsnObjDefine=$this->GetMsnObjDefine($aMessage); + if ($MsnObjDefine !== '') { + $SendString="MIME-Version: 1.0\r\nContent-Type: text/x-mms-emoticon\r\n\r\n$MsnObjDefine"; + $len = strlen($SendString); + // TODO handle failure during write to socket + $this->sb_writeln($socket, $id, "MSG $id N $len"); + $this->sb_writedata($socket, $SendString); + } + $len = strlen($aMessage); + // TODO handle failure during write to socket + $this->sb_writeln($socket, $id, "MSG $id N $len"); + $this->sb_writedata($socket, $aMessage); + + // Don't close the SB session, we might as well leave it open + + return true; + } + + /** + * Send a message to a user on another network + * + * @param string $to Intended recipient + * @param string $message Message + * @param integer $network Network + * @return void + */ + private function sendOtherNetworkMessage($to, $message, $network) { + $message = $this->getMessage($message, $network); + $len = strlen($message); + // TODO Introduce error checking for message sending + $this->ns_writeln("UUM $this->id $to $network 1 $len"); + $this->ns_writedata($Message); + $this->debug_message("*** Sent to $to (network: $network):\n$Message"); + return true; + } + + /** + * Send a message + * + * @param string $to To address in form user@host.com(@network) + * where network is 1 for MSN, 32 for Yahoo + * and 'Offline' for offline messages + * @param string $message Message + */ + public function sendMessage($to, $message) { + if ($message != '') { + list($name, $host, $network) = explode('@', $to); + $network = $network == '' ? 1 : $network; + $recipient = $name.$host; + + if ($network === 1) { + if (!isset($this->switchBoardSessionLookup[$recipient]) && (!isset($this->switchBoardSessions[$recipient]) + || time() - $this->switchBoardSessions[$recipient]['XFRReqTime'] > $this->XFRReqTimeout)) { + $this->debug_message("*** No existing SB session or request has timed out"); + $this->reqSBSession($recipient); + return false; + } else { + $socket = $this->switchBoardSessionLookup[$to]; + if ($this->switchBoardSessions[(int) $socket]['offline']) { + $this->debug_message("*** Contact ($recipient) offline, sending OIM"); + $this->endSBSession($socket); + return $this->sendMessage($recipient.'@Offline', $message); + } else { + $this->debug_message("*** Attempting to send message to $recipient using existing SB session"); + + if ($this->sendMessageViaSB($recipient, $message)) { + $this->debug_message('*** Message sent successfully'); + return true; + } else { + $this->debug_message('*** Message sending failed, requesting new SB session'); + $this->reqSBSession($to); + return false; + } + } + } + } elseif ($network == 'Offline') { + //Send OIM + //FIXME: 修正Send OIM + $lockkey = ''; + $re_login = false; + for ($i = 0; $i < $this->oim_try; $i++) { + if (($oim_result = $this->sendOIM($recipient, $message, $lockkey)) === true) break; + if (is_array($oim_result) && $oim_result['challenge'] !== false) { + // need challenge lockkey + $this->debug_message("*** Need challenge code for ".$oim_result['challenge']); + $lockkey = $this->getChallenge($oim_result['challenge']); + continue; + } + if ($oim_result === false || $oim_result['auth_policy'] !== false) { + if ($re_login) { + $this->debug_message("*** Can't send OIM, but we already re-logged-in again, so returning false"); + return false; + } + $this->debug_message("*** Can't send OIM, maybe ticket expired, trying to login again"); + + // Maybe we need to re-login again + if (!$this->get_passport_ticket()) { + $this->debug_message("*** Can't re-login, something went wrong here, returning false"); + return false; + } + $this->debug_message("*** Getting new ticket and trying again"); + continue; + } + } + } else { + // Other network + return $this->sendOtherNetworkMessage($recipient, $message, $network); + } + } + return true; + } + + /** + * OIM methods + */ + + /** + * Get OIM mail data + * + * @return string mail data or false on failure + */ + function getOIM_maildata() { + preg_match('#t=(.*)&p=(.*)#', $this->ticket['web_ticket'], $matches); + if (count($matches) == 0) { + $this->debug_message('*** No web ticket?'); + return false; + } + $t = htmlspecialchars($matches[1]); + $p = htmlspecialchars($matches[2]); + $XML = ' + + + + '.$t.' +

'.$p.'

+
+
+ + + +
'; + + $header_array = array( + 'SOAPAction: '.OIM_MAILDATA_SOAP, + 'Content-Type: text/xml; charset=utf-8', + 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.BUILDVER.')' + ); + + $this->debug_message('*** URL: '.OIM_MAILDATA_URL); + $this->debug_message("*** Sending SOAP:\n$XML"); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, OIM_MAILDATA_URL); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); + $data = curl_exec($curl); + $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + $this->debug_message("*** Get Result:\n$data"); + + if ($http_code != 200) { + $this->debug_message("*** Could not get OIM maildata! http code: $http_code"); + return false; + } + + // See #XML_Data + preg_match('#]*)>(.*)#', $data, $matches); + if (count($matches) == 0) { + $this->debug_message('*** Could not get OIM maildata'); + return false; + } + return $matches[2]; + } + + /** + * Fetch OIM message with given id + * + * @param string $msgid + * @return string Message or false on failure + */ + function getOIM_message($msgid) { + preg_match('#t=(.*)&p=(.*)#', $this->ticket['web_ticket'], $matches); + if (count($matches) == 0) { + $this->debug_message('*** No web ticket?'); + return false; } - $sMsg = base64_decode($sOIM); - //$this->debug_message("*** we get OIM ($msgid): $sMsg"); + $t = htmlspecialchars($matches[1]); + $p = htmlspecialchars($matches[2]); - // delete OIM + // read OIM $XML = ' - - - '.$msgid.' - - + + '.$msgid.' + false + '; $header_array = array( - 'SOAPAction: '.$this->oim_del_soap, + 'SOAPAction: '.OIM_READ_SOAP, 'Content-Type: text/xml; charset=utf-8', - 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.$this->buildver.')' + 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.BUILDVER.')' ); - //$this->debug_message("*** URL: $this->oim_del_url"); - //$this->debug_message("*** Sending SOAP:\n$XML"); + $this->debug_message('*** URL: '.OIM_READ_URL); + $this->debug_message("*** Sending SOAP:\n$XML"); $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->oim_del_url); + curl_setopt($curl, CURLOPT_URL, OIM_READ_URL); curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); @@ -1483,1354 +1703,1173 @@ class MSN { $data = curl_exec($curl); $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); - //$this->debug_message("*** Get Result:\n$data"); - - if ($http_code != 200) - $this->debug_message("*** Could not delete OIM: $msgid, http code = $http_code"); - else - $this->debug_message("*** OIM ($msgid) deleted"); - return $sMsg; - } - - /** - * Log out and close the NS connection - * - * @return void - */ - private function NSLogout() { - if (is_resource($this->NSfp) && !feof($this->NSfp)) { - // logout now - // NS: >>> OUT - $this->ns_writeln("OUT"); - fclose($this->NSfp); - $this->NSfp = false; - $this->debug_message("*** Logged out"); - } - } - - /** - * Sleep for the given number of seconds - * - * @param integer $wait Number of seconds to sleep for - */ - private function NSRetryWait($wait) { - $this->debug_message("*** Sleeping for $wait seconds before retrying"); - sleep($wait); - } - - /** - * Generate challenge response - * - * @param string $code - * @return string challenge response code - */ - function getChallenge($code) { - // MSNP15 - // http://msnpiki.msnfanatic.com/index.php/MSNP11:Challenges - // Step 1: The MD5 Hash - $md5Hash = md5($code.$this->prod_key); - $aMD5 = @explode("\0", chunk_split($md5Hash, 8, "\0")); - for ($i = 0; $i < 4; $i++) { - $aMD5[$i] = implode('', array_reverse(@explode("\0", chunk_split($aMD5[$i], 2, "\0")))); - $aMD5[$i] = (0 + base_convert($aMD5[$i], 16, 10)) & 0x7FFFFFFF; - } - - // Step 2: A new string - $chl_id = $code.$this->prod_id; - $chl_id .= str_repeat('0', 8 - (strlen($chl_id) % 8)); - - $aID = @explode("\0", substr(chunk_split($chl_id, 4, "\0"), 0, -1)); - for ($i = 0; $i < count($aID); $i++) { - $aID[$i] = implode('', array_reverse(@explode("\0", chunk_split($aID[$i], 1, "\0")))); - $aID[$i] = 0 + base_convert(bin2hex($aID[$i]), 16, 10); - } - - // Step 3: The 64 bit key - $magic_num = 0x0E79A9C1; - $str7f = 0x7FFFFFFF; - $high = 0; - $low = 0; - for ($i = 0; $i < count($aID); $i += 2) { - $temp = $aID[$i]; - $temp = bcmod(bcmul($magic_num, $temp), $str7f); - $temp = bcadd($temp, $high); - $temp = bcadd(bcmul($aMD5[0], $temp), $aMD5[1]); - $temp = bcmod($temp, $str7f); - - $high = $aID[$i+1]; - $high = bcmod(bcadd($high, $temp), $str7f); - $high = bcadd(bcmul($aMD5[2], $high), $aMD5[3]); - $high = bcmod($high, $str7f); - - $low = bcadd(bcadd($low, $high), $temp); - } - - $high = bcmod(bcadd($high, $aMD5[1]), $str7f); - $low = bcmod(bcadd($low, $aMD5[3]), $str7f); - - $new_high = bcmul($high & 0xFF, 0x1000000); - $new_high = bcadd($new_high, bcmul($high & 0xFF00, 0x100)); - $new_high = bcadd($new_high, bcdiv($high & 0xFF0000, 0x100)); - $new_high = bcadd($new_high, bcdiv($high & 0xFF000000, 0x1000000)); - // we need integer here - $high = 0+$new_high; - - $new_low = bcmul($low & 0xFF, 0x1000000); - $new_low = bcadd($new_low, bcmul($low & 0xFF00, 0x100)); - $new_low = bcadd($new_low, bcdiv($low & 0xFF0000, 0x100)); - $new_low = bcadd($new_low, bcdiv($low & 0xFF000000, 0x1000000)); - // we need integer here - $low = 0+$new_low; - - // we just use 32 bits integer, don't need the key, just high/low - // $key = bcadd(bcmul($high, 0x100000000), $low); - - // Step 4: Using the key - $md5Hash = md5($code.$this->prod_key); - $aHash = @explode("\0", chunk_split($md5Hash, 8, "\0")); - - $hash = ''; - $hash .= sprintf("%08x", (0 + base_convert($aHash[0], 16, 10)) ^ $high); - $hash .= sprintf("%08x", (0 + base_convert($aHash[1], 16, 10)) ^ $low); - $hash .= sprintf("%08x", (0 + base_convert($aHash[2], 16, 10)) ^ $high); - $hash .= sprintf("%08x", (0 + base_convert($aHash[3], 16, 10)) ^ $low); - - return $hash; - } - - /** - * Generate the data to send a message - * - * @param string $sMessage Message - * @param integer $network Network - */ - private function getMessage($sMessage, $network = 1) { - $msg_header = "MIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nX-MMS-IM-Format: FN=$this->font_fn; EF=$this->font_ef; CO=$this->font_co; CS=0; PF=22\r\n\r\n"; - $msg_header_len = strlen($msg_header); - if ($network == 1) - $maxlen = $this->max_msn_message_len - $msg_header_len; - else - $maxlen = $this->max_yahoo_message_len - $msg_header_len; - $sMessage = str_replace("\r", '', $sMessage); - $msg = substr($sMessage, 0, $maxlen); - return $msg_header.$msg; - } - - // read data for specified size - private function ns_readdata($size) { - $data = ''; - $count = 0; - while (!feof($this->NSfp)) { - $buf = @fread($this->NSfp, $size - $count); - $data .= $buf; - $count += strlen($buf); - if ($count >= $size) break; - } - $this->debug_message("NS: data ($size/$count) <<<\n$data"); - return $data; - } - - // read one line - private function ns_readln() { - $data = @fgets($this->NSfp, 4096); - if ($data !== false) { - $data = trim($data); - $this->debug_message("NS: <<< $data"); - } - return $data; - } - - // write to server, append \r\n, also increase id - private function ns_writeln($data) { - @fwrite($this->NSfp, $data."\r\n"); - $this->debug_message("NS: >>> $data"); - $this->id++; - return; - } - - // write data to server - private function ns_writedata($data) { - @fwrite($this->NSfp, $data); - $this->debug_message("NS: >>> $data"); - return; - } + $this->debug_message("*** Get Result:\n$data"); - // read data for specified size for SB - private function sb_readdata($socket, $size) { - $data = ''; - $count = 0; - while (!feof($socket)) { - $buf = @fread($socket, $size - $count); - $data .= $buf; - $count += strlen($buf); - if ($count >= $size) break; + if ($http_code != 200) { + $this->debug_message("*** Can't get OIM: $msgid, http code = $http_code"); + return false; } - $this->debug_message("SB: data ($size/$count) <<<\n$data"); - return $data; - } - // read one line for SB - private function sb_readln($socket) { - $data = @fgets($socket, 4096); - if ($data !== false) { - $data = trim($data); - $this->debug_message("SB: <<< $data"); + // why can't use preg_match('#(.*)#', $data, $matches)? + // multi-lines? + $start = strpos($data, ''); + $end = strpos($data, ''); + if ($start === false || $end === false || $start > $end) { + $this->debug_message("*** Can't get OIM: $msgid"); + return false; } - return $data; - } - - // write to server for SB, append \r\n, also increase id - // switchboard server only accept \r\n, it will lost connection if just \n only - private function sb_writeln($socket, &$id, $data) { - @fwrite($socket, $data."\r\n"); - $this->debug_message("SB: >>> $data"); - $id++; - return; - } - - // write data to server - private function sb_writedata($socket, $data) { - @fwrite($socket, $data); - $this->debug_message("SB: >>> $data"); - return; - } - - // show debug information - function debug_message($str) { - if (!$this->debug) return; - if ($this->debug===STDOUT) echo $str."\n"; - /*$fname=MSN_CLASS_LOG_DIR.DIRECTORY_SEPARATOR.'msn_'.strftime('%Y%m%d').'.debug'; - $fp = fopen($fname, 'at'); - if ($fp) { - fputs($fp, strftime('%m/%d/%y %H:%M:%S').' ['.posix_getpid().'] '.$str."\n"); - fclose($fp); - return; - }*/ - // still show debug information, if we can't open log_file - echo $str."\n"; - return; - } - - function dump_binary($str) { - $buf = ''; - $a_str = ''; - $h_str = ''; - $len = strlen($str); - for ($i = 0; $i < $len; $i++) { - if (($i % 16) == 0) { - if ($buf !== '') { - $buf .= "$h_str $a_str\n"; + $lines = substr($data, $start + 18, $end - $start); + $aLines = @explode("\n", $lines); + $header = true; + $ignore = false; + $sOIM = ''; + foreach ($aLines as $line) { + $line = rtrim($line); + if ($header) { + if ($line === '') { + $header = false; + continue; } - $buf .= sprintf("%04X:", $i); - $a_str = ''; - $h_str = ''; + continue; } - $ch = ord($str[$i]); - if ($ch < 32) - $a_str .= '.'; - else - $a_str .= chr($ch); - $h_str .= sprintf(" %02X", $ch); + // stop at empty lines + if ($line === '') break; + $sOIM .= $line; } - if ($h_str !== '') - $buf .= "$h_str $a_str\n"; - return $buf; - } + $sMsg = base64_decode($sOIM); + //$this->debug_message("*** we get OIM ($msgid): $sMsg"); - /** - * - * @param $FilePath 圖檔路徑 - * @param $Type 檔案類型 3=>大頭貼,2表情圖案 - * @return array - */ - private function MsnObj($FilePath,$Type=3) - { - if (!($FileSize=filesize($FilePath))) return ''; - $Location = md5($FilePath); - $Friendly = md5($FilePath.$Type); - if (isset($this->MsnObjMap[$Location])) return $this->MsnObjMap[$Location]; - $sha1d = base64_encode(sha1(file_get_contents($FilePath), true)); - $sha1c = base64_encode(sha1("Creator".$this->user."Size$FileSize"."Type$Type"."Location$Location"."Friendly".$Friendly."SHA1D$sha1d",true)); - $this->MsnObjArray[$Location] = $FilePath; - $MsnObj = ''; - $this->MsnObjMap[$Location] = $MsnObj; - $this->debug_message("*** p2p: addMsnObj $FilePath::$MsnObj\n"); - return $MsnObj; - } + // delete OIM + $XML = ' + + + + '.$t.' +

'.$p.'

+
+
+ + + + '.$msgid.' + + + +
'; - private function linetoArray($lines) { - $lines = str_replace("\r", '', $lines); - $lines = explode("\n", $lines); - foreach ($lines as $line) { - if (!isset($line{3})) continue; - list($Key,$Val) = explode(':', $line); - $Data[trim($Key)] = trim($Val); - } - return $Data; - } + $header_array = array( + 'SOAPAction: '.OIM_DEL_SOAP, + 'Content-Type: text/xml; charset=utf-8', + 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.BUILDVER.')' + ); - private function GetPictureFilePath($Context) { - $MsnObj = base64_decode($Context); - if (preg_match('/location="(.*?)"/i', $MsnObj, $Match)) - $location = $Match[1]; - $this->debug_message("*** p2p: PictureFile[$location] ::All".print_r($this->MsnObjArray,true)."\n"); - if ($location && isset($this->MsnObjArray[$location])) - return $this->MsnObjArray[$location]; - return false; - } + $this->debug_message('*** URL: '.OIM_DEL_URL); + $this->debug_message("*** Sending SOAP:\n$XML"); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, OIM_DEL_URL); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); + $data = curl_exec($curl); + $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + $this->debug_message("*** Get Result:\n$data"); - private function GetMsnObjDefine($Message) { - $DefineString = ''; - if (is_array($this->Emotions)) - foreach ($this->Emotions as $Pattern => $FilePath) { - if (strpos($Message, $Pattern)!==false) - $DefineString .= "$Pattern\t".$this->MsnObj($FilePath, 2)."\t"; - } - return $DefineString; + if ($http_code != 200) + $this->debug_message("*** Could not delete OIM: $msgid, http code = $http_code"); + else + $this->debug_message("*** OIM ($msgid) deleted"); + return $sMsg; } - + /** - * Read and handle incoming command from NS + * Send offline message * - * @return void + * @param string $to Intended recipient + * @param string $sMessage Message + * @param string $lockkey Lock key + * @return mixed true on success or error data */ - private function nsReceive() { - // Sign in again if not signed in or socket failed - if (!is_resource($this->NSfp) || self::socketcheck($this->NSfp)) { - $this->callHandler('Reconnect'); - $this->NSRetryWait($this->retry_wait); - $this->signon(); - return; - } - - $data = $this->ns_readln(); - if ($data === false) { - // There was no data / an error when reading from the socket so reconnect - $this->callHandler('Reconnect'); - $this->NSRetryWait($this->retry_wait); - $this->signon(); - return; - } else { - switch (substr($data, 0, 3)) { - case 'SBS': - // after 'USR {id} OK {user} {verify} 0' response, the server will send SBS and profile to us - // NS: <<< SBS 0 null - break; + private function sendOIM($to, $sMessage, $lockkey) { + $XML = ' + + + + + + + http://messenger.msn.com + 1 + + + + text + MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: base64 +X-OIM-Message-Type: OfflineMessage +X-OIM-Run-Id: {DAB68CFA-38C9-449B-945E-38AFA51E50A7} +X-OIM-Sequence-Num: 1 - case 'RFS': - // FIXME: - // NS: <<< RFS ??? - // refresh ADL, so we re-send it again - if (is_array($this->aADL)) { - foreach ($this->aADL as $str) { - $len = strlen($str); - // NS: >>> ADL {id} {size} - $this->ns_writeln("ADL $this->id $len"); - $this->ns_writedata($str); - } - } - break; +'.chunk_split(base64_encode($sMessage)).' + + +'; - case 'LST': - // NS: <<< LST {email} {alias} 11 0 - @list(/* LST */, $email, /* alias */,) = @explode(' ', $data); - @list($u_name, $u_domain) = @explode('@', $email); - if (!isset($this->aContactList[$u_domain][$u_name][1])) { - $this->aContactList[$u_domain][$u_name][1]['Allow'] = 'Allow'; - $this->debug_message("*** Added to contact list: $u_name@$u_domain"); - } - break; + $header_array = array( + 'SOAPAction: '.OIM_SEND_SOAP, + 'Content-Type: text/xml', + 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.BUILDVER.')' + ); - case 'ADL': - // randomly, we get ADL command, someone add us to their contact list for MSNP15 - // NS: <<< ADL 0 {size} - @list(/* ADL */, /* 0 */, $size,) = @explode(' ', $data); - if (is_numeric($size) && $size > 0) { - $data = $this->ns_readdata($size); - preg_match('##', $data, $matches); - if (is_array($matches) && count($matches) > 0) { - $u_domain = $matches[1]; - $u_name = $matches[2]; - $network = $matches[4]; - if (isset($this->aContactList[$u_domain][$u_name][$network])) - $this->debug_message("*** Someone (network: $network) added us to their list (but already in our list): $u_name@$u_domain"); - else { - $re_login = false; - $cnt = 0; - foreach (array('Allow', 'Reverse') as $list) { - if (!$this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) { - if ($re_login) { - $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list"); - continue; - } - $aTickets = $this->get_passport_ticket(); - if (!$aTickets || !is_array($aTickets)) { - // failed to login? ignore it - $this->debug_message("*** Could not re-login, something wrong here"); - $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list"); - continue; - } - $re_login = true; - $this->ticket = $aTickets; - $this->debug_message("**** Got new ticket, trying again"); - if (!$this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) { - $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list"); - continue; - } - } - $this->aContactList[$u_domain][$u_name][$network][$list] = false; - $cnt++; - } - $this->debug_message("*** Someone (network: $network) added us to their list: $u_name@$u_domain"); - } - $str = ''; - $len = strlen($str); + $this->debug_message('*** URL: '.OIM_SEND_URL); + $this->debug_message("*** Sending SOAP:\n$XML"); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, OIM_SEND_URL); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); + $data = curl_exec($curl); + $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + $this->debug_message("*** Get Result:\n$data"); - $this->callHandler('AddedToList', array('screenname' => $u_name.'@'.$u_domain, 'network' => $network)); - } - else - $this->debug_message("*** Someone added us to their list: $data"); - } - break; + if ($http_code == 200) { + $this->debug_message("*** OIM sent for $to"); + return true; + } - case 'RML': - // randomly, we get RML command, someome remove us to their contact list for MSNP15 - // NS: <<< RML 0 {size} - @list(/* RML */, /* 0 */, $size,) = @explode(' ', $data); - if (is_numeric($size) && $size > 0) { - $data = $this->ns_readdata($size); - preg_match('##', $data, $matches); - if (is_array($matches) && count($matches) > 0) { - $u_domain = $matches[1]; - $u_name = $matches[2]; - $network = $matches[4]; - if (isset($this->aContactList[$u_domain][$u_name][$network])) { - $aData = $this->aContactList[$u_domain][$u_name][$network]; + $challenge = false; + $auth_policy = false; + // the lockkey is invalid, authenticated fail, we need challenge it again + // 364763969 + preg_match("#(.*)#", $data, $matches); + if (count($matches) != 0) { + // yes, we get new LockKeyChallenge + $challenge = $matches[2]; + $this->debug_message("*** OIM need new challenge ($challenge) for $to"); + } + // auth policy error + // MBI_SSL + preg_match("#(.*)#", $data, $matches); + if (count($matches) != 0) { + $auth_policy = $matches[2]; + $this->debug_message("*** OIM need new auth policy ($auth_policy) for $to"); + } + if ($auth_policy === false && $challenge === false) { + //q0:AuthenticationFailed + preg_match("#(.*)#", $data, $matches); + if (count($matches) == 0) { + // no error, we assume the OIM is sent + $this->debug_message("*** OIM sent for $to"); + return true; + } + $err_code = $matches[2]; + //Exception of type 'System.Web.Services.Protocols.SoapException' was thrown. + preg_match("#(.*)#", $data, $matches); + if (count($matches) > 0) + $err_msg = $matches[1]; + else + $err_msg = ''; + $this->debug_message("*** OIM failed for $to"); + $this->debug_message("*** OIM Error code: $err_code"); + $this->debug_message("*** OIM Error Message: $err_msg"); + return false; + } + return array('challenge' => $challenge, 'auth_policy' => $auth_policy); + } + + /** + * Contact / Membership list methods + */ + + /** + * Fetch contact list + * + * @return boolean true on success + */ + private function UpdateContacts() { + $ABApplicationHeaderArray = array( + 'ABApplicationHeader' => array( + ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), + 'ApplicationId' => 'CFE80F9D-180F-4399-82AB-413F33A1FA11', + 'IsMigration' => false, + 'PartnerScenario' => 'ContactSave' + ) + ); - foreach ($aData as $list => $id) - $this->delMemberFromList($id, $u_name.'@'.$u_domain, $network, $list); + $ABApplicationHeader = new SoapHeader('http://www.msn.com/webservices/AddressBook', 'ABApplicationHeader', $this->Array2SoapVar($ABApplicationHeaderArray)); + $ABFindAllArray = array( + 'ABFindAll' => array( + ':' => array('xmlns'=>'http://www.msn.com/webservices/AddressBook'), + 'abId' => '00000000-0000-0000-0000-000000000000', + 'abView' => 'Full', + 'lastChange' => '0001-01-01T00:00:00.0000000-08:00', + ) + ); + $ABFindAll = new SoapParam($this->Array2SoapVar($ABFindAllArray), 'ABFindAll'); + $this->ABService->__setSoapHeaders(array($ABApplicationHeader, $this->ABAuthHeader)); + $this->Contacts = array(); + try { + $this->debug_message('*** Updating Contacts...'); + $Result = $this->ABService->ABFindAll($ABFindAll); + $this->debug_message("*** Result:\n".print_r($Result, true)."\n".$this->ABService->__getLastResponse()); + foreach($Result->ABFindAllResult->contacts->Contact as $Contact) + $this->Contacts[$Contact->contactInfo->passportName] = $Contact; + } catch(Exception $e) { + $this->debug_message("*** Update Contacts Error \nRequest:".$this->ABService->__getLastRequest()."\nError:".$e->getMessage()); + return false; + } + return true; + } - unset($this->aContactList[$u_domain][$u_name][$network]); - $this->debug_message("*** Someone (network: $network) removed us from their list: $u_name@$u_domain"); - } - else - $this->debug_message("*** Someone (network: $network) removed us from their list (but not in our list): $u_name@$u_domain"); + /** + * Add contact + * + * @param string $email + * @param integer $network + * @param string $display + * @param boolean $sendADL + * @return boolean true on success + */ + private function addContact($email, $network, $display = '', $sendADL = false) { + if ($network != 1) return true; + if (isset($this->Contacts[$email])) return true; - $this->callHandler('RemovedFromList', array('screenname' => $u_name.'@'.$u_domain, 'network' => $network)); - } - else - $this->debug_message("*** Someone removed us from their list: $data"); - } - break; + $ABContactAddArray = array( + 'ABContactAdd' => array( + ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), + 'abId' => '00000000-0000-0000-0000-000000000000', + 'contacts' => array( + 'Contact' => array( + ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), + 'contactInfo' => array( + 'contactType' => 'LivePending', + 'passportName' => $email, + 'isMessengerUser' => true, + 'MessengerMemberInfo' => array( + 'DisplayName' => $email + ) + ) + ) + ), + 'options' => array( + 'EnableAllowListManagement' => true + ) + ) + ); + $ABContactAdd = new SoapParam($this->Array2SoapVar($ABContactAddArray), 'ABContactAdd'); + try { + $this->debug_message("*** Adding Contact $email..."); + $this->ABService->ABContactAdd($ABContactAdd); + } catch(Exception $e) { + $this->debug_message("*** Add Contact Error \nRequest:".$this->ABService->__getLastRequest()."\nError:".$e->getMessage()); + return false; + } + if ($sendADL && !feof($this->NSfp)) { + @list($u_name, $u_domain) = @explode('@', $email); + foreach (array('1', '2') as $l) { + $str = ''; + $len = strlen($str); + // NS: >>> ADL {id} {size} + //TODO introduce error checking + $this->ns_writeln("ADL $this->id $len"); + $this->ns_writedata($str); + } + } + $this->UpdateContacts(); + return true; + } - case 'MSG': - // randomly, we get MSG notification from server - // NS: <<< MSG Hotmail Hotmail {size} - @list(/* MSG */, /* Hotmail */, /* Hotmail */, $size,) = @explode(' ', $data); - if (is_numeric($size) && $size > 0) { - $data = $this->ns_readdata($size); - $aLines = @explode("\n", $data); - $header = true; - $ignore = false; - $maildata = ''; - foreach ($aLines as $line) { - $line = rtrim($line); - if ($header) { - if ($line === '') { - $header = false; - continue; - } - if (strncasecmp($line, 'Content-Type:', 13) == 0) { - if (strpos($line, 'text/x-msmsgsinitialmdatanotification') === false && strpos($line, 'text/x-msmsgsoimnotification') === false) { - // we just need text/x-msmsgsinitialmdatanotification - // or text/x-msmsgsoimnotification - $ignore = true; - break; - } - } - continue; - } - if (strncasecmp($line, 'Mail-Data:', 10) == 0) { - $maildata = trim(substr($line, 10)); - break; - } - } - if ($ignore) { - $this->debug_message("*** Ignoring MSG for: $line"); - break; - } - if ($maildata == '') { - $this->debug_message("*** Ignoring MSG not for OIM"); - break; - } - $re_login = false; - if (strcasecmp($maildata, 'too-large') == 0) { - $this->debug_message("*** Large mail-data, need to get the data via SOAP"); - $maildata = $this->getOIM_maildata(); - if ($maildata === false) { - $this->debug_message("*** Could not get mail-data via SOAP"); + /** + * Remove contact from list + * + * @param integer $memberID + * @param string $email + * @param integer $network + * @param string $list + */ + function delMemberFromList($memberID, $email, $network, $list) { + if ($network != 1 && $network != 32) return true; + if ($memberID === false) return true; + $user = $email; + $ticket = htmlspecialchars($this->ticket['contact_ticket']); + if ($network == 1) + $XML = ' + + + + 996CDE1E-AA53-4477-B943-2BE802EA6166 + false + ContactMsgrAPI + + + false + '.$ticket.' + + + + + + 0 + Messenger + + + + + '.$list.' + + + Passport + '.$memberID.' + Accepted + + + + + + +'; + else + $XML = ' + + + + 996CDE1E-AA53-4477-B943-2BE802EA6166 + false + ContactMsgrAPI + + + false + '.$ticket.' + + + + + + 0 + Messenger + + + + + '.$list.' + + + Email + '.$memberID.' + Accepted + + + + + + +'; - // maybe we need to re-login again - $aTickets = $this->get_passport_ticket(); - if (!$aTickets || !is_array($aTickets)) { - // failed to login? ignore it - $this->debug_message("*** Could not re-login, something wrong here, ignoring this OIM"); - break; - } - $re_login = true; - $this->ticket = $aTickets; - $this->debug_message("*** Got new ticket, trying again"); - $maildata = $this->getOIM_maildata(); - if ($maildata === false) { - $this->debug_message("*** Could not get mail-data via SOAP, and re-login already attempted, ignoring this OIM"); - break; - } - } - } - // could be a lots of ..., so we can't use preg_match here - $p = $maildata; - $aOIMs = array(); - while (1) { - $start = strpos($p, ''); - $end = strpos($p, ''); - if ($start === false || $end === false || $start > $end) break; - $end += 4; - $sOIM = substr($p, $start, $end - $start); - $aOIMs[] = $sOIM; - $p = substr($p, $end); - } - if (count($aOIMs) == 0) { - $this->debug_message("*** Ignoring empty OIM"); - break; - } - foreach ($aOIMs as $maildata) { - // T: 11 for MSN, 13 for Yahoo - // S: 6 for MSN, 7 for Yahoo - // RT: the datetime received by server - // RS: already read or not - // SZ: size of message - // E: sender - // I: msgid - // F: always 00000000-0000-0000-0000-000000000009 - // N: sender alias - preg_match('#(.*)#', $maildata, $matches); - if (count($matches) == 0) { - $this->debug_message("*** Ignoring OIM maildata without type"); - continue; - } - $oim_type = $matches[1]; - if ($oim_type = 13) - $network = 32; - else - $network = 1; - preg_match('#(.*)#', $maildata, $matches); - if (count($matches) == 0) { - $this->debug_message("*** Ignoring OIM maildata without sender"); - continue; - } - $oim_sender = $matches[1]; - preg_match('#(.*)#', $maildata, $matches); - if (count($matches) == 0) { - $this->debug_message("*** Ignoring OIM maildata without msgid"); - continue; - } - $oim_msgid = $matches[1]; - preg_match('#(.*)#', $maildata, $matches); - $oim_size = (count($matches) == 0) ? 0 : $matches[1]; - preg_match('#(.*)#', $maildata, $matches); - $oim_time = (count($matches) == 0) ? 0 : $matches[1]; - $this->debug_message("*** OIM received from $oim_sender, Time: $oim_time, MSGID: $oim_msgid, size: $oim_size"); - $sMsg = $this->getOIM_message($oim_msgid); - if ($sMsg === false) { - $this->debug_message("*** Could not get OIM, msgid = $oim_msgid"); - if ($re_login) { - $this->debug_message("*** Could not get OIM via SOAP, and re-login already attempted, ignoring this OIM"); - continue; - } - $aTickets = $this->get_passport_ticket(); - if (!$aTickets || !is_array($aTickets)) { - // failed to login? ignore it - $this->debug_message("*** Could not re-login, something wrong here, ignoring this OIM"); - continue; - } - $re_login = true; - $this->ticket = $aTickets; - $this->debug_message("*** get new ticket, try it again"); - $sMsg = $this->getOIM_message($oim_msgid); - if ($sMsg === false) { - $this->debug_message("*** Could not get OIM via SOAP, and re-login already attempted, ignoring this OIM"); - continue; - } - } - $this->debug_message("*** MSG (Offline) from $oim_sender (network: $network): $sMsg"); - $this->callHandler('IMin', array('sender' => $oim_sender, 'message' => $sMsg, 'network' => $network, 'offline' => true)); - } - } - break; + $header_array = array( + 'SOAPAction: '.DELMEMBER_SOAP, + 'Content-Type: text/xml; charset=utf-8', + 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)' + ); - case 'UBM': - // randomly, we get UBM, this is the message from other network, like Yahoo! - // NS: <<< UBM {email} $network $type {size} - @list(/* UBM */, $from_email, $network, $type, $size,) = @explode(' ', $data); - if (is_numeric($size) && $size > 0) { - $data = $this->ns_readdata($size); - $aLines = @explode("\n", $data); - $header = true; - $ignore = false; - $sMsg = ''; - foreach ($aLines as $line) { - $line = rtrim($line); - if ($header) { - if ($line === '') { - $header = false; - continue; - } - if (strncasecmp($line, 'TypingUser:', 11) == 0) { - $ignore = true; - break; - } - continue; - } - $aSubLines = @explode("\r", $line); - foreach ($aSubLines as $str) { - if ($sMsg !== '') - $sMsg .= "\n"; - $sMsg .= $str; - } - } - if ($ignore) { - $this->debug_message("*** Ignoring message from $from_email: $line"); - break; - } - $this->debug_message("*** MSG from $from_email (network: $network): $sMsg"); - $this->callHandler('IMin', array('sender' => $from_email, 'message' => $sMsg, 'network' => $network, 'offline' => false)); - } - break; + $this->debug_message('*** URL: '.DELMEMBER_URL); + $this->debug_message("*** Sending SOAP:\n$XML"); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, DELMEMBER_URL); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); + $data = curl_exec($curl); + $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + $this->debug_message("*** Get Result:\n$data"); - case 'UBX': - // randomly, we get UBX notification from server - // NS: <<< UBX email {network} {size} - @list(/* UBX */, /* email */, /* network */, $size,) = @explode(' ', $data); - // we don't need the notification data, so just ignore it - if (is_numeric($size) && $size > 0) - $this->ns_readdata($size); - break; + if ($http_code != 200) { + preg_match('#(.*)(.*)#', $data, $matches); + if (count($matches) == 0) { + $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list"); + return false; + } + $faultcode = trim($matches[1]); + $faultstring = trim($matches[2]); + if (strcasecmp($faultcode, 'soap:Client') || stripos($faultstring, 'Member does not exist') === false) { + $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list, error code: $faultcode, $faultstring"); + return false; + } + $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list, not present in list"); + return true; + } + $this->debug_message("*** Member successfully deleted (network: $network) $email ($memberID) from $list list"); + return true; + } - case 'CHL': - // randomly, we'll get challenge from server - // NS: <<< CHL 0 {code} - @list(/* CHL */, /* 0 */, $chl_code,) = @explode(' ', $data); - $fingerprint = $this->getChallenge($chl_code); - // NS: >>> QRY {id} {product_id} 32 - // NS: >>> fingerprint - $this->ns_writeln("QRY $this->id $this->prod_id 32"); - $this->ns_writedata($fingerprint); - $this->ns_writeln("CHG $this->id NLN $this->clientid"); - if ($this->PhotoStickerFile !== false) - $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile))); - break; - case 'CHG': - // NS: <<< CHG {id} {status} {code} - // ignore it - // change our status to online first - break; + /** + * Add contact to list + * + * @param string $email + * @param integer $network + * @param string $list + */ + function addMemberToList($email, $network, $list) { + if ($network != 1 && $network != 32) return true; + $ticket = htmlspecialchars($this->ticket['contact_ticket']); + $user = $email; - case 'XFR': - // sometimes, NS will redirect to another NS - // MSNP9 - // NS: <<< XFR {id} NS {server} 0 {server} - // MSNP15 - // NS: <<< XFR {id} NS {server} U D - // for normal switchboard XFR - // NS: <<< XFR {id} SB {server} CKI {cki} U messenger.msn.com 0 - @list(/* XFR */, /* {id} */, $server_type, $server, /* CKI */, $cki_code, /* ... */) = @explode(' ', $data); - @list($ip, $port) = @explode(':', $server); - if ($server_type != 'SB') { - // maybe exit? - // this connection will close after XFR - $this->NSLogout(); - continue; - } + if ($network == 1) + $XML = ' + + + + 996CDE1E-AA53-4477-B943-2BE802EA6166 + false + ContactMsgrAPI + + + false + '.$ticket.' + + + + + + 0 + Messenger + + + + + '.$list.' + + + Passport + Accepted + '.$user.' + + + + + + +'; + else + $XML = ' + + + + 996CDE1E-AA53-4477-B943-2BE802EA6166 + false + ContactMsgrAPI + + + false + '.$ticket.' + + + + + + 0 + Messenger + + + + + '.$list.' + + + Email + Accepted + '.$user.' + + + MSN.IM.BuddyType + 32:YAHOO + + + + + + + + +'; + $header_array = array( + 'SOAPAction: '.ADDMEMBER_SOAP, + 'Content-Type: text/xml; charset=utf-8', + 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)' + ); - $this->debug_message("NS: <<< XFR SB"); - $user = array_shift($this->waitingForXFR); - $bSBresult = $this->switchboard_control($ip, $port, $cki_code, $User, $Message); - /* - $bSBresult = $this->switchboard_control($ip, $port, $cki_code, $aMSNUsers[$nCurrentUser], $sMessage); - if ($bSBresult === false) { - // error for switchboard - $this->debug_message("!!! error for sending message to ".$aMSNUsers[$nCurrentUser]); - $aOfflineUsers[] = $aMSNUsers[$nCurrentUser]; - }*/ - break; - case 'QNG': - // NS: <<< QNG {time} - @list(/* QNG */, $ping_wait) = @explode(' ', $data); - $this->callHandler('Pong', $ping_wait); - break; + $this->debug_message('*** URL: '.ADDMEMBER_URL); + $this->debug_message("*** Sending SOAP:\n$XML"); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, ADDMEMBER_URL); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); + $data = curl_exec($curl); + $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + $this->debug_message("*** Get Result:\n$data"); - case 'RNG': - if ($this->PhotoStickerFile !== false) - $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile))); - else - $this->ns_writeln("CHG $this->id NLN $this->clientid"); - // someone is trying to talk to us - // NS: <<< RNG {session_id} {server} {auth_type} {ticket} {email} {alias} U {client} 0 - $this->debug_message("NS: <<< RNG $data"); - @list(/* RNG */, $sid, $server, /* auth_type */, $ticket, $email, $name, ) = @explode(' ', $data); - @list($sb_ip, $sb_port) = @explode(':', $server); - $this->debug_message("*** RING from $email, $sb_ip:$sb_port"); - $this->addContact($email, 1, $email, true); - $this->connectToSBSession('Passive', $sb_ip, $sb_port, $email, array('sid' => $sid, 'ticket' => $ticket)); - break; - case 'OUT': - // force logout from NS - // NS: <<< OUT xxx - $this->debug_message("*** LOGOUT from NS"); - return $this->NsLogout(); + if ($http_code != 200) { + preg_match('#(.*)(.*)#', $data, $matches); + if (count($matches) == 0) { + $this->debug_message("*** Could not add member (network: $network) $email to $list list"); + return false; + } + $faultcode = trim($matches[1]); + $faultstring = trim($matches[2]); + if (strcasecmp($faultcode, 'soap:Client') || stripos($faultstring, 'Member already exists') === false) { + $this->debug_message("*** Could not add member (network: $network) $email to $list list, error code: $faultcode, $faultstring"); + return false; + } + $this->debug_message("*** Could not add member (network: $network) $email to $list list, already present"); + return true; + } + $this->debug_message("*** Member successfully added (network: $network) $email to $list list"); + return true; + } - default: - $code = substr($data,0,3); - if (is_numeric($code)) { - $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List"; - $this->debug_message("*** NS: $this->error"); + /** + * Get membership lists + * + * @param mixed $returnData Membership list or false on failure + */ + function getMembershipList($returnData = false) { + $ticket = htmlspecialchars($this->ticket['contact_ticket']); + $XML = ' + + + + 996CDE1E-AA53-4477-B943-2BE802EA6166 + false + Initial + + + false + '.$ticket.' + + + + + + + Messenger + Invitation + SocialNetwork + Space + Profile + + + + +'; + $header_array = array( + 'SOAPAction: '.MEMBERSHIP_SOAP, + 'Content-Type: text/xml; charset=utf-8', + 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)' + ); + $this->debug_message('*** URL: '.MEMBERSHIP_URL); + $this->debug_message("*** Sending SOAP:\n$XML"); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, MEMBERSHIP_URL); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); + $data = curl_exec($curl); + $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + $this->debug_message("*** Get Result:\n$data"); + + if ($http_code != 200) return false; + $p = $data; + $aMemberships = array(); + while (1) { + //$this->debug_message("search p = $p"); + $start = strpos($p, ''); + $end = strpos($p, ''); + if ($start === false || $end === false || $start > $end) break; + //$this->debug_message("start = $start, end = $end"); + $end += 13; + $sMembership = substr($p, $start, $end - $start); + $aMemberships[] = $sMembership; + //$this->debug_message("add sMembership = $sMembership"); + $p = substr($p, $end); + } + //$this->debug_message("aMemberships = ".var_export($aMemberships, true)); - return $this->NsLogout(); - } - break; + $aContactList = array(); + foreach ($aMemberships as $sMembership) { + //$this->debug_message("sMembership = $sMembership"); + if (isset($matches)) unset($matches); + preg_match('#(.*)#', $sMembership, $matches); + if (count($matches) == 0) continue; + $sMemberRole = $matches[1]; + //$this->debug_message("MemberRole = $sMemberRole"); + if ($sMemberRole != 'Allow' && $sMemberRole != 'Reverse' && $sMemberRole != 'Pending') continue; + $p = $sMembership; + if (isset($aMembers)) unset($aMembers); + $aMembers = array(); + while (1) { + //$this->debug_message("search p = $p"); + $start = strpos($p, 'debug_message("add sMember = $sMember"); + $p = substr($p, $end); + } + //$this->debug_message("aMembers = ".var_export($aMembers, true)); + foreach ($aMembers as $sMember) { + //$this->debug_message("sMember = $sMember"); + if (isset($matches)) unset($matches); + preg_match('##', $sMember, $matches); + if (count($matches) == 0) continue; + $sMemberType = $matches[1]; + //$this->debug_message("MemberType = $sMemberType"); + $network = -1; + preg_match('#(.*)#', $sMember, $matches); + if (count($matches) == 0) continue; + $id = $matches[1]; + if ($sMemberType == 'PassportMember') { + if (strpos($sMember, 'Passport') === false) continue; + $network = 1; + preg_match('#(.*)#', $sMember, $matches); + } + else if ($sMemberType == 'EmailMember') { + if (strpos($sMember, 'Email') === false) continue; + // Value is 32: or 32:YAHOO + preg_match('#MSN.IM.BuddyType(.*):(.*)#', $sMember, $matches); + if (count($matches) == 0) continue; + if ($matches[1] != 32) continue; + $network = 32; + preg_match('#(.*)#', $sMember, $matches); + } + if ($network == -1) continue; + if (count($matches) > 0) { + $email = $matches[1]; + @list($u_name, $u_domain) = @explode('@', $email); + if ($u_domain == NULL) continue; + $aContactList[$u_domain][$u_name][$network][$sMemberRole] = $id; + $this->debug_message("*** Adding new contact (network: $network, status: $sMemberRole): $u_name@$u_domain ($id)"); + } } } + return $aContactList; } - + /** - * Read and handle incoming command/message from - * a switchboard session socket + * MsnObj related methods */ - private function sbReceive($socket) { - $intsocket = (int) $socket; - $session = &$this->switchBoardSessions[$intsocket]; - - if (feof($socket)) { - // Unset session lookup value - unset($this->switchBoardSessionLookup[$session['to']]); - - // Unset session itself - unset($this->switchBoardSessions[$intsocket]); - return; - } - - $id = &$session['id']; - - $data = $this->sb_readln($socket); - $code = substr($data, 0, 3); - switch($code) { - case 'IRO': - // SB: <<< IRO {id} {rooster} {roostercount} {email} {alias} {clientid} - @list(/* IRO */, /* id */, $cur_num, $total, $email, $alias, $clientid) = @explode(' ', $data); - $this->debug_message("*** $email joined session"); - $session['joined'] = true; - break; - case 'BYE': - $this->debug_message("*** Quit for BYE"); - $this->endSBSession(); - break; - case 'USR': - // SB: <<< USR {id} OK {user} {alias} - // we don't need the data, just ignore it - // request user to join this switchboard - // SB: >>> CAL {id} {user} - $this->sb_writeln($socket, $id, "CAL $this->id $user"); - break; - case 'CAL': - // SB: <<< CAL {id} RINGING {?} - // we don't need this, just ignore, and wait for other response - $session['id']++; - break; - case 'JOI': - // SB: <<< JOI {user} {alias} {clientid?} - // someone join us - // we don't need the data, just ignore it - // no more user here - $session['joined'] = true; - break; - case 'MSG': - // SB: <<< MSG {email} {alias} {len} - @list(/* MSG */, $from_email, /* alias */, $len, ) = @explode(' ', $data); - $len = trim($len); - $data = $this->sb_readdata($socket, $len); - $aLines = @explode("\n", $data); - $header = true; - $ignore = false; - $is_p2p = false; - $sMsg = ''; - foreach ($aLines as $line) { - $line = rtrim($line); - if ($header) { - if ($line === '') { - $header = false; - continue; - } - if (strncasecmp($line, 'TypingUser:', 11) == 0) { - // typing notification, just ignore - $ignore = true; - break; - } - if (strncasecmp($line, 'Chunk:', 6) == 0) { - // we don't handle any split message, just ignore - $ignore = true; - break; - } - if (strncasecmp($line, 'Content-Type: application/x-msnmsgrp2p', 38) == 0) { - // p2p message, ignore it, but we need to send acknowledgement for it... - $is_p2p = true; - $p = strstr($data, "\n\n"); - $sMsg = ''; - if ($p === false) { - $p = strstr($data, "\r\n\r\n"); - if ($p !== false) - $sMsg = substr($p, 4); - } - else - $sMsg = substr($p, 2); - break; - } - if (strncasecmp($line, 'Content-Type: application/x-', 28) == 0) { - // ignore all application/x-... message - // for example: - // application/x-ms-ink => ink message - $ignore = true; - break; - } - if (strncasecmp($line, 'Content-Type: text/x-', 21) == 0) { - // ignore all text/x-... message - // for example: - // text/x-msnmsgr-datacast => nudge, voice clip.... - // text/x-mms-animemoticon => customized animemotion word - $ignore = true; - break; - } - continue; - } - if ($sMsg !== '') - $sMsg .= "\n"; - $sMsg .= $line; - } - if ($ignore) { - $this->debug_message("*** Ignoring SB data from $from_email: $line"); - break; - } - if ($is_p2p) { - // we will ignore any p2p message after sending acknowledgement - $ignore = true; - $len = strlen($sMsg); - $this->debug_message("*** p2p message from $from_email, size $len"); - // header = 48 bytes - // content >= 0 bytes - // footer = 4 bytes - // so it need to >= 52 bytes - /*if ($len < 52) { - $this->debug_message("*** p2p: size error, less than 52!"); - break; - }*/ - $aDwords = @unpack("V12dword", $sMsg); - if (!is_array($aDwords)) { - $this->debug_message("*** p2p: header unpack error!"); - break; - } - $this->debug_message("*** p2p: dump received message:\n".$this->dump_binary($sMsg)); - $hdr_SessionID = $aDwords['dword1']; - $hdr_Identifier = $aDwords['dword2']; - $hdr_DataOffsetLow = $aDwords['dword3']; - $hdr_DataOffsetHigh = $aDwords['dword4']; - $hdr_TotalDataSizeLow = $aDwords['dword5']; - $hdr_TotalDataSizeHigh = $aDwords['dword6']; - $hdr_MessageLength = $aDwords['dword7']; - $hdr_Flag = $aDwords['dword8']; - $hdr_AckID = $aDwords['dword9']; - $hdr_AckUID = $aDwords['dword10']; - $hdr_AckSizeLow = $aDwords['dword11']; - $hdr_AckSizeHigh = $aDwords['dword12']; - $this->debug_message("*** p2p: header SessionID = $hdr_SessionID"); - $this->debug_message("*** p2p: header Inentifier = $hdr_Identifier"); - $this->debug_message("*** p2p: header Data Offset Low = $hdr_DataOffsetLow"); - $this->debug_message("*** p2p: header Data Offset High = $hdr_DataOffsetHigh"); - $this->debug_message("*** p2p: header Total Data Size Low = $hdr_TotalDataSizeLow"); - $this->debug_message("*** p2p: header Total Data Size High = $hdr_TotalDataSizeHigh"); - $this->debug_message("*** p2p: header MessageLength = $hdr_MessageLength"); - $this->debug_message("*** p2p: header Flag = $hdr_Flag"); - $this->debug_message("*** p2p: header AckID = $hdr_AckID"); - $this->debug_message("*** p2p: header AckUID = $hdr_AckUID"); - $this->debug_message("*** p2p: header AckSize Low = $hdr_AckSizeLow"); - $this->debug_message("*** p2p: header AckSize High = $hdr_AckSizeHigh"); - if ($hdr_Flag == 2) { - //This is an ACK from SB ignore.... - $this->debug_message("*** p2p: //This is an ACK from SB ignore....:\n"); - break; - } - $MsgBody = $this->linetoArray(substr($sMsg, 48, -4)); - $this->debug_message("*** p2p: body".print_r($MsgBody, true)); - if (($MsgBody['EUF-GUID']=='{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}')&&($PictureFilePath=$this->GetPictureFilePath($MsgBody['Context']))) { - while (true) { - if ($this->sb_readln($socket) === false) break; - } - $this->debug_message("*** p2p: Inv hdr:\n".$this->dump_binary(substr($sMsg, 0, 48))); - preg_match('/{([0-9A-F\-]*)}/i', $MsgBody['Via'], $Matches); - $BranchGUID = $Matches[1]; - //it's an invite to send a display picture. - $new_id = ~$hdr_Identifier; - $hdr = pack( - "LLLLLLLLLLLL", $hdr_SessionID, - $new_id, - 0, 0, - $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, - 0, - 2, - $hdr_Identifier, - $hdr_AckID, - $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh - ); - $footer = pack("L", 0); - $message = "MIME-Version: 1.0\r\nContent-Type: application/x-msnmsgrp2p\r\nP2P-Dest: $from_email\r\n\r\n$hdr$footer"; - $len = strlen($message); - $this->sb_writeln($socket, $id, "MSG $this->id D $len"); - $this->sb_writedata($socket, $message); - $this->debug_message("*** p2p: send display picture acknowledgement for $hdr_SessionID"); - $this->debug_message("*** p2p: Invite ACK message:\n".$this->dump_binary($message)); - $this->sb_readln($socket); // Read ACK; - $this->debug_message("*** p2p: Invite ACK Hdr:\n".$this->dump_binary($hdr)); - $new_id -= 3; - //Send 200 OK message - $MessageContent="SessionID: ".$MsgBody['SessionID']."\r\n\r\n".pack("C", 0); - $MessagePayload= - "MSNSLP/1.0 200 OK\r\n". - "To: \r\n". - "From: user.">\r\n". - "Via: ".$MsgBody['Via']."\r\n". - "CSeq: ".($MsgBody['CSeq']+1)."\r\n". - "Call-ID: ".$MsgBody['Call-ID']."\r\n". - "Max-Forwards: 0\r\n". - "Content-Type: application/x-msnmsgr-sessionreqbody\r\n". - "Content-Length: ".strlen($MessageContent)."\r\n\r\n". - $MessageContent; - $hdr_TotalDataSizeLow=strlen($MessagePayload); - $hdr_TotalDataSizeHigh=0; - $hdr = pack( - "LLLLLLLLLLLL", $hdr_SessionID, - $new_id, - 0, 0, - $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, - strlen($MessagePayload), - 0, - rand(), - 0, - 0, 0 - ); + + /** + * + * @param $FilePath 圖檔路徑 + * @param $Type 檔案類型 3=>大頭貼,2表情圖案 + * @return array + */ + private function MsnObj($FilePath, $Type = 3) { + if (!($FileSize=filesize($FilePath))) return ''; + $Location = md5($FilePath); + $Friendly = md5($FilePath.$Type); + if (isset($this->MsnObjMap[$Location])) return $this->MsnObjMap[$Location]; + $sha1d = base64_encode(sha1(file_get_contents($FilePath), true)); + $sha1c = base64_encode(sha1("Creator".$this->user."Size$FileSize"."Type$Type"."Location$Location"."Friendly".$Friendly."SHA1D$sha1d", true)); + $this->MsnObjArray[$Location] = $FilePath; + $MsnObj = ''; + $this->MsnObjMap[$Location] = $MsnObj; + $this->debug_message("*** p2p: addMsnObj $FilePath::$MsnObj\n"); + return $MsnObj; + } - $message = - "MIME-Version: 1.0\r\n". - "Content-Type: application/x-msnmsgrp2p\r\n". - "P2P-Dest: $from_email\r\n\r\n$hdr$MessagePayload$footer"; - $this->sb_writeln($socket, $id, "MSG $this->id D ".strlen($message)); - $this->sb_writedata($socket, $message); - $this->debug_message("*** p2p: dump 200 ok message:\n".$this->dump_binary($message)); - $this->sb_readln($socket); // Read ACK; + private function GetPictureFilePath($Context) { + $MsnObj = base64_decode($Context); + if (preg_match('/location="(.*?)"/i', $MsnObj, $Match)) + $location = $Match[1]; + $this->debug_message("*** p2p: PictureFile[$location] ::All".print_r($this->MsnObjArray,true)."\n"); + if ($location && isset($this->MsnObjArray[$location])) + return $this->MsnObjArray[$location]; + return false; + } - $this->debug_message("*** p2p: 200 ok:\n".$this->dump_binary($hdr)); - // send data preparation message - // send 4 null bytes as data - $hdr_TotalDataSizeLow = 4; - $hdr_TotalDataSizeHigh = 0 ; - $new_id++; - $hdr = pack( - "LLLLLLLLLLLL", - $MsgBody['SessionID'], - $new_id, - 0, 0, - $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, - $hdr_TotalDataSizeLow, - 0, - rand(), - 0, - 0, 0 - ); - $message = - "MIME-Version: 1.0\r\n". - "Content-Type: application/x-msnmsgrp2p\r\n". - "P2P-Dest: $from_email\r\n\r\n$hdr".pack('L', 0)."$footer"; - $this->sb_writeln($socket, $id, "MSG $this->id D ".strlen($message)); - $this->sb_writedata($socket, $message); - $this->debug_message("*** p2p: dump send Data preparation message:\n".$this->dump_binary($message)); - $this->debug_message("*** p2p: Data Prepare Hdr:\n".$this->dump_binary($hdr)); - $this->sb_readln($socket); // Read ACK; + private function GetMsnObjDefine($Message) { + $DefineString = ''; + if (is_array($this->Emotions)) + foreach ($this->Emotions as $Pattern => $FilePath) { + if (strpos($Message, $Pattern) !== false) + $DefineString .= "$Pattern\t".$this->MsnObj($FilePath, 2)."\t"; + } + return $DefineString; + } + + /** + * Socket methods + */ + + /** + * Read data of specified size from NS socket + * + * @param integer $size Size to read + * @return string Data read + */ + private function ns_readdata($size) { + $data = ''; + $count = 0; + while (!feof($this->NSfp)) { + $buf = @fread($this->NSfp, $size - $count); + $data .= $buf; + $count += strlen($buf); + if ($count >= $size) break; + } + $this->debug_message("NS: data ($size/$count) <<<\n$data"); + return $data; + } - // send Data Content.. - $footer=pack('N',1); - $new_id++; - $FileSize=filesize($PictureFilePath); - if ($hTitle=fopen($PictureFilePath,'rb')) { - $Offset = 0; - //$new_id++; - while (!feof($hTitle)) { - $FileContent = fread($hTitle, 1024); - $FileContentSize = strlen($FileContent); - $hdr = pack( - "LLLLLLLLLLLL", - $MsgBody['SessionID'], - $new_id, - $Offset, 0, - $FileSize, 0, - $FileContentSize, - 0x20, - rand(), - 0, - 0, 0 - ); - $message = - "MIME-Version: 1.0\r\n". - "Content-Type: application/x-msnmsgrp2p\r\n". - "P2P-Dest: $from_email\r\n\r\n$hdr$FileContent$footer"; - $this->sb_writeln($socket, $id, "MSG $this->id D ".strlen($message)); - $this->sb_writedata($socket, $message); - $this->debug_message("*** p2p: dump send Data Content message $Offset / $FileSize :\n".$this->dump_binary($message)); - $this->debug_message("*** p2p: Data Content Hdr:\n".$this->dump_binary($hdr)); - //$this->SB_readln($socket);//Read ACK; - $Offset += $FileContentSize; - } - } - //Send Bye - /* - $MessageContent="\r\n".pack("C", 0); - $MessagePayload= - "BYE MSNMSGR:MSNSLP/1.0\r\n". - "To: \r\n". - "From: user.">\r\n". - "Via: MSNSLP/1.0/TLP ;branch={".$BranchGUID."}\r\n". - "CSeq: 0\r\n". - "Call-ID: ".$MsgBody['Call-ID']."\r\n". - "Max-Forwards: 0\r\n". - "Content-Type: application/x-msnmsgr-sessionclosebody\r\n". - "Content-Length: ".strlen($MessageContent)."\r\n\r\n".$MessageContent; - $footer=pack('N',0); - $hdr_TotalDataSizeLow=strlen($MessagePayload); - $hdr_TotalDataSizeHigh=0; - $new_id++; - $hdr = pack("LLLLLLLLLLLL", - 0, - $new_id, - 0, 0, - $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, - 0, - 0, - rand(), - 0, - 0,0); - $message = - "MIME-Version: 1.0\r\n". - "Content-Type: application/x-msnmsgrp2p\r\n". - "P2P-Dest: $from_email\r\n\r\n$hdr$MessagePayload$footer"; - $this->sb_writeln($socket, $id, "MSG $id D ".strlen($message)); - $id++; - $this->sb_writedata($socket, $message); - $this->debug_message("*** p2p: dump send BYE message :\n".$this->dump_binary($message)); - */ - break; - } - //TODO: - //if ($hdr_Flag == 2) { - // just send ACK... - // $this->sb_writeln($socket, $id, "ACK $id"); - // break; - //} - if ($hdr_SessionID == 4) { - // ignore? - $this->debug_message("*** p2p: ignore flag 4"); - break; - } - $finished = false; - if ($hdr_TotalDataSizeHigh == 0) { - // only 32 bites size - if (($hdr_MessageLength + $hdr_DataOffsetLow) == $hdr_TotalDataSizeLow) - $finished = true; - } - else { - // we won't accept any file transfer - // so I think we won't get any message size need to use 64 bits - // 64 bits size here, can't count directly... - $totalsize = base_convert(sprintf("%X%08X", $hdr_TotalDataSizeHigh, $hdr_TotalDataSizeLow), 16, 10); - $dataoffset = base_convert(sprintf("%X%08X", $hdr_DataOffsetHigh, $hdr_DataOffsetLow), 16, 10); - $messagelength = base_convert(sprintf("%X", $hdr_MessageLength), 16, 10); - $now_size = bcadd($dataoffset, $messagelength); - if (bccomp($now_size, $totalsize) >= 0) - $finished = true; - } - if (!$finished) { - // ignore not finished split packet - $this->debug_message("*** p2p: ignore split packet, not finished"); - break; - } - //$new_id = ~$hdr_Identifier; - /* - $new_id++; - $hdr = pack("LLLLLLLLLLLL", $hdr_SessionID, - $new_id, - 0, 0, - $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh, - 0, - 2, - $hdr_Identifier, - $hdr_AckID, - $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh); - $footer = pack("L", 0); - $message = "MIME-Version: 1.0\r\nContent-Type: application/x-msnmsgrp2p\r\nP2P-Dest: $from_email\r\n\r\n$hdr$footer"; - $len = strlen($message); - $this->sb_writeln($socket, $id, "MSG $id D $len"); - $id++; - $this->sb_writedata($socket, $message); - $this->debug_message("*** p2p: send acknowledgement for $hdr_SessionID"); - $this->debug_message("*** p2p: dump sent message:\n".$this->dump_binary($hdr.$footer)); - */ - break; - } - $this->debug_message("*** MSG from $from_email: $sMsg"); - $this->callHandler('IMin', array('sender' => $from_email, 'message' => $sMsg, 'network' => $network, 'offline' => false)); - break; - case '217': - $this->debug_message("*** User $user is offline. Trying OIM."); - $session['offline'] = true; - break; - default: - if (is_numeric($code)) { - $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List"; - $this->debug_message("*** SB: $this->error"); - $sessionEnd=true; - } - break; + /** + * Read line from the NS socket + * + * @return string Data read + */ + private function ns_readln() { + $data = @fgets($this->NSfp, 4096); + if ($data !== false) { + $data = trim($data); + $this->debug_message("NS: <<< $data"); + } + return $data; + } + + /** + * Write line to NS socket + * + * Also increments id + * + * @param string $data Line to write to socket + * @return void + */ + private function ns_writeln($data) { + @fwrite($this->NSfp, $data."\r\n"); + $this->debug_message("NS: >>> $data"); + $this->id++; + } + + /** + * Write data to NS socket + * + * @param string $data Data to write to socket + * @return void + */ + private function ns_writedata($data) { + @fwrite($this->NSfp, $data); + $this->debug_message("NS: >>> $data"); + } + + /** + * Read data of specified size from given SB socket + * + * @param resource $socket SB socket + * @param integer $size Size to read + * @return string Data read + */ + private function sb_readdata($socket, $size) { + $data = ''; + $count = 0; + while (!feof($socket)) { + $buf = @fread($socket, $size - $count); + $data .= $buf; + $count += strlen($buf); + if ($count >= $size) break; } + $this->debug_message("SB: data ($size/$count) <<<\n$data"); + return $data; } /** - * Called when we want to end a switchboard session - * or a switchboard session ends - * - * @param resource $socket Socket - * @param boolean $killsession Whether to delete the session - * @return void - */ - private function endSBSession($socket, $killsession = false) { - if (!self::socketcheck($socket)) { - $this->sb_writeln($socket, $fake = 0, 'OUT'); + * Read line from given SB socket + * + * @param resource $socket SB Socket + * @return string Line read + */ + private function sb_readln($socket) { + $data = @fgets($socket, 4096); + if ($data !== false) { + $data = trim($data); + $this->debug_message("SB: <<< $data"); } - @fclose($socket); - - // Unset session lookup value - $intsocket = (int) $socket; - unset($this->switchBoardSessionLookup[$this->switchBoardSessions[$intsocket]['to']]); - - // Unset session itself - unset($this->switchBoardSessions[$intsocket]); + return $data; } /** - * Checks for new data and calls appropriate methods - * - * This method is usually called in an infinite loop to keep checking for new data - * + * Write line to given SB socket + * + * Also increments id + * + * @param resource $socket SB socket + * @param integer $id Reference to SB id + * @param string $data Line to write * @return void */ - public function receive() { - // First, get an array of sockets that have data that is ready to be read - $ready = array(); - $ready = $this->getSockets(); - $numrdy = stream_select($ready, $w = NULL, $x = NULL, NULL); - - // Now that we've waited for something, go through the $ready - // array and read appropriately + private function sb_writeln($socket, &$id, $data) { + @fwrite($socket, $data."\r\n"); + $this->debug_message("SB: >>> $data"); + $id++; + } - foreach ($ready as $socket) { - if ($socket == $this->NSfp) { - $this->nsReceive(); - } else { - $this->sbReceive($socket); - } - } + /** + * Write data to given SB socket + * + * @param resource $socket SB socket + * @param $data Data to write to socket + * @return void + */ + private function sb_writedata($socket, $data) { + @fwrite($socket, $data); + $this->debug_message("SB: >>> $data"); } /** - * Send a request for a switchboard session + * Get all the sockets currently in use * - * @param string $to Target email for switchboard session + * @return array Array of socket resources */ - private function reqSBSession($to) { - $this->debug_message("*** Request SB for $to"); - $this->ns_writeln("XFR $this->id SB"); - - // Add to the queue of those waiting for a switchboard session reponse - $this->switchBoardSessions[$to] = array( - 'to' => $to, - 'socket' => NULL, - 'id' => 1, - 'joined' => false, - 'offline' => false, - 'XFRReqTime' => time() - ); - $this->waitingForXFR[] = &$this->switchBoardSessions[$to]; + public function getSockets() { + return array_merge(array($this->NSfp), $this->switchBoardSessionLookup); } /** - * Following an XFR or RNG, connect to the switchboard session + * Checks socket for end of file * - * @param string $mode Mode, either 'Active' (in the case of XFR) or 'Passive' (in the case of RNG) - * @param string $ip IP of Switchboard - * @param integer $port Port of Switchboard - * @param string $to User on other end of Switchboard - * @param array $param Array of parameters - 'cki', 'ticket', 'sid' - * @return boolean true if successful + * @param resource $socket Socket to check + * @return boolean true if end of file (socket) */ - private function connectToSBSession($mode, $ip, $port, $to, $param) { - $this->debug_message("*** SB: Trying to connect to switchboard server $ip:$port"); + private static function socketcheck($socket){ + $info = stream_get_meta_data($socket); + return $info['eof']; + } + + /** + * Key generation methods + */ + + private function derive_key($key, $magic) { + $hash1 = mhash(MHASH_SHA1, $magic, $key); + $hash2 = mhash(MHASH_SHA1, $hash1.$magic, $key); + $hash3 = mhash(MHASH_SHA1, $hash1, $key); + $hash4 = mhash(MHASH_SHA1, $hash3.$magic, $key); + return $hash2.substr($hash4, 0, 4); + } - $socket = @fsockopen($ip, $port, $errno, $errstr, $this->timeout); - if (!$socket) { - $this->debug_message("*** SB: Can't connect to $ip:$port, error => $errno, $errstr"); - return false; - } + private function generateLoginBLOB($key, $challenge) { + $key1 = base64_decode($key); + $key2 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY HASH'); + $key3 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY ENCRYPTION'); - // Store the socket in the lookup array - $this->switchBoardSessionLookup[$to] = $socket; + // get hash of challenge using key2 + $hash = mhash(MHASH_SHA1, $challenge, $key2); - // Store the socket in the sessions array - $intsocket = (int) $socket; - $this->switchBoardSessions[$to] = array( - 'to' => $to, - 'socket' => $socket, - 'id' => 1, - 'joined' => false, - 'offline' => false, - 'XFRReqTime' => time() - ); + // get 8 bytes random data + $iv = substr(base64_encode(rand(1000,9999).rand(1000,9999)), 2, 8); - // Change the index of the session to the socket - $this->switchBoardSessions[$intsocket] = $this->switchBoardSessions[$to]; - unset($this->switchBoardSessions[$to]); + $cipher = mcrypt_cbc(MCRYPT_3DES, $key3, $challenge."\x08\x08\x08\x08\x08\x08\x08\x08", MCRYPT_ENCRYPT, $iv); - $id = &$this->switchBoardSessions[$intsocket]['id']; + $blob = pack('LLLLLLL', 28, 1, 0x6603, 0x8004, 8, 20, 72); + $blob .= $iv; + $blob .= $hash; + $blob .= $cipher; - if ($mode == 'Active') { - $cki_code = $param['cki']; + return base64_encode($blob); + } + + /** + * Generate challenge response + * + * @param string $code + * @return string challenge response code + */ + private function getChallenge($code) { + // MSNP15 + // http://msnpiki.msnfanatic.com/index.php/MSNP11:Challenges + // Step 1: The MD5 Hash + $md5Hash = md5($code.PROD_KEY); + $aMD5 = @explode("\0", chunk_split($md5Hash, 8, "\0")); + for ($i = 0; $i < 4; $i++) { + $aMD5[$i] = implode('', array_reverse(@explode("\0", chunk_split($aMD5[$i], 2, "\0")))); + $aMD5[$i] = (0 + base_convert($aMD5[$i], 16, 10)) & 0x7FFFFFFF; + } - // SB: >>> USR {id} {user} {cki} - $this->sb_writeln($socket, $id, "USR $id $this->user $cki_code"); - } else { - // Passive - $ticket = $param['ticket']; - $sid = $param['sid']; + // Step 2: A new string + $chl_id = $code.PROD_ID; + $chl_id .= str_repeat('0', 8 - (strlen($chl_id) % 8)); - // SB: >>> ANS {id} {user} {ticket} {session_id} - $this->sb_writeln($socket, $id, "ANS $id $this->user $ticket $sid"); + $aID = @explode("\0", substr(chunk_split($chl_id, 4, "\0"), 0, -1)); + for ($i = 0; $i < count($aID); $i++) { + $aID[$i] = implode('', array_reverse(@explode("\0", chunk_split($aID[$i], 1, "\0")))); + $aID[$i] = 0 + base_convert(bin2hex($aID[$i]), 16, 10); } - } - /** - * Send a message via an existing SB session - * - * @param string $to Recipient for message - * @param string $message Message - * @return boolean true on success - */ - private function sendMessageViaSB($to, $message) { - $socket = $this->switchBoardSessionLookup[$to]; - if (self::socketcheck($socket)) { - return false; - } + // Step 3: The 64 bit key + $magic_num = 0x0E79A9C1; + $str7f = 0x7FFFFFFF; + $high = 0; + $low = 0; + for ($i = 0; $i < count($aID); $i += 2) { + $temp = $aID[$i]; + $temp = bcmod(bcmul($magic_num, $temp), $str7f); + $temp = bcadd($temp, $high); + $temp = bcadd(bcmul($aMD5[0], $temp), $aMD5[1]); + $temp = bcmod($temp, $str7f); - if (!$this->switchBoardSessions[$to]['joined']) { - // If our participant has not joined the session yet we can't message them! - return false; + $high = $aID[$i+1]; + $high = bcmod(bcadd($high, $temp), $str7f); + $high = bcadd(bcmul($aMD5[2], $high), $aMD5[3]); + $high = bcmod($high, $str7f); + + $low = bcadd(bcadd($low, $high), $temp); } - $intsocket = (int) $socket; - $id = &$this->switchBoardSessions[$intsocket]['id']; + $high = bcmod(bcadd($high, $aMD5[1]), $str7f); + $low = bcmod(bcadd($low, $aMD5[3]), $str7f); - $aMessage = $this->getMessage($Message); - //CheckEmotion... - $MsnObjDefine=$this->GetMsnObjDefine($aMessage); - if ($MsnObjDefine !== '') { - $SendString="MIME-Version: 1.0\r\nContent-Type: text/x-mms-emoticon\r\n\r\n$MsnObjDefine"; - $len = strlen($SendString); - // TODO handle failure during write to socket - $this->sb_writeln($socket, $id, "MSG $id N $len"); - $this->sb_writedata($socket, $SendString); + $new_high = bcmul($high & 0xFF, 0x1000000); + $new_high = bcadd($new_high, bcmul($high & 0xFF00, 0x100)); + $new_high = bcadd($new_high, bcdiv($high & 0xFF0000, 0x100)); + $new_high = bcadd($new_high, bcdiv($high & 0xFF000000, 0x1000000)); + // we need integer here + $high = 0+$new_high; + + $new_low = bcmul($low & 0xFF, 0x1000000); + $new_low = bcadd($new_low, bcmul($low & 0xFF00, 0x100)); + $new_low = bcadd($new_low, bcdiv($low & 0xFF0000, 0x100)); + $new_low = bcadd($new_low, bcdiv($low & 0xFF000000, 0x1000000)); + // we need integer here + $low = 0+$new_low; + + // we just use 32 bits integer, don't need the key, just high/low + // $key = bcadd(bcmul($high, 0x100000000), $low); + + // Step 4: Using the key + $md5Hash = md5($code.PROD_KEY); + $aHash = @explode("\0", chunk_split($md5Hash, 8, "\0")); + + $hash = ''; + $hash .= sprintf("%08x", (0 + base_convert($aHash[0], 16, 10)) ^ $high); + $hash .= sprintf("%08x", (0 + base_convert($aHash[1], 16, 10)) ^ $low); + $hash .= sprintf("%08x", (0 + base_convert($aHash[2], 16, 10)) ^ $high); + $hash .= sprintf("%08x", (0 + base_convert($aHash[3], 16, 10)) ^ $low); + + return $hash; + } + + /** + * Utility methods + */ + + private function Array2SoapVar($Array, $ReturnSoapVarObj = true, $TypeName = null, $TypeNameSpace = null) { + $ArrayString = ''; + foreach($Array as $Key => $Val) { + if ($Key{0} == ':') continue; + $Attrib = ''; + if (is_array($Val[':'])) { + foreach ($Val[':'] as $AttribName => $AttribVal) + $Attrib .= " $AttribName = '$AttribVal'"; + } + if ($Key{0} == '!') { + //List Type Define + $Key = substr($Key,1); + foreach ($Val as $ListKey => $ListVal) { + if ($ListKey{0} == ':') continue; + if (is_array($ListVal)) $ListVal = $this->Array2SoapVar($ListVal, false); + elseif (is_bool($ListVal)) $ListVal = $ListVal ? 'true' : 'false'; + $ArrayString .= "<$Key$Attrib>$ListVal"; + } + continue; + } + if (is_array($Val)) $Val = $this->Array2SoapVar($Val, false); + elseif (is_bool($Val)) $Val = $Val ? 'true' : 'false'; + $ArrayString .= "<$Key$Attrib>$Val"; } - $len = strlen($aMessage); - // TODO handle failure during write to socket - $this->sb_writeln($socket, $id, "MSG $id N $len"); - $this->sb_writedata($socket, $aMessage); - - // Don't close the SB session, we might as well leave it open - - return true; + if ($ReturnSoapVarObj) return new SoapVar($ArrayString, XSD_ANYXML, $TypeName, $TypeNameSpace); + return $ArrayString; } - + + private function linetoArray($lines) { + $lines = str_replace("\r", '', $lines); + $lines = explode("\n", $lines); + foreach ($lines as $line) { + if (!isset($line{3})) continue; + list($Key, $Val) = explode(':', $line); + $Data[trim($Key)] = trim($Val); + } + return $Data; + } + /** - * Send offline message - * - * @param string $to Intended recipient - * @param string $sMessage Message - * @param string $lockkey Lock key - * @return mixed true on success or error data - */ - private function sendOIM($to, $sMessage, $lockkey) { - $XML = ' - - - - - - - http://messenger.msn.com - 1 - - - - text - MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: base64 -X-OIM-Message-Type: OfflineMessage -X-OIM-Run-Id: {DAB68CFA-38C9-449B-945E-38AFA51E50A7} -X-OIM-Sequence-Num: 1 + * Get Passport ticket + * + * @param string $url URL string (Optional) + * @return mixed Array of tickets or false on failure + */ + private function get_passport_ticket($url = '') { + $user = $this->user; + $password = htmlspecialchars($this->password); -'.chunk_split(base64_encode($sMessage)).' - - -'; + if ($url === '') + $passport_url = PASSPORT_URL; + else + $passport_url = $url; - $header_array = array( - 'SOAPAction: '.$this->oim_send_soap, - 'Content-Type: text/xml', - 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.$this->buildver.')' - ); + $XML = ' + +
+ + {7108E71A-9926-4FCB-BCC9-9A9D3F32E423} + 4 + 1 + + AQAAAAIAAABsYwQAAAAxMDMz + + + + '.$user.' + '.$password.' + + +
+ + + + http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue + + + http://Passport.NET/tb + + + + + http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue + + + messengerclear.live.com + + + + + + http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue + + + messenger.msn.com + + + + + + http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue + + + contacts.msn.com + + + + + + http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue + + + messengersecure.live.com + + + + + + http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue + + + spaces.live.com + + + + + + http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue + + + storage.msn.com + + + + + + +
'; - $this->debug_message("*** URL: $this->oim_send_url"); + $this->debug_message("*** URL: $passport_url"); $this->debug_message("*** Sending SOAP:\n$XML"); $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $this->oim_send_url); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array); + curl_setopt($curl, CURLOPT_URL, $passport_url); + if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, $XML); $data = curl_exec($curl); @@ -2838,145 +2877,160 @@ X-OIM-Sequence-Num: 1 curl_close($curl); $this->debug_message("*** Get Result:\n$data"); - if ($http_code == 200) { - $this->debug_message("*** OIM sent for $to"); - return true; + if ($http_code != 200) { + // sometimes, redirect to another URL + // MSNP15 + //psf:Redirect + //https://msnia.login.live.com/pp450/RST.srf + //Authentication Failure + if (strpos($data, 'psf:Redirect') === false) { + $this->debug_message("*** Could not get passport ticket! http code = $http_code"); + return false; + } + preg_match("#(.*)#", $data, $matches); + if (count($matches) == 0) { + $this->debug_message('*** Redirected, but could not get redirect URL!'); + return false; + } + $redirect_url = $matches[1]; + if ($redirect_url == $passport_url) { + $this->debug_message('*** Redirected, but to same URL!'); + return false; + } + $this->debug_message("*** Redirected to $redirect_url"); + return $this->get_passport_ticket($redirect_url); } - $challenge = false; - $auth_policy = false; - // the lockkey is invalid, authenticated fail, we need challenge it again - // 364763969 - preg_match("#(.*)#", $data, $matches); - if (count($matches) != 0) { - // yes, we get new LockKeyChallenge - $challenge = $matches[2]; - $this->debug_message("*** OIM need new challenge ($challenge) for $to"); - } - // auth policy error - // MBI_SSL - preg_match("#(.*)#", $data, $matches); - if (count($matches) != 0) { - $auth_policy = $matches[2]; - $this->debug_message("*** OIM need new auth policy ($auth_policy) for $to"); - } - if ($auth_policy === false && $challenge === false) { - //q0:AuthenticationFailed - preg_match("#(.*)#", $data, $matches); - if (count($matches) == 0) { - // no error, we assume the OIM is sent - $this->debug_message("*** OIM sent for $to"); - return true; + // sometimes, redirect to another URL, also return 200 + // MSNP15 + //psf:Redirect + //https://msnia.login.live.com/pp450/RST.srf + //Authentication Failure + if (strpos($data, 'psf:Redirect') !== false) { + preg_match("#(.*)#", $data, $matches); + if (count($matches) != 0) { + $redirect_url = $matches[1]; + if ($redirect_url == $passport_url) { + $this->debug_message('*** Redirected, but to same URL!'); + return false; + } + $this->debug_message("*** Redirected to $redirect_url"); + return $this->get_passport_ticket($redirect_url); } - $err_code = $matches[2]; - //Exception of type 'System.Web.Services.Protocols.SoapException' was thrown. - preg_match("#(.*)#", $data, $matches); - if (count($matches) > 0) - $err_msg = $matches[1]; - else - $err_msg = ''; - $this->debug_message("*** OIM failed for $to"); - $this->debug_message("*** OIM Error code: $err_code"); - $this->debug_message("*** OIM Error Message: $err_msg"); + } + + // no Redurect faultcode or URL + // we should get the ticket here + + // we need ticket and secret code + // RST1: messengerclear.live.com + // t=tick&p= + // binary secret + // RST2: messenger.msn.com + // t=tick + // RST3: contacts.msn.com + // t=tick&p= + // RST4: messengersecure.live.com + // t=tick&p= + // RST5: spaces.live.com + // t=tick&p= + // RST6: storage.msn.com + // t=tick&p= + preg_match("#". + "(.*)(.*)". + "(.*)(.*)". + "(.*)(.*)". + "(.*)(.*)". + "(.*)(.*)". + "(.*)(.*)". + "(.*)(.*)". + "#", + $data, $matches); + + // no ticket found! + if (count($matches) == 0) { + $this->debug_message('*** Could not get passport ticket!'); return false; } - return array('challenge' => $challenge, 'auth_policy' => $auth_policy); - } + //$this->debug_message(var_export($matches, true)); + // matches[0]: all data + // matches[1]: RST1 (messengerclear.live.com) ticket + // matches[2]: ... + // matches[3]: RST1 (messengerclear.live.com) binary secret + // matches[4]: ... + // matches[5]: RST2 (messenger.msn.com) ticket + // matches[6]: ... + // matches[7]: RST3 (contacts.msn.com) ticket + // matches[8]: ... + // matches[9]: RST4 (messengersecure.live.com) ticket + // matches[10]: ... + // matches[11]: RST5 (spaces.live.com) ticket + // matches[12]: ... + // matches[13]: RST6 (storage.live.com) ticket + // matches[14]: ... + + // so + // ticket => $matches[1] + // secret => $matches[3] + // web_ticket => $matches[5] + // contact_ticket => $matches[7] + // oim_ticket => $matches[9] + // space_ticket => $matches[11] + // storage_ticket => $matches[13] + + // yes, we get ticket + $aTickets = array( + 'ticket' => html_entity_decode($matches[1]), + 'secret' => html_entity_decode($matches[3]), + 'web_ticket' => html_entity_decode($matches[5]), + 'contact_ticket' => html_entity_decode($matches[7]), + 'oim_ticket' => html_entity_decode($matches[9]), + 'space_ticket' => html_entity_decode($matches[11]), + 'storage_ticket' => html_entity_decode($matches[13]) + ); + $this->ticket = $aTickets; + //$this->debug_message(var_export($aTickets, true)); + $ABAuthHeaderArray = array( + 'ABAuthHeader' => array( + ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'), + 'ManagedGroupRequest' => false, + 'TicketToken' => htmlspecialchars($this->ticket['contact_ticket']), + ) + ); + $this->ABAuthHeader = new SoapHeader('http://www.msn.com/webservices/AddressBook', 'ABAuthHeader', $this->Array2SoapVar($ABAuthHeaderArray)); + return $aTickets; + } + /** - * Send a message to a user on another network - * - * @param string $to Intended recipient - * @param string $message Message - * @param integer $network Network - * @return void - */ - private function sendOtherNetworkMessage($to, $message, $network) { - $message = $this->getMessage($message, $network); - $len = strlen($message); - // TODO Introduce error checking for message sending - $this->ns_writeln("UUM $this->id $to $network 1 $len"); - $this->ns_writedata($Message); - $this->debug_message("*** Sent to $to (network: $network):\n$Message"); - return true; + * Generate the data to send a message + * + * @param string $sMessage Message + * @param integer $network Network + * @return string Message data + */ + private function getMessage($sMessage, $network = 1) { + $msg_header = "MIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nX-MMS-IM-Format: FN=$this->font_fn; EF=$this->font_ef; CO=$this->font_co; CS=0; PF=22\r\n\r\n"; + $msg_header_len = strlen($msg_header); + if ($network == 1) + $maxlen = $this->max_msn_message_len - $msg_header_len; + else + $maxlen = $this->max_yahoo_message_len - $msg_header_len; + $sMessage = str_replace("\r", '', $sMessage); + $msg = substr($sMessage, 0, $maxlen); + return $msg_header.$msg; } - + /** - * Send a message - * - * @param string $to To address in form user@host.com(@network) - * where network is 1 for MSN, 32 for Yahoo - * and 'Offline' for offline messages - * @param string $message Message - */ - public function sendMessage($to, $message) { - if ($message != '') { - list($name, $host, $network) = explode('@', $to); - $network = $network == '' ? 1 : $network; - $recipient = $name.$host; - - if ($network === 1) { - if (!isset($this->switchBoardSessionLookup[$recipient]) && (!isset($this->switchBoardSessions[$recipient]) - || time() - $this->switchBoardSessions[$recipient]['XFRReqTime'] > $this->XFRReqTimeout)) { - $this->debug_message("*** No existing SB session or request has timed out"); - $this->reqSBSession($recipient); - return false; - } else { - $socket = $this->switchBoardSessionLookup[$to]; - if ($this->switchBoardSessions[(int) $socket]['offline']) { - $this->debug_message("*** Contact ($recipient) offline, sending OIM"); - $this->endSBSession($socket); - return $this->sendMessage($recipient.'@Offline', $message); - } else { - $this->debug_message("*** Attempting to send message to $recipient using existing SB session"); - - if ($this->sendMessageViaSB($recipient, $message)) { - $this->debug_message('*** Message sent successfully'); - return true; - } else { - $this->debug_message('*** Message sending failed, requesting new SB session'); - $this->reqSBSession($to); - return false; - } - } - } - } elseif ($network == 'Offline') { - //Send OIM - //FIXME: 修正Send OIM - $lockkey = ''; - $re_login = false; - for ($i = 0; $i < $this->oim_try; $i++) { - if (($oim_result = $this->sendOIM($recipient, $message, $lockkey)) === true) break; - if (is_array($oim_result) && $oim_result['challenge'] !== false) { - // need challenge lockkey - $this->debug_message("*** Need challenge code for ".$oim_result['challenge']); - $lockkey = $this->getChallenge($oim_result['challenge']); - continue; - } - if ($oim_result === false || $oim_result['auth_policy'] !== false) { - if ($re_login) { - $this->debug_message("*** Can't send OIM, but we already re-logged-in again, so returning false"); - return false; - } - $this->debug_message("*** Can't send OIM, maybe ticket expired, trying to login again"); - - // Maybe we need to re-login again - if (!$this->get_passport_ticket()) { - $this->debug_message("*** Can't re-login, something went wrong here, returning false"); - return false; - } - $this->debug_message("*** Getting new ticket and trying again"); - continue; - } - } - } else { - // Other network - return $this->sendOtherNetworkMessage($recipient, $message, $network); - } - } - return true; + * Sleep for the given number of seconds + * + * @param integer $wait Number of seconds to sleep for + */ + private function NSRetryWait($wait) { + $this->debug_message("*** Sleeping for $wait seconds before retrying"); + sleep($wait); } - + /** * Sends a ping command * @@ -2988,49 +3042,7 @@ X-OIM-Sequence-Num: 1 // NS: >>> PNG $this->ns_writeln("PNG"); } - - /** - * Methods to return sockets / check socket status - */ - - /** - * Get the NS socket - * - * @return resource NS socket - */ - public function getNSSocket() { - return $this->NSfp; - } - - /** - * Get the Switchboard sockets currently in use - * - * @return array Array of Switchboard sockets - */ - public function getSBSockets() { - return $this->switchBoardSessionLookup; - } - - /** - * Get all the sockets currently in use - * - * @return array Array of socket resources - */ - public function getSockets() { - return array_merge(array($this->NSfp), $this->switchBoardSessionLookup); - } - - /** - * Checks socket for end of file - * - * @param resource $socket Socket to check - * @return boolean true if end of file (socket) - */ - private static function socketcheck($socket){ - $info = stream_get_meta_data($socket); - return $info['eof']; - } - + /** * Methods to add / call callbacks */ @@ -3075,4 +3087,50 @@ X-OIM-Sequence-Num: 1 return false; } } + + /** + * Debugging methods + */ + + /** + * Print message if debugging is enabled + * + * @param string $str Message to print + */ + private function debug_message($str) { + if (!$this->debug) return; + echo $str."\n"; + } + + /** + * Dump binary data + * + * @param string $str Data string + * @return Binary data + */ + private function dump_binary($str) { + $buf = ''; + $a_str = ''; + $h_str = ''; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + if (($i % 16) == 0) { + if ($buf !== '') { + $buf .= "$h_str $a_str\n"; + } + $buf .= sprintf("%04X:", $i); + $a_str = ''; + $h_str = ''; + } + $ch = ord($str[$i]); + if ($ch < 32) + $a_str .= '.'; + else + $a_str .= chr($ch); + $h_str .= sprintf(" %02X", $ch); + } + if ($h_str !== '') + $buf .= "$h_str $a_str\n"; + return $buf; + } }