SaslAuthenticator::_resultPredicate()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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