Completed
Push — master ( 701151...0cad11 )
by Günter
01:43
created

Client::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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