Passed
Push — develop ( 743253...17d957 )
by Manuele
01:37
created

NuVotifier::preparePackageV2()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 15
ccs 10
cts 10
cp 1
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 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\Server;
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\Vote\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 Votifier
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 13
    public function isProtocolV2(): bool
47
    {
48 13
        return $this->protocolV2;
49
    }
50
51
    /**
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 15
    public function setProtocolV2(bool $isProtocolV2): self
59
    {
60 15
        $this->protocolV2 = $isProtocolV2;
61
62 15
        return $this;
63
    }
64
65
    /**
66
     * Gets the token from the config.yml.
67
     *
68
     * @return string|null returns The token from the config.yml.
69
     */
70 1
    public function getToken(): ?string
71
    {
72 1
        return $this->token;
73
    }
74
75
    /**
76
     * Sets the token from the config.yml.
77
     *
78
     * @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 15
    public function setToken(?string $token): self
83
    {
84 15
        $this->token = $token;
85
86 15
        return $this;
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     *
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 12
    public function sendVote(VoteInterface ...$votes): void
100
    {
101 12
        if (!$this->isProtocolV2()) {
102 1
            call_user_func_array([$this, 'parent::sendVote'], func_get_args());
103
104 1
            return;
105
        }
106
107 11
        foreach ($votes as $vote) {
108
            // Connect to the server
109 11
            $socket = $this->getSocket();
110 11
            $socket->open($this->getHost(), $this->getPort());
111
112
            // Check whether the connection really belongs to a NuVotifier plugin
113 11
            if (!$this->verifyConnection($header = $socket->read(64))) {
114 3
                throw new NotVotifierException();
115
            }
116
117
            // Extract the challenge
118 8
            $headerParts = explode(' ', $header);
119 8
            $challenge = mb_substr($headerParts[2], 0, -1);
120
121
            // Update the timestamp of the vote being sent
122 8
            $vote->setTimestamp(new DateTime());
123
124
            // Send the vote
125 8
            $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 7
            $result = json_decode($socket->read(256));
138 6
            if ('ok' !== $result->status) {
139 5
                if ('Challenge is not valid' === $result->error) {
140 1
                    throw new NuVotifierChallengeInvalidException();
141 4
                } elseif (preg_match('/Unknown service \'(.*)\'/', $result->error, $matches)) {
142 1
                    throw new NuVotifierUnknownServiceException();
143 3
                } elseif ('Signature is not valid (invalid token?)' === $result->error) {
144 1
                    throw new NuVotifierSignatureInvalidException();
145 2
                } elseif ('Username too long' === $result->error) {
146 1
                    throw new NuVotifierUsernameTooLongException();
147
                }
148 1
                throw new NuVotifierException('Unknown NuVotifier Exception');
149
            }
150
151
            // Make sure to close the connection after package was sent
152 1
            $socket->__destruct();
153
        }
154 1
    }
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 12
    protected function verifyConnection(?string $header): bool
164
    {
165 12
        $header_parts = explode(' ', $header);
166 12
        if (null === $header || false === mb_strpos($header, 'VOTIFIER') || 3 !== count($header_parts)) {
167 3
            return false;
168
        }
169
170 9
        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 8
    protected function preparePackageV2(VoteInterface $vote, string $challenge): string
182
    {
183 8
        $payloadJson = json_encode(
184
            [
185 8
                'username' => $vote->getUsername(),
186 8
                'serviceName' => $vote->getServiceName(),
187 8
                'timestamp' => $vote->getTimestamp(),
188 8
                'address' => $vote->getAddress(),
189 8
                'challenge' => $challenge,
190
            ]
191
        );
192 8
        $signature = base64_encode(hash_hmac('sha256', $payloadJson, $this->token, true));
193 8
        $messageJson = json_encode(['signature' => $signature, 'payload' => $payloadJson]);
194
195 8
        return pack('nn', 0x733a, mb_strlen($messageJson)) . $messageJson;
196
    }
197
}
198