Skip to content

Commit

Permalink
Merge pull request #707 from vichan-devel/revert-704-revert-688-add-d…
Browse files Browse the repository at this point in the history
…ep-injection

Revert "Revert "Share REST call code and separate components via dependency injection""
  • Loading branch information
RealAngeleno committed Mar 31, 2024
2 parents 4094437 + 67475c2 commit be3437e
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 105 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"inc/mod/auth.php",
"inc/lock.php",
"inc/queue.php",
"inc/functions.php"
"inc/functions.php",
"inc/driver/http-driver.php",
"inc/service/captcha-queries.php"
]
},
"license": "Tinyboard + vichan",
Expand Down
2 changes: 2 additions & 0 deletions inc/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,8 @@
$config['error']['captcha'] = _('You seem to have mistyped the verification.');
$config['error']['flag_undefined'] = _('The flag %s is undefined, your PHP version is too old!');
$config['error']['flag_wrongtype'] = _('defined_flags_accumulate(): The flag %s is of the wrong type!');
$config['error']['remote_io_error'] = _('IO error while interacting with a remote service.');
$config['error']['local_io_error'] = _('IO error while interacting with a local resource or service.');


// Moderator errors
Expand Down
28 changes: 28 additions & 0 deletions inc/context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
namespace Vichan;

defined('TINYBOARD') or exit;

use Vichan\Driver\{HttpDriver, HttpDrivers};


interface Context {
public function getHttpDriver(): HttpDriver;
}

class AppContext implements Context {
private array $config;
private ?HttpDriver $http;


public function __construct(array $config) {
$this->config = $config;
}

public function getHttpDriver(): HttpDriver {
if (is_null($this->http)) {
$this->http = HttpDrivers::getHttpDriver($this->config['upload_by_url_timeout'], $this->config['max_filesize']);
}
return $this->http;
}
}
151 changes: 151 additions & 0 deletions inc/driver/http-driver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php // Honestly this is just a wrapper for cURL. Still useful to mock it and have an OOP API on PHP 7.
namespace Vichan\Driver;

use RuntimeException;

defined('TINYBOARD') or exit;


class HttpDrivers {
private const DEFAULT_USER_AGENT = 'Tinyboard';


public static function getHttpDriver(int $timeout, int $max_file_size): HttpDriver {
return new HttpDriver($timeout, self::DEFAULT_USER_AGENT, $max_file_size);
}
}

class HttpDriver {
private mixed $inner;
private int $timeout;
private string $user_agent;
private int $max_file_size;


private function resetTowards(string $url, int $timeout): void {
curl_reset($this->inner);
curl_setopt_array($this->inner, array(
CURLOPT_URL => $url,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_USERAGENT => $this->user_agent,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
));
}

private function setSizeLimit(): void {
// Adapted from: https://stackoverflow.com/a/17642638
curl_setopt($this->inner, CURLOPT_NOPROGRESS, false);

if (PHP_MAJOR_VERSION >= 8 && PHP_MINOR_VERSION >= 2) {
curl_setopt($this->inner, CURLOPT_XFERINFOFUNCTION, function($res, $next_dl, $dl, $next_up, $up) {
return (int)($dl <= $this->max_file_size);
});
} else {
curl_setopt($this->inner, CURLOPT_PROGRESSFUNCTION, function($res, $next_dl, $dl, $next_up, $up) {
return (int)($dl <= $this->max_file_size);
});
}
}

function __construct($timeout, $user_agent, $max_file_size) {
$this->inner = curl_init();
$this->timeout = $timeout;
$this->user_agent = $user_agent;
$this->max_file_size = $max_file_size;
}

function __destruct() {
curl_close($this->inner);
}

/**
* Execute a GET request.
*
* @param string $endpoint Uri endpoint.
* @param ?array $data Optional GET parameters.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return string Returns the body of the response.
* @throws RuntimeException Throws on IO error.
*/
public function requestGet(string $endpoint, ?array $data, int $timeout = 0): string {
if (!empty($data)) {
$endpoint .= '?' . http_build_query($data);
}
if ($timeout == 0) {
$timeout = $this->timeout;
}

$this->resetTowards($endpoint, $timeout);
curl_setopt($this->inner, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($this->inner);

if ($ret === false) {
throw new \RuntimeException(curl_error($this->inner));
}
return $ret;
}

/**
* Execute a POST request.
*
* @param string $endpoint Uri endpoint.
* @param ?array $data Optional POST parameters.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return string Returns the body of the response.
* @throws RuntimeException Throws on IO error.
*/
public function requestPost(string $endpoint, ?array $data, int $timeout = 0): string {
if ($timeout == 0) {
$timeout = $this->timeout;
}

$this->resetTowards($endpoint, $timeout);
curl_setopt($this->inner, CURLOPT_POST, true);
if (!empty($data)) {
curl_setopt($this->inner, CURLOPT_POSTFIELDS, http_build_query($data));
}
curl_setopt($this->inner, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($this->inner);

if ($ret === false) {
throw new \RuntimeException(curl_error($this->inner));
}
return $ret;
}

/**
* Download the url's target with curl.
*
* @param string $url Url to the file to download.
* @param ?array $data Optional GET parameters.
* @param resource $fd File descriptor to save the content to.
* @param int $timeout Optional request timeout in seconds. Use the default timeout if 0.
* @return bool Returns true on success, false if the file was too large.
* @throws RuntimeException Throws on IO error.
*/
public function requestGetInto(string $endpoint, ?array $data, mixed $fd, int $timeout = 0): bool {
if (!empty($data)) {
$endpoint .= '?' . http_build_query($data);
}
if ($timeout == 0) {
$timeout = $this->timeout;
}

$this->resetTowards($endpoint, $timeout);
curl_setopt($this->inner, CURLOPT_FAILONERROR, true);
curl_setopt($this->inner, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($this->inner, CURLOPT_FILE, $fd);
curl_setopt($this->inner, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
$this->setSizeLimit();
$ret = curl_exec($this->inner);

if ($ret === false) {
if (curl_errno($this->inner) === CURLE_ABORTED_BY_CALLBACK) {
return false;
}

throw new \RuntimeException(curl_error($this->inner));
}
return true;
}
}
102 changes: 102 additions & 0 deletions inc/service/captcha-queries.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php // Verify captchas server side.
namespace Vichan\Service;

use Vichan\Driver\HttpDriver;

defined('TINYBOARD') or exit;


class RemoteCaptchaQuery {
private HttpDriver $http;
private string $secret;
private string $endpoint;


/**
* Creates a new CaptchaRemoteQueries instance using the google recaptcha service.
*
* @param HttpDriver $http The http client.
* @param string $secret Server side secret.
* @return CaptchaRemoteQueries A new captcha query instance.
*/
public static function withRecaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery {
return new self($http, $secret, 'https://www.google.com/recaptcha/api/siteverify');
}

/**
* Creates a new CaptchaRemoteQueries instance using the hcaptcha service.
*
* @param HttpDriver $http The http client.
* @param string $secret Server side secret.
* @return CaptchaRemoteQueries A new captcha query instance.
*/
public static function withHCaptcha(HttpDriver $http, string $secret): RemoteCaptchaQuery {
return new self($http, $secret, 'https://hcaptcha.com/siteverify');
}

private function __construct(HttpDriver $http, string $secret, string $endpoint) {
$this->http = $http;
$this->secret = $secret;
$this->endpoint = $endpoint;
}

/**
* Checks if the user at the remote ip passed the captcha.
*
* @param string $response User provided response.
* @param string $remote_ip User ip.
* @return bool Returns true if the user passed the captcha.
* @throws RuntimeException|JsonException Throws on IO errors or if it fails to decode the answer.
*/
public function verify(string $response, string $remote_ip): bool {
$data = array(
'secret' => $this->secret,
'response' => $response,
'remoteip' => $remote_ip
);

$ret = $this->http->requestGet($this->endpoint, $data);
$resp = json_decode($ret, true, 16, JSON_THROW_ON_ERROR);

return isset($resp['success']) && $resp['success'];
}
}

class NativeCaptchaQuery {
private HttpDriver $http;
private string $domain;
private string $provider_check;


/**
* @param HttpDriver $http The http client.
* @param string $domain The server's domain.
* @param string $provider_check Path to the endpoint.
*/
function __construct(HttpDriver $http, string $domain, string $provider_check) {
$this->http = $http;
$this->domain = $domain;
$this->provider_check = $provider_check;
}

/**
* Checks if the user at the remote ip passed the native vichan captcha.
*
* @param string $extra Extra http parameters.
* @param string $user_text Remote user's text input.
* @param string $user_cookie Remote user cookie.
* @return bool Returns true if the user passed the check.
* @throws RuntimeException Throws on IO errors.
*/
public function verify(string $extra, string $user_text, string $user_cookie): bool {
$data = array(
'mode' => 'check',
'text' => $user_text,
'extra' => $extra,
'cookie' => $user_cookie
);

$ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data);
return $ret === '1';
}
}
Loading

0 comments on commit be3437e

Please sign in to comment.