From 44b7b3409446068ee0a8ce79aa59a8fba2c2d3b7 Mon Sep 17 00:00:00 2001 From: Dominique Fournier Date: Fri, 5 Jan 2018 09:47:20 +0000 Subject: [PATCH] Add tcp client and tcp server support git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@4014 bf3deb0d-5f1a-0410-827f-c0cc1f45334c --- Tests/tcpclientTest.php | 72 ++++++++++ tcpclient.php | 249 +++++++++++++++++++++++++++++++++ tcpserver.php | 301 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 622 insertions(+) create mode 100644 Tests/tcpclientTest.php create mode 100644 tcpclient.php create mode 100644 tcpserver.php diff --git a/Tests/tcpclientTest.php b/Tests/tcpclientTest.php new file mode 100644 index 0000000..411be41 --- /dev/null +++ b/Tests/tcpclientTest.php @@ -0,0 +1,72 @@ +preferIPv4 (true); + $tcpclient->connect (); + $tcpclient->send ("GET / HTTP/1.1\r\n". + "Host: www.google.fr\r\n". + "User-Agent: DomFramework\r\n". + "Accept: *"."/*\r\n". + "\r\n"); + $res = ""; + while (($read = $tcpclient->read ()) !== "") + $res .= $read."\r\n"; + $tcpclient->disconnect (); + $this->assertSame (substr ($res, 0, 15), "HTTP/1.1 200 OK"); + } + + public function test_GoogleIPv4orIpv6 () + { + $tcpclient = new \tcpclient ("www.google.fr", 80); + $tcpclient->connect (); + $tcpclient->send ("GET / HTTP/1.1\r\n". + "Host: www.google.fr\r\n". + "User-Agent: DomFramework\r\n". + "Accept: *"."/*\r\n". + "\r\n"); + $res = ""; + while (($read = $tcpclient->read ()) !== "") + $res .= $read."\r\n"; + $tcpclient->disconnect (); + $this->assertSame (substr ($res, 0, 15), "HTTP/1.1 200 OK"); + } + + public function test_GoogleSSL () + { + $tcpclient = new \tcpclient ("www.google.fr", 443); + $tcpclient->connect (); + $tcpclient->cryptoEnable (true); + $tcpclient->send ("GET / HTTP/1.1\r\n". + "Host: www.google.fr\r\n". + "User-Agent: DomFramework\r\n". + "Accept: */*\r\n". + "\r\n"); + $res = ""; + while (($read = $tcpclient->read ()) !== "") + $res .= $read."\r\n"; + $tcpclient->disconnect (); + $this->assertSame (substr ($res, 0, 15), "HTTP/1.1 200 OK"); + } + + public function test_GoogleSSLIPv6 () + { + $tcpclient = new \tcpclient ("ipv6.google.com", 443); + $tcpclient->connect (); + $tcpclient->cryptoEnable (true); + $tcpclient->send ("GET / HTTP/1.1\r\n". + "Host: www.google.fr\r\n". + "User-Agent: DomFramework\r\n". + "Accept: */*\r\n". + "\r\n"); + $res = ""; + while (($read = $tcpclient->read ()) !== "") + $res .= $read."\r\n"; + $tcpclient->disconnect (); + $this->assertSame (substr ($res, 0, 15), "HTTP/1.1 200 OK"); + } +} diff --git a/tcpclient.php b/tcpclient.php new file mode 100644 index 0000000..40ea465 --- /dev/null +++ b/tcpclient.php @@ -0,0 +1,249 @@ +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_AAAA + DNS_A + DNS_CNAME); + 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", 500); + if (empty ($this->ipv4) && empty ($this->ipv6)) + throw new \Exception ("Can not find the IP for $ipOrName : ". + "No A or AAAA record", 500); + } + $port = intval ($port); + if ($port < 0 || $port > 65535) + throw new \Exception ("Invalid port provided to connection to server", + 500); + $this->ipOrName = $ipOrName; + $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; + } + + /** 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); + if ($socket === false) + continue; + $this->socket = $socket; + return $this->socket; + } + 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 $cryptoMethod The cryptoMethod allowed + * @return false if the client can not found a encryption method with the + * server + */ + public function cryptoEnable ($val, + $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT) + { + if ($this->socket === null) + throw new \Exception ("Can not send to server $this->ipOrName : ". + "The server is not connected", 500); + // Setting the options allow the IP to be decided by the connect and valid + // the certificate of the server by the name + $options = array ("ssl" => array ( + "peer_name" => $this->ipOrName, + // PHP doesn't supports *.google.com, so I need to disable the peer name + // verification. Error is : + // Peer certificate CN=`*.google.com' did not match expected + // CN=`ipv6.l.google.com' + "verify_peer_name" => false, + "SNI_enabled" => true, + )); + stream_set_blocking ($this->socket, true); + stream_context_set_option ($this->socket, $options); + return stream_socket_enable_crypto ($this->socket, !!$val, $cryptoMethod); + } + + /** 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); + $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); + if ($this->readMode === "text") + { + $read = stream_get_line ($this->socket, $maxLength, "\r\n"); + if ($read === false) + throw new \Exception ("Can not read from server", 500); + } + else + { + $read = @fread ($this->socket, $maxLength); + if ($read === false) + throw new \Exception ("Can not read from server" , 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 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; + } +} diff --git a/tcpserver.php b/tcpserver.php new file mode 100644 index 0000000..b332d48 --- /dev/null +++ b/tcpserver.php @@ -0,0 +1,301 @@ +maxChild; + $this->maxChild = intval ($val); + 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 the address, port and handler that will be enabled by loop + * @param string $address The server address (can be 0.0.0.0 for all IPv4 + * interfaces or :: for all the IPv4 and IPv6 interfaces) + * @param integer $port The port to listen + * @param callable $handler The handler that will be called when a client is + * connected to the address:port + */ + public function init ($address, $port, $handler) + { + $this->handlers["$address:$port"] = $handler; + $this->addresses[] = $address; + $this->ports[] = $port; + return $this; + } + + /** Start the main loop after the init + */ + public function loop () + { + foreach ($this->addresses as $key => $address) + { + $port = $this->ports[$key]; + if (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) + $sockServer[$key] = stream_socket_server ("tcp://$address:$port", + $errno, $errstr); + elseif (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) + $sockServer[$key] = stream_socket_server ("tcp://[$address]:$port", + $errno, $errstr); + else + throw new \Exception ("Can't create socket : invalid address provided"); + if ($sockServer[$key] === false) + { + throw new \Exception ("Can't create socket : $errstr"); + } + //printf ("Listening on %s:%d...\n", $address, $port); + } + $nbChild = 1; + while (1) + { + // Watch if a child process is zombie, then destroy it + if (pcntl_wait ($status, WNOHANG) > 0) + $nbChild--; + $read = $sockServer; + $write = null; + $except = null; + if (stream_select ($read, $write, $except, 0, 200000) < 1) + continue; + foreach ($read as $sock) + { + $client = stream_socket_accept ($sock); + $name = stream_socket_get_name ($client, false); + $pos = strrpos ($name, ":"); + $localPort = substr ($name, $pos+1); + $localAddress = substr ($name, 0, $pos); + $name = stream_socket_get_name ($client, true); + $pos = strrpos ($name, ":"); + $port = substr ($name, $pos+1); + $address = substr ($name, 0, $pos); + $handlerAddress = $localAddress; + if (! key_exists ("$handlerAddress:$localPort", $this->handlers)) + { + // If the address of the handler doesn't exists, the socket is maybe + // waiting on all the interfaces addresses + if (filter_var ($localAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) + $handlerAddress = "0.0.0.0"; + elseif (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) + $handlerAddress = "::"; + if (! key_exists ("$handlerAddress:$localPort", $this->handlers)) + { + printf ("Can't find the handler for %s:%d\n", $localAddress, + $localPort); + continue; + } + } + //echo "Enter $address:$port > $localAddress:$localPort\n"; + $handler = $this->handlers["$handlerAddress:$localPort"]; + // We got a connection fork to manage it + if ($nbChild > $this->maxChild) + { + printf ("Too much child process %d : Abort the connection !!\n", + $nbChild); + @stream_socket_shutdown ($client, STREAM_SHUT_RDWR); + continue; + } + $pid = pcntl_fork(); + if ($pid == -1) + { + throw new \Exception ("TCPServer can not fork", 500); + } + else if ($pid) + { + // parent process: return to the main loop + $nbChild++; + continue; + } + + // In the child. Will call the handler with the actual tcpserver object + // as parameter + $this->parent = false; + $this->socket = $client; + if (is_array ($handler)) + { + $object = $handler[0]; + $method = $handler[1]; + $object->$method ($this); + } + else + { + $function = $handler; + $function ($this); + } + exit; + } + } + } + + /** In child, get the socket to direct access + * @return resource The socket with the client + */ + public function getSock () + { + if ($this->parent === true) + throw new \Exception ("Can not return the socket in parent mode", 500); + if ($this->socket === null) + throw new \Exception ("Can not send to client : not connected", 500); + return $this->socket; + } + + /** Get an array with the peer address, peer port, local address and local + * port + * @return array array ("peer address", peer port, "local address", local + * port) + */ + public function getInfo () + { + if ($this->parent === true) + throw new \Exception ("Can not get info in parent mode", 500); + if ($this->socket === null) + throw new \Exception ("Can not send to client : 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); + } + + /** 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 $cryptoMethod The cryptoMethod allowed + */ + public function cryptoEnable ($val, + $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_SERVER) + { + if ($this->socket === null) + throw new \Exception ("Can not send to server $this->ipOrName : ". + "The server is not connected", 500); + stream_set_blocking ($this->socket, true); + return stream_socket_enable_crypto ($this->socket, !!$val, $cryptoMethod); + } + + /** Set context SSL option. + * @param array $options The ssl array to set + */ + public function setSSLOptions ($options) + { + if ($this->socket === null) + throw new \Exception ("Can not send to server $this->ipOrName : ". + "The server is not connected", 500); + return stream_context_set_option ($this->socket, array ("ssl" => $options)); + } + + /** Send data to the client + * @param mixed $data The data to send + * @return the length of data sent + */ + public function send ($data) + { + if ($this->parent === true) + throw new \Exception ("Can not send data in parent mode", 500); + if ($this->socket === null) + throw new \Exception ("Can not send to client : not connected", 500); + $length = strlen ($data); + $sentLen = @fwrite ($this->socket, $data); + if ($sentLen < $length) + throw new \Exception ("Can not send data to client", 500); + return $sentLen; + } + + /** Read the data from the client. + * 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 or the first \n. + * @param integer $maxLength Limit the length of the data from the server + * @return The content + */ + public function read ($maxLength = 1024) + { + if ($this->parent === true) + throw new \Exception ("Can not read data in parent mode", 500); + if ($this->socket === null) + throw new \Exception ("Can not read from client : not connected", 500); + if ($this->readMode === "text") + { + $read = stream_get_line ($this->socket, $maxLength, "\r\n"); + if ($read === false) + throw new \Exception ("Can not read from client : ". + error_get_last ()["message"], 500); + } + else + { + $read = @fread ($this->socket, $maxLength); + if ($read === false) + throw new \Exception ("Can not read from client : ". + error_get_last ()["message"], 500); + } + return $read; + } + + /** Disconnect the socket + */ + public function disconnect () + { + if ($this->parent === true) + throw new \Exception ("Can not send data in parent mode", 500); + if ($this->socket === null) + throw new \Exception ("Can not disconnect client : not connected", 500); + @stream_socket_shutdown ($this->socket, STREAM_SHUT_RDWR); + $this->socket = null; + } +}