Completed
Push — master ( fcd09b...a3140b )
by Kacper
08:42
created

SaslAuthenticator::_resultPredicate()   A

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