Completed
Pull Request — master (#64)
by
unknown
02:14 queued 34s
created

Client::getFrame()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
cc 2
nc 2
nop 0
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 AfriCC\EPP\Frame\ResponseFactory;
16
use Exception;
17
18
/**
19
 * A high level TCP (SSL) based client for the Extensible Provisioning Protocol (EPP)
20
 *
21
 * @see http://tools.ietf.org/html/rfc5734
22
 */
23
class Client extends AbstractClient implements ClientInterface
24
{
25
    protected $socket;
26
    protected $chunk_size;
27
    protected $verify_peer_name;
28
29
    public function __construct(array $config)
30
    {
31
        parent::__construct($config);
32
33
        if (!empty($config['chunk_size'])) {
34
            $this->chunk_size = (int) $config['chunk_size'];
35
        } else {
36
            $this->chunk_size = 1024;
37
        }
38
39
        if (!empty($config['verify_peer_name'])) {
40
            $this->verify_peer_name = (bool) $config['verify_peer_name'];
41
        } else {
42
            $this->verify_peer_name = true;
43
        }
44
45
        if (!$this->port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->port of type integer|false is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
46
            // if not set, default port is 700
47
            $this->port = 700;
48
        }
49
    }
50
51
    public function __destruct()
52
    {
53
        $this->close();
54
    }
55
56
    /**
57
     * Setup context in case of ssl connection
58
     *
59
     * @return resource|null
60
     */
61
    private function setupContext()
62
    {
63
        if ($this->ssl) {
64
            $context = stream_context_create();
65
            stream_context_set_option($context, 'ssl', 'verify_peer', false);
66
            stream_context_set_option($context, 'ssl', 'verify_peer_name', $this->verify_peer_name);
67
            stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
68
69
            if ($this->local_cert !== null) {
70
                stream_context_set_option($context, 'ssl', 'local_cert', $this->local_cert);
71
72
                if ($this->passphrase) {
73
                    stream_context_set_option($context, 'ssl', 'passphrase', $this->passphrase);
74
                }
75
            }
76
            if ($this->ca_cert !== null) {
77
                stream_context_set_option($context, 'ssl', 'cafile', $this->ca_cert);
78
            }
79
            if ($this->pk_cert !== null) {
80
                stream_context_set_option($context, 'ssl', 'local_pk', $this->pk_cert);
81
            }
82
83
            return $context;
84
        }
85
86
        return null;
87
    }
88
89
    /**
90
     * Setup connection socket
91
     *
92
     * @param resource|null $context SSL context or null in case of tcp connection
93
     *
94
     * @throws Exception
95
     */
96
    private function setupSocket($context = null)
97
    {
98
        $proto = $this->ssl ? 'ssl' : 'tcp';
99
        $target = sprintf('%s://%s:%d', $proto, $this->host, $this->port);
100
101
        $errno = 0;
102
        $errstr = '';
103
104
        $this->socket = @stream_socket_client($target, $errno, $errstr, $this->connect_timeout, STREAM_CLIENT_CONNECT, $context);
105
106
        if ($this->socket === false) {
107
            throw new Exception($errstr, $errno);
108
        }
109
110
        // set stream time out
111
        if (!stream_set_timeout($this->socket, $this->timeout)) {
112
            throw new Exception('unable to set stream timeout');
113
        }
114
115
        // set to non-blocking
116
        if (!stream_set_blocking($this->socket, 0)) {
117
            throw new Exception('unable to set blocking');
118
        }
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     *
124
     * @see \AfriCC\EPP\ClientInterface::connect()
125
     */
126
    public function connect($newPassword = false)
127
    {
128
        $context = $this->setupContext();
129
        $this->setupSocket($context);
130
131
        // get greeting
132
        $greeting = $this->getFrame();
133
134
        // login
135
        $this->login($newPassword);
136
137
        // return greeting
138
        return $greeting;
139
    }
140
141
    /**
142
     * Closes a previously opened EPP connection
143
     */
144
    public function close()
145
    {
146
        if ($this->active()) {
147
            // send logout frame
148
            $this->request(new LogoutCommand());
149
150
            return fclose($this->socket);
151
        }
152
153
        return false;
154
    }
155
156
    /**
157
     * Get an EPP frame from the server.
158
     */
159
    public function getFrame()
160
    {
161
        $header = $this->recv(4);
162
163
        // Unpack first 4 bytes which is our length
164
        $unpacked = unpack('N', $header);
165
        $length = $unpacked[1];
166
167
        if ($length < 5) {
168
            throw new Exception(sprintf('Got a bad frame header length of %d bytes from peer', $length));
169
        } else {
170
            $length -= 4;
171
172
            return ResponseFactory::build($this->recv($length));
173
        }
174
    }
175
176
    /**
177
     * sends a XML-based frame to the server
178
     *
179
     * @param FrameInterface $frame the frame to send to the server
180
     */
181
    public function sendFrame(FrameInterface $frame)
182
    {
183
        // some frames might require a client transaction identifier, so let us
184
        // inject it before sending the frame
185
        if ($frame instanceof TransactionAwareInterface) {
186
            $frame->setClientTransactionId($this->generateClientTransactionId());
187
        }
188
189
        $buffer = (string) $frame;
190
        $header = pack('N', mb_strlen($buffer, 'ASCII') + 4);
191
192
        return $this->send($header . $buffer);
193
    }
194
195
    /**
196
     * a wrapper around sendFrame() and getFrame()
197
     */
198
    public function request(FrameInterface $frame)
199
    {
200
        $this->sendFrame($frame);
201
202
        return $this->getFrame();
203
    }
204
205
    protected function log($message, $color = '0;32')
206
    {
207
        if ($message === '' || !$this->debug) {
208
            return;
209
        }
210
        echo sprintf("\033[%sm%s\033[0m", $color, $message);
211
    }
212
213
    /**
214
     * check if socket is still active
215
     *
216
     * @return bool
217
     */
218
    private function active()
219
    {
220
        return !is_resource($this->socket) || feof($this->socket) ? false : true;
221
    }
222
223
    /**
224
     * receive socket data
225
     *
226
     * @param int $length
227
     *
228
     * @throws Exception
229
     *
230
     * @return string
231
     */
232
    private function recv($length)
233
    {
234
        $result = '';
235
236
        $info = stream_get_meta_data($this->socket);
237
        $hard_time_limit = time() + $this->timeout + 2;
238
239
        while (!$info['timed_out'] && !feof($this->socket)) {
240
            // Try read remaining data from socket
241
            $buffer = @fread($this->socket, $length - mb_strlen($result, 'ASCII'));
242
243
            // If the buffer actually contains something then add it to the result
244
            if ($buffer !== false) {
245
                $this->log($buffer);
246
                $result .= $buffer;
247
248
                // break if all data received
249
                if (mb_strlen($result, 'ASCII') === $length) {
250
                    break;
251
                }
252
            } else {
253
                // sleep 0.25s
254
                usleep(250000);
255
            }
256
257
            // update metadata
258
            $info = stream_get_meta_data($this->socket);
259
            if (time() >= $hard_time_limit) {
260
                throw new Exception('Timeout while reading from EPP Server');
261
            }
262
        }
263
264
        // check for timeout
265
        if ($info['timed_out']) {
266
            throw new Exception('Timeout while reading data from socket');
267
        }
268
269
        return $result;
270
    }
271
272
    /**
273
     * send data to socket
274
     *
275
     * @param string $buffer
276
     */
277
    private function send($buffer)
278
    {
279
        $info = stream_get_meta_data($this->socket);
280
        $hard_time_limit = time() + $this->timeout + 2;
281
        $length = mb_strlen($buffer, 'ASCII');
282
283
        $pos = 0;
284
        while (!$info['timed_out'] && !feof($this->socket)) {
285
            // Some servers don't like a lot of data, so keep it small per chunk
286
            $wlen = $length - $pos;
287
288
            if ($wlen > $this->chunk_size) {
289
                $wlen = $this->chunk_size;
290
            }
291
292
            // try write remaining data from socket
293
            $written = @fwrite($this->socket, mb_substr($buffer, $pos, $wlen, 'ASCII'), $wlen);
294
295
            // If we read something, bump up the position
296
            if ($written) {
297
                if ($this->debug) {
298
                    $this->log(mb_substr($buffer, $pos, $wlen, 'ASCII'), '1;31');
299
                }
300
                $pos += $written;
301
302
                // break if all written
303
                if ($pos === $length) {
304
                    break;
305
                }
306
            } else {
307
                // sleep 0.25s
308
                usleep(250000);
309
            }
310
311
            // update metadata
312
            $info = stream_get_meta_data($this->socket);
313
            if (time() >= $hard_time_limit) {
314
                throw new Exception('Timeout while writing to EPP Server');
315
            }
316
        }
317
318
        // check for timeout
319
        if ($info['timed_out']) {
320
            throw new Exception('Timeout while writing data to socket');
321
        }
322
323
        if ($pos !== $length) {
324
            throw new Exception('Writing short %d bytes', $length - $pos);
325
        }
326
327
        return $pos;
328
    }
329
}
330