Bearsampp 2026.9.23
Loading...
Searching...
No Matches
HttpClient Class Reference

Static Public Member Functions

static applyCurlSslOptions ($ch, $verify=true)
static checkInternetState ()
static downloadFile (string $url, string $filePath, $progressBar=false, $verify=true)
static fetchGet ($url, $verify=true)
static getApiJson ($url, $verify=true)
static getCacertPath ()
static getChangelogUrl ($utmSource=true)
static getCurlHttpHeaders ($url, $verify=true)
static getFopenHttpHeaders ($url, $verify=true)
static getGithubUrl ($type='user', $user=APP_GITHUB_USER, $repo=null, $branch=null, $path=null)
static getGithubUserUrl ()
static getHeaders ($host, $port, $ssl=false)
static getHttpHeaders ($pingUrl, $verify=true)
static getSslStreamContext ($verify=true, $url=null)
static getWebsiteUrl ($path='', $fragment='', $utmSource=true)
static getWebsiteUrlNoUtm ($path='', $fragment='')
static isGithubHost ($url)
static proxyDownload ($url, $filePath, $progressBar=false, $verify=true)
static proxyFetch ($url, $method='GET', $verify=true)
static redactUrl ($url)

Data Fields

const PROXY_CONNECT_TIMEOUT = 30
const PROXY_LOW_SPEED_LIMIT = 1024
const PROXY_LOW_SPEED_TIME = 30

Static Private Member Functions

static safeUrlForLog ($url)

Detailed Description

Definition at line 19 of file class.httpclient.php.

Member Function Documentation

◆ applyCurlSslOptions()

applyCurlSslOptions ( $ch,
$verify = true )
static

Applies certificate verification options to a cURL handle.

When $verify is true the peer certificate is checked against the bundled CA bundle. Pass false only for local/self-signed endpoints (e.g. the app's own Apache SSL).

Parameters
resource$chThe cURL handle.
bool$verifyWhether to verify the peer certificate. Defaults to true.
Returns
void

Definition at line 497 of file class.httpclient.php.

498 {
499 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify);
500 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify ? 2 : 0);
501
502 if ($verify) {
503 $cacert = self::getCacertPath();
504 if (is_file($cacert)) {
505 curl_setopt($ch, CURLOPT_CAINFO, $cacert);
506 }
507 }
508 }
static getCacertPath()

References getCacertPath().

Referenced by fetchGet(), getCurlHttpHeaders(), proxyDownload(), and proxyFetch().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ checkInternetState()

checkInternetState ( )
static

Checks the current state of the internet connection.

Opens a verified TLS connection to the project's own website over port 443. This keeps the probe encrypted (no plaintext traffic to a third party) and independent of external hosts such as Google.

Returns
bool True if the internet connection is active, false otherwise.

Definition at line 830 of file class.httpclient.php.

831 {
832 $host = str_replace(['https://', 'http://'], '', APP_WEBSITE);
833 $host = rtrim(parse_url($host, PHP_URL_HOST) ?: $host, '/');
834
835 // Verified TLS context: we only need to establish a clean TLS handshake, not
836 // fetch any content, so a successful connection is enough to confirm internet.
837 $connected = @stream_socket_client(
838 'tls://' . $host . ':443',
839 $errno,
840 $errstr,
841 5,
842 STREAM_CLIENT_CONNECT,
843 self::getSslStreamContext()
844 );
845
846 if ($connected) {
847 fclose($connected);
848
849 return true;
850 }
851
852 return false;
853 }
const APP_WEBSITE
Definition root.php:20

References APP_WEBSITE.

Referenced by QuickPick\installModule().

Here is the caller graph for this function:

◆ downloadFile()

downloadFile ( string $url,
string $filePath,
$progressBar = false,
$verify = true )
static

Downloads a file from a given URL and saves it to a specified file path.

GitHub-hosted archives are downloaded through the GitHub proxy. Non-GitHub URLs are streamed over a verified TLS connection in 8KB chunks to avoid loading the whole file into memory, emitting one JSON progress line per chunk when $progressBar is enabled.

Parameters
string$urlThe URL from which to fetch the file content.
string$filePathThe path where the file content should be saved.
bool$progressBarOptional. Whether to display a progress bar during the download process. Default is false.
bool$verifyWhether to verify the peer certificate. Defaults to true. Pass false only for local/self-signed endpoints.
Returns
array Returns the file path if successful, or an array with an error message if an error occurs.

Definition at line 756 of file class.httpclient.php.

757 {
758 // GitHub-hosted module archives are downloaded through the GitHub proxy so
759 // the client never holds or transmits a GitHub token. The body is streamed
760 // in 8KB chunks to avoid loading the whole archive into memory.
761 if (self::isGithubHost($url)) {
762 Log::trace('downloadFile() downloading via GitHub proxy: ' . $url);
763 $downloaded = self::proxyDownload($url, $filePath, $progressBar, $verify);
764 if (!$downloaded) {
765 Log::error('Error fetching content from URL: ' . $url);
766
767 return ['error' => 'Error fetching module'];
768 }
769
770 return ['success' => true];
771 }
772
773 Log::trace('downloadFile() downloading directly (non-GitHub): ' . $url);
774
775 // Open the URL for reading. The verified SSL context makes sure the file is
776 // fetched over a properly authenticated HTTPS connection.
777 $inputStream = @fopen($url, 'rb', false, self::getSslStreamContext(true, $url));
778 if ($inputStream === false) {
779 Log::error('Error fetching content from URL: ' . $url);
780
781 return ['error' => 'Error fetching module'];
782 }
783
784 // Open the file for writing
785 $outputStream = @fopen($filePath, 'wb');
786 if ($outputStream === false) {
787 Log::error('Error opening file for writing: ' . $filePath);
788 fclose($inputStream);
789
790 return ['error' => 'Error saving module'];
791 }
792
793 // Read and write in chunks to avoid memory overload
794 $bufferSize = 8096; // 8KB
795 $chunksRead = 0;
796
797 while (!feof($inputStream)) {
798 $buffer = fread($inputStream, $bufferSize);
799 fwrite($outputStream, $buffer);
800 $chunksRead++;
801
802 // Send progress update
803 if ($progressBar) {
804 $progress = $chunksRead;
805 echo json_encode(['progress' => $progress]) . PHP_EOL;
806
807 // Check if output buffering is active before calling ob_flush()
808 if (ob_get_length() !== false) {
809 ob_flush();
810 }
811 flush();
812 }
813 }
814
815 fclose($inputStream);
816 fclose($outputStream);
817
818 return ['success' => true];
819 }
static proxyDownload($url, $filePath, $progressBar=false, $verify=true)
static trace($data, $file=null)
static error($data, $file=null)

References Log\error(), proxyDownload(), and Log\trace().

Referenced by QuickPick\fetchAndUnzipModule().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ fetchGet()

fetchGet ( $url,
$verify = true )
static

Sends a GET request to the specified URL and returns the trimmed response body.

This is the generic non-proxied GET method. For GitHub-hosted URLs, prefer {

See also
proxyFetch()} which routes through the proxy and never exposes a token to the client.

The peer certificate is verified against the bundled CA bundle unless $verify is false (used only for local/self-signed endpoints).

Parameters
string$urlThe URL to send the GET request to.
bool$verifyWhether to verify the peer certificate. Defaults to true.
Returns
string The trimmed response body, or empty string on failure.

Definition at line 305 of file class.httpclient.php.

306 {
307 $ch = curl_init();
308 curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
309 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
310 curl_setopt($ch, CURLOPT_VERBOSE, true);
311 curl_setopt($ch, CURLOPT_URL, $url);
312 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
313 'User-Agent: ' . APP_GITHUB_USERAGENT . ' (https://github.com/' . APP_GITHUB_USER . '/' . APP_GITHUB_REPO . ')',
314 'Accept: application/json, text/plain, */*',
315 ));
316 self::applyCurlSslOptions($ch, $verify);
317 $data = curl_exec($ch);
318 if (curl_errno($ch)) {
319 Log::error('CURL Error: ' . curl_error($ch));
320 }
321
322 return trim((string)$data);
323 }
static applyCurlSslOptions($ch, $verify=true)
const APP_GITHUB_USERAGENT
Definition root.php:25
const APP_GITHUB_USER
Definition root.php:23
const APP_GITHUB_REPO
Definition root.php:24

References APP_GITHUB_REPO, APP_GITHUB_USER, APP_GITHUB_USERAGENT, applyCurlSslOptions(), and Log\error().

Referenced by getApiJson().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getApiJson()

getApiJson ( $url,
$verify = true )
static

Sends a GET request to the specified URL and returns the response.

GitHub-hosted URLs are fetched through the GitHub proxy so no token is ever sent by the client. Non-GitHub URLs are fetched directly over verified TLS.

Parameters
string$urlThe URL to send the GET request to.
bool$verifyWhether to verify the peer certificate. Defaults to true.
Returns
string The trimmed response data from the URL.

Definition at line 336 of file class.httpclient.php.

337 {
338 Log::trace('[VCHK-3] getApiJson() sending GET request to: ' . $url);
339
340 if (self::isGithubHost($url)) {
341 $result = self::proxyFetch($url, 'GET', $verify);
342 if ($result === false || (int)$result['status'] !== 200) {
343 Log::error('GitHub request via proxy failed for: ' . $url);
344 Log::trace('[VCHK-3] getApiJson() EXIT - proxy request failed');
345
346 return '';
347 }
348
349 Log::trace('[VCHK-3] getApiJson() response length: ' . strlen((string)$result['body']));
350
351 return trim($result['body']);
352 }
353
354 $data = self::fetchGet($url, $verify);
355
356 Log::trace('[VCHK-3] getApiJson() response length: ' . strlen($data));
357
358 return $data;
359 }
$result
static fetchGet($url, $verify=true)
static proxyFetch($url, $method='GET', $verify=true)

References $result, Log\error(), fetchGet(), proxyFetch(), and Log\trace().

Referenced by Util\getLatestVersion(), and QuickPick\rebuildQuickpickJson().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getCacertPath()

getCacertPath ( )
static

Returns the path to the CA bundle shipped with the PHP installation.

Returns
string The absolute path to the cacert.pem file.

Definition at line 391 of file class.httpclient.php.

392 {
393 return Path::getPhpPath() . '/extras/ssl/cacert.pem';
394 }
static getPhpPath($aetrayPath=false)

References Path\getPhpPath().

Referenced by applyCurlSslOptions(), and getSslStreamContext().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getChangelogUrl()

getChangelogUrl ( $utmSource = true)
static

Constructs the URL to the changelog page, optionally including UTM parameters.

Parameters
bool$utmSourceWhether to include UTM source parameters.
Returns
string The URL to the changelog page.

Definition at line 169 of file class.httpclient.php.

170 {
171 return self::getWebsiteUrl('doc/changelog', null, $utmSource);
172 }
static getWebsiteUrl($path='', $fragment='', $utmSource=true)

References getWebsiteUrl().

Here is the call graph for this function:

◆ getCurlHttpHeaders()

getCurlHttpHeaders ( $url,
$verify = true )
static

Retrieves HTTP headers from a given URL using cURL.

The peer certificate is verified against the bundled CA bundle unless $verify is false (used only for local/self-signed endpoints).

Parameters
string$urlThe URL from which to fetch the headers.
bool$verifyWhether to verify the peer certificate. Defaults to true.
Returns
array An array of headers if successful, otherwise an empty array.

Definition at line 95 of file class.httpclient.php.

96 {
97 $result = array();
98
99 $ch = curl_init();
100 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
101 curl_setopt($ch, CURLOPT_VERBOSE, true);
102 curl_setopt($ch, CURLOPT_HEADER, true);
103 curl_setopt($ch, CURLOPT_URL, $url);
104 self::applyCurlSslOptions($ch, $verify);
105
106 $response = @curl_exec($ch);
107 if (empty($response)) {
108 return $result;
109 }
110
111 // Cap the logged response to a small prefix so headers/body never saturate
112 // the log and any transient sensitive data in the body is not written verbatim.
113 Log::trace('getCurlHttpHeaders: ' . substr($response, 0, 512));
114 $responseHeaders = explode("\r\n\r\n", $response, 2);
115 if (!isset($responseHeaders[0]) || empty($responseHeaders[0])) {
116 return $result;
117 }
118
119 return explode("\n", $responseHeaders[0]);
120 }

References $response, $result, applyCurlSslOptions(), and Log\trace().

Referenced by getHttpHeaders().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getFopenHttpHeaders()

getFopenHttpHeaders ( $url,
$verify = true )
static

Retrieves HTTP headers from a given URL using the fopen function.

The stream context verifies the peer certificate against the bundled CA bundle unless $verify is false (used only for local/self-signed endpoints).

Parameters
string$urlThe URL from which to fetch the headers.
bool$verifyWhether to verify the peer certificate. Defaults to true.
Returns
array An array of headers if successful, otherwise an empty array.

Definition at line 70 of file class.httpclient.php.

71 {
72 $result = array();
73
74 $fp = @fopen($url, 'r', false, self::getSslStreamContext($verify));
75 if ($fp) {
76 $meta = stream_get_meta_data($fp);
77 $result = isset($meta['wrapper_data']) ? $meta['wrapper_data'] : $result;
78 fclose($fp);
79 }
80
81 return $result;
82 }

References $result.

Referenced by getHttpHeaders().

Here is the caller graph for this function:

◆ getGithubUrl()

getGithubUrl ( $type = 'user',
$user = APP_GITHUB_USER,
$repo = null,
$branch = null,
$path = null )
static

Generates various GitHub URLs based on the specified type.

Parameters
string$typeThe type of URL ('user', 'repo', 'raw'). Defaults to 'user'.
string$userThe GitHub username. Defaults to 'Bearsampp'.
string | null$repoThe repository name (required for 'repo' and 'raw' types).
string | null$branchThe branch name (required for 'raw' type).
string | null$pathThe file path (required for 'raw' type).
Returns
string|false The generated URL or false on invalid input.

Definition at line 185 of file class.httpclient.php.

186 {
187 if (empty($user) || !is_string($user)) {
188 return false;
189 }
190
191 // Encode as URL path segment (not query encoding)
192 $user = rawurlencode($user);
193
194 switch ($type) {
195 case 'user':
196 return "https://github.com/{$user}";
197
198 case 'repo':
199 if (empty($repo) || !is_string($repo)) {
200 return false;
201 }
202 $repo = rawurlencode($repo);
203
204 return "https://github.com/{$user}/{$repo}";
205
206 case 'issues':
207 if (empty($repo) || !is_string($repo)) {
208 return false;
209 }
210 $repo = rawurlencode($repo);
211
212 return "https://github.com/{$user}/{$repo}/issues";
213
214 case 'raw':
215 if (empty($repo) || empty($branch) || empty($path) || !is_string($repo) || !is_string($branch) || !is_string($path)) {
216 return false;
217 }
218 $repo = rawurlencode($repo);
219 $branch = rawurlencode($branch);
220
221 $path = ltrim($path, '/');
222 $segments = array_map('rawurlencode', explode('/', $path));
223 $pathEncoded = implode('/', $segments);
224
225 return "https://raw.githubusercontent.com/{$user}/{$repo}/{$branch}/{$pathEncoded}";
226
227 default:
228 return false;
229 }
230 }

References APP_GITHUB_USER.

Referenced by getGithubUserUrl().

Here is the caller graph for this function:

◆ getGithubUserUrl()

getGithubUserUrl ( )
static

Gets the GitHub user URL for Bearsampp.

Returns
string The GitHub user URL.

Definition at line 237 of file class.httpclient.php.

238 {
239 return self::getGithubUrl('user', APP_GITHUB_USER);
240 }
static getGithubUrl($type='user', $user=APP_GITHUB_USER, $repo=null, $branch=null, $path=null)

References APP_GITHUB_USER, and getGithubUrl().

Referenced by ActionAbout\__construct(), and ActionAbout\processWindow().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getHeaders()

getHeaders ( $host,
$port,
$ssl = false )
static

Retrieves the initial response line from a specified host and port using a socket connection.

This is a local connectivity probe (used to detect which local service owns a port). Certificate verification is intentionally disabled here: it only reads the first response line from localhost/self-signed services and never processes untrusted content.

Parameters
string$hostThe host name or IP address to connect to.
int$portThe port number to connect to.
bool$sslWhether to use SSL (defaults to false).
Returns
array An array containing the first line of the response, split into parts, or an empty array if unsuccessful.

Definition at line 256 of file class.httpclient.php.

257 {
258 $result = array();
259 $context = stream_context_create(array(
260 'ssl' => array(
261 'verify_peer' => false,
262 'verify_peer_name' => false,
263 'allow_self_signed' => true,
264 )
265 ));
266
267 $fp = @stream_socket_client(($ssl ? 'ssl://' : '') . $host . ':' . $port, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $context);
268 if ($fp) {
269 $out = fgets($fp);
270 $result = explode(PHP_EOL, $out);
271 @fclose($fp);
272 }
273
274 if (!empty($result)) {
275 $rebuildResult = array();
276 foreach ($result as $row) {
277 $row = trim($row);
278 if (!empty($row)) {
279 $rebuildResult[] = $row;
280 }
281 }
282 $result = $rebuildResult;
283
284 Log::trace('getHeaders: ' . count($result) . ' header(s)');
285 }
286
287 return $result;
288 }
$port

References $port, $result, and Log\trace().

Referenced by BinMailpit\checkPort(), and BinXlight\checkPort().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getHttpHeaders()

getHttpHeaders ( $pingUrl,
$verify = true )
static

Retrieves HTTP headers from a given URL using either cURL or fopen, depending on availability.

Parameters
string$pingUrlThe URL to ping for headers.
bool$verifyWhether to verify the peer certificate. Defaults to true. Pass false only for local/self-signed endpoints (e.g. the app's own Apache SSL on localhost).
Returns
array An array of HTTP headers.

Definition at line 35 of file class.httpclient.php.

36 {
37 if (function_exists('curl_version')) {
38 $result = self::getCurlHttpHeaders($pingUrl, $verify);
39 } else {
40 $result = self::getFopenHttpHeaders($pingUrl, $verify);
41 }
42
43 if (!empty($result)) {
44 $rebuildResult = array();
45 foreach ($result as $row) {
46 $row = trim($row);
47 if (!empty($row)) {
48 $rebuildResult[] = $row;
49 }
50 }
51 $result = $rebuildResult;
52
53 Log::trace('getHttpHeaders: ' . count($result) . ' header(s)');
54 }
55
56 return $result;
57 }
static getFopenHttpHeaders($url, $verify=true)
static getCurlHttpHeaders($url, $verify=true)

References $result, getCurlHttpHeaders(), getFopenHttpHeaders(), and Log\trace().

Referenced by BinApache\checkPort().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getSslStreamContext()

getSslStreamContext ( $verify = true,
$url = null )
static

Builds a stream context that verifies the peer certificate against the bundled CA bundle.

GitHub-hosted content is fetched through the GitHub proxy (APP_GITHUB_PROXY_URL) instead of directly, so no token is ever attached to fopen-based requests here. The $url parameter is kept for API compatibility.

Parameters
bool$verifyWhether to verify the peer certificate. Defaults to true.
string | null$urlOptional target URL (unused; kept for compatibility).
Returns
resource The stream context.

Definition at line 468 of file class.httpclient.php.

469 {
470 $ssl = array(
471 'verify_peer' => $verify,
472 'verify_peer_name' => $verify,
473 'allow_self_signed' => !$verify,
474 );
475
476 if ($verify) {
477 $cacert = self::getCacertPath();
478 if (is_file($cacert)) {
479 $ssl['cafile'] = $cacert;
480 }
481 }
482
483 return stream_context_create(array('ssl' => $ssl));
484 }

References getCacertPath().

Referenced by QuickPick\checkDownloadId(), QuickPick\checkQuickpickJson(), and QuickPick\fetchChecksumFromSidecar().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getWebsiteUrl()

getWebsiteUrl ( $path = '',
$fragment = '',
$utmSource = true )
static

Constructs a complete website URL with optional path, fragment, and UTM source parameters.

Parameters
string$pathOptional path to append to the base URL.
string$fragmentOptional fragment to append to the URL.
bool$utmSourceWhether to include UTM source parameters. Defaults to true.
Returns
string The constructed URL.

Definition at line 131 of file class.httpclient.php.

132 {
133 global $bearsamppCore;
134
135 $url = APP_WEBSITE;
136 if (!empty($path)) {
137 $url .= '/' . ltrim($path, '/');
138 }
139 if ($utmSource) {
140 $url = rtrim($url, '/') . '/?utm_source=bearsampp-' . $bearsamppCore->getAppVersion();
141 }
142 if (!empty($fragment)) {
143 $url .= $fragment;
144 }
145
146 return $url;
147 }
global $bearsamppCore

References $bearsamppCore, and APP_WEBSITE.

Referenced by getChangelogUrl(), TplAppApache\getMenuApache(), TplAppMailpit\getMenuMailpit(), TplAppMariadb\getMenuMariadb(), TplAppMemcached\getMenuMemcached(), TplAppMysql\getMenuMysql(), TplAppNodejs\getMenuNodejs(), TplAppPhp\getMenuPhp(), TplAppPostgresql\getMenuPostgresql(), TplAppXlight\getMenuXlight(), QuickPick\getQuickpickMenu(), TplApp\getSectionMenuRight(), getWebsiteUrlNoUtm(), and ActionAbout\processWindow().

Here is the caller graph for this function:

◆ getWebsiteUrlNoUtm()

getWebsiteUrlNoUtm ( $path = '',
$fragment = '' )
static

Constructs a website URL without UTM parameters.

Parameters
string$pathOptional path to append to the base URL.
string$fragmentOptional fragment to append to the URL.
Returns
string The constructed URL without UTM parameters.

Definition at line 157 of file class.httpclient.php.

158 {
159 return self::getWebsiteUrl($path, $fragment, false);
160 }

References getWebsiteUrl().

Referenced by ActionAbout\__construct().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ isGithubHost()

isGithubHost ( $url)
static

Determines whether a URL is hosted on GitHub.

Used to decide which requests must be routed through the GitHub proxy. Only these hosts are ever sent to the proxy; everything else (e.g. the QuickPick license API or mirror servers) is fetched directly.

Parameters
string$urlThe URL to check.
Returns
bool True when the URL host is a GitHub endpoint.

Definition at line 372 of file class.httpclient.php.

373 {
374 $host = strtolower(parse_url($url, PHP_URL_HOST) ?: '');
375
376 return in_array($host, array(
377 'github.com',
378 'api.github.com',
379 'raw.githubusercontent.com',
380 'objects.githubusercontent.com',
381 'codeload.github.com',
382 'gist.github.com',
383 ), true);
384 }

Referenced by QuickPick\checkQuickpickJson(), and QuickPick\fetchChecksumFromSidecar().

Here is the caller graph for this function:

◆ proxyDownload()

proxyDownload ( $url,
$filePath,
$progressBar = false,
$verify = true )
static

Downloads a resource through the GitHub proxy, streaming the body to a file.

Used for large payloads (module archives) fetched from GitHub hosts. The response body is streamed in chunks to the given file so it is never held in memory, and an optional progress bar emits one JSON progress line per 8KB chunk, matching the behaviour of the legacy stream download.

Parameters
string$urlThe target GitHub URL to download.
string$filePathLocal path to write the body to.
bool$progressBarWhether to emit progress lines. Defaults to false.
bool$verifyWhether to verify the peer certificate. Defaults to true. Pass false only for local/self-signed endpoints.
Returns
bool True when the download completed with a 2xx status, false otherwise.

Definition at line 641 of file class.httpclient.php.

642 {
643 if (!self::isGithubHost($url)) {
644 Log::error('[PROXY] proxyDownload() blocked non-GitHub URL: ' . self::safeUrlForLog($url));
645
646 return false;
647 }
648
649 Log::trace('[PROXY] proxyDownload() START target: ' . self::safeUrlForLog($url) . ' -> ' . $filePath);
650
651 $outputStream = @fopen($filePath, 'wb');
652 if ($outputStream === false) {
653 Log::trace('[PROXY] proxyDownload() FAILED - cannot open output file: ' . $filePath);
654
655 return false;
656 }
657
658 $status = 0;
659 $chunksRead = 0;
660
661 $ch = curl_init();
662 curl_setopt($ch, CURLOPT_URL, APP_GITHUB_PROXY_URL);
663 curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
664 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
665 curl_setopt($ch, CURLOPT_POST, true);
666 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array(
667 'url' => (string)$url,
668 'method' => 'GET',
669 )));
670 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
671 'Content-Type: application/json',
672 'X-Bearsampp-Key: ' . APP_GITHUB_PROXY_KEY,
673 'User-Agent: ' . APP_GITHUB_USERAGENT . ' (https://github.com/' . APP_GITHUB_USER . '/' . APP_GITHUB_REPO . ')',
674 ));
675 self::applyCurlSslOptions($ch, $verify);
676
677 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::PROXY_CONNECT_TIMEOUT);
678 curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, self::PROXY_LOW_SPEED_LIMIT);
679 curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, self::PROXY_LOW_SPEED_TIME);
680
681 // Capture the status line from the response headers without writing them to the file.
682 curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $line) use (&$status) {
683 if (preg_match('#^HTTP/\S+\s+(\d{3})#', $line, $match) === 1) {
684 $status = (int)$match[1];
685 }
686
687 return strlen($line);
688 });
689
690 // Stream the body to the output file in chunks, mirroring the legacy
691 // 8KB chunk loop and progress reporting.
692 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use (&$outputStream, &$chunksRead, $progressBar) {
693 if ($outputStream === false) {
694 return -1; // abort the transfer
695 }
696
697 $len = fwrite($outputStream, $data);
698 if ($len === false) {
699 return -1;
700 }
701
702 $chunksRead++;
703 if ($progressBar) {
704 echo json_encode(array('progress' => $chunksRead)) . PHP_EOL;
705
706 if (ob_get_length() !== false) {
707 ob_flush();
708 }
709 flush();
710 }
711
712 return $len;
713 });
714
715 $success = (curl_exec($ch) !== false);
716 $error = curl_error($ch);
717
718 // CURLINFO_RESPONSE_CODE is the authoritative status: it works even when
719 // the raw header lines cannot be parsed (e.g. HTTP/2 ":status" pseudo
720 // headers, interim 1xx blocks, or a proxy omitting a status line), which
721 // the header callback above may silently miss. The callback capture is
722 // only kept as a fallback for the rare case cURL reports no code.
723 $curlStatus = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
724 if ($curlStatus > 0) {
725 $status = $curlStatus;
726 }
727
728 fclose($outputStream);
729
730 if (!$success) {
731 Log::error('Proxy download failed: ' . $error);
732 Log::trace('[PROXY] proxyDownload() FAILED - status ' . $status . ', target: ' . self::safeUrlForLog($url) . ' - ' . $error);
733 } else {
734 Log::trace('[PROXY] proxyDownload() END - status ' . $status . ', chunks ' . $chunksRead . ', target: ' . self::safeUrlForLog($url));
735 }
736
737 return $success && $status >= 200 && $status < 300;
738 }
const APP_GITHUB_PROXY_URL
Definition root.php:44
const APP_GITHUB_PROXY_KEY
Definition root.php:45

References APP_GITHUB_PROXY_KEY, APP_GITHUB_PROXY_URL, APP_GITHUB_REPO, APP_GITHUB_USER, APP_GITHUB_USERAGENT, applyCurlSslOptions(), Log\error(), and Log\trace().

Referenced by downloadFile().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ proxyFetch()

proxyFetch ( $url,
$method = 'GET',
$verify = true )
static

Fetches a resource through the GitHub proxy.

Sends a POST to APP_GITHUB_PROXY_URL carrying the target URL and method as a JSON body, authenticated with the shared public proxy key. The GitHub PAT never leaves the proxy server, so the client holds no token at all.

The proxy key is never logged.

Parameters
string$urlThe target GitHub URL to retrieve.
string$methodThe upstream method: GET, HEAD or POST. Defaults to GET.
bool$verifyWhether to verify the peer certificate. Defaults to true.
Returns
array|false An array with 'status', 'headers' (associative) and 'body', or false when the proxy request itself failed. Header name keys are normalized to lowercase; the first occurrence of a duplicate header name wins.

Definition at line 528 of file class.httpclient.php.

529 {
530 if (!self::isGithubHost($url)) {
531 Log::error('[PROXY] proxyFetch() blocked non-GitHub URL: ' . self::safeUrlForLog($url));
532
533 return false;
534 }
535
536 $method = strtoupper($method);
537 Log::trace('[PROXY] proxyFetch() ' . $method . ' target: ' . self::safeUrlForLog($url));
538
539 $payload = array(
540 'url' => (string)$url,
541 'method' => $method,
542 );
543
544 $ch = curl_init();
545 curl_setopt($ch, CURLOPT_URL, APP_GITHUB_PROXY_URL);
546 curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
547 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
548 curl_setopt($ch, CURLOPT_HEADER, true);
549 curl_setopt($ch, CURLOPT_POST, true);
550 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
551 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
552 'Content-Type: application/json',
553 'X-Bearsampp-Key: ' . APP_GITHUB_PROXY_KEY,
554 'User-Agent: ' . APP_GITHUB_USERAGENT . ' (https://github.com/' . APP_GITHUB_USER . '/' . APP_GITHUB_REPO . ')',
555 ));
556 self::applyCurlSslOptions($ch, $verify);
557
558 $response = curl_exec($ch);
559 if ($response === false) {
560 Log::error('Proxy request failed: ' . curl_error($ch));
561 Log::trace('[PROXY] proxyFetch() FAILED - target: ' . self::safeUrlForLog($url) . ' - ' . curl_error($ch));
562
563 return false;
564 }
565
566 $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
567 $headerSize = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE);
568
569 $response = (string)$response;
570
571 $headers = array();
572 $body = '';
573
574 if ($headerSize > 0 && strlen($response) >= $headerSize) {
575 // CURLINFO_HEADER_SIZE includes every received header block, so the
576 // body is exactly the bytes after it - even when the response contains
577 // interim header sections (e.g. "100 Continue" or redirects) that make
578 // a naive "first \r\n\r\n" split corrupt.
579 $headerData = substr($response, 0, $headerSize);
580 $body = substr($response, $headerSize);
581
582 // Keep only the LAST header block (the final status line and headers).
583 $blocks = preg_split('/\r?\n\r?\n/', trim($headerData));
584 $headerBlock = end($blocks);
585
586 foreach (explode("\r\n", $headerBlock) as $line) {
587 if (strpos($line, ':') === false) {
588 continue;
589 }
590 list($name, $value) = explode(':', $line, 2);
591 // HTTP header names are case-insensitive, so normalize to a
592 // consistent lowercase key. The first occurrence of a duplicate
593 // name still wins (matching the previous behaviour).
594 $name = strtolower(trim($name));
595 $value = trim($value);
596 if ($name !== '' && !isset($headers[$name])) {
597 $headers[$name] = $value;
598 }
599 }
600 } else {
601 // Defensive fallback: no usable header size reported. Keep the whole
602 // body, but still surface any headers found after the first separator.
603 $separatorPos = strpos($response, "\r\n\r\n");
604 $body = ($separatorPos === false) ? $response : substr($response, $separatorPos + 4);
605 if ($separatorPos !== false) {
606 foreach (explode("\r\n", substr($response, 0, $separatorPos)) as $line) {
607 if (strpos($line, ':') === false) {
608 continue;
609 }
610 list($name, $value) = explode(':', $line, 2);
611 $name = strtolower(trim($name));
612 $value = trim($value);
613 if ($name !== '' && !isset($headers[$name])) {
614 $headers[$name] = $value;
615 }
616 }
617 }
618 }
619
620 Log::trace('[PROXY] proxyFetch() END - status ' . $status . ', body length: ' . strlen($body));
621
622 return array('status' => $status, 'headers' => $headers, 'body' => $body);
623 }

References $response, APP_GITHUB_PROXY_KEY, APP_GITHUB_PROXY_URL, APP_GITHUB_REPO, APP_GITHUB_USER, APP_GITHUB_USERAGENT, applyCurlSslOptions(), Log\error(), and Log\trace().

Referenced by QuickPick\checkQuickpickJson(), QuickPick\fetchChecksumFromSidecar(), and getApiJson().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ redactUrl()

redactUrl ( $url)
static

Redacts sensitive query-string parameters from a URL before it is logged.

URLs passed to this class can carry credentials such as an API key or a license/download ID (e.g. ?api_key=...&download_id=...). Writing those verbatim into log files would leak them, so any matching parameter's value is masked.

Parameters
string$urlThe URL to redact.
Returns
string The URL with sensitive parameter values replaced by '******'.

Definition at line 408 of file class.httpclient.php.

409 {
410 $url = (string)$url;
411 if ($url === '' || strpos($url, '?') === false) {
412 return $url;
413 }
414
415 $sensitiveParams = ['api_key', 'download_id', 'token', 'access_token', 'auth', 'password', 'passwd', 'key', 'secret'];
416
417 [$base, $query] = explode('?', $url, 2);
418 parse_str($query, $params);
419
420 foreach ($params as $name => &$value) {
421 foreach ($sensitiveParams as $sensitive) {
422 if (stripos($name, $sensitive) !== false) {
423 $value = '******';
424 break;
425 }
426 }
427 }
428
429 return $base . '?' . http_build_query($params);
430 }

◆ safeUrlForLog()

safeUrlForLog ( $url)
staticprivate

Returns a log-safe representation of a URL, keeping only scheme + host + path.

Query strings, fragments, and any embedded credentials are stripped so sensitive tokens never appear in log files.

Parameters
string$urlThe URL to sanitise.
Returns
string The safe representation (e.g. "https://github.com/foo/bar").

Definition at line 442 of file class.httpclient.php.

443 {
444 $url = (string)$url;
445 $scheme = parse_url($url, PHP_URL_SCHEME) ?: 'https';
446 $host = parse_url($url, PHP_URL_HOST) ?: '';
447 $path = parse_url($url, PHP_URL_PATH) ?: '';
448
449 if ($host === '') {
450 return '(invalid-url)';
451 }
452
453 return $scheme . '://' . $host . $path;
454 }

Field Documentation

◆ PROXY_CONNECT_TIMEOUT

const PROXY_CONNECT_TIMEOUT = 30

Definition at line 21 of file class.httpclient.php.

◆ PROXY_LOW_SPEED_LIMIT

const PROXY_LOW_SPEED_LIMIT = 1024

Definition at line 22 of file class.httpclient.php.

◆ PROXY_LOW_SPEED_TIME

const PROXY_LOW_SPEED_TIME = 30

Definition at line 23 of file class.httpclient.php.


The documentation for this class was generated from the following file: