<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see
 * <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\executables;

use seekquarry\atto\TurnSite;
use seekquarry\atto\TimeLimitedTurnAuthenticator;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\CrawlDaemon;

if (php_sapi_name() != 'cli' &&
    !defined("seekquarry\\yioop\\configs\\IS_OWN_WEB_SERVER")) {
    echo "BAD REQUEST";
    exit();
}
/** Yioop does not cache while a background program runs @ignore */
$_SERVER["USE_CACHE"] = false;
/* Logging is wanted here. Other code turns it off for its own work
   and a program started from a request inherits that, so it is said
   plainly before anything is written. */
$_SERVER["NO_LOGGING"] = false;
/** for crawlLog, the settings and the relay itself */
require_once __DIR__ . "/../library/Utility.php";
require_once __DIR__ . "/../library/atto_servers/TurnSite.php";
if (!C\PROFILE) {
    echo "Please configure the search engine instance by visiting" .
        " its web interface on localhost.\n";
    exit();
}
/**
 * TurnServer runs the relay two browsers use to reach each other when
 * neither can be reached directly. A call between two people at home
 * usually cannot be made directly: each is behind a router that lets
 * nothing in unasked. The relay stands where both can reach it and
 * passes what one sends to the other.
 *
 * It is started and stopped the way Yioop's other background programs
 * are, from the Manage Machines screen or by
 * CrawlDaemon::start("TurnServer"), and reports whether it is running in
 * the same way. What it listens on, who may use it, and the range of
 * ports it hands out come from TURN_BIND, TURN_PORT, TURN_REALM,
 * TURN_SECRET and TURN_RELAY_PORTS in Config.php.
 *
 * @author Chris Pollett
 */
class TurnServer
{
    /**
     * How often, in seconds, the relay stops to ask whether it has been
     * told to end. A stop is a request written to a file, so nothing
     * notices it until something looks.
     */
    const PROCESS_POLL_INTERVAL = 1;
    /**
     * How often, in seconds, the relay writes a line saying it is still
     * up, what address it took and how many calls it is carrying. A log
     * that stops growing then means the relay stopped, rather than
     * meaning it had nothing to say.
     */
    const ALIVE_LOG_INTERVAL = 10;
    /**
     * Builds the daemon. What the relay needs is read at start time
     * rather than here, so a site owner who changes a setting between
     * stopping and starting gets the new one without anything else
     * being restarted.
     */
    public function __construct()
    {
    }
    /**
     * Turns the site settings into what the relay is told: the address
     * and port to take, the name and secret a browser gives, the realm
     * the relay names itself by, and the lowest and highest port it may
     * hand out. A range written the wrong way round, or one port, or
     * none, leaves the relay with no range and its own choosing.
     *
     * @param string $bind address the relay should take
     * @param mixed $port port the relay should take
     * @param string $user name a browser gives the relay
     * @param string $secret secret a browser gives with that name
     * @param string $realm name the relay calls itself by
     * @param string $ports lowest and highest port to hand out, with a
     *      dash between them
     * @return array bind, port, users and realm, and low and high
     *      where a range was named
     */
    public static function deriveRelayConfig($bind, $port, $user,
        $secret, $realm, $ports)
    {
        $said = ["BIND" => $bind, "TURN_PORT" => intval($port),
            "users" => [$user => $secret], "realm" => $realm];
        $range = explode("-", (string)$ports);
        if (count($range) == 2 && intval($range[0]) > 0 &&
            intval($range[1]) >= intval($range[0])) {
            $said["low"] = intval($range[0]);
            $said["high"] = intval($range[1]);
        }
        return $said;
    }
    /**
     * deriveRelayPublicAddress works out the address a browser out on
     * the internet can reach the relay's relayed ports at, which the
     * relay hands a browser as where to reach an allocation. The site's
     * own host is where a browser
     * already reaches the relay for its offer and answer, so that host
     * is the one to advertise; it is read from the site's base address,
     * or the name server address where the base gives no host. A host
     * name is resolved to an address, since a relayed address a browser
     * carries has to be an address rather than a name; an address
     * resolves to itself. The loopback and empty are given back as the
     * empty string, since neither serves a browser off the machine and
     * the relay treats the empty string as a sign to keep to loopback.
     *
     * @return string the address to advertise, or the empty string
     *      where none was found or only the loopback was
     */
    public static function deriveRelayPublicAddress()
    {
        $host = parse_url(C\baseUrl(), PHP_URL_HOST);
        if (empty($host)) {
            $host = parse_url(C\p('NAME_SERVER'), PHP_URL_HOST);
        }
        if (empty($host) || $host === 'localhost' ||
            $host === '127.0.0.1' || $host === '::1') {
            return "";
        }
        $address = gethostbyname($host);
        /* gethostbyname gives back the name unchanged where it cannot
           resolve it; that is no use as a relayed address, so treat it
           as none found. An address handed in comes back unchanged too,
           which is what is wanted. */
        if ($address === $host && !filter_var($host,
            FILTER_VALIDATE_IP)) {
            return "";
        }
        return $address;
    }
    /**
     * Starts the relay. CrawlDaemon calls this. It sets up the log
     * file, the process file and the signal handlers, builds the relay
     * from the site's settings, and hands over to the relay's own
     * listening loop, which does not return while the relay is up.
     */
    public function start()
    {
        global $argv;
        CrawlDaemon::init($argv, "TurnServer");
        L\crawlLog("\n\nInitialize logger..", "TurnServer", true);
        $told = self::deriveRelayConfig(C\p('TURN_BIND'),
            C\p('TURN_PORT'), C\p('TURN_USER'), C\p('TURN_SECRET'),
            C\p('TURN_REALM'), C\p('TURN_RELAY_PORTS'));
        $relay = new TurnSite();
        /* Send each thing the relay notices -- a datagram arriving, an
           allocation handed out, a request turned away -- to the daemon
           log, so a call that never reaches the relay can be told from
           one the relay turned away. Without this the log showed only
           the ten-second "still up" line and a browser could fail to
           reach the relay with nothing written at all. */
        $relay->onLog(function ($line) {
            L\crawlLog($line, "TurnServer");
        });
        /* The relay takes a token that runs out rather than a lasting
           password, since a page hands its words to every browser that
           opens it. The key is the written secret where there is one and
           the site key where there is not, which is the same choice the
           page makes when it works those words out. */
        $who = new TimeLimitedTurnAuthenticator(
            (C\p('TURN_SECRET') === "") ? C\p('AUTH_KEY') :
            C\p('TURN_SECRET'));
        $relay->auth($who)
            ->realm($told["realm"])
            ->software("Yioop TurnServer");
        if (isset($told["low"])) {
            $relay->relayPortRange($told["low"], $told["high"]);
        }
        /* Work out the address a browser can reach the relayed ports at
           and hand it to the relay, so the address it puts in an
           allocation is one the other browser can reach rather than the
           loopback. The site's own host is where a browser already
           reaches the relay, so resolve that to an address; where it is
           already an address gethostbyname gives it back unchanged.
           Where nothing resolves, the relay keeps to the loopback. */
        $relay_public_host = self::deriveRelayPublicAddress();
        if ($relay_public_host !== "") {
            $relay->relayAddress($relay_public_host);
            L\crawlLog("Relayed ports advertised at " .
                $relay_public_host);
        } else {
            L\crawlLog("No public address found for the relay; it will " .
                "carry a call only between browsers on this machine");
        }
        $bind = $told["BIND"];
        $port = $told["TURN_PORT"];
        L\crawlLog("PHP Version in use:  " . phpversion());
        L\crawlLog("Listening on $bind:$port for realm " .
            C\p('TURN_REALM'));
        L\crawlLog("Handing out ports " . C\p('TURN_RELAY_PORTS'));
        /* Between rounds of its loop the relay asks whether a stop
           has been requested, from Manage Machines or the command
           line, and ends where one has. */
        $said_alive = time();
        $relay->betweenRounds(function ($relay) use (&$said_alive) {
            if (!CrawlDaemon::processHandler()) {
                $relay->stop();
                return;
            }
            if (time() - $said_alive < self::ALIVE_LOG_INTERVAL) {
                return;
            }
            $said_alive = time();
            L\crawlLog("Up on " . $relay->boundAddress() .
                ", carrying " . $relay->carrying() . " call(s)");
        }, self::PROCESS_POLL_INTERVAL);
        $served = $relay->listen(['BIND' => $bind,
            'TURN_PORT' => $port]);
        L\crawlLog("Stopped listening on $bind:$port" .
            (($served === false) ? " without taking the port" : ""));
    }
}
/*
 * Instantiate and start the relay, unless this file was included only
 * to define the class for a test.
 */
if (!defined("seekquarry\\yioop\\executables\\TURN_SERVER_TEST_LOAD")) {
    $server = new TurnServer();
    $server->start();
}
