Completed
Pull Request — master (#82)
by
unknown
01:30
created

Client::close()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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