<?php

namespace CleantalkSP\SpbctWP\Helpers;

/**
 * CleanTalk Security Helper class
 *
 * @depends       \CleantalkSP\Common\Helper
 *
 * @package       Security Plugin by CleanTalk
 * @subpackage    Helper
 * @Version       2.0
 * @author        Cleantalk team (welcome@cleantalk.org)
 * @copyright (C) 2014 CleanTalk team (http://cleantalk.org)
 * @license       GNU/GPL: http://www.gnu.org/copyleft/gpl.html
 * @see           https://github.com/CleanTalk/security-malware-firewall
 */
class HTTP extends \CleantalkSP\Common\Helpers\HTTP
{
    /**
     * Get CleanTalk API servers from DNS
     *
     * @return array
     */
    public static function getCleantalksAPIServersFromDNS()
    {
        $servers = [];
        $dns_records = @dns_get_record('api.cleantalk.org', DNS_A);
        if (false === $dns_records) {
            $dns_records = array();
        }

        foreach ($dns_records as $record) {
            if (isset($record['ip'])) {
                $server_host = gethostbyaddr($record['ip']);
                if ( $server_host !== false && $server_host !== $record['ip'] ) {
                    $servers[$server_host] = $record['ip'];
                }
            }
        }

        return $servers;
    }

    /**
     * Sort CleanTalks API servers by response time
     * Wrapper for self::sortHostsByResponseTime()
     *
     * @return array
     */
    public static function getCleantalksAPIServersOrderedByResponseTime()
    {
        return static::sortHostsByResponseTime(
        // Get only apix*.cleantalk.org domains from cleantalk servers
            array_filter(
                static::getCleantalksAPIServersFromDNS(),
                static function ($key) {
                    return (bool)preg_match('/^apix\d\d?\.cleantalk\.org$/', $key);
                },
                ARRAY_FILTER_USE_KEY
            )
        );
    }

    public static function sortHostsByResponseTime($hosts)
    {
        // Get response time for each passed url/host
        $output_records = array();
        $ping_method = self::getPingMethod();
        foreach ($hosts as $dns_name => $host) {
            $output_records[] = array(
                'ping' => self::pingSpbc($host, $ping_method),
                'host' => $host,
                'dns'  => is_numeric($dns_name) ? 'unknown' : $dns_name,
            );
        }

        // Sort by ping value
        $pings = array_column($output_records, 'ping');
        array_multisort(
            $pings,
            SORT_ASC,
            SORT_NUMERIC,
            $output_records
        );

        return $output_records;
    }

    /**
     * @param $host
     * @param $ping_method
     * @return float
     */
    public static function pingSpbc($host, $ping_method)
    {
        if (method_exists(self::class, $ping_method)) {
            return self::$ping_method($host);
        } else {
            return 0.001;
        }
    }

    /**
     * @return string
     */
    private static function getPingMethod()
    {
        if (function_exists('fsockopen') && is_callable('fsockopen')) {
            return 'pingFSockOpen';
        }

        if (function_exists('stream_socket_client') && is_callable('stream_socket_client')) {
            return 'pingStreamSocketClient';
        }

        if (
            function_exists('socket_create') && is_callable('socket_create') &&
            function_exists('socket_set_option') && is_callable('socket_set_option') &&
            function_exists('socket_connect') && is_callable('socket_connect') &&
            function_exists('socket_send') && is_callable('socket_send') &&
            function_exists('socket_read') && is_callable('socket_read') &&
            function_exists('socket_close') && is_callable('socket_close') &&
            defined('AF_INET') &&
            defined('SOL_SOCKET') &&
            defined('SOCK_RAW') &&
            defined('SO_RCVTIMEO')
        ) {
            return 'pingSocketConnect';
        }

        return 'pingCurlRequest';
    }

    /**
     * @param $host
     * @param $port
     * @param $timeout
     * @return false|float
     * @psalm-suppress UnusedMethod
     */
    private static function pingStreamSocketClient($host, $port = 80, $timeout = 1)
    {
        $starttime = microtime(true);
        $file = @stream_socket_client("tcp://$host:$port", $errno, $errstr, $timeout);
        $stoptime = microtime(true);

        if (!$file) {
            return false; // Host is down or unreachable
        }

        fclose($file);

        // Calculate the ping time in milliseconds
        $ping = ($stoptime - $starttime) * 1000;
        return round($ping, 4);
    }

    /**
     * @param $host
     * @return float
     * @psalm-suppress UnusedMethod
     */
    private static function pingFSockOpen($host)
    {
        $starttime = microtime(true);
        $file      = @fsockopen($host, 80, $errno, $errstr, 1500 / 1000);
        $stoptime  = microtime(true);

        if (! $file) {
            $ping = 1500 / 1000;  // Site is down
        } else {
            $ping = ($stoptime - $starttime);
            $ping = round($ping, 4); // Cut microseconds part
            fclose($file);
        }

        // Convert seconds to milliseconds (0.712 s to 712 ms)
        return round($ping * 1000, 4);
    }

    /**
     * @param $host
     * @param $timeout
     * @return float
     * @psalm-suppress UnusedMethod
     */
    private static function pingSocketConnect($host, $timeout = 1)
    {
        try {
            /* ICMP ping packet with a pre-calculated checksum */
            $package = "\x08\x00\x7d\x4b\x00\x00\x00\x00PingHost";
            $socket  = socket_create(AF_INET, SOCK_RAW, 1);
            socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $timeout, 'usec' => 0));
            $connected = socket_connect($socket, $host, 80);
            $ts = microtime(true);
            $sent = socket_send($socket, $package, strLen($package), 0);
            if ($connected && $sent && socket_read($socket, 255)) {
                $result = microtime(true) - $ts;
            } else {
                return self::pingCurlRequest($host);
            }
            socket_close($socket);
            return round($result * 1000, 4);
        } catch (\Exception $e) {
            return self::pingCurlRequest($host);
        }
    }

    /**
     * @param $host
     * @return float
     * @psalm-suppress UnusedMethod
     */
    private static function pingCurlRequest($host)
    {
        $options = array(
            CURLOPT_URL            => 'http://' . $host,
            CURLOPT_TIMEOUT        => 1500,
            CURLOPT_LOW_SPEED_TIME => 25,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 5000,
            CURLOPT_FORBID_REUSE   => true,
            CURLOPT_POST           => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_HTTPHEADER     => array(
                'Expect:', // Fix for large data and old servers http://php.net/manual/ru/function.curl-setopt.php#82418
                'Expires: ' . date(DATE_RFC822, mktime(0, 0, 0, 1, 1, 1971)),
                'Cache-Control: no-store, no-cache, must-revalidate',
                'Cache-Control: post-check=0, pre-check=0',
                'Pragma: no-cache',
            ),
            CURLOPT_FOLLOWLOCATION => false,
        );


        // Make a request
        $ch = curl_init();

        curl_setopt_array($ch, $options);

        $starttime = microtime(true);

        $request_result = curl_exec($ch);    // Gather request result

        if ($request_result === false) {
            $request_result = array('error' => curl_error($ch));
        }

        curl_close($ch);

        $stoptime  = microtime(true);

        if (!empty($request_result['error'])) {
            $ping = 1500 / 1000;  // Site is down
        } else {
            $ping = ($stoptime - $starttime);
            $ping = round($ping, 4); // Cut microseconds part
        }

        // Convert seconds to milliseconds (0.712 s to 712 ms)
        return round($ping * 1000, 4);
    }
}
