Passed
Push — develop ( f821dc...ade763 )
by Manuele
13:55
created

NuVotifier   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 167
Duplicated Lines 0 %

Test Coverage

Coverage 58.14%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 18
eloc 45
c 3
b 0
f 0
dl 0
loc 167
ccs 25
cts 43
cp 0.5814
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A isProtocolV2() 0 3 1
A preparePackageV2() 0 15 1
A verifyConnection() 0 8 4
B sendVote() 0 54 9
A setToken() 0 5 1
A getToken() 0 3 1
A setProtocolV2() 0 5 1
1
<?php
2
3
/**
4
 * Votifier PHP Client
5
 *
6
 * @package   VotifierClient
7
 * @author    Manuele Vaccari <[email protected]>
8
 * @copyright Copyright (c) 2017-2020 Manuele Vaccari <[email protected]>
9
 * @license   https://github.com/D3strukt0r/votifier-client-php/blob/master/LICENSE.txt GNU General Public License v3.0
10
 * @link      https://github.com/D3strukt0r/votifier-client-php
11
 */
12
13
namespace D3strukt0r\VotifierClient\ServerType;
14
15
use D3strukt0r\VotifierClient\Exception\NotVotifierException;
16
use D3strukt0r\VotifierClient\Exception\NuVotifierChallengeInvalidException;
17
use D3strukt0r\VotifierClient\Exception\NuVotifierException;
18
use D3strukt0r\VotifierClient\Exception\NuVotifierSignatureInvalidException;
19
use D3strukt0r\VotifierClient\Exception\NuVotifierUnknownServiceException;
20
use D3strukt0r\VotifierClient\Exception\NuVotifierUsernameTooLongException;
21
use D3strukt0r\VotifierClient\VoteType\VoteInterface;
22
use DateTime;
23
24
use function count;
25
26
/**
27
 * The Class to access a server which uses the plugin "NuVotifier".
28
 */
29
class NuVotifier extends ClassicVotifier
30
{
31
    /**
32
     * @var bool use version 2 of the protocol
33
     */
34
    private $protocolV2 = false;
35
36
    /**
37
     * @var string|null The token from the config.yml.
38
     */
39
    private $token;
40
41
    /**
42
     * Checks whether the connection uses the version 2 protocol.
43
     *
44
     * @return bool returns true, if using the new version of NuVotifier or false otherwise
45
     */
46
    public function isProtocolV2(): bool
47
    {
48
        return $this->protocolV2;
49
    }
50
51 4
    /**
52
     * Sets whether to use version 2 of the protocol.
53
     *
54
     * @param bool $isProtocolV2 Whether to use version 2 of the protocol
55
     *
56
     * @return $this returns the class itself, for doing multiple things at once
57
     */
58 4
    public function setProtocolV2(bool $isProtocolV2): self
59 4
    {
60 4
        $this->protocolV2 = $isProtocolV2;
61
62
        return $this;
63
    }
64
65 4
    /**
66
     * Gets the token from the config.yml.
67 4
     *
68 4
     * @return string|null returns The token from the config.yml.
69 4
     */
70
    public function getToken(): ?string
71
    {
72
        return $this->token;
73
    }
74
75
    /**
76 1
     * Sets the token from the config.yml.
77
     *
78 1
     * @param string|null $token The token from the config.yml.
79
     *
80
     * @return $this returns the class itself, for doing multiple things at once
81
     */
82
    public function setToken(?string $token): self
83
    {
84 1
        $this->token = $token;
85
86 1
        return $this;
87 1
    }
88 1
89
    /**
90
     * {@inheritdoc}
91 1
     *
92
     * @throws NuVotifierChallengeInvalidException NuVotifier says the challenge was invalid
93
     * @throws NuVotifierException                 General NuVotifier Exception (an unknown exception)
94
     * @throws NuVotifierSignatureInvalidException NuVotifier says the signature was invalid
95
     * @throws NuVotifierUnknownServiceException   NuVotifier says that the service is unknown (the token doesn't belong
96
     *                                             to the service name)
97
     * @throws NuVotifierUsernameTooLongException  NuVotifier says the username is too long
98
     */
99
    public function sendVote(VoteInterface ...$votes): void
100
    {
101
        if (!$this->isProtocolV2()) {
102 1
            call_user_func_array([$this, 'parent::sendVote'], func_get_args());
103
104 1
            return;
105
        }
106 1
107 1
        foreach ($votes as $vote) {
108 1
            // Connect to the server
109 1
            $socket = $this->getSocket();
110 1
            $socket->open($this->getHost(), $this->getPort());
111
112
            // Check whether the connection really belongs to a NuVotifier plugin
113 1
            if (!$this->verifyConnection($header = $socket->read(64))) {
114 1
                throw new NotVotifierException();
115
            }
116 1
117
            // Extract the challenge
118
            $headerParts = explode(' ', $header);
119
            $challenge = mb_substr($headerParts[2], 0, -1);
120
121
            // Update the timestamp of the vote being sent
122
            $vote->setTimestamp(new DateTime());
123
124
            // Send the vote
125
            $socket->write($package = $this->preparePackageV2($vote, $challenge));
126
127
            // Check is the vote was successful
128
            /*
129
             * https://github.com/NuVotifier/NuVotifier/blob/master/common/src/main/java/com/vexsoftware/votifier/net/protocol/VotifierProtocol2Decoder.java
130
             * Examples:
131
             * {"status":"ok"}
132
             * {"status":"error","cause":"CorruptedFrameException","error":"Challenge is not valid"}
133
             * {"status":"error","cause":"CorruptedFrameException","error":"Unknown service 'xxx'"}
134
             * {"status":"error","cause":"CorruptedFrameException","error":"Signature is not valid (invalid token?)"}
135
             * {"status":"error","cause":"CorruptedFrameException","error":"Username too long"} (over 16 characters)
136
             */
137
            $result = json_decode($socket->read(256));
138
            if ('ok' !== $result->status) {
139
                if ('Challenge is not valid' === $result->error) {
140
                    throw new NuVotifierChallengeInvalidException();
141
                } elseif (preg_match('/Unknown service \'(.*)\'/', $result->error, $matches)) {
142
                    throw new NuVotifierUnknownServiceException();
143
                } elseif ('Signature is not valid (invalid token?)' === $result->error) {
144
                    throw new NuVotifierSignatureInvalidException();
145
                } elseif ('Username too long' === $result->error) {
146
                    throw new NuVotifierUsernameTooLongException();
147
                }
148
                throw new NuVotifierException('Unknown NuVotifier Exception');
149
            }
150
151
            // Make sure to close the connection after package was sent
152
            $socket->__destruct();
153
        }
154
    }
155
156
    /**
157
     * Verifies that the connection is correct.
158
     *
159
     * @param string|null $header The header that the plugin usually sends
160
     *
161
     * @return bool returns true if connections is available, otherwise false
162
     */
163
    protected function verifyConnection(?string $header): bool
164
    {
165
        $header_parts = explode(' ', $header);
166
        if (null === $header || false === mb_strpos($header, 'VOTIFIER') || 3 !== count($header_parts)) {
167
            return false;
168
        }
169
170
        return true;
171
    }
172
173
    /**
174
     * Prepares the vote package to be sent as version 2 protocol package.
175
     *
176
     * @param VoteInterface $vote      The vote package with information
177
     * @param string        $challenge The challenge sent by the server
178
     *
179
     * @return string returns the string to be sent to the server
180
     */
181
    protected function preparePackageV2(VoteInterface $vote, string $challenge): string
182
    {
183
        $payloadJson = json_encode(
184
            [
185
                'username' => $vote->getUsername(),
186
                'serviceName' => $vote->getServiceName(),
187
                'timestamp' => $vote->getTimestamp(),
188
                'address' => $vote->getAddress(),
189
                'challenge' => $challenge,
190
            ]
191
        );
192
        $signature = base64_encode(hash_hmac('sha256', $payloadJson, $this->token, true));
193
        $messageJson = json_encode(['signature' => $signature, 'payload' => $payloadJson]);
194
195
        return pack('nn', 0x733a, mb_strlen($messageJson)) . $messageJson;
196
    }
197
}
198