Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
+286
-87
@@ -11,75 +11,130 @@
|
||||
namespace IPS\Http\Request;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use CurlHandle;
|
||||
use CurlMultiHandle;
|
||||
use IPS\Application;
|
||||
use IPS\Http\Response;
|
||||
use IPS\Http\Url;
|
||||
use IPS\IPS;
|
||||
use IPS\Log;
|
||||
use Pdp\Domain;
|
||||
use Pdp\Rules;
|
||||
use function array_replace;
|
||||
use function curl_setopt;
|
||||
use function define;
|
||||
use function defined;
|
||||
use function in_array;
|
||||
use function intval;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function parse_url;
|
||||
use const CURLOPT_WRITEFUNCTION;
|
||||
use const IPS\DEBUG_LOG;
|
||||
use const PHP_URL_HOST;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* cURL REST Class
|
||||
*
|
||||
* @method put( mixed $data = NULL ): Response
|
||||
* @method delete( mixed $data = NULL ): Response
|
||||
*/
|
||||
class _Curl
|
||||
class Curl
|
||||
{
|
||||
/**
|
||||
* @brief URL
|
||||
*/
|
||||
protected $url = NULL;
|
||||
protected ?Url $url = NULL;
|
||||
|
||||
/**
|
||||
* @brief Curl Handle
|
||||
*/
|
||||
protected $curl = NULL;
|
||||
protected null|false|CurlHandle $curl = NULL;
|
||||
|
||||
/**
|
||||
* @brief Has the Content-Type header been set?
|
||||
* @note Because cURL will automatically set the Content-Type header to multipart/form-data if we send a POST request with an array, we need to change it to a string if we want to send a different Content-Type
|
||||
* @see <a href='http://www.php.net/manual/en/function.curl-setopt.php'>PHP: curl_setopt - Manual</a>
|
||||
*/
|
||||
protected $modifiedContentType = FALSE;
|
||||
protected bool $modifiedContentType = FALSE;
|
||||
|
||||
/**
|
||||
* @brief HTTP Version
|
||||
*/
|
||||
protected $httpVersion = '1.1';
|
||||
protected string $httpVersion = '1.1';
|
||||
|
||||
/**
|
||||
* @brief Timeout
|
||||
*/
|
||||
protected $timeout = 5;
|
||||
protected int $timeout = 5;
|
||||
|
||||
/**
|
||||
* @brief Follow redirects?
|
||||
*/
|
||||
protected $followRedirects = TRUE;
|
||||
protected int|bool $followRedirects = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Allowed protocols
|
||||
*/
|
||||
protected $allowedProtocols = array();
|
||||
protected array $allowedProtocols = array();
|
||||
|
||||
/**
|
||||
* @brief Data sent
|
||||
*/
|
||||
protected $dataForLog = NULL;
|
||||
protected mixed $dataForLog = NULL;
|
||||
|
||||
/**
|
||||
* @brief Headers sent
|
||||
*/
|
||||
protected $headersForLog = [];
|
||||
protected array $headersForLog = [];
|
||||
|
||||
/**
|
||||
* @brief Flag used to show this request is going to the internal cloud server
|
||||
*/
|
||||
public bool $internalSignedRequest = false;
|
||||
|
||||
/**
|
||||
* @brief Limit how much data we fetch
|
||||
*/
|
||||
protected int|null $bytesToGet = null;
|
||||
|
||||
/**
|
||||
* @brief Untrusted endpoint, redirect only if the host is the same, so google.com > www.google.com is ok, but google.com > badActor.com is not
|
||||
*/
|
||||
protected bool $untrusted = false;
|
||||
|
||||
/**
|
||||
* @brief Allowed content types (null allows all)
|
||||
*/
|
||||
protected array|null $allowedContentTypes = NULL;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
public array|null $cicHeaders = null;
|
||||
|
||||
/**
|
||||
* Contructor
|
||||
*
|
||||
* @param \IPS\Http\Url $url URL
|
||||
* @param int $timeout Timeout (in seconds)
|
||||
* @param string $httpVersion HTTP Version
|
||||
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
|
||||
* @param array|null $allowedProtocols Protocols allowed (if NULL we default to array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' ))
|
||||
* @param Url $url URL
|
||||
* @param int $timeout Timeout (in seconds)
|
||||
* @param string|null $httpVersion HTTP Version
|
||||
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
|
||||
* @param array|null $allowedProtocols Protocols allowed (if NULL we default to array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' ))
|
||||
* @param array|null $allowedContentTypes Allowed content types (if null, it will allow all)
|
||||
* @param bool $untrusted Untrusted endpoint, redirect only if the host is the same, so google.com > www.google.com is ok, but google.com > badActor.com is not
|
||||
* @param int|null $bytesToGet Number of bytes to get. null means get everything you greedy piggie
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $url, $timeout=5, $httpVersion=NULL, $followRedirects=TRUE, $allowedProtocols=NULL )
|
||||
public function __construct( Url $url, int $timeout=5, string $httpVersion=NULL, bool|int $followRedirects=TRUE, array $allowedProtocols=NULL, array $allowedContentTypes=NULL, bool $untrusted=false, int|null $bytesToGet = null)
|
||||
{
|
||||
/* Init */
|
||||
$this->url = $url;
|
||||
@@ -88,6 +143,9 @@ class _Curl
|
||||
$this->timeout = $timeout;
|
||||
$this->followRedirects = $followRedirects;
|
||||
$this->allowedProtocols = $allowedProtocols ?: array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' );
|
||||
$this->allowedContentTypes = $allowedContentTypes;
|
||||
$this->bytesToGet = $bytesToGet;
|
||||
$this->untrusted = $untrusted;
|
||||
|
||||
/* Need to adjust if this is FTP */
|
||||
$user = null;
|
||||
@@ -160,7 +218,7 @@ class _Curl
|
||||
public function __destruct()
|
||||
{
|
||||
curl_close( $this->curl );
|
||||
if( static::$_multiHandle instanceof \CurlMultiHandle )
|
||||
if( static::$_multiHandle instanceof CurlMultiHandle )
|
||||
{
|
||||
curl_multi_close( static::$_multiHandle );
|
||||
}
|
||||
@@ -169,11 +227,11 @@ class _Curl
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* @param string $username Username
|
||||
* @param string $password Password
|
||||
* @param string $username Username
|
||||
* @param string $password Password
|
||||
* @return static
|
||||
*/
|
||||
public function login( $username, $password )
|
||||
public function login( string $username, string $password ): static
|
||||
{
|
||||
curl_setopt_array( $this->curl, array(
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
@@ -186,11 +244,19 @@ class _Curl
|
||||
/**
|
||||
* Set Headers
|
||||
*
|
||||
* @param array $headers Key/Value pair of headers
|
||||
* @param array $headers Key/Value a pair of headers
|
||||
* @return static
|
||||
*/
|
||||
public function setHeaders( $headers )
|
||||
public function setHeaders( array $headers ): static
|
||||
{
|
||||
if ( Application::appIsEnabled('cloud') )
|
||||
{
|
||||
if ( $this->internalSignedRequest and isset( $this->cicHeaders ) )
|
||||
{
|
||||
$headers = array_replace( $headers, $this->cicHeaders );
|
||||
}
|
||||
}
|
||||
|
||||
$extra = array();
|
||||
|
||||
foreach ( $headers as $k => $v )
|
||||
@@ -237,14 +303,14 @@ class _Curl
|
||||
/**
|
||||
* Toggle SSL checks
|
||||
*
|
||||
* @param boolean $value True will enable SSL checks, false will disable them
|
||||
* @param boolean $value True will enable SSL checks, false will disable them
|
||||
* @return static
|
||||
*/
|
||||
public function sslCheck( $value=TRUE )
|
||||
public function sslCheck( bool $value=TRUE ): static
|
||||
{
|
||||
curl_setopt_array( $this->curl, array(
|
||||
CURLOPT_SSL_VERIFYHOST => ( $value ) ? 2 : FALSE,
|
||||
CURLOPT_SSL_VERIFYPEER => (boolean) $value
|
||||
CURLOPT_SSL_VERIFYPEER => $value
|
||||
) );
|
||||
|
||||
return $this;
|
||||
@@ -255,24 +321,24 @@ class _Curl
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function forceTls()
|
||||
public function forceTls(): static
|
||||
{
|
||||
$curlVersionData = curl_version();
|
||||
if ( preg_match( '/^OpenSSL\/(\d+\.\d+\.\d+)/', $curlVersionData['ssl_version'], $openSSLVersionData ) )
|
||||
{
|
||||
if ( version_compare( $openSSLVersionData[1], '1.0.1', '>=' ) )
|
||||
{
|
||||
if ( !\defined('CURL_SSLVERSION_TLSv1_2') ) // constant not defined in PHP < 5.5
|
||||
if ( !defined('CURL_SSLVERSION_TLSv1_2') ) // constant not defined in PHP < 5.5
|
||||
{
|
||||
\define( 'CURL_SSLVERSION_TLSv1_2', 6 );
|
||||
define( 'CURL_SSLVERSION_TLSv1_2', 6 );
|
||||
}
|
||||
curl_setopt( $this->curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2 );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !\defined('CURL_SSLVERSION_TLSv1') ) // constant not defined in PHP < 5.5
|
||||
if ( !defined('CURL_SSLVERSION_TLSv1') ) // constant not defined in PHP < 5.5
|
||||
{
|
||||
\define( 'CURL_SSLVERSION_TLSv1', 1 );
|
||||
define( 'CURL_SSLVERSION_TLSv1', 1 );
|
||||
}
|
||||
curl_setopt( $this->curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1 );
|
||||
}
|
||||
@@ -284,11 +350,11 @@ class _Curl
|
||||
/**
|
||||
* HTTP GET
|
||||
*
|
||||
* @param mixed $data Data to send with the GET request
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
* @param mixed|null $data Data to send with the GET request
|
||||
* @return Response
|
||||
* @throws CurlException
|
||||
*/
|
||||
public function get( $data=NULL )
|
||||
public function get( mixed $data=NULL ): Response
|
||||
{
|
||||
/* Specify that this is a GET request */
|
||||
curl_setopt( $this->curl, CURLOPT_HTTPGET, TRUE );
|
||||
@@ -299,18 +365,26 @@ class _Curl
|
||||
curl_setopt( $this->curl, CURLOPT_POSTFIELDS, $data );
|
||||
}
|
||||
|
||||
return $this->_executeAndFollowRedirects( 'GET', NULL );
|
||||
return $this->_executeAndFollowRedirects( 'GET' );
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP POST
|
||||
*
|
||||
* @param mixed $data Data to post (can be array or string)
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
* @param mixed|null $data Data to post (can be array or string)
|
||||
* @return Response
|
||||
* @throws CurlException
|
||||
*/
|
||||
public function post( $data=NULL )
|
||||
{
|
||||
public function post( mixed $data=NULL ): Response
|
||||
{
|
||||
if ( Application::appIsEnabled('cloud') )
|
||||
{
|
||||
if ( $this->internalSignedRequest )
|
||||
{
|
||||
$data = \IPS\Cicloud\prepareInternalPayload( $data );
|
||||
}
|
||||
}
|
||||
|
||||
/* Specify that this is a POST request */
|
||||
curl_setopt( $this->curl, CURLOPT_POST, TRUE );
|
||||
|
||||
@@ -324,10 +398,10 @@ class _Curl
|
||||
/**
|
||||
* HTTP HEAD
|
||||
*
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
* @return Response
|
||||
* @throws CurlException
|
||||
*/
|
||||
public function head()
|
||||
public function head(): Response
|
||||
{
|
||||
/* Specify the request method */
|
||||
curl_setopt( $this->curl, CURLOPT_CUSTOMREQUEST, 'HEAD' );
|
||||
@@ -336,19 +410,19 @@ class _Curl
|
||||
curl_setopt( $this->curl, CURLOPT_NOBODY, true );
|
||||
|
||||
/* Execute */
|
||||
return $this->_executeAndFollowRedirects( 'HEAD', NULL );
|
||||
return $this->_executeAndFollowRedirects( 'HEAD' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic Method: __call
|
||||
* Used for other HTTP methods (like PUT and DELETE)
|
||||
*
|
||||
* @param string $method Method (A HTTP method)
|
||||
* @param array $params Parameters (a single parameter with data to post, which can be an array or a string)
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
* @param string $method Method (A HTTP method)
|
||||
* @param array $params Parameters (a single parameter with data to post, which can be an array or a string)
|
||||
* @return Response
|
||||
* @throws CurlException
|
||||
*/
|
||||
public function __call( $method, $params )
|
||||
public function __call( string $method, array $params )
|
||||
{
|
||||
/* Specify the request method */
|
||||
curl_setopt( $this->curl, CURLOPT_CUSTOMREQUEST, mb_strtoupper( $method ) );
|
||||
@@ -366,13 +440,13 @@ class _Curl
|
||||
/**
|
||||
* Data to send
|
||||
*
|
||||
* @param mixed $data Data to post (can be array or string)
|
||||
* @param mixed|null $data Data to post (can be array or string)
|
||||
* @return mixed
|
||||
*/
|
||||
protected function _dataToSend( $data=NULL )
|
||||
protected function _dataToSend( mixed $data=NULL ): mixed
|
||||
{
|
||||
$this->dataForLog = $data;
|
||||
if ( !$this->modifiedContentType and \is_array( $data ) )
|
||||
if ( !$this->modifiedContentType and is_array( $data ) )
|
||||
{
|
||||
$data = http_build_query( $data, '', '&' );
|
||||
}
|
||||
@@ -382,19 +456,61 @@ class _Curl
|
||||
/**
|
||||
* Execute the request
|
||||
*
|
||||
* @todo Remove if multi handle works out long term.
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
* @return Response
|
||||
* @throws CurlException
|
||||
*@todo Remove if multi handle works out long term.
|
||||
*/
|
||||
protected function _execute()
|
||||
protected function _execute(): Response
|
||||
{
|
||||
/* Execute */
|
||||
$output = curl_exec( $this->curl );
|
||||
|
||||
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
|
||||
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
|
||||
if ( $this->bytesToGet !== null )
|
||||
{
|
||||
\IPS\Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
|
||||
/* Why not CURLOPT_RANGE? Servers can choose to ignore it (https://curl.se/libcurl/c/CURLOPT_RANGE.html) */
|
||||
$data = '';
|
||||
$headerProcessed = false;
|
||||
$headers = '';
|
||||
$bytesToGet = $this->bytesToGet;
|
||||
curl_setopt( $this->curl, CURLOPT_WRITEFUNCTION, function( $ch, $chunk ) use ( &$data, &$headerProcessed, &$headers, $bytesToGet )
|
||||
{
|
||||
if ( ! $headerProcessed )
|
||||
{
|
||||
// Find the end of the headers
|
||||
$endOfHeaders = strpos( $chunk, "\r\n" );
|
||||
|
||||
if ( $endOfHeaders !== false )
|
||||
{
|
||||
$headerProcessed = true; // Now start processing the content
|
||||
}
|
||||
else
|
||||
{
|
||||
$headers .= $chunk;
|
||||
return strlen( $chunk ); // No headers ending found, skip this part of data
|
||||
}
|
||||
}
|
||||
|
||||
$length = strlen( $data ) + strlen( $chunk );
|
||||
|
||||
if ( $length >= $bytesToGet )
|
||||
{
|
||||
$data .= substr( $chunk, 0, $bytesToGet - strlen( $data ) );
|
||||
return 0;
|
||||
}
|
||||
$data .= $chunk;
|
||||
return strlen( $chunk );
|
||||
} );
|
||||
|
||||
curl_exec( $this->curl );
|
||||
$output = $headers . $data;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Execute */
|
||||
$output = curl_exec( $this->curl );
|
||||
}
|
||||
|
||||
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
|
||||
if ( defined('\IPS\DEBUG_LOG') and DEBUG_LOG )
|
||||
{
|
||||
Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
|
||||
}
|
||||
|
||||
/* Errors? */
|
||||
@@ -410,22 +526,62 @@ class _Curl
|
||||
}
|
||||
|
||||
/* Return it */
|
||||
return new \IPS\Http\Response( $output );
|
||||
$response = new Response( $output );
|
||||
|
||||
if ( ! $this->isContentTypeValid( $response ) )
|
||||
{
|
||||
throw new CurlException( 'Invalid content type' );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the returned content type valid?
|
||||
*
|
||||
* @param Response $response
|
||||
* @return bool
|
||||
*/
|
||||
protected function isContentTypeValid( Response $response ): bool
|
||||
{
|
||||
if ( $this->allowedContentTypes !== null and $response->httpHeaders !== null )
|
||||
{
|
||||
$headers = array_change_key_case( $response->httpHeaders, CASE_LOWER );
|
||||
$contentType = $headers['content-type'] ?? 'unknown/unknown';
|
||||
if ( strstr( $contentType, ';' ) )
|
||||
{
|
||||
$contentType = explode( ';', $contentType );
|
||||
$contentType = $contentType[0];
|
||||
}
|
||||
|
||||
if ( ! in_array( strtolower( $contentType ), $this->allowedContentTypes ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @breif Store the cURL Multi Handle
|
||||
*/
|
||||
protected static $_multiHandle;
|
||||
protected static null|false|CurlMultiHandle $_multiHandle = NULL;
|
||||
|
||||
/**
|
||||
* Execute the request via cURL Multi - We use this to cache and share connections between requests
|
||||
*
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
* @return Response
|
||||
* @throws CurlException
|
||||
*/
|
||||
protected function _executeMh(): \IPS\Http\Response
|
||||
protected function _executeMh(): Response
|
||||
{
|
||||
/* If we're restricting by bytes, then we need to use the plain execute */
|
||||
if( $this->bytesToGet !== null )
|
||||
{
|
||||
return $this->_execute();
|
||||
}
|
||||
|
||||
/* Init multi handle if needed */
|
||||
if( empty( static::$_multiHandle ) )
|
||||
{
|
||||
@@ -436,7 +592,7 @@ class _Curl
|
||||
curl_multi_add_handle( static::$_multiHandle, $this->curl );
|
||||
|
||||
/* Execute request and wait for response */
|
||||
$active = NULL;
|
||||
$active = 0;
|
||||
do
|
||||
{
|
||||
$ret = curl_multi_exec( static::$_multiHandle, $active );
|
||||
@@ -461,9 +617,9 @@ class _Curl
|
||||
curl_multi_remove_handle( static::$_multiHandle, $this->curl );
|
||||
|
||||
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
|
||||
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
|
||||
if ( defined('\IPS\DEBUG_LOG') and DEBUG_LOG )
|
||||
{
|
||||
\IPS\Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
|
||||
Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
|
||||
}
|
||||
|
||||
/* Errors? */
|
||||
@@ -479,24 +635,31 @@ class _Curl
|
||||
}
|
||||
|
||||
/* Return it */
|
||||
return new \IPS\Http\Response( $output );
|
||||
$response = new Response( $output );
|
||||
|
||||
if ( ! $this->isContentTypeValid( $response ) )
|
||||
{
|
||||
throw new CurlException( 'Invalid content type' );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the request and follow redirects id necessary
|
||||
*
|
||||
* @param string $method Request method to use
|
||||
* @param NULL|array $params Parameters to send with request
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
* @param string $method Request method to use
|
||||
* @param array|null $params Parameters to send with request
|
||||
* @return Response
|
||||
* @throws CurlException
|
||||
*/
|
||||
protected function _executeAndFollowRedirects( $method, $params=NULL )
|
||||
protected function _executeAndFollowRedirects( string $method, mixed $params=NULL ): Response
|
||||
{
|
||||
/* Execute */
|
||||
$response = $this->_executeMh();
|
||||
|
||||
|
||||
/* Either return it or follow it */
|
||||
if ( $this->followRedirects and \in_array( $response->httpResponseCode, array( 301, 302, 303, 307, 308 ) ) )
|
||||
if ( $this->followRedirects and in_array( $response->httpResponseCode, array( 301, 302, 303, 307, 308 ) ) )
|
||||
{
|
||||
/* Fix missing hostname in location */
|
||||
foreach( $response->httpHeaders as $k => $v )
|
||||
@@ -507,19 +670,54 @@ class _Curl
|
||||
}
|
||||
}
|
||||
|
||||
if( parse_url( $location, PHP_URL_HOST ) === NULL )
|
||||
if( isset( $location ) and parse_url( $location, PHP_URL_HOST ) === NULL )
|
||||
{
|
||||
$location = $this->url->data['scheme'] . '://' . $this->url->data['host'] . $location;
|
||||
}
|
||||
|
||||
$newRequest = \IPS\Http\Url::external( $location );
|
||||
|
||||
if( !\in_array( $newRequest->data['scheme'], $this->allowedProtocols ) )
|
||||
if ( isset( $location ) and $this->untrusted )
|
||||
{
|
||||
throw new \IPS\Http\Request\Exception( 'protocol_not_followed' );
|
||||
/* We want to make sure that the main domain is the same. So domain.com/foo can redirect to domain.com/newfoo and domain.co.uk can redirect to www.domain.co.uk but domain.co.uk cannot redirect to anotherdomain.com */
|
||||
$pass = false;
|
||||
if ( strtolower( $this->url->data['host'] ) === strtolower( parse_url( $location, PHP_URL_HOST ) ) )
|
||||
{
|
||||
// 1: Do a simple test: sub.domain.tld/foo -> sub.domain.tld/bar
|
||||
$pass = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 2: Now we need to check for sub.domain.tld to www.domain.tld, or domain.tld to www.domain.tld
|
||||
IPS::$PSR0Namespaces['Pdp'] = \IPS\ROOT_PATH .'/system/3rd_party/Pdp';
|
||||
|
||||
$publicSuffixList = Rules::fromPath( \IPS\ROOT_PATH .'/system/3rd_party/Pdp/tlds.dat' );
|
||||
$newDomain = Domain::fromIDNA2008( parse_url( $location, PHP_URL_HOST ) );
|
||||
$result = $publicSuffixList->resolve( $newDomain );
|
||||
$newLocation = $result->registrableDomain()->toString();
|
||||
|
||||
$oldDomain = Domain::fromIDNA2008( $this->url->data['host'] );
|
||||
$result = $publicSuffixList->resolve( $oldDomain );
|
||||
$oldLocation = $result->registrableDomain()->toString();
|
||||
|
||||
if ( strtolower( $oldLocation ) === strtolower( $newLocation ) )
|
||||
{
|
||||
$pass = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $pass )
|
||||
{
|
||||
throw new Exception( 'untrusted_redirect' );
|
||||
}
|
||||
}
|
||||
|
||||
$newRequest = $newRequest->request( $this->timeout, $this->httpVersion, \is_int( $this->followRedirects ) ? ( $this->followRedirects - 1 ) : $this->followRedirects );
|
||||
$newRequest = Url::external( $location );
|
||||
|
||||
if( !in_array( $newRequest->data['scheme'], $this->allowedProtocols ) )
|
||||
{
|
||||
throw new Exception( 'protocol_not_followed' );
|
||||
}
|
||||
|
||||
$newRequest = $newRequest->request( $this->timeout, $this->httpVersion, is_int( $this->followRedirects ) ? ( $this->followRedirects - 1 ) : $this->followRedirects );
|
||||
return $newRequest->$method( $params );
|
||||
}
|
||||
return $response;
|
||||
@@ -529,4 +727,5 @@ class _Curl
|
||||
/**
|
||||
* CURL Exception Class
|
||||
*/
|
||||
class CurlException extends \IPS\Http\Request\Exception { }
|
||||
class CurlException extends Exception
|
||||
{ }
|
||||
@@ -11,13 +11,17 @@
|
||||
namespace IPS\Http\Request;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use RuntimeException;
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request Exception Class
|
||||
*/
|
||||
class _Exception extends \RuntimeException { }
|
||||
class Exception extends RuntimeException { }
|
||||
@@ -1,309 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Sockets REST Class
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 18 Mar 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Http\Request;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sockets REST Class
|
||||
*/
|
||||
class _Sockets
|
||||
{
|
||||
/**
|
||||
* @brief URL
|
||||
*/
|
||||
protected $url = NULL;
|
||||
|
||||
/**
|
||||
* @brief Stream context
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @brief HTTP Version
|
||||
*/
|
||||
protected $httpVersion = '1.1';
|
||||
|
||||
/**
|
||||
* @brief Timeout
|
||||
*/
|
||||
protected $timeout = 5;
|
||||
|
||||
/**
|
||||
* @brief Headers
|
||||
*/
|
||||
protected $headers = array();
|
||||
|
||||
/**
|
||||
* @brief Follow redirects?
|
||||
*/
|
||||
protected $followRedirects = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Allowed protocols
|
||||
*/
|
||||
protected $allowedProtocols = array();
|
||||
|
||||
/**
|
||||
* Contructor
|
||||
*
|
||||
* @param \IPS\Http\Url $url URL
|
||||
* @param int $timeout Timeout (in seconds)
|
||||
* @param string $httpVersion HTTP Version
|
||||
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
|
||||
* @param array|null $allowedProtocols Protocols allowed (if NULL we default to array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' ))
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $url, $timeout=5, $httpVersion=NULL, $followRedirects=TRUE, $allowedProtocols=NULL )
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->context = stream_context_create();
|
||||
$this->httpVersion = $httpVersion ?: '1.1';
|
||||
$this->timeout = $timeout;
|
||||
$this->followRedirects = $followRedirects;
|
||||
$this->allowedProtocols = $allowedProtocols ?: array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' );
|
||||
|
||||
/* Set our basic settings */
|
||||
stream_context_set_option( $this->context, array(
|
||||
'http' => array(
|
||||
'protocol_version' => $httpVersion,
|
||||
'follow_location' => $followRedirects,
|
||||
'timeout' => $timeout,
|
||||
'ignore_errors' => TRUE,
|
||||
),
|
||||
'ssl' => array(
|
||||
'verify_peer' => FALSE,
|
||||
'crypto_method' => STREAM_CRYPTO_METHOD_ANY_CLIENT
|
||||
)
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* @param string $username Username
|
||||
* @param string $password Password
|
||||
* @return static (for daisy chaining)
|
||||
*/
|
||||
public function login( $username, $password )
|
||||
{
|
||||
$this->setHeaders( array( 'Authorization' => 'Basic ' . base64_encode( "{$username}:{$password}" ) ) );
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Headers
|
||||
*
|
||||
* @param array $headers Key/Value pair of headers
|
||||
* @return static
|
||||
*/
|
||||
public function setHeaders( $headers )
|
||||
{
|
||||
$this->headers = array_merge( $this->headers, $headers );
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle SSL checks
|
||||
*
|
||||
* @param boolean $value True will enable SSL checks, false will disable them
|
||||
* @return static
|
||||
*/
|
||||
public function sslCheck( $value=TRUE )
|
||||
{
|
||||
stream_context_set_option( $this->context, array(
|
||||
'ssl' => array(
|
||||
'verify_peer_name' => ( $value ) ? 2 : FALSE,
|
||||
'verify_peer' => (boolean) $value,
|
||||
)
|
||||
) );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force TLS
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function forceTls()
|
||||
{
|
||||
if ( \defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT') )
|
||||
{
|
||||
stream_context_set_option( $this->context, array(
|
||||
'ssl' => array(
|
||||
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
|
||||
)
|
||||
) );
|
||||
}
|
||||
elseif ( \defined('STREAM_CRYPTO_METHOD_TLS_CLIENT') )
|
||||
{
|
||||
stream_context_set_option( $this->context, array(
|
||||
'ssl' => array(
|
||||
'crypto_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT
|
||||
)
|
||||
) );
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic Method: __call
|
||||
* Used for other HTTP methods (like PUT and DELETE)
|
||||
*
|
||||
* @param string $method Method (A HTTP method)
|
||||
* @param array $params Parameters (a single parameter with data to post, which can be an array or a string)
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
*/
|
||||
public function __call( $method, $params )
|
||||
{
|
||||
$method = mb_strtoupper( $method );
|
||||
|
||||
/* The data (string or array) will be the first parameter */
|
||||
if ( isset( $params[0] ) && \is_array( $params[0] ) )
|
||||
{
|
||||
$this->setHeaders( array( 'Content-Type' => 'application/x-www-form-urlencoded' ) );
|
||||
$data = http_build_query( $params[0], '', '&' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$data = ( isset( $params[0] ) ? $params[0] : NULL );
|
||||
}
|
||||
|
||||
/* Set the method and the Content-Length header if this is a POST, PUT or PATCH request */
|
||||
stream_context_set_option( $this->context, 'http', 'method', $method );
|
||||
|
||||
if( $data )
|
||||
{
|
||||
$this->setHeaders( array( 'Content-Length' => \strlen( $data ) ) );
|
||||
}
|
||||
|
||||
/* Parse URL */
|
||||
if ( isset( $this->url->data['user'] ) or isset( $this->url->data['pass'] ) )
|
||||
{
|
||||
$this->login( isset( $this->url->data['user'] ) ? $this->url->data['user'] : NULL, isset( $this->url->data['pass'] ) ? $this->url->data['pass'] : NULL );
|
||||
}
|
||||
|
||||
$hostname = sprintf( '%s%s:%d',
|
||||
( $this->url->data['scheme'] === 'https' ) ? 'ssl://' : '',
|
||||
$this->url->data['host'],
|
||||
isset( $this->url->data['port'] )
|
||||
? $this->url->data['port']
|
||||
: ( $this->url->data['scheme'] === 'http' ? 80 : 443 )
|
||||
);
|
||||
|
||||
/* Open connection */
|
||||
try
|
||||
{
|
||||
$resource = stream_socket_client( $hostname, $errno, $errstr, $this->timeout, \STREAM_CLIENT_CONNECT, $this->context );
|
||||
}
|
||||
/* Catch issues that may arise, such as DNS failure */
|
||||
catch( \ErrorException $e )
|
||||
{
|
||||
throw new SocketsException( $e->getMessage(), $e->getCode() );
|
||||
}
|
||||
|
||||
if ( $resource === FALSE )
|
||||
{
|
||||
throw new SocketsException( $errstr, $errno );
|
||||
}
|
||||
|
||||
/* Get the location */
|
||||
$location = $this->url->data['path'] ? \IPS\Http\Url::encodeComponent( \IPS\Http\Url::COMPONENT_PATH, $this->url->data['path'] ) : '';
|
||||
$location .= ( \count( $this->url->queryString ) ) ? '?' . \IPS\Http\Url::convertQueryAsArrayToString( $this->url->queryString, true ) : '';
|
||||
$location .= $this->url->data['fragment'] ? '#' . \IPS\Http\Url::encodeComponent( \IPS\Http\Url::COMPONENT_FRAGMENT, $this->url->data['fragment'] ) : '';
|
||||
|
||||
/* Send request */
|
||||
$request = mb_strtoupper( $method ) . ' /' . ltrim( $location, '/' ) . " HTTP/{$this->httpVersion}\r\n";
|
||||
$request .= "Host: {$this->url->data['host']}" . ( isset( $this->url->data['port'] ) ? ":{$this->url->data['port']}" : '' ) . "\r\n";
|
||||
|
||||
$headersForLog = [];
|
||||
foreach ( $this->headers as $k => $v )
|
||||
{
|
||||
$request .= "{$k}: {$v}\r\n";
|
||||
$headersForLog[ $k ] = "{$k}: {$v}";
|
||||
}
|
||||
|
||||
$request .= "Connection: Close\r\n";
|
||||
$request .= "\r\n";
|
||||
|
||||
if ( $data )
|
||||
{
|
||||
$request .= $data;
|
||||
}
|
||||
|
||||
\fwrite( $resource, $request );
|
||||
|
||||
/* Read response */
|
||||
stream_set_timeout( $resource, $this->timeout );
|
||||
$status = stream_get_meta_data( $resource );
|
||||
|
||||
$response = '';
|
||||
while( !feof($resource) and !$status['timed_out'] )
|
||||
{
|
||||
$response .= \fgets( $resource, 8192 );
|
||||
$status = stream_get_meta_data( $resource );
|
||||
}
|
||||
|
||||
/* Close connection */
|
||||
\fclose( $resource );
|
||||
|
||||
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
|
||||
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
|
||||
{
|
||||
\IPS\Log::debug( "\n\n------------------------------------\nSOCKETS REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $headersForLog ) . "\n\n{$request}\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $response, 'request' );
|
||||
}
|
||||
|
||||
/* Interpret response */
|
||||
$response = new \IPS\Http\Response( $response );
|
||||
|
||||
/* Either return it or follow it */
|
||||
if ( $this->followRedirects and \in_array( $response->httpResponseCode, array( 301, 302, 303, 307, 308 ) ) )
|
||||
{
|
||||
/* Fix missing hostname in location */
|
||||
foreach( $response->httpHeaders as $k => $v )
|
||||
{
|
||||
if( mb_strtolower( $k ) == 'location' )
|
||||
{
|
||||
$location = $v;
|
||||
}
|
||||
}
|
||||
|
||||
if( parse_url( $location, PHP_URL_HOST ) === NULL )
|
||||
{
|
||||
$location = $this->url->data['scheme'] . '://' . $this->url->data['host'] . $location;
|
||||
}
|
||||
|
||||
$newRequest = \IPS\Http\Url::external( $location );
|
||||
|
||||
if( !\in_array( $newRequest->data['scheme'], $this->allowedProtocols ) )
|
||||
{
|
||||
throw new \IPS\Http\Request\Exception( 'protocol_not_followed' );
|
||||
}
|
||||
|
||||
$newRequest = $newRequest->request( $this->timeout, $this->httpVersion, \is_int( $this->followRedirects ) ? ( $this->followRedirects - 1 ) : $this->followRedirects );
|
||||
return $newRequest->$method( $params );
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sockets Exception Class
|
||||
*/
|
||||
class SocketsException extends \IPS\Http\Request\Exception { }
|
||||
Reference in new issue
Block a user