address_local,$this->port_local); socket_getpeername($connection,$this->address_remote,$this->port_remote); Log::info(sprintf('%s:+ Connection host [%s] on port [%d]',self::LOGKEY,$this->address_remote,$this->port_remote)); $this->connection = $connection; } public function __get($key) { switch ($key) { case 'address_remote': case 'port_remote': return $this->{$key}; case 'cps': case 'speed': return Arr::get($this->session,$key); default: throw new \Exception(sprintf('%s:! Unknown key [%s]:',self::LOGKEY,$key)); } } public function __set($key,$value) { switch ($key) { case 'cps': case 'speed': return $this->session[$key] = $value; default: throw new \Exception(sprintf('%s:! Unknown key [%s]:',self::LOGKEY,$key)); } } /** * We'll add to our transmit buffer and if doesnt have space, we'll empty it first * * @param string $data * @return void * @throws \Exception */ public function buffer_add(string $data): void { if ($this->DEBUG) Log::debug(sprintf('%s:+ Start [%s] (%d)',self::LOGKEY,$data,strlen($data))); //$rc = self::OK; //$tx_ptr = self::TX_BUF_SIZE-$this->tx_free; $ptr = 0; $num_bytes = strlen($data); $this->tty_status = self::TTY_SUCCESS; while ($num_bytes) { if ($this->DEBUG) Log::debug(sprintf('%s: - Num Bytes [%d]: TX Free [%d]',self::LOGKEY,$num_bytes,$this->tx_free)); if ($num_bytes > $this->tx_free) { do { $this->buffer_flush(5); if ($this->tty_status == self::TTY_SUCCESS) { $n = min($this->tx_free,$num_bytes); $this->tx_buf = substr($data,$ptr,$n); $this->tx_free -= $n; $num_bytes -= $n; $ptr += $n; } } while ($this->tty_status != self::TTY_SUCCESS); } else { if ($this->DEBUG) Log::debug(sprintf('%s: - Remaining data to send [%d]',self::LOGKEY,$num_bytes)); $this->tx_buf .= substr($data,$ptr,$num_bytes); $this->tx_free -= $num_bytes; $num_bytes = 0; } } if ($this->DEBUG) Log::debug(sprintf('%s:= End [%s]',self::LOGKEY,strlen($this->tx_buf))); } /** * Clear our TX buffer */ public function buffer_clear(): void { $this->tx_buf = ''; $this->tx_free = self::TX_BUF_SIZE; } /** * Empty our TX buffer * * @param int $timeout * @return int * @throws \Exception */ public function buffer_flush(int $timeout): int { if ($this->DEBUG) Log::debug(sprintf('%s:+ Start [%d]',self::LOGKEY,$timeout)); $rc = self::OK; $tx_ptr = 0; $restsize = self::TX_BUF_SIZE-$this->tx_free; $tm = $this->timer_set($timeout); while (self::TX_BUF_SIZE != $this->tx_free) { $tv = $this->timer_rest($tm); if ($rc = $this->canSend($tv)>0) { if ($this->DEBUG) Log::debug(sprintf('%s: - Sending [%d]',self::LOGKEY,$restsize)); $rc = $this->send(substr($this->tx_buf,$tx_ptr,$restsize),0); Log::debug(sprintf('%s: - Sent [%d] (%s)',self::LOGKEY,$rc,Str::limit($this->tx_buf,15))); if ($rc == $restsize) { $this->tx_buf = ''; $tx_ptr = 0; $this->tx_free += $rc; $this->buffer_clear(); } else if ($rc > 0) { $tx_ptr += $rc; $restsize -= $rc; } } else { return $rc; } // @todo Enable a delay for slow clients //sleep(1); if ($this->timer_expired($tm)) return self::ERROR; } if ($this->DEBUG) Log::debug(sprintf('%s:= End [%d]',self::LOGKEY,$rc)); return $rc; } /** * @param int $timeout * @return int * @throws \Exception */ public function canSend(int $timeout): int { $write = [$this->connection]; return $this->socketSelect(NULL,$write,NULL,$timeout); } /** * Close the connection with the client */ public function close(): void { socket_shutdown($this->connection); socket_close($this->connection); } /** * Create a client socket * @param string $address * @param int $port * @return static * @throws SocketException */ public static function create(string $address,int $port): self { Log::debug(sprintf('%s:+ Creating connection to [%s:%d]',self::LOGKEY,$address,$port)); $sort = collect(['AAAA','A']); // We only look at AAAA/A records $resolved = collect(dns_get_record($address,DNS_AAAA|DNS_A)) ->filter(function($item) use ($sort) { return $sort->search(Arr::get($item,'type')) !== FALSE; }) ->sort(function($item) use ($sort) { return $sort->search(Arr::get($item,'type')); }); if (! $resolved->count()) throw new SocketException(SocketException::CANT_CONNECT,sprintf('%s doesnt resolved to an IPv4/IPv6 address',$address)); $result = FALSE; foreach ($resolved as $address) { try { $try = Arr::get($address,Arr::get($address,'type') == 'AAAA' ? 'ipv6' : 'ip'); if (! $try) continue; Log::alert(sprintf('%s: - Trying [%s:%d]',self::LOGKEY,$try,$port)); /* Create a TCP/IP socket. */ $socket = socket_create(Arr::get($address,'type') == 'AAAA' ? AF_INET6 : AF_INET,SOCK_STREAM,SOL_TCP); if ($socket === FALSE) throw new SocketException(SocketException::CANT_CREATE_SOCKET,socket_strerror(socket_last_error($socket))); $result = socket_connect($socket,$try,$port); break; } catch (\ErrorException $e) { // If 'Cannot assign requested address' if (socket_last_error($socket) == 99) continue; throw new SocketException(SocketException::CANT_CONNECT,socket_strerror(socket_last_error($socket))); } } if ($result === FALSE) throw new SocketException(SocketException::CANT_CONNECT,socket_strerror(socket_last_error($socket))); return new self($socket); } /** * @param int $timeout * @return int * @note use socketSelect() * @throws \Exception */ public function hasData(int $timeout): int { $read = [$this->connection]; return $this->rx_left ?: $this->socketSelect($read,NULL,NULL,$timeout); } /** * Read data from the socket. * If we only want 1 character, we'll return the ASCII value of the data received * * @param int $timeout * @param int $len * @return int|string * @throws SocketException */ public function read(int $timeout,int $len=1024) { if ($this->DEBUG) Log::debug(sprintf('%s:+ Start [%d] (%d)',self::LOGKEY,$len,$timeout)); if ($timeout AND ($this->hasData($timeout) === 0)) return ''; $buf = ''; try { $rc = socket_recv($this->connection,$buf, $len,MSG_DONTWAIT); } catch (\Exception $e) { Log::error(sprintf('%s: - socket_recv Exception [%s]',self::LOGKEY,$e->getMessage())); throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x)); } if ($this->DEBUG) Log::debug(sprintf('%s: - Read [%d]',self::LOGKEY,$rc)); if ($rc === FALSE) throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x)); // If our buffer is null, see if we have any out of band data. // @todo We throw an errorexception when the socket is closed by the remote I think. if (($rc == 0) && is_nulL($buf) && ($this->hasData(0) > 0)) { try { socket_recv($this->connection,$buf, $len,MSG_OOB); } catch (\Exception $e) { throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x)); } } return is_null($buf) ? '' : $buf; } /** * Read a character from the remote. * We'll buffer everything received * * @param int $timeout * @return int * @throws SocketException */ public function read_ch(int $timeout): int { if ($this->DEBUG) Log::debug(sprintf('%s:+ Start [%d] - rx_left[%d], rx_ptr[%d]',self::LOGKEY,$timeout,$this->rx_left,$this->rx_ptr)); // If our buffer is empty, we'll try and read from the remote if ($this->rx_left == 0) { if ($this->hasData($timeout) > 0) { try { if (! strlen($this->rx_buf = $this->read(0,self::RX_BUF_SIZE))) { if ($this->DEBUG) Log::debug(sprintf('%s: - Nothing read',self::LOGKEY)); return self::TTY_TIMEOUT; } } catch (\Exception $e) { return ($e->getCode() == 11) ? self::TTY_TIMEOUT : self::ERROR; } if ($this->DEBUG) Log::info(sprintf('%s: - Read [%d] bytes',self::LOGKEY,strlen($this->rx_buf))); $this->rx_ptr = 0; $this->rx_left = strlen($this->rx_buf); } else { return self::TTY_TIMEOUT; } } $rc = ord(substr($this->rx_buf,$this->rx_ptr,1)); $this->rx_left--; $this->rx_ptr++; if ($this->DEBUG) Log::debug(sprintf('%s:= Return [%x] (%c)',self::LOGKEY,$rc,$rc)); return $rc; } public function rx_purge(): void { $this->rx_ptr = $this->rx_left = 0; $this->rx_buf = ''; } /** * Send data to the client * * @param $message * @param int $timeout * @param null $length * @return false|int * @throws \Exception */ public function send($message,int $timeout,$length=NULL) { if ($timeout AND (! $rc = $this->canSend($timeout))) return $rc; if (is_null($length)) $length = strlen($message); return socket_write($this->connection,$message,$length); } /** * Wait for data on a socket * * @param array|null $read * @param array|null $write * @param array|null $except * @param int $timeout * @return int * @throws \Exception */ private function socketSelect(?array $read,?array $write,?array $except,int $timeout): int { $rc = socket_select($read,$write,$except,$timeout); if ($rc === FALSE) throw new \Exception('Socket Error: '.socket_strerror(socket_last_error())); if ($this->DEBUG) Log::debug(sprintf('Socket select returned [%d] (%d)',$rc,$timeout),['read'=>$read,'write'=>$write,'except'=>$except]); return $rc; } public function timer_expired(int $timer): int { return (time()>=$timer); } public function timer_rest(int $timer): int { return (($timer)-time()); } public function timer_set(int $expire): int { return (time()+$expire); } /** * See if we there is data waiting to collect, or if we can send * * @param bool $read * @param bool $write * @param int $timeout * @return int * @throws \Exception */ public function ttySelect(bool $read,bool $write, int $timeout): int { $read = $read ? [$this->connection] : NULL; $write = $write ? [$this->connection] : NULL; return $this->socketSelect($read,$write,NULL,$timeout); } }