/** * This handles all the different XML elements * @param array $xml * @access public * @return bool */ public function response($xml) { if (!is_array($xml) || !count($xml)) { return false; } // did we get multiple elements? do one after another // array('message' => ..., 'presence' => ...) if (count($xml) > 1) { foreach ($xml as $key => $value) { $this->response(array($key => $value)); } return; } else { // or even multiple elements of the same type? // array('message' => array(0 => ..., 1 => ...)) if (count(reset($xml)) > 1) { foreach (reset($xml) as $value) { $this->response(array(key($xml) => array(0 => $value))); } return; } } switch (key($xml)) { case 'stream:stream': // Connection initialised (or after authentication). Not much to do here... if (isset($xml['stream:stream'][0]['#']['stream:features'])) { // we already got all info we need $this->features = $xml['stream:stream'][0]['#']; } else { $this->features = $this->listen(); } $second_time = isset($this->session['id']); $this->session['id'] = $xml['stream:stream'][0]['@']['id']; if ($second_time) { // If we are here for the second time after TLS, we need to continue logging in $this->login(); return; } // go on with authentication? if (isset($this->features['stream:features'][0]['#']['bind'])) { return $this->response($this->features); } break; case 'stream:features': // Resource binding after successful authentication if (isset($this->session['authenticated'])) { // session required? $this->session['sess_required'] = isset($xml['stream:features'][0]['#']['session']); $this->send("<iq type='set' id='bind_1'>\n\t\t\t\t\t\t\t\t <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>\n\t\t\t\t\t\t\t \t <resource>" . Jabber::jspecialchars($this->resource) . "</resource>\n </bind>\n </iq>"); return $this->response($this->listen()); } // Let's use TLS if SSL is not enabled and we can actually use it if ($this->session['security'] == SECURITY_TLS && isset($xml['stream:features'][0]['#']['starttls'])) { $this->log('Switching to TLS.'); $this->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>\n"); return $this->response($this->listen()); } // Does the server support SASL authentication? // I hope so, because we do (and no other method). if (isset($xml['stream:features'][0]['#']['mechanisms'][0]['@']['xmlns']) && $xml['stream:features'][0]['#']['mechanisms'][0]['@']['xmlns'] == 'urn:ietf:params:xml:ns:xmpp-sasl') { // Now decide on method $methods = array(); foreach ($xml['stream:features'][0]['#']['mechanisms'][0]['#']['mechanism'] as $value) { $methods[] = $value['#']; } // we prefer this one if (in_array('DIGEST-MD5', $methods)) { $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>"); // we don't want to use this (neither does the server usually) if no encryption is in place # http://www.xmpp.org/extensions/attic/jep-0078-1.7.html # The plaintext mechanism SHOULD NOT be used unless the underlying stream is encrypted (using SSL or TLS) # and the client has verified that the server certificate is signed by a trusted certificate authority. } else { if (in_array('PLAIN', $methods) && (isset($this->session['ssl']) || isset($this->session['tls']))) { $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" . base64_encode(chr(0) . $this->user . '@' . $this->server . chr(0) . $this->password) . "</auth>"); } else { if (in_array('ANONYMOUS', $methods)) { $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>"); // not good... } else { $this->log('Error: No authentication method supported.'); $this->disconnect(); return false; } } } return $this->response($this->listen()); } else { // ok, this is it. bye. $this->log('Error: Server does not offer SASL authentication.'); $this->disconnect(); return false; } break; case 'challenge': // continue with authentication...a challenge literally -_- $decoded = base64_decode($xml['challenge'][0]['#']); $decoded = Jabber::parse_data($decoded); if (!isset($decoded['digest-uri'])) { $decoded['digest-uri'] = 'xmpp/' . $this->server; } // better generate a cnonce, maybe it's needed $decoded['cnonce'] = base64_encode(md5(uniqid(mt_rand(), true))); // second challenge? if (isset($decoded['rspauth'])) { $this->send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"); } else { $response = array('username' => $this->user, 'response' => $this->encrypt_password(array_merge($decoded, array('nc' => '00000001'))), 'charset' => 'utf-8', 'nc' => '00000001', 'qop' => 'auth'); // the only option we support anyway foreach (array('nonce', 'digest-uri', 'realm', 'cnonce') as $key) { if (isset($decoded[$key])) { $response[$key] = $decoded[$key]; } } $this->send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" . base64_encode(Jabber::implode_data($response)) . "</response>"); } return $this->response($this->listen()); case 'failure': $this->log('Error: Server sent "failure".'); $this->disconnect(); return false; case 'proceed': // continue switching to TLS $meta = stream_get_meta_data($this->connection); socket_set_blocking($this->connection, 1); if (!stream_socket_enable_crypto($this->connection, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { $this->log('Error: TLS mode change failed.'); return false; } socket_set_blocking($this->connection, $meta['blocked']); $this->session['tls'] = true; // new stream $this->send("<?xml version='1.0' encoding='UTF-8' ?" . ">\n"); $this->send("<stream:stream to='{$this->server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n"); return $this->response($this->listen()); case 'success': // Yay, authentication successful. $this->send("<stream:stream to='{$this->server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n"); $this->session['authenticated'] = true; return $this->response($this->listen()); // we have to wait for another response // we have to wait for another response case 'iq': // we are not interested in IQs we did not expect if (!isset($xml['iq'][0]['@']['id'])) { return false; } // multiple possibilities here switch ($xml['iq'][0]['@']['id']) { case 'bind_1': $this->session['jid'] = $xml['iq'][0]['#']['bind'][0]['#']['jid'][0]['#']; // and (maybe) yet another request to be able to send messages *finally* if ($this->session['sess_required']) { $this->send("<iq to='{$this->server}'\n type='set'\n id='sess_1'>\n <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>\n </iq>"); return $this->response($this->listen()); } return true; case 'sess_1': return true; case 'reg_1': $this->send("<iq type='set' id='reg_2'>\n <query xmlns='jabber:iq:register'>\n <username>" . Jabber::jspecialchars($this->user) . "</username>\n <password>" . Jabber::jspecialchars($this->password) . "</password>\n </query>\n </iq>"); return $this->response($this->listen()); case 'reg_2': // registration end if (isset($xml['iq'][0]['#']['error'])) { $this->log('Warning: Registration failed.'); return false; } return true; case 'unreg_1': return true; default: $this->log('Notice: Received unexpected IQ.'); return false; } break; case 'message': // we are only interested in content... if (!isset($xml['message'][0]['#']['body'])) { return false; } $message['body'] = $xml['message'][0]['#']['body'][0]['#']; $message['from'] = $xml['message'][0]['@']['from']; if (isset($xml['message'][0]['#']['subject'])) { $message['subject'] = $xml['message'][0]['#']['subject'][0]['#']; } $this->session['messages'][] = $message; break; default: // hm...don't know this response $this->log('Notice: Unknown server response (' . key($xml) . ')'); return false; } }