Completed
Pull Request — master (#85)
by
unknown
01:50
created

Client::sendFrame()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
nc 1
cc 1
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\Command\Logout as LogoutCommand;
15
use Exception;
16
use Psr\Log\LoggerAwareInterface;
17
use Psr\Log\LoggerAwareTrait;
18
19
/**
20
 * A high level TCP (SSL) based client for the Extensible Provisioning Protocol (EPP)
21
 *
22
 * @see http://tools.ietf.org/html/rfc5734
23
 *
24
 * As this class deals directly with sockets it's untestable
25
 * @codeCoverageIgnore
26
 */
27
class Client extends AbstractClient implements ClientInterface, LoggerAwareInterface
28
{
29
    use LoggerAwareTrait;
30
31
    protected $socket;
32
    protected $chunk_size;
33
    protected $verify_peer_name;
34
35
    public function __construct(array $config, ObjectSpec $objectSpec = null)
36
    {
37
        parent::__construct($config, $objectSpec);
38
39
        if (!empty($config['chunk_size'])) {
40
            $this->chunk_size = (int) $config['chunk_size'];
41
        } else {
42
            $this->chunk_size = 1024;
43
        }
44
45
        if (isset($config['verify_peer_name'])) {
46
            $this->verify_peer_name = (bool) $config['verify_peer_name'];
47
        } else {
48
            $this->verify_peer_name = true;
49
        }
50
51
        if ($this->port === false) {
52
            // if not set, default port is 700
53
            $this->port = 700;
54
        }
55
    }
56
57
    public function __destruct()
58
    {
59
        $this->close();
60
    }
61
62
    /**
63
     * Setup context in case of ssl connection
64
     *
65
     * @return resource|null
66
     */
67
    private function setupContext()
68
    {
69
        if (!$this->ssl) {
70
            return null;
71
        }
72
73
        $context = stream_context_create();
74
75
        $options_array = [
76
            'verify_peer' => false,
77
            'verify_peer_name' => $this->verify_peer_name,
78
            'allow_self_signed' => true,
79
            'local_cert' => $this->local_cert,
80
            'passphrase' => $this->passphrase,
81
            'cafile' => $this->ca_cert,
82
            'local_pk' => $this->pk_cert,
83
        ];
84
85
        // filter out empty user provided values
86
        $options_array = array_filter(
87
            $options_array,
88
            function ($var) {
89
                return !is_null($var);
90
            }
91
        );
92
93
        $options = ['ssl' => $options_array];
94
95
        // stream_context_set_option accepts array of options in form of $arr['wrapper']['option'] = value
96
        stream_context_set_option($context, $options);
97
98
        return $context;
99
    }
100
101
    /**
102
     * Setup connection socket
103
     *
104
     * @param resource|null $context SSL context or null in case of tcp connection
105
     *
106
     * @throws Exception on socket errors
107
     */
108
    private function setupSocket($context = null)
109
    {
110
        $proto = $this->ssl ? 'ssl' : 'tcp';
111
        $target = sprintf('%s://%s:%d', $proto, $this->host, $this->port);
112
113
        $errno = 0;
114
        $errstr = '';
115
116
        $this->socket = @stream_socket_client($target, $errno, $errstr, $this->connect_timeout, STREAM_CLIENT_CONNECT, $context);
117
118
        if ($this->socket === false) {
119
            // Socket initialization may fail, before system call connect()
120
            // so the $errno is 0 and $errstr isn't populated .
121
            // see https://www.php.net/manual/en/function.stream-socket-client.php#refsect1-function.stream-socket-client-errors
122
            throw new Exception(sprintf('problem initializing socket: %s code: [%d]', $errstr, $errno), $errno);
123
        }
124
125
        // set stream time out
126
        if (!stream_set_timeout($this->socket, $this->timeout)) {
127
            throw new Exception('unable to set stream timeout');
128
        }
129
130
        // set to non-blocking
131
        if (!stream_set_blocking($this->socket, 0)) {
132
            throw new Exception('unable to set blocking');
133
        }
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     *
139
     * @see \AfriCC\EPP\ClientInterface::connect()
140
     */
141
    public function connect($newPassword = false)
142
    {
143
        $context = $this->setupContext();
144
        $this->setupSocket($context);
145
146
        // get greeting
147
        $greeting = $this->getFrame();
148
149
        // login
150
        $this->login($newPassword);
151
152
        // return greeting
153
        return $greeting;
154
    }
155
156
    /**
157
     * Closes a previously opened EPP connection
158
     */
159
    public function close()
160
    {
161
        if ($this->active()) {
162
            // send logout frame
163
            $this->request(new LogoutCommand($this->objectSpec));
164
165
            return fclose($this->socket);
166
        }
167
168
        return false;
169
    }
170
171
    public function getFrame()
172
    {
173
        $hard_time_limit = time() + $this->timeout + 2;
174
        do {
175
            $header = $this->recv(4);
176
        } while (empty($header) && (time() < $hard_time_limit));
177
178
        if (time() >= $hard_time_limit) {
179
            throw new Exception('Timeout while reading header from EPP Server');
180
        }
181
182
        // Unpack first 4 bytes which is our length
183
        $unpacked = unpack('N', $header);
184
        $length = $unpacked[1];
185
186
        if ($length < 5) {
187
            throw new Exception(sprintf('Got a bad frame header length of %d bytes from peer', $length));
188
        } else {
189
            $length -= 4;
190
191
            return $this->recv($length);
192
        }
193
    }
194
195
    public function sendFrame(FrameInterface $frame)
196
    {
197
        $buffer = (string) $frame;
198
        $header = pack('N', mb_strlen($buffer, 'ASCII') + 4);
199
200
        return $this->send($header . $buffer);
201
    }
202
203
    protected function debugLog($message, $color = '0;32')
204
    {
205
        if ($message === '' || !$this->debug) {
206
            return;
207
        }
208
        echo sprintf("\033[%sm%s\033[0m", $color, $message);
209
    }
210
211
    /**
212
     * check if socket is still active
213
     *
214
     * @return bool
215
     */
216
    private function active()
217
    {
218
        return !is_resource($this->socket) || feof($this->socket) ? false : true;
219
    }
220
221
    /**
222
     * receive socket data
223
     *
224
     * @param int $length
225
     *
226
     * @throws Exception
227
     *
228
     * @return string
229
     */
230
    private function recv($length)
231
    {
232
        $result = '';
233
234
        $info = stream_get_meta_data($this->socket);
235
        $hard_time_limit = time() + $this->timeout + 2;
236
237
        while (!$info['timed_out'] && !feof($this->socket)) {
238
            // Try read remaining data from socket
239
            $buffer = @fread($this->socket, $length - mb_strlen($result, 'ASCII'));
240
241
            // If the buffer actually contains something then add it to the result
242
            if ($buffer !== false) {
243
                $this->debuLog($buffer);
0 ignored issues
show
Bug introduced by
The method debuLog() does not exist on AfriCC\EPP\Client. Did you maybe mean debugLog()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
244
                $result .= $buffer;
245
246
                // break if all data received
247
                if (mb_strlen($result, 'ASCII') === $length) {
248
                    break;
249
                }
250
            } else {
251
                // sleep 0.25s
252
                usleep(250000);
253
            }
254
255
            // update metadata
256
            $info = stream_get_meta_data($this->socket);
257
            if (time() >= $hard_time_limit) {
258
                throw new Exception('Timeout while reading from EPP Server');
259
            }
260
        }
261
262
        // check for timeout
263
        if ($info['timed_out']) {
264
            throw new Exception('Timeout while reading data from socket');
265
        }
266
267
        return $result;
268
    }
269
270
    /**
271
     * send data to socket
272
     *
273
     * @param string $buffer
274
     */
275
    private function send($buffer)
276
    {
277
        $info = stream_get_meta_data($this->socket);
278
        $hard_time_limit = time() + $this->timeout + 2;
279
        $length = mb_strlen($buffer, 'ASCII');
280
281
        $pos = 0;
282
        while (!$info['timed_out'] && !feof($this->socket)) {
283
            // Some servers don't like a lot of data, so keep it small per chunk
284
            $wlen = $length - $pos;
285
286
            if ($wlen > $this->chunk_size) {
287
                $wlen = $this->chunk_size;
288
            }
289
290
            // try write remaining data from socket
291
            $written = @fwrite($this->socket, mb_substr($buffer, $pos, $wlen, 'ASCII'), $wlen);
292
293
            // If we read something, bump up the position
294
            if ($written) {
295
                if ($this->debug) {
296
                    $this->debugLog(mb_substr($buffer, $pos, $wlen, 'ASCII'), '1;31');
297
                }
298
                $pos += $written;
299
300
                // break if all written
301
                if ($pos === $length) {
302
                    break;
303
                }
304
            } else {
305
                // sleep 0.25s
306
                usleep(250000);
307
            }
308
309
            // update metadata
310
            $info = stream_get_meta_data($this->socket);
311
            if (time() >= $hard_time_limit) {
312
                throw new Exception('Timeout while writing to EPP Server');
313
            }
314
        }
315
316
        // check for timeout
317
        if ($info['timed_out']) {
318
            throw new Exception('Timeout while writing data to socket');
319
        }
320
321
        if ($pos !== $length) {
322
            throw new Exception('Writing short %d bytes', $length - $pos);
323
        }
324
325
        return $pos;
326
    }
327
}
328