Completed
Push — master ( cd1596...641e7a )
by Kacper
03:28
created

SaslAuthenticator::tryMechanism()   B

Complexity

Conditions 3
Paths 7

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3.0494

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 18
c 2
b 0
f 0
nc 7
nop 1
dl 0
loc 27
ccs 14
cts 17
cp 0.8235
crap 3.0494
rs 8.8571
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 1
    public function __construct($password, Sasl $sasl = null)
53
    {
54 1
        $this->_password = $password;
55 1
        $this->_sasl = $sasl ?: new Sasl();
56 1
    }
57
58 1
    public function setClient(XmppClient $client)
59
    {
60 1
        parent::setClient($client);
61
62
        $client->on('features', function (Features $features) {
63
            return !$this->auth($features);
64 1
        });
65 1
    }
66
67 1
    public function auth(Features $features)
68
    {
69 1
        if (!empty($features->mechanisms)) {
70 1
            foreach ($features->mechanisms as $name) {
71 1
                if($this->tryMechanism($name)) {
72 1
                    return true;
73
                }
74
            }
75
        }
76
77
        return false;
78
    }
79
80 1
    private function tryMechanism($name) {
81
        try {
82 1
            $mechanism = $this->_sasl->factory($name, [
83 1
                'authcid'  => $this->_client->jid->local,
84 1
                'secret'   => $this->_password,
85 1
                'hostname' => $this->_client->jid->domain,
86 1
                'service'  => 'xmpp'
87
            ]);
88
89 1
            $this->_client->getLogger()->debug('Starting auth using {name} mechanism.', ['name' => $name]);
90
91 1
            $auth = new XmlElement('auth', self::XMLNS);
92 1
            $auth->setAttribute('mechanism', $name);
93
94 1
            $auth->append(base64_encode(
95 1
                $mechanism instanceof ChallengeAuthenticationInterface
96
                    ? $this->mechanismWithChallenge($mechanism)
97 1
                    : $this->mechanismWithoutChallenge($mechanism)
98
            ));
99
100 1
            $this->_client->write($auth);
101
102 1
            return true;
103
        } catch (InvalidArgumentException $e) {
104
            return false;
105
        }
106
    }
107
108
    private function mechanismWithChallenge(ChallengeAuthenticationInterface $mechanism) {
109
        try {
110
            $response = base64_encode($mechanism->createResponse());
111
        } catch (InvalidArgumentException $e) {
112
            $response = '=';
113
        }
114
115
        $callback = $this->_client->on('element', function (XmlElement $challenge) use ($mechanism) {
116
            $this->handleChallenge($challenge, $mechanism);
117
        }, with\all(with\tag('challenge'), with\xmlns(self::XMLNS)));
118
119
        $this->_client->once('element', function (XmlElement $result) use ($callback) {
120
            $this->_client->removeListener('element', $callback);
121
            $this->handleAuthResult($result);
122
        }, with\all(with\any(with\tag('success'), with\tag('failure')), with\xmlns(self::XMLNS)));
123
124
        return $response;
125
    }
126
127
    private function mechanismWithoutChallenge(AuthenticationInterface $mechanism)
128
    {
129 1
        $this->_client->once('element', function (XmlElement $result) {
130
            $this->handleAuthResult($result);
131 1
        }, with\all(with\any(with\tag('success'), with\tag('failure')), with\xmlns(self::XMLNS)));
132
133 1
        return $mechanism->createResponse();
134
    }
135
136
    private function handleChallenge(XmlElement $challenge, AuthenticationInterface $mechanism)
137
    {
138
        $response = new XmlElement('response', self::XMLNS);
139
        $response->append(base64_encode($mechanism->createResponse(base64_decode($challenge->innerXml))));
140
141
        $this->_client->write($response);
142
    }
143
144
    private function handleAuthResult(XmlElement $result)
145
    {
146
        if ($result->localName === 'failure') {
147
            throw new AuthenticationException('Unable to auth. '.trim($result->innerXml));
148
        }
149
150
        $this->_client->getLogger()->info('Successfully authorized as {name}.', ['name' => (string)$this->_client->jid]);
151
        $this->_client->restart();
152
    }
153
154
    public function setPassword(string $password)
155
    {
156
        $this->_password = $password;
157
    }
158
}
159