1<?php
2
3/**
4 * DNS Library for handling lookups and updates.
5 *
6 * Copyright (c) 2020, Mike Pultz <mike@mikepultz.com>. All rights reserved.
7 *
8 * See LICENSE for more details.
9 *
10 * @category  Networking
11 * @package   Net_DNS2
12 * @author    Mike Pultz <mike@mikepultz.com>
13 * @copyright 2020 Mike Pultz <mike@mikepultz.com>
14 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
15 * @link      https://netdns2.com/
16 * @since     File available since Release 0.6.0
17 *
18 */
19
20/*
21 * check to see if the socket defines exist; if they don't, then define them
22 */
23if (defined('SOCK_STREAM') == false) {
24    define('SOCK_STREAM', 1);
25}
26if (defined('SOCK_DGRAM') == false) {
27    define('SOCK_DGRAM', 2);
28}
29
30/**
31 * Socket handling class using the PHP Streams
32 *
33 */
34class Net_DNS2_Socket
35{
36    private $sock;
37    private $type;
38    private $host;
39    private $port;
40    private $timeout;
41    private $context;
42
43    /*
44     * the local IP and port we'll send the request from
45     */
46    private $local_host;
47    private $local_port;
48
49    /*
50     * the last error message on the object
51     */
52    public $last_error;
53
54    /*
55     * date the socket connection was created, and the date it was last used
56     */
57    public $date_created;
58    public $date_last_used;
59
60    /*
61     * type of sockets
62     */
63    const SOCK_STREAM   = SOCK_STREAM;
64    const SOCK_DGRAM    = SOCK_DGRAM;
65
66    /**
67     * constructor - set the port details
68     *
69     * @param integer $type    the socket type
70     * @param string  $host    the IP address of the DNS server to connect to
71     * @param integer $port    the port of the DNS server to connect to
72     * @param integer $timeout the timeout value to use for socket functions
73     *
74     * @access public
75     *
76     */
77    public function __construct($type, $host, $port, $timeout)
78    {
79        $this->type         = $type;
80        $this->host         = $host;
81        $this->port         = $port;
82        $this->timeout      = $timeout;
83        $this->date_created = microtime(true);
84    }
85
86    /**
87     * destructor
88     *
89     * @access public
90     */
91    public function __destruct()
92    {
93        $this->close();
94    }
95
96    /**
97     * sets the local address/port for the socket to bind to
98     *
99     * @param string $address the local IP address to bind to
100     * @param mixed  $port    the local port to bind to, or 0 to let the socket
101     *                        function select a port
102     *
103     * @return boolean
104     * @access public
105     *
106     */
107    public function bindAddress($address, $port = 0)
108    {
109        $this->local_host = $address;
110        $this->local_port = $port;
111
112        return true;
113    }
114
115    /**
116     * opens a socket connection to the DNS server
117     *
118     * @return boolean
119     * @access public
120     *
121     */
122    public function open()
123    {
124        //
125        // create a list of options for the context
126        //
127        $opts = [ 'socket' => [] ];
128
129        //
130        // bind to a local IP/port if it's set
131        //
132        if (strlen($this->local_host) > 0) {
133
134            $opts['socket']['bindto'] = $this->local_host;
135            if ($this->local_port > 0) {
136
137                $opts['socket']['bindto'] .= ':' . $this->local_port;
138            }
139        }
140
141        //
142        // create the context
143        //
144        $this->context = @stream_context_create($opts);
145
146        //
147        // create socket
148        //
149        $errno;
150        $errstr;
151
152        switch($this->type) {
153        case Net_DNS2_Socket::SOCK_STREAM:
154
155            if (Net_DNS2::isIPv4($this->host) == true) {
156
157                $this->sock = @stream_socket_client(
158                    'tcp://' . $this->host . ':' . $this->port,
159                    $errno, $errstr, $this->timeout,
160                    STREAM_CLIENT_CONNECT, $this->context
161                );
162            } else if (Net_DNS2::isIPv6($this->host) == true) {
163
164                $this->sock = @stream_socket_client(
165                    'tcp://[' . $this->host . ']:' . $this->port,
166                    $errno, $errstr, $this->timeout,
167                    STREAM_CLIENT_CONNECT, $this->context
168                );
169            } else {
170
171                $this->last_error = 'invalid address type: ' . $this->host;
172                return false;
173            }
174
175            break;
176
177        case Net_DNS2_Socket::SOCK_DGRAM:
178
179            if (Net_DNS2::isIPv4($this->host) == true) {
180
181                $this->sock = @stream_socket_client(
182                    'udp://' . $this->host . ':' . $this->port,
183                    $errno, $errstr, $this->timeout,
184                    STREAM_CLIENT_CONNECT, $this->context
185                );
186            } else if (Net_DNS2::isIPv6($this->host) == true) {
187
188                $this->sock = @stream_socket_client(
189                    'udp://[' . $this->host . ']:' . $this->port,
190                    $errno, $errstr, $this->timeout,
191                    STREAM_CLIENT_CONNECT, $this->context
192                );
193            } else {
194
195                $this->last_error = 'invalid address type: ' . $this->host;
196                return false;
197            }
198
199            break;
200
201        default:
202            $this->last_error = 'Invalid socket type: ' . $this->type;
203            return false;
204        }
205
206        if ($this->sock === false) {
207
208            $this->last_error = $errstr;
209            return false;
210        }
211
212        //
213        // set it to non-blocking and set the timeout
214        //
215        @stream_set_blocking($this->sock, 0);
216        @stream_set_timeout($this->sock, $this->timeout);
217
218        return true;
219    }
220
221    /**
222     * closes a socket connection to the DNS server
223     *
224     * @return boolean
225     * @access public
226     *
227     */
228    public function close()
229    {
230        if (is_resource($this->sock) === true) {
231
232            @fclose($this->sock);
233        }
234        return true;
235    }
236
237    /**
238     * writes the given string to the DNS server socket
239     *
240     * @param string $data a binary packed DNS packet
241     *
242     * @return boolean
243     * @access public
244     *
245     */
246    public function write($data)
247    {
248        $length = strlen($data);
249        if ($length == 0) {
250
251            $this->last_error = 'empty data on write()';
252            return false;
253        }
254
255        $read   = null;
256        $write  = [ $this->sock ];
257        $except = null;
258
259        //
260        // increment the date last used timestamp
261        //
262        $this->date_last_used = microtime(true);
263
264        //
265        // select on write
266        //
267        $result = stream_select($read, $write, $except, $this->timeout);
268        if ($result === false) {
269
270            $this->last_error = 'failed on write select()';
271            return false;
272
273        } else if ($result == 0) {
274
275            $this->last_error = 'timeout on write select()';
276            return false;
277        }
278
279        //
280        // if it's a TCP socket, then we need to packet and send the length of the
281        // data as the first 16bit of data.
282        //
283        if ($this->type == Net_DNS2_Socket::SOCK_STREAM) {
284
285            $s = chr($length >> 8) . chr($length);
286
287            if (@fwrite($this->sock, $s) === false) {
288
289                $this->last_error = 'failed to fwrite() 16bit length';
290                return false;
291            }
292        }
293
294        //
295        // write the data to the socket
296        //
297        $size = @fwrite($this->sock, $data);
298        if ( ($size === false) || ($size != $length) ) {
299
300            $this->last_error = 'failed to fwrite() packet';
301            return false;
302        }
303
304        return true;
305    }
306
307    /**
308     * reads a response from a DNS server
309     *
310     * @param integer &$size    the size of the DNS packet read is passed back
311     * @param integer $max_size the max data size returned.
312     *
313     * @return mixed         returns the data on success and false on error
314     * @access public
315     *
316     */
317    public function read(&$size, $max_size)
318    {
319        $read   = [ $this->sock ];
320        $write  = null;
321        $except = null;
322
323        //
324        // increment the date last used timestamp
325        //
326        $this->date_last_used = microtime(true);
327
328        //
329        // make sure our socket is non-blocking
330        //
331        @stream_set_blocking($this->sock, 0);
332
333        //
334        // select on read
335        //
336        $result = stream_select($read, $write, $except, $this->timeout);
337        if ($result === false) {
338
339            $this->last_error = 'error on read select()';
340            return false;
341
342        } else if ($result == 0) {
343
344            $this->last_error = 'timeout on read select()';
345            return false;
346        }
347
348        $data = '';
349        $length = $max_size;
350
351        //
352        // if it's a TCP socket, then the first two bytes is the length of the DNS
353        // packet- we need to read that off first, then use that value for the
354        // packet read.
355        //
356        if ($this->type == Net_DNS2_Socket::SOCK_STREAM) {
357
358            if (($data = fread($this->sock, 2)) === false) {
359
360                $this->last_error = 'failed on fread() for data length';
361                return false;
362            }
363            if (strlen($data) == 0)
364            {
365                $this->last_error = 'failed on fread() for data length';
366                return false;
367            }
368
369            $length = ord($data[0]) << 8 | ord($data[1]);
370            if ($length < Net_DNS2_Lookups::DNS_HEADER_SIZE) {
371
372                return false;
373            }
374        }
375
376        //
377        // at this point, we know that there is data on the socket to be read,
378        // because we've already extracted the length from the first two bytes.
379        //
380        // so the easiest thing to do, is just turn off socket blocking, and
381        // wait for the data.
382        //
383        @stream_set_blocking($this->sock, 1);
384
385        //
386        // read the data from the socket
387        //
388        $data = '';
389
390        //
391        // the streams socket is weird for TCP sockets; it doesn't seem to always
392        // return all the data properly; but the looping code I added broke UDP
393        // packets- my fault-
394        //
395        // the sockets library works much better.
396        //
397        if ($this->type == Net_DNS2_Socket::SOCK_STREAM) {
398
399            $chunk = '';
400            $chunk_size = $length;
401
402            //
403            // loop so we make sure we read all the data
404            //
405            while (1) {
406
407                $chunk = fread($this->sock, $chunk_size);
408                if ($chunk === false) {
409
410                    $this->last_error = 'failed on fread() for data';
411                    return false;
412                }
413
414                $data .= $chunk;
415                $chunk_size -= strlen($chunk);
416
417                if (strlen($data) >= $length) {
418                    break;
419                }
420            }
421
422        } else {
423
424            //
425            // if it's UDP, it's a single fixed-size frame, and the streams library
426            // doesn't seem to have a problem reading it.
427            //
428            $data = fread($this->sock, $length);
429            if ($length === false) {
430
431                $this->last_error = 'failed on fread() for data';
432                return false;
433            }
434        }
435
436        $size = strlen($data);
437
438        return $data;
439    }
440}
441