Completed
Push — master ( 641e7a...5eff09 )
by Kacper
02:52
created

SaslAuthenticator::handleAuthResult()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 2
eloc 5
c 3
b 0
f 1
nc 2
nop 1
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 2
rs 9.6666
1
<?php
2
/**
3
 * XMPP Library
4
 *
5
 * Copyright (C) 2016, Some right reserved.
6
 *
7
 * @author Kacper "Kadet" Donat <[email protected]>
8
 *
9
 * Contact with author:
10
 * Xmpp: [email protected]
11
 * E-mail: [email protected]
12
 *
13
 * From Kadet with love.
14
 */
15
16
namespace Kadet\Xmpp\Module;
17
18
use Fabiang\Sasl\Authentication\AuthenticationInterface;
19
use Fabiang\Sasl\Authentication\ChallengeAuthenticationInterface;
20
use Fabiang\Sasl\Exception\InvalidArgumentException;
21
use Fabiang\Sasl\Sasl;
22
use Kadet\Xmpp\Exception\Protocol\AuthenticationException;
23
use Kadet\Xmpp\Stream\Features;
24
use Kadet\Xmpp\Utils\filter as with;
25
use Kadet\Xmpp\Xml\XmlElement;
26
use Kadet\Xmpp\XmppClient;
27
28
class SaslAuthenticator extends ClientModule implements Authenticator
29
{
30
    const XMLNS = 'urn:ietf:params:xml:ns:xmpp-sasl';
31
32
    /**
33
     * Client's password used in authorisation.
34
     *
35
     * @var string
36
     */
37
    private $_password;
38
39
    /**
40
     * Factory used to create mechanisms
41
     *
42
     * @var Sasl
43
     */
44
    private $_sasl;
45
46
    /**
47
     * Authentication constructor.
48
     *
49
     * @param string $password Client's password
50
     * @param Sasl   $sasl     Factory used to create mechanisms
51
     */
52 4
    public function __construct($password, Sasl $sasl = null)
53
    {
54 4
        $this->setPassword($password);
55 4
        $this->_sasl = $sasl ?: new Sasl();
56 4
    }
57
58 4
    public function setClient(XmppClient $client)
59
    {
60 4
        parent::setClient($client);
61
62
        $client->on('features', function (Features $features) {
63
            return !$this->auth($features);
64 4
        });
65 4
    }
66
67 4
    public function auth(Features $features)
68
    {
69 4
        if (!empty($features->mechanisms)) {
70 4
            foreach ($features->mechanisms as $name) {
71 4
                if($this->tryMechanism($name)) {
72 4
                    return true;
73
                }
74
            }
75
        }
76
77
        return false;
78
    }
79
80 4
    private function tryMechanism($name) {
81
        try {
82 4
            $mechanism = $this->_sasl->factory($name, [
83 4
                'authcid'  => $this->_client->jid->local,
84 4
                'secret'   => $this->_password,
85 4
                'hostname' => $this->_client->jid->domain,
86 4
                'service'  => 'xmpp'
87
            ]);
88
89 4
            $this->_client->getLogger()->debug('Starting auth using {name} mechanism.', ['name' => $name]);
90
91 4
            $auth = new XmlElement('auth', self::XMLNS);
92 4
            $auth->setAttribute('mechanism', $name);
93
94 4
            $auth->append(base64_encode(
95
                $mechanism instanceof ChallengeAuthenticationInterface
96 2
                    ? $this->mechanismWithChallenge($mechanism)
97 4
                    : $this->mechanismWithoutChallenge($mechanism)
98
            ));
99
100 4
            $this->_client->write($auth);
101
102 4
            return true;
103
        } catch (InvalidArgumentException $e) {
104
            return false;
105
        }
106
    }
107
108 2
    private function mechanismWithChallenge(ChallengeAuthenticationInterface $mechanism) {
109
        try {
110 2
            $response = base64_encode($mechanism->createResponse());
111 2
        } catch (InvalidArgumentException $e) {
112 2
            $response = '=';
113
        }
114
115
        $callback = $this->_client->on('element', function (XmlElement $challenge) use ($mechanism) {
116 2
            $this->handleChallenge($challenge, $mechanism);
117 2
        }, with\all(with\tag('challenge'), with\xmlns(self::XMLNS)));
118
119
        $this->_client->once('element', function (XmlElement $result) use ($callback) {
120 2
            $this->_client->removeListener('element', $callback);
121 2
            $this->handleAuthResult($result);
122 2
        }, with\all(with\any(with\tag('success'), with\tag('failure')), with\xmlns(self::XMLNS)));
123
124 2
        return $response;
125
    }
126
127
    private function mechanismWithoutChallenge(AuthenticationInterface $mechanism)
128
    {
129 2
        $this->_client->once('element', function (XmlElement $result) {
130 2
            $this->handleAuthResult($result);
131 2
        }, with\all(with\any(with\tag('success'), with\tag('failure')), with\xmlns(self::XMLNS)));
132
133 2
        return $mechanism->createResponse();
134
    }
135
136 2
    private function handleChallenge(XmlElement $challenge, AuthenticationInterface $mechanism)
137
    {
138 2
        $response = new XmlElement('response', self::XMLNS);
139 2
        $response->append(base64_encode($mechanism->createResponse(base64_decode($challenge->innerXml))));
140
141 2
        $this->_client->write($response);
142 2
    }
143
144 4
    private function handleAuthResult(XmlElement $result)
145
    {
146 4
        if ($result->localName === 'failure') {
147 2
            throw new AuthenticationException('Unable to auth. '.trim($result->innerXml));
148
        }
149
150 2
        $this->_client->getLogger()->info('Successfully authorized as {name}.', ['name' => (string)$this->_client->jid]);
151 2
        $this->_client->restart();
152 2
    }
153
154 4
    public function setPassword(string $password)
155
    {
156 4
        $this->_password = $password;
157 4
    }
158
}
159