Completed
Push — master ( 939cef...4aee11 )
by Kacper
03:59
created

SaslAuthenticator::setPassword()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

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