DayZ-Stat-Server/GameQ/Server.php
2021-02-02 15:14:05 +01:00

380 lines
9.4 KiB
PHP

<?php
/**
* This file is part of GameQ.
*
* GameQ is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* GameQ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace GameQ;
use GameQ\Exception\Server as Exception;
/**
* Server class to represent each server entity
*
* @author Austin Bischoff <austin@codebeard.com>
*/
class Server
{
/*
* Server array keys
*/
const SERVER_TYPE = 'type';
const SERVER_HOST = 'host';
const SERVER_ID = 'id';
const SERVER_OPTIONS = 'options';
/*
* Server options keys
*/
/*
* Use this option when the query_port and client connect ports are different
*/
const SERVER_OPTIONS_QUERY_PORT = 'query_port';
/**
* The protocol class for this server
*
* @type \GameQ\Protocol
*/
protected $protocol = null;
/**
* Id of this server
*
* @type string
*/
public $id = null;
/**
* IP Address of this server
*
* @type string
*/
public $ip = null;
/**
* The server's client port (connect port)
*
* @type int
*/
public $port_client = null;
/**
* The server's query port
*
* @type int
*/
public $port_query = null;
/**
* Holds other server specific options
*
* @type array
*/
protected $options = [];
/**
* Holds the sockets already open for this server
*
* @type array
*/
protected $sockets = [];
/**
* Construct the class with the passed options
*
* @param array $server_info
*
* @throws \GameQ\Exception\Server
*/
public function __construct(array $server_info = [])
{
// Check for server type
if (!array_key_exists(self::SERVER_TYPE, $server_info) || empty($server_info[self::SERVER_TYPE])) {
throw new Exception("Missing server info key '" . self::SERVER_TYPE . "'!");
}
// Check for server host
if (!array_key_exists(self::SERVER_HOST, $server_info) || empty($server_info[self::SERVER_HOST])) {
throw new Exception("Missing server info key '" . self::SERVER_HOST . "'!");
}
// IP address and port check
$this->checkAndSetIpPort($server_info[self::SERVER_HOST]);
// Check for server id
if (array_key_exists(self::SERVER_ID, $server_info) && !empty($server_info[self::SERVER_ID])) {
// Set the server id
$this->id = $server_info[self::SERVER_ID];
} else {
// Make an id so each server has an id when returned
$this->id = sprintf('%s:%d', $this->ip, $this->port_client);
}
// Check and set server options
if (array_key_exists(self::SERVER_OPTIONS, $server_info)) {
// Set the options
$this->options = $server_info[self::SERVER_OPTIONS];
}
try {
// Make the protocol class for this type
$class = new \ReflectionClass(
sprintf('GameQ\\Protocols\\%s', ucfirst(strtolower($server_info[self::SERVER_TYPE])))
);
$this->protocol = $class->newInstanceArgs([$this->options]);
} catch (\ReflectionException $e) {
throw new Exception("Unable to locate Protocols class for '{$server_info[self::SERVER_TYPE]}'!");
}
// Check and set any server options
$this->checkAndSetServerOptions();
unset($server_info, $class);
}
/**
* Check and set the ip address for this server
*
* @param $ip_address
*
* @throws \GameQ\Exception\Server
*/
protected function checkAndSetIpPort($ip_address)
{
// Test for IPv6
if (substr_count($ip_address, ':') > 1) {
// See if we have a port, input should be in the format [::1]:27015 or similar
if (strstr($ip_address, ']:')) {
// Explode to get port
$server_addr = explode(':', $ip_address);
// Port is the last item in the array, remove it and save
$this->port_client = (int)array_pop($server_addr);
// The rest is the address, recombine
$this->ip = implode(':', $server_addr);
unset($server_addr);
} else {
// Just the IPv6 address, no port defined, fail
throw new Exception(
"The host address '{$ip_address}' is missing the port. All "
. "servers must have a port defined!"
);
}
// Now let's validate the IPv6 value sent, remove the square brackets ([]) first
if (!filter_var(trim($this->ip, '[]'), FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV6,])) {
throw new Exception("The IPv6 address '{$this->ip}' is invalid.");
}
} else {
// We have IPv4 with a port defined
if (strstr($ip_address, ':')) {
list($this->ip, $this->port_client) = explode(':', $ip_address);
// Type case the port
$this->port_client = (int)$this->port_client;
} else {
// No port, fail
throw new Exception(
"The host address '{$ip_address}' is missing the port. All "
. "servers must have a port defined!"
);
}
// Validate the IPv4 value, if FALSE is not a valid IP, maybe a hostname. Try to resolve
if (!filter_var($this->ip, FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV4,])
&& $this->ip === gethostbyname($this->ip)
) {
// When gethostbyname() fails it returns the original string
// so if ip and the result from gethostbyname() are equal this failed.
throw new Exception("Unable to resolve the host '{$this->ip}' to an IP address.");
}
}
}
/**
* Check and set any server specific options
*/
protected function checkAndSetServerOptions()
{
// Specific query port defined
if (array_key_exists(self::SERVER_OPTIONS_QUERY_PORT, $this->options)) {
$this->port_query = (int)$this->options[self::SERVER_OPTIONS_QUERY_PORT];
} else {
// Do math based on the protocol class
$this->port_query = $this->protocol->findQueryPort($this->port_client);
}
}
/**
* Set an option for this server
*
* @param $key
* @param $value
*
* @return $this
*/
public function setOption($key, $value)
{
$this->options[$key] = $value;
return $this; // Make chainable
}
/**
* Return set option value
*
* @param mixed $key
*
* @return mixed
*/
public function getOption($key)
{
return (array_key_exists($key, $this->options)) ? $this->options[$key] : null;
}
/**
* Get the ID for this server
*
* @return string
*/
public function id()
{
return $this->id;
}
/**
* Get the IP address for this server
*
* @return string
*/
public function ip()
{
return $this->ip;
}
/**
* Get the client port for this server
*
* @return int
*/
public function portClient()
{
return $this->port_client;
}
/**
* Get the query port for this server
*
* @return int
*/
public function portQuery()
{
return $this->port_query;
}
/**
* Return the protocol class for this server
*
* @return \GameQ\Protocol
*/
public function protocol()
{
return $this->protocol;
}
/**
* Get the join link for this server
*
* @return string
*/
public function getJoinLink()
{
return sprintf($this->protocol->joinLink(), $this->ip, $this->portClient());
}
/*
* Socket holding
*/
/**
* Add a socket for this server to be reused
*
* @codeCoverageIgnore
*
* @param \GameQ\Query\Core $socket
*/
public function socketAdd(Query\Core $socket)
{
$this->sockets[] = $socket;
}
/**
* Get a socket from the list to reuse, if any are available
*
* @codeCoverageIgnore
*
* @return \GameQ\Query\Core|null
*/
public function socketGet()
{
$socket = null;
if (count($this->sockets) > 0) {
$socket = array_pop($this->sockets);
}
return $socket;
}
/**
* Clear any sockets still listed and attempt to close them
*
* @codeCoverageIgnore
*/
public function socketCleanse()
{
// Close all of the sockets available
foreach ($this->sockets as $socket) {
/* @var $socket \GameQ\Query\Core */
$socket->close();
}
// Reset the sockets list
$this->sockets = [];
}
}