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