*/ /** TCP Client * Allow to create TCP connections to a server. * If both IPv6 and IPv4 are allowed by the server, try in IPv6 then back in * IPv4 if it doesn't works. * If the name of the server is provided instead of the IP, look for all the * IP address, manage the CNAME aliases. * If multiple addresses are available in IPv6 or IPv4, randomize them to allow * round-robin connections to the server. * Manage the timeout, send a command, receive a max number of bytes, * allow SSL trafic with CA verification */ class tcpclient { // PROPERTIES // // {{{ /** The IPv6 allowed for the server */ private $ipv6 = array (); /** The IPv4 allowed for the server */ private $ipv4 = array (); /** The ipOrName parameter */ private $ipOrName = null; /** The port to connect to the server */ private $port = null; /** Prefer the IPv4 connection either the IPv6 is valid */ private $preferIPv4 = false; /** The internal socket connected to the server */ private $socket = null; /** Read in Text mode or in Binary mode */ private $readMode = "text"; /** The timeout before aborting the connection * 30s by default */ private $timeout = 30; // }}} /** Initialize the object, by setting the name or the IP of the server * @param string $ipOrName The IP or the name of the server * @param integer $port The port of the server to connect */ public function __construct ($ipOrName, $port) // {{{ { $providedIpOrName = $ipOrName; if (filter_var ($ipOrName, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) $this->ipv4 = array ($ipOrName); elseif (filter_var ($ipOrName, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) $this->ipv6 = array ($ipOrName); else { $i = 0; while (empty ($this->ipv4) && empty ($this->ipv6) && $i < 10) { $nsRecords = @dns_get_record ($ipOrName, DNS_A + DNS_AAAA); if ($nsRecords === false || $nsRecords == array ()) { // There is some problems with CNAME if they are not defined. // So enter in this case only if there is no other solution $nsRecords = @dns_get_record ($ipOrName, DNS_CNAME); if ($nsRecords === false || $nsRecords == array ()) throw new \Exception ("Can not find the IP for $ipOrName : ". "DNS Error (No A, AAAA, CNAME entries)", 523); } foreach ($nsRecords as $val) { if ($val["type"] === "CNAME") $ipOrName = $val["target"]; elseif ($val["type"] === "A") $this->ipv4[] = $val["ip"]; elseif ($val["type"] === "AAAA") $this->ipv6[] = $val["ipv6"]; } $i++; } if ($i >= 10) throw new \Exception ("Can not find the IP for $ipOrName : ". "CNAME loop", 404); if (empty ($this->ipv4) && empty ($this->ipv6)) throw new \Exception ("Can not find the IP for $ipOrName : ". "No A or AAAA record", 404); } $port = intval ($port); if ($port < 0 || $port > 65535) throw new \Exception ("Invalid port provided to connection to server", 500); $this->ipOrName = $providedIpOrName; $this->port = $port; shuffle ($this->ipv6); shuffle ($this->ipv4); } // }}} /** Set/get the preferIPv4 property * @param boolean|null $preferIPv4 The preferIPv4 property to set (or to get * if null) */ public function preferIPv4 ($preferIPv4 = null) // {{{ { if ($preferIPv4 === null) return $this->preferIPv4; if ($this->socket !== null) throw new \Exception ("Can not connect in IPv4 prefered to server ". " $this->ipOrName : The server is already connected", 500); $this->preferIPv4 = !!$preferIPv4; return $this; } // }}} /** Set/get the read mode : text or binary * @param string|null $readMode The mode to set (or get if null) */ public function readMode ($readMode = null) // {{{ { if ($readMode === null) return $this->readMode; if ($readMode !== "text" && $readMode !== "binary") throw new \Exception ("Invalid readMode provided (nor text nor binary)", 500); $this->readMode = $readMode; return $this; } // }}} /** Set/get the timeout * @param integer|null $timeout The timeout in seconds */ public function timeout ($timeout = null) // {{{ { if ($timeout === null) return $this->timeout; $this->timeout = intval ($timeout); return $this; } // }}} /** Initialize the connection to the server * Return the socket */ public function connect () // {{{ { if ($this->preferIPv4) $ips = array_merge ($this->ipv4, $this->ipv6); else $ips = array_merge ($this->ipv6, $this->ipv4); foreach ($ips as $ip) { if (filter_var ($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) $ip = "[$ip]"; $socket = @stream_socket_client ("tcp://$ip:$this->port", $errno, $errstr, $this->timeout); if ($socket === false) continue; $this->socket = $socket; return $this->socket; } if ($errno === 110) throw new \Exception ("Can not connect to server $this->ipOrName : ". "Connection timed out", 522); if ($errno === 111) throw new \Exception ("Can not connect to server $this->ipOrName : ". "Connection refused", 521); if ($errno === 113) throw new \Exception ("Can not connect to server $this->ipOrName : ". "No route to host", 523); throw new \Exception ("Can not connect to server $this->ipOrName : ". $errstr, 500); } // }}} /** Activate the SSL connection. * Put the socket in blocking mode, as it is mandatory to have SSL connection * @param boolean $val True to activate, false to disable SSL * @param integer|null $cryptoMethod The cryptoMethod allowed * @param array|null $options Can overload the SSL options if needed * @return false if the client can not found a encryption method with the * server */ public function cryptoEnable ($val, $cryptoMethod = null, $options = array ()) // {{{ { if ($this->socket === null) throw new \Exception ("Can not send to server $this->ipOrName : ". "The server is not connected", 500); if ($cryptoMethod === null) $cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT| STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT| STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; $optionsBase = array ("ssl" => array ( "peer_name" => $this->ipOrName, "verify_peer" => true, "verify_peer_name" => true, "capture_peer_cert" => true, "capture_peer_cert_chain" => true, "SNI_enabled" => true, )); $optionsMerged["ssl"] = array_merge ($optionsBase["ssl"], $options); stream_set_blocking ($this->socket, true); stream_context_set_option ($this->socket, $optionsMerged); $php_errormsg = ""; ini_set ("track_errors", 1); $rc = @stream_socket_enable_crypto ($this->socket, !!$val, $cryptoMethod); ini_set ("track_errors", 0); if ($rc === false) throw new \Exception ("Can not enable crypto to '$this->ipOrName' : ". substr (strrchr ($php_errormsg, ":"), 1), 500); return $rc; } // }}} /** Send a data to the server. * The connection must be established * @param mixed $data The data to send */ public function send ($data) // {{{ { if ($this->socket === null) throw new \Exception ("Can not send to server $this->ipOrName : ". "The server is not connected", 500); stream_set_timeout ($this->socket, $this->timeout); $length = strlen ($data); $sentLen = @fwrite ($this->socket, $data); if ($sentLen < $length) throw new \Exception ("Can not send to server $this->ipOrName", 500); return $sentLen; } // }}} /** Read the data from the server. * The connection must be established * Use the readMode in text or binary (text by default) * In text mode, the read return when found the first \r\n, and doesn't * returns the \r\n. * @param integer $maxLength Limit the length of the data from the server * @return The content */ public function read ($maxLength = 1024) // {{{ { if ($this->socket === null) throw new \Exception ("Can not read from server $this->ipOrName : ". "The server is not connected", 500); stream_set_timeout ($this->socket, $this->timeout); if ($this->readMode === "text") { $read = stream_get_line ($this->socket, $maxLength, "\r\n"); if ($read === false) throw new \Exception ("Can not read from server in text", 500); } else { $read = fread ($this->socket, $maxLength); if ($read === false) throw new \Exception ("Can not read from server in binary" , 500); } return $read; } // }}} /** Disconnect the socket */ public function disconnect () // {{{ { if ($this->socket === null) throw new \Exception ("Can not disconnect server $this->ipOrName : ". "The server is not connected", 500); @stream_socket_shutdown ($this->socket, STREAM_SHUT_RDWR); $this->socket = null; } // }}} /** Get meta data, like the timeout state, the crypto protocol and ciphers... */ public function getMeta () // {{{ { return stream_get_meta_data ($this->socket); } // }}} /** Get the connection peer address, peer port and localaddress and localport * @return array */ public function getInfo () // {{{ { if ($this->socket === null) throw new \Exception ("Can not getInfo for server $this->ipOrName : ". "The server is not connected", 500); $name = stream_socket_get_name ($this->socket, false); $pos = strrpos ($name, ":"); $localPort = substr ($name, $pos+1); $localAddress = substr ($name, 0, $pos); $name = stream_socket_get_name ($this->socket, true); $pos = strrpos ($name, ":"); $port = substr ($name, $pos+1); $address = substr ($name, 0, $pos); return array ($address, $port, $localAddress, $localPort); } // }}} /** Get the socket to direct access * @return resource The socket with the client */ public function getSock () // {{{ { return $this->socket; } // }}} }