Passed
Branch master (a6a24c)
by kacper
04:55
created

BinLogSocketConnect::isWriteSuccessful()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 6.087

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 12
ccs 3
cts 10
cp 0.3
crap 6.087
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace MySQLReplication\BinLog;
4
5
use MySQLReplication\BinaryDataReader\BinaryDataReader;
6
use MySQLReplication\Config\Config;
7
use MySQLReplication\Definitions\ConstCapabilityFlags;
8
use MySQLReplication\Definitions\ConstCommand;
9
use MySQLReplication\Gtid\GtidException;
10
use MySQLReplication\Gtid\GtidFactory;
11
use MySQLReplication\Repository\RepositoryInterface;
12
use MySQLReplication\Socket\SocketInterface;
13
14
/**
15
 * Class BinLogSocketConnect
16
 * @package MySQLReplication\BinLog
17
 */
18
class BinLogSocketConnect
19
{
20
    /**
21
     * @var bool
22
     */
23
    private $checkSum = false;
24
    /**
25
     * @var RepositoryInterface
26
     */
27
    private $repository;
28
    /**
29
     * http://dev.mysql.com/doc/internals/en/auth-phase-fast-path.html 00 FE
30
     * @var array
31
     */
32
    private $packageOkHeader = [0, 254];
33
    /**
34
     * @var SocketInterface
35
     */
36
    private $socket;
37
    /**
38
     * 2^24 - 1 16m
39
     * @var int
40
     */
41
    private $binaryDataMaxLength = 16777215;
42
43
    /**
44
     * @param RepositoryInterface $repository
45
     * @param SocketInterface $socket
46
     * @throws BinLogException
47
     * @throws \MySQLReplication\Gtid\GtidException
48
     * @throws \MySQLReplication\Socket\SocketException
49
     */
50 54
    public function __construct(
51
        RepositoryInterface $repository,
52
        SocketInterface $socket
53
    ) {
54 54
        $this->repository = $repository;
55 54
        $this->socket = $socket;
56
57 54
        $this->socket->connectToStream(Config::getHost(), Config::getPort());
58 54
        BinLogServerInfo::parsePackage($this->getResponse(false), $this->repository->getVersion());
59 54
        $this->authenticate();
60 54
        $this->getBinlogStream();
61 54
    }
62
63
    /**
64
     * @return bool
65
     */
66 54
    public function getCheckSum()
67
    {
68 54
        return $this->checkSum;
69
    }
70
71
    /**
72
     * @param bool $checkResponse
73
     * @return string
74
     * @throws \MySQLReplication\BinLog\BinLogException
75
     * @throws \MySQLReplication\Socket\SocketException
76
     */
77 54
    public function getResponse($checkResponse = true)
78
    {
79 54
        $header = $this->socket->readFromSocket(4);
80 54
        if ('' === $header) {
81
            return '';
82
        }
83 54
        $dataLength = unpack('L', $header[0] . $header[1] . $header[2] . chr(0))[1];
0 ignored issues
show
Bug introduced by
The call to unpack() has too few arguments starting with offset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

83
        $dataLength = /** @scrutinizer ignore-call */ unpack('L', $header[0] . $header[1] . $header[2] . chr(0))[1];

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
84
85 54
        $result = $this->socket->readFromSocket($dataLength);
86 54
        if (true === $checkResponse) {
87 54
            $this->isWriteSuccessful($result);
88 54
        }
89
90 54
        return $result;
91
    }
92
93
    /**
94
     * @param string $data
95
     * @throws BinLogException
96
     */
97 54
    private function isWriteSuccessful($data)
98
    {
99 54
        $head = ord($data[0]);
100 54
        if (!in_array($head, $this->packageOkHeader, true)) {
101
            $errorCode = unpack('v', $data[1] . $data[2])[1];
0 ignored issues
show
Bug introduced by
The call to unpack() has too few arguments starting with offset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

101
            $errorCode = /** @scrutinizer ignore-call */ unpack('v', $data[1] . $data[2])[1];

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
102
            $errorMessage = '';
103
            $packetLength = strlen($data);
104
            for ($i = 9; $i < $packetLength; ++$i) {
105
                $errorMessage .= $data[$i];
106
            }
107
108
            throw new BinLogException($errorMessage, $errorCode);
109
        }
110 54
    }
111
112
    /**
113
     * @throws BinLogException
114
     * @throws \MySQLReplication\Socket\SocketException
115
     * @link http://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41
116
     */
117 54
    private function authenticate()
118
    {
119 54
        $data = pack('L', ConstCapabilityFlags::getCapabilities());
120 54
        $data .= pack('L', $this->binaryDataMaxLength);
121 54
        $data .= chr(33);
122 54
        for ($i = 0; $i < 23; $i++) {
123 54
            $data .= chr(0);
124 54
        }
125 54
        $result = sha1(Config::getPassword(), true) ^ sha1(
126 54
                BinLogServerInfo::getSalt() . sha1(sha1(Config::getPassword(), true), true), true
127 54
            );
128
129 54
        $data = $data . Config::getUser() . chr(0) . chr(strlen($result)) . $result;
130 54
        $str = pack('L', strlen($data));
131 54
        $s = $str[0] . $str[1] . $str[2];
132 54
        $data = $s . chr(1) . $data;
133
134 54
        $this->socket->writeToSocket($data);
135 54
        $this->getResponse();
136 54
    }
137
138
    /**
139
     * @throws BinLogException
140
     * @throws GtidException
141
     * @throws \MySQLReplication\Socket\SocketException
142
     */
143 54
    private function getBinlogStream()
144
    {
145 54
        $this->checkSum = $this->repository->isCheckSum();
146 54
        if ($this->checkSum) {
147 54
            $this->execute('SET @master_binlog_checksum = @@global.binlog_checksum');
148 54
        }
149
150 54
        if (0 !== Config::getHeartbeatPeriod()) {
151
            // master_heartbeat_period is nanoseconds
152
            $this->execute('SET @master_heartbeat_period = ' . Config::getHeartbeatPeriod() * 1000000000);
153
        }
154
155 54
        $this->registerSlave();
156
157 54
        if ('' !== Config::getMariaDbGtid()) {
158
            $this->execute('SET @mariadb_slave_capability = 4');
159
            $this->execute('SET @slave_connect_state = \'' . Config::getMariaDbGtid() . '\'');
160
            $this->execute('SET @slave_gtid_strict_mode = 0');
161
            $this->execute('SET @slave_gtid_ignore_duplicates = 0');
162
        }
163 54
        if ('' !== Config::getGtid()) {
164
            $this->setBinLogDumpGtid();
165
        } else {
166 54
            $this->setBinLogDump();
167
        }
168 54
    }
169
170
    /**
171
     * @param string $sql
172
     * @throws BinLogException
173
     * @throws \MySQLReplication\Socket\SocketException
174
     */
175 54
    private function execute($sql)
176
    {
177 54
        $this->socket->writeToSocket(pack('LC', strlen($sql) + 1, 0x03) . $sql);
178 54
        $this->getResponse();
179 54
    }
180
181
    /**
182
     * @see https://dev.mysql.com/doc/internals/en/com-register-slave.html
183
     * @throws BinLogException
184
     * @throws \MySQLReplication\Socket\SocketException
185
     */
186 54
    private function registerSlave()
187
    {
188 54
        $host = gethostname();
189 54
        $hostLength = strlen($host);
190 54
        $userLength = strlen(Config::getUser());
191 54
        $passLength = strlen(Config::getPassword());
192
193 54
        $data = pack('l', 18 + $hostLength + $userLength + $passLength);
194 54
        $data .= chr(ConstCommand::COM_REGISTER_SLAVE);
195 54
        $data .= pack('V', Config::getSlaveId());
196 54
        $data .= pack('C', $hostLength);
197 54
        $data .= $host;
198 54
        $data .= pack('C', $userLength);
199 54
        $data .= Config::getUser();
200 54
        $data .= pack('C', $passLength);
201 54
        $data .= Config::getPassword();
202 54
        $data .= pack('v', Config::getPort());
203 54
        $data .= pack('V', 0);
204 54
        $data .= pack('V', 0);
205
206 54
        $this->socket->writeToSocket($data);
207 54
        $this->getResponse();
208 54
    }
209
210
    /**
211
     * @see https://dev.mysql.com/doc/internals/en/com-binlog-dump-gtid.html
212
     * @throws BinLogException
213
     * @throws GtidException
214
     * @throws \MySQLReplication\Socket\SocketException
215
     */
216
    private function setBinLogDumpGtid()
217
    {
218
        $collection = GtidFactory::makeCollectionFromString(Config::getGtid());
219
220
        $data = pack('l', 26 + $collection->getEncodedLength()) . chr(ConstCommand::COM_BINLOG_DUMP_GTID);
221
        $data .= pack('S', 0);
222
        $data .= pack('I', Config::getSlaveId());
223
        $data .= pack('I', 3);
224
        $data .= chr(0);
225
        $data .= chr(0);
226
        $data .= chr(0);
227
        $data .= BinaryDataReader::pack64bit(4);
228
        $data .= pack('I', $collection->getEncodedLength());
229
        $data .= $collection->getEncoded();
230
231
        $this->socket->writeToSocket($data);
232
        $this->getResponse();
233
    }
234
235
    /**
236
     * @see https://dev.mysql.com/doc/internals/en/com-binlog-dump.html
237
     * @throws BinLogException
238
     * @throws \MySQLReplication\Socket\SocketException
239
     */
240 54
    private function setBinLogDump()
241
    {
242 54
        $binFilePos = Config::getBinLogPosition();
243 54
        $binFileName = Config::getBinLogFileName();
244 54
        if (0 === $binFilePos && '' === $binFileName) {
245 54
            $master = $this->repository->getMasterStatus();
246 54
            $binFilePos = $master['Position'];
247 54
            $binFileName = $master['File'];
248 54
        }
249
250 54
        $data = pack('i', strlen($binFileName) + 11) . chr(ConstCommand::COM_BINLOG_DUMP);
251 54
        $data .= pack('I', $binFilePos);
252 54
        $data .= pack('v', 0);
253 54
        $data .= pack('I', Config::getSlaveId());
254 54
        $data .= $binFileName;
255
256 54
        $this->socket->writeToSocket($data);
257 54
        $this->getResponse();
258 54
    }
259
}
260