Completed
Pull Request — master (#85)
by
unknown
01:26
created

Client::log()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
nc 2
cc 3
nop 2
1
<?php
2
3
/**
4
 * This file is part of the php-epp2 library.
5
 *
6
 * (c) Gunter Grodotzki <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE file
9
 * that was distributed with this source code.
10
 */
11
12
namespace AfriCC\EPP;
13
14
use AfriCC\EPP\Frame\Command\Logout as LogoutCommand;
15
use Exception;
16
use Psr\Log\LoggerAwareInterface;
17
use Psr\Log\LoggerAwareTrait;
18
19
/**
20
 * A high level TCP (SSL) based client for the Extensible Provisioning Protocol (EPP)
21
 *
22
 * @see http://tools.ietf.org/html/rfc5734
23
 *
24
 * As this class deals directly with sockets it's untestable
25
 * @codeCoverageIgnore
26
 */
27
class Client extends AbstractClient implements ClientInterface, LoggerAwareInterface
28
{
29
    use LoggerAwareTrait;
30
31
    protected $socket;
32
    protected $chunk_size;
33
    protected $verify_peer_name;
34
35
    public function __construct(array $config, ObjectSpec $objectSpec = null)
36
    {
37
        parent::__construct($config, $objectSpec);
38
39
        if (!empty($config['chunk_size'])) {
40
            $this->chunk_size = (int) $config['chunk_size'];
41
        } else {
42
            $this->chunk_size = 1024;
43
        }
44
45
        if (isset($config['verify_peer_name'])) {
46
            $this->verify_peer_name = (bool) $config['verify_peer_name'];
47
        } else {
48
            $this->verify_peer_name = true;
49
        }
50
51
        if ($this->port === false) {
52
            // if not set, default port is 700
53
            $this->port = 700;
54
        }
55
    }
56
57
    public function __destruct()
58
    {
59
        $this->close();
60
    }
61
62
    /**
63
     * Setup context in case of ssl connection
64
     *
65
     * @return resource|null
66
     */
67
    private function setupContext()
68
    {
69
        if (!$this->ssl) {
70
            return null;
71
        }
72
73
        $context = stream_context_create();
74
75
        $options_array = [
76
            'verify_peer' => false,
77
            'verify_peer_name' => $this->verify_peer_name,
78
            'allow_self_signed' => true,
79
            'local_cert' => $this->local_cert,
80
            'passphrase' => $this->passphrase,
81
            'cafile' => $this->ca_cert,
82
            'local_pk' => $this->pk_cert,
83
        ];
84
85
        // filter out empty user provided values
86
        $options_array = array_filter(
87
            $options_array,
88
            function ($var) {
89
                return !is_null($var);
90
            }
91
        );
92
93
        $options = ['ssl' => $options_array];
94
95
        // stream_context_set_option accepts array of options in form of $arr['wrapper']['option'] = value
96
        stream_context_set_option($context, $options);
97
98
        return $context;
99
    }
100
101
    /**
102
     * Setup connection socket
103
     *
104
     * @param resource|null $context SSL context or null in case of tcp connection
105
     *
106
     * @throws Exception on socket errors
107
     */
108
    private function setupSocket($context = null)
109
    {
110
        $proto = $this->ssl ? 'ssl' : 'tcp';
111
        $target = sprintf('%s://%s:%d', $proto, $this->host, $this->port);
112
113
        $errno = 0;
114
        $errstr = '';
115
116
        $this->socket = @stream_socket_client($target, $errno, $errstr, $this->connect_timeout, STREAM_CLIENT_CONNECT, $context);
117
118
        if ($this->socket === false) {
119
            // Socket initialization may fail, before system call connect()
120
            // so the $errno is 0 and $errstr isn't populated .
121
            // see https://www.php.net/manual/en/function.stream-socket-client.php#refsect1-function.stream-socket-client-errors
122
            throw new Exception(sprintf('problem initializing socket: %s code: [%d]', $errstr, $errno), $errno);
123
        }
124
125
        // set stream time out
126
        if (!stream_set_timeout($this->socket, $this->timeout)) {
127
            throw new Exception('unable to set stream timeout');
128
        }
129
130
        // set to non-blocking
131
        if (!stream_set_blocking($this->socket, 0)) {
132
            throw new Exception('unable to set blocking');
133
        }
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     *
139
     * @see \AfriCC\EPP\ClientInterface::connect()
140
     */
141
    public function connect($newPassword = false)
142
    {
143
        $context = $this->setupContext();
144
        $this->setupSocket($context);
145
146
        // get greeting
147
        $greeting = $this->getFrame();
148
149
        // login
150
        $this->login($newPassword);
151
152
        // return greeting
153
        return $greeting;
154
    }
155
156
    /**
157
     * Closes a previously opened EPP connection
158
     */
159
    public function close()
160
    {
161
        if ($this->active()) {
162
            // send logout frame
163
            $this->request(new LogoutCommand($this->objectSpec));
164
165
            return fclose($this->socket);
166
        }
167
168
        return false;
169
    }
170
171
    public function getFrame()
172
    {
173
        $hard_time_limit = time() + $this->timeout + 2;
174
        do {
175
            $header = $this->recv(4);
176
        } while (empty($header) && (time() < $hard_time_limit));
177
178
        if (time() >= $hard_time_limit) {
179
            throw new Exception('Timeout while reading header from EPP Server');
180
        }
181
182
        // Unpack first 4 bytes which is our length
183
        $unpacked = unpack('N', $header);
184
        $length = $unpacked[1];
185
186
        if ($length < 5) {
187
            throw new Exception(sprintf('Got a bad frame header length of %d bytes from peer', $length));
188
        } else {
189
            $length -= 4;
190
191
            return $this->recv($length);
192
        }
193
    }
194
195
    public function sendFrame(FrameInterface $frame)
196
    {
197
        $buffer = (string) $frame;
198
        $header = pack('N', mb_strlen($buffer, 'ASCII') + 4);
199
200
        return $this->send($header . $buffer);
201
    }
202
203
    protected function debugLog($message, $color = '0;32')
204
    {
205
        if ($message === '' || !$this->debug) {
206
            return;
207
        }
208
        echo sprintf("\033[%sm%s\033[0m", $color, $message);
209
    }
210
211
    /**
212
     * check if socket is still active
213
     *
214
     * @return bool
215
     */
216
    private function active()
217
    {
218
        return !is_resource($this->socket) || feof($this->socket) ? false : true;
219
    }
220
221
    /**
222
     * receive socket data
223
     *
224
     * @param int $length
225
     *
226
     * @throws Exception
227
     *
228
     * @return string
229
     */
230
    private function recv($length)
231
    {
232
        $result = '';
233
234
        $info = stream_get_meta_data($this->socket);
235
        $hard_time_limit = time() + $this->timeout + 2;
236
237
        while (!$info['timed_out'] && !feof($this->socket)) {
238
            // Try read remaining data from socket
239
            $buffer = @fread($this->socket, $length - mb_strlen($result, 'ASCII'));
240
241
            // If the buffer actually contains something then add it to the result
242
            if ($buffer !== false) {
243
                $this->debugLog($buffer);
244
                $result .= $buffer;
245
246
                // break if all data received
247
                if (mb_strlen($result, 'ASCII') === $length) {
248
                    break;
249
                }
250
            } else {
251
                // sleep 0.25s
252
                usleep(250000);
253
            }
254
255
            // update metadata
256
            $info = stream_get_meta_data($this->socket);
257
            if (time() >= $hard_time_limit) {
258
                throw new Exception('Timeout while reading from EPP Server');
259
            }
260
        }
261
262
        // check for timeout
263
        if ($info['timed_out']) {
264
            throw new Exception('Timeout while reading data from socket');
265
        }
266
267
        return $result;
268
    }
269
270
    /**
271
     * send data to socket
272
     *
273
     * @param string $buffer
274
     */
275
    private function send($buffer)
276
    {
277
        $info = stream_get_meta_data($this->socket);
278
        $hard_time_limit = time() + $this->timeout + 2;
279
        $length = mb_strlen($buffer, 'ASCII');
280
281
        $pos = 0;
282
        while (!$info['timed_out'] && !feof($this->socket)) {
283
            // Some servers don't like a lot of data, so keep it small per chunk
284
            $wlen = $length - $pos;
285
286
            if ($wlen > $this->chunk_size) {
287
                $wlen = $this->chunk_size;
288
            }
289
290
            // try write remaining data from socket
291
            $written = @fwrite($this->socket, mb_substr($buffer, $pos, $wlen, 'ASCII'), $wlen);
292
293
            // If we read something, bump up the position
294
            if ($written) {
295
                if ($this->debug) {
296
                    $this->debugLog(mb_substr($buffer, $pos, $wlen, 'ASCII'), '1;31');
297
                }
298
                $pos += $written;
299
300
                // break if all written
301
                if ($pos === $length) {
302
                    break;
303
                }
304
            } else {
305
                // sleep 0.25s
306
                usleep(250000);
307
            }
308
309
            // update metadata
310
            $info = stream_get_meta_data($this->socket);
311
            if (time() >= $hard_time_limit) {
312
                throw new Exception('Timeout while writing to EPP Server');
313
            }
314
        }
315
316
        // check for timeout
317
        if ($info['timed_out']) {
318
            throw new Exception('Timeout while writing data to socket');
319
        }
320
321
        if ($pos !== $length) {
322
            throw new Exception('Writing short %d bytes', $length - $pos);
323
        }
324
325
        return $pos;
326
    }
327
}
328