Completed
Pull Request — master (#34)
by
unknown
02:01
created

Client::httpParseHeaders()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 15
nc 5
nop 1
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\ResponseFactory;
15
use AfriCC\EPP\Frame\Response as ResponseFrame;
16
use AfriCC\EPP\Frame\Hello as HelloFrame;
17
use AfriCC\EPP\Frame\Command\Login as LoginCommand;
18
use AfriCC\EPP\Frame\Command\Logout as LogoutCommand;
19
use Exception;
20
21
/**
22
 * A high level TCP (SSL) based client for the Extensible Provisioning Protocol (EPP)
23
 * @link http://tools.ietf.org/html/rfc5734
24
 */
25
class Client
26
{
27
    protected $socket;
28
    protected $host;
29
    protected $port;
30
    protected $username;
31
    protected $password;
32
    protected $services;
33
    protected $serviceExtensions;
34
    protected $protocol;
35
    protected $local_cert;
36
    protected $passphrase;
37
    protected $debug;
38
    protected $connect_timeout;
39
    protected $timeout;
40
    protected $chunk_size;
41
    protected $curl_cookie;
42
43
    public function __construct(array $config)
44
    {
45
        if (!empty($config['host'])) {
46
            $this->host = (string) $config['host'];
47
        }
48
49
        if (!empty($config['port'])) {
50
            $this->port = (int) $config['port'];
51
        } else {
52
            $this->port = 700;
53
        }
54
55
        if (!empty($config['username'])) {
56
            $this->username = (string) $config['username'];
57
        }
58
59
        if (!empty($config['password'])) {
60
            $this->password = (string) $config['password'];
61
        }
62
63
        if (!empty($config['services']) && is_array($config['services'])) {
64
            $this->services = $config['services'];
65
66
            if (!empty($config['serviceExtensions']) && is_array($config['serviceExtensions'])) {
67
                $this->serviceExtensions = $config['serviceExtensions'];
68
            }
69
        }
70
71
        if (!empty($config['protocol'])) {
72
            $this->protocol = (string) $config['protocol'];
73
        }
74
75
        if (!empty($config['local_cert'])) {
76
            $this->local_cert = (string) $config['local_cert'];
77
78
            if (!is_readable($this->local_cert)) {
79
                throw new Exception(sprintf('unable to read local_cert: %s', $this->local_cert));
80
            }
81
82
            if (!empty($config['passphrase'])) {
83
                $this->passphrase = $config['passphrase'];
84
            }
85
        }
86
87
        if (!empty($config['debug'])) {
88
            $this->debug = true;
89
        } else {
90
            $this->debug = false;
91
        }
92
93
        if (!empty($config['connect_timeout'])) {
94
            $this->connect_timeout = (int) $config['connect_timeout'];
95
        } else {
96
            $this->connect_timeout = 4;
97
        }
98
99
        if (!empty($config['timeout'])) {
100
            $this->timeout = (int) $config['timeout'];
101
        } else {
102
            $this->timeout = 8;
103
        }
104
105
        if (!empty($config['chunk_size'])) {
106
            $this->chunk_size = (int) $config['chunk_size'];
107
        } else {
108
            $this->chunk_size = 1024;
109
        }
110
    }
111
112
    public function __destruct()
113
    {
114
        $this->close();
115
    }
116
117
    /**
118
     * Open a new connection to the EPP server
119
     */
120
    public function connect()
121
    {
122
        if ($this->protocol == 'http' || $this->protocol == 'https') {
123
            $proto = $this->protocol;
124
        } elseif ($this->protocol == 'ssl' || $this->protocol == 'tls') {
125
            $proto = $this->protocol;
126
127
            $context = stream_context_create();
128
            stream_context_set_option($context, 'ssl', 'verify_peer', false);
129
            stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
130
131
            if ($this->local_cert !== null) {
132
                stream_context_set_option($context, 'ssl', 'local_cert', $this->local_cert);
133
134
                if ($this->passphrase) {
135
                    stream_context_set_option($context, 'ssl', 'passphrase', $this->passphrase);
136
                }
137
            }
138
        } else {
139
            $proto = 'tcp';
140
        }
141
142
        if ($this->protocol != 'http' && $this->protocol != 'https') {
143
            $target = sprintf('%s://%s:%d', $proto, $this->host, $this->port);
144
145
            if (isset($context) && is_resource($context)) {
146
                $this->socket = @stream_socket_client($target, $errno, $errstr, $this->connect_timeout, STREAM_CLIENT_CONNECT, $context);
147
            } else {
148
                $this->socket = @stream_socket_client($target, $errno, $errstr, $this->connect_timeout, STREAM_CLIENT_CONNECT);
149
            }
150
151
            if ($this->socket === false) {
152
                throw new Exception($errstr, $errno);
153
            }
154
155
            // set stream time out
156
            if (!stream_set_timeout($this->socket, $this->timeout)) {
157
                throw new Exception('unable to set stream timeout');
158
            }
159
160
            // set to non-blocking
161
            if (!stream_set_blocking($this->socket, 0)) {
162
                throw new Exception('unable to set blocking');
163
            }
164
        }
165
166
        // get greeting
167
        if ($this->protocol == 'http' || $this->protocol == 'https') {
168
            $frame = new HelloFrame;
169
            $greeting = $this->request($frame);
170
        } else {
171
            $greeting = $this->getFrame();
172
        }
173
174
        // login
175
        $this->login();
176
177
        // return greeting
178
        return $greeting;
179
    }
180
181
    /**
182
     * Closes a previously opened EPP connection
183
     */
184
    public function close()
185
    {
186
        if ($this->protocol == 'http' || $this->protocol == 'https') {
187
            // send logout frame
188
            $this->request(new LogoutCommand);
189
            return true;
190
        } else {
191
            if ($this->active()) {
192
                // send logout frame
193
                $this->request(new LogoutCommand);
194
                return fclose($this->socket);
195
            }
196
            return false;
197
        }
198
    }
199
200
    /**
201
     * Get an EPP frame from the server.
202
     */
203
    public function getFrame()
204
    {
205
        $header = $this->recv(4);
206
207
        // Unpack first 4 bytes which is our length
208
        $unpacked = unpack('N', $header);
209
        $length = $unpacked[1];
210
211
        if ($length < 5) {
212
            throw new Exception(sprintf('Got a bad frame header length of %d bytes from peer', $length));
213
        } else {
214
            $length -= 4;
215
            return ResponseFactory::build($this->recv($length));
216
        }
217
    }
218
219
    /**
220
     * sends a XML-based frame to the server
221
     * @param FrameInterface $frame the frame to send to the server
222
     */
223
    public function sendFrame(FrameInterface $frame)
224
    {
225
        // some frames might require a client transaction identifier, so let us
226
        // inject it before sending the frame
227
        if ($frame instanceof TransactionAwareInterface) {
228
            $frame->setClientTransactionId($this->generateClientTransactionId());
229
        }
230
231
        if ($this->protocol == 'http' || $this->protocol == 'https') {
232
            if ($this->port == 80 || $this->port == 443) {
233
                $target = sprintf('%s://%s', $this->protocol, $this->host);
234
            } else {
235
                $target = sprintf('%s://%s:%d', $this->protocol, $this->host, $this->port);
236
            }
237
238
            $curlHandle = curl_init();
239
240
            curl_setopt($curlHandle, CURLOPT_URL, $target);
241
            curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 0);
242
            curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, 0);
243
            curl_setopt($curlHandle, CURLOPT_POST, 1);
244
            curl_setopt($curlHandle, CURLOPT_POSTFIELDS, trim($frame));
245
            curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array('Content-Type: text/xml'));
246
            curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
247
248
            if ($this->local_cert !== null) {
249
                curl_setopt($curlHandle, CURLOPT_SSLCERT, $this->local_cert);
250
                if ($this->passphrase) {
251
                    curl_setopt($curlHandle, CURLOPT_SSLCERTPASSWD, $this->passphrase);
252
                }
253
            }
254
255
            if ($this->curl_cookie !== null) {
256
                curl_setopt($curlHandle, CURLOPT_COOKIE, $this->curl_cookie);
257
            }
258
259
            curl_setopt($curlHandle, CURLINFO_HEADER_OUT, 1);
260
            curl_setopt($curlHandle, CURLOPT_HEADER, 1);
261
262
            $response = curl_exec($curlHandle);
263
264
            if ($response === false) {
265
                $curlerror = curl_error($curlHandle);
266
                curl_close($curlHandle);
267
                throw new Exception($curlerror.' ('.$target.')');
268
            } else {
269
                $header_size = curl_getinfo($curlHandle, CURLINFO_HEADER_SIZE);
270
                $curlHeader = substr($response, 0, $header_size);
271
                $res = substr($response, $header_size);
272
                curl_close($curlHandle);
273
            }
274
275
            $curlHeader = $this->httpParseHeaders($curlHeader);
276
277
            if (!empty($curlHeader['Set-Cookie'])) {
278
                $this->curl_cookie = $curlHeader['Set-Cookie'];
279
            }
280
281
            return $res;
282
        } else {
283
            $buffer = (string) $frame;
284
            $header = pack('N', mb_strlen($buffer, 'ASCII') + 4);
285
            return $this->send($header.$buffer);
286
        }
287
    }
288
289
    /**
290
     * a wrapper around sendFrame() and getFrame()
291
     */
292
    public function request(FrameInterface $frame)
293
    {
294
        if ($this->protocol == 'http' || $this->protocol == 'https') {
295
            return $this->sendFrame($frame);
296
        } else {
297
            $this->sendFrame($frame);
298
299
            return $this->getFrame();
300
        }
301
    }
302
303
    /**
304
     * check if socket is still active
305
     * @return boolean
306
     */
307
    public function active()
308
    {
309
        return (!is_resource($this->socket) || feof($this->socket) ? false : true);
310
    }
311
312
    protected function login()
313
    {
314
        // send login command
315
        $login = new LoginCommand;
316
        $login->setClientId($this->username);
317
        $login->setPassword($this->password);
318
        $login->setVersion('1.0');
319
        $login->setLanguage('en');
320
321
        if (!empty($this->services) && is_array($this->services)) {
322
            foreach ($this->services as $urn) {
323
                $login->addService($urn);
324
            }
325
326
            if (!empty($this->serviceExtensions) && is_array($this->serviceExtensions)) {
327
                foreach ($this->serviceExtensions as $extension) {
328
                    $login->addServiceExtension($extension);
329
                }
330
            }
331
        }
332
333
        $response = $this->request($login);
334
        unset($login);
335
336
        // check if login was successful
337
        if (!($response instanceof ResponseFrame)) {
338
            throw new Exception('there was a problem logging onto the EPP server');
339
        } elseif ($response->code() !== 1000) {
340
            throw new Exception($response->message(), $response->code());
341
        }
342
    }
343
344
    protected function log($message, $color = '0;32')
345
    {
346
        if ($message === '') {
347
            return;
348
        }
349
        echo sprintf("\033[%sm%s\033[0m", $color, $message);
350
    }
351
352
    protected function generateClientTransactionId()
353
    {
354
        return Random::id(64, $this->username);
355
    }
356
357
    /**
358
     * receive socket data
359
     * @param int $length
360
     * @throws Exception
361
     * @return string
362
     */
363
    private function recv($length)
364
    {
365
        $result = '';
366
367
        $info = stream_get_meta_data($this->socket);
368
        $hard_time_limit = time() + $this->timeout + 2;
369
370
        while (!$info['timed_out'] && !feof($this->socket)) {
371
372
            // Try read remaining data from socket
373
            $buffer = @fread($this->socket, $length - mb_strlen($result, 'ASCII'));
374
375
            // If the buffer actually contains something then add it to the result
376
            if ($buffer !== false) {
377
                if ($this->debug) {
378
                    $this->log($buffer);
379
                }
380
381
                $result .= $buffer;
382
383
                // break if all data received
384
                if (mb_strlen($result, 'ASCII') === $length) {
385
                    break;
386
                }
387
            } else {
388
                // sleep 0.25s
389
                usleep(250000);
390
            }
391
392
            // update metadata
393
            $info = stream_get_meta_data($this->socket);
394
            if (time() >= $hard_time_limit) {
395
                throw new Exception('Timeout while reading from EPP Server');
396
            }
397
        }
398
399
        // check for timeout
400
        if ($info['timed_out']) {
401
            throw new Exception('Timeout while reading data from socket');
402
        }
403
404
        return $result;
405
    }
406
407
    /**
408
     * send data to socket
409
     * @param string $buffer
410
     */
411
    private function send($buffer)
412
    {
413
        $info = stream_get_meta_data($this->socket);
414
        $hard_time_limit = time() + $this->timeout + 2;
415
        $length = mb_strlen($buffer, 'ASCII');
416
417
        $pos = 0;
418
        while (!$info['timed_out'] && !feof($this->socket)) {
419
            // Some servers don't like alot of data, so keep it small per chunk
420
            $wlen = $length - $pos;
421
422
            if ($wlen > $this->chunk_size) {
423
                $wlen = $this->chunk_size;
424
            }
425
426
            // try write remaining data from socket
427
            $written = @fwrite($this->socket, mb_substr($buffer, $pos, $wlen, 'ASCII'), $wlen);
428
429
            // If we read something, bump up the position
430
            if ($written) {
431
                if ($this->debug) {
432
                    $this->log(mb_substr($buffer, $pos, $wlen, 'ASCII'), '1;31');
433
                }
434
                $pos += $written;
435
436
                // break if all written
437
                if ($pos === $length) {
438
                    break;
439
                }
440
            } else {
441
                // sleep 0.25s
442
                usleep(250000);
443
            }
444
445
            // update metadata
446
            $info = stream_get_meta_data($this->socket);
447
            if (time() >= $hard_time_limit) {
448
                throw new Exception('Timeout while writing to EPP Server');
449
            }
450
        }
451
452
        // check for timeout
453
        if ($info['timed_out']) {
454
            throw new Exception('Timeout while writing data to socket');
455
        }
456
457
        if ($pos !== $length) {
458
            throw new Exception('Writing short %d bytes', $length - $pos);
459
        }
460
461
        return $pos;
462
    }
463
464
    private function httpParseHeaders($header) {
465
        $retVal = array();
466
        $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header));
467
        foreach( $fields as $field ) {
468
            if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
469
                $match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
470
                if( isset($retVal[$match[1]]) ) {
471
                    if ( is_array( $retVal[$match[1]] ) ) {
472
                        $i = count($retVal[$match[1]]);
473
                        $retVal[$match[1]][$i] = $match[2];
474
                    }
475
                    else {
476
                        $retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
477
                    }
478
                } else {
479
                    $retVal[$match[1]] = trim($match[2]);
480
                }
481
            }
482
        }
483
        return $retVal;
484
    }
485
}
486