Completed
Pull Request — master (#64)
by
unknown
01:32
created

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