dolibarr 25.0.0-alpha
geturl.lib.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2008-2020 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
4 * Copyright (C) 2025-2026 Frédéric France <frederic.france@free.fr>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 * or see https://www.gnu.org/
19 */
20
51function getURLContent($url, $postorget = 'GET', $param = '', $followlocation = 1, $addheaders = array(), $allowedschemes = array('http', 'https'), $localurl = 0, $ssl_verifypeer = -1, $timeoutconnect = 0, $timeoutresponse = 0, $otherCurlOptions = array(), $morelogsuffix = '')
52{
53 // Get global variables for proxy use
54 $USE_PROXY = getDolGlobalInt('MAIN_PROXY_USE');
55 $PROXY_HOST = getDolGlobalString('MAIN_PROXY_HOST');
56 $PROXY_PORT = getDolGlobalInt('MAIN_PROXY_PORT');
57 $PROXY_USER = getDolGlobalString('MAIN_PROXY_USER');
58 $PROXY_PASS = getDolGlobalString('MAIN_PROXY_PASS');
59
60 dol_syslog("getURLContent postorget=".$postorget." URL=".$url);
61 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
62 dol_syslog("getURLContent postorget=".$postorget." URL=".$url." json_encode(param)=".json_encode($param), LOG_DEBUG, 0, '_curl');
63 }
64 if ($morelogsuffix) {
65 dol_syslog("getURLContent postorget=".$postorget." URL=".$url." json_encode(param)=".json_encode($param), LOG_DEBUG, 0, $morelogsuffix);
66 }
67
68 if (!function_exists('curl_init')) {
69 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
70 dol_syslog("getURLContent PHP curl library must be installed", LOG_DEBUG, 0, '_curl');
71 }
72 if ($morelogsuffix) {
73 dol_syslog("getURLContent PHP curl library must be installed", LOG_DEBUG, 0, $morelogsuffix);
74 }
75
76 return array('http_code' => 500, 'content' => '', 'curl_error_no' => 1, 'curl_error_msg' => 'PHP curl library must be installed');
77 }
78
79 //setting the curl parameters.
80 $ch = curl_init();
81
82 /*print $API_Endpoint."-".$API_version."-".$PAYPAL_API_USER."-".$PAYPAL_API_PASSWORD."-".$PAYPAL_API_SIGNATURE."<br>";
83 print $USE_PROXY."-".$gv_ApiErrorURL."<br>";
84 print $nvpStr;
85 exit;*/
86 // The verbose output goes to the stderr of the process (Apache error log, output of a cron job...) and includes the request
87 // headers with their credentials, so it is only enabled together with the curl debug log.
88 curl_setopt($ch, CURLOPT_VERBOSE, getDolGlobalInt('MAIN_CURL_DEBUG') ? true : false);
89 curl_setopt($ch, CURLOPT_USERAGENT, 'Dolibarr geturl function'); // set the Dolibarr user agent name
90
91 // We use @ here because this may return warning if safe mode is on or open_basedir is on (following location is forbidden when safe mode is on).
92 // We force value to false so we will manage redirection ourself later.
93 @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
94
95 if (is_array($addheaders) && count($addheaders)) {
96 curl_setopt($ch, CURLOPT_HTTPHEADER, $addheaders);
97 }
98 curl_setopt($ch, CURLINFO_HEADER_OUT, true); // To be able to retrieve request header and log it
99
100 if (getDolGlobalInt('MAIN_CURL_GET_RESPONSE_HEADER')) {
101 curl_setopt($ch, CURLOPT_HEADER, true); // To be able to retrieve response header
102 }
103
104 // By default use the TLS version decided by PHP.
105 // You can force, if supported a version like TLSv1 or TLSv1.2
106 if (getDolGlobalString('MAIN_CURL_SSLVERSION')) {
107 $sslversion = is_numeric(getDolGlobalString('MAIN_CURL_SSLVERSION')) ? getDolGlobalInt('MAIN_CURL_SSLVERSION') : constant(getDolGlobalString('MAIN_CURL_SSLVERSION'));
108 curl_setopt($ch, CURLOPT_SSLVERSION, (int) $sslversion);
109 }
110 //curl_setopt($ch, CURLOPT_SSLVERSION, 6); for tls 1.2
111
112 // Turning on or off the ssl target certificate
113 if ($ssl_verifypeer < 0) {
114 global $dolibarr_main_prod;
115 $ssl_verifypeer = ($dolibarr_main_prod ? true : false);
116 }
117 if (getDolGlobalString('MAIN_CURL_DISABLE_VERIFYPEER')) {
118 $ssl_verifypeer = 0;
119 }
120
121 // Turning off the server and peer verification(TrustManager Concept).
122 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, ($ssl_verifypeer ? true : false));
123
124 // 0 to not check the names
125 // 1 to check the existence of a common name in the SSL peer certificate
126 // 2 to check the existence of a common name and also verify that it matches the hostname provided.
127 // In production environments the value of this option should be kept at 2 (default value).
128 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, ($ssl_verifypeer ? 2 : 0));
129
130 // Restrict use to some protocols only
131 $protocols = 0;
132 $redir_list = array();
133 if (is_array($allowedschemes)) {
134 foreach ($allowedschemes as $allowedscheme) {
135 if ($allowedscheme == 'http') {
136 $protocols |= CURLPROTO_HTTP;
137 $redir_list["HTTP"] = 1;
138 } elseif ($allowedscheme == 'https') {
139 $protocols |= CURLPROTO_HTTPS;
140 $redir_list["HTTPS"] = 1;
141 } elseif ($allowedscheme == 'ftp') {
142 $protocols |= CURLPROTO_FTP;
143 $redir_list["FTP"] = 1;
144 } elseif ($allowedscheme == 'ftps') {
145 $protocols |= CURLPROTO_FTPS;
146 $redir_list["FTPS"] = 1;
147 }
148 }
149 } else {
150 return array('http_code' => 500, 'content' => '', 'curl_error_no' => 1, 'curl_error_msg' => 'Parameter allowedschemes of getURLContent must be an array of protocol schemes');
151 }
152
153 $newtimeoutconnect = ($timeoutconnect ? $timeoutconnect : getDolGlobalInt('MAIN_USE_CONNECT_TIMEOUT', 5));
154 $newtimeoutresponse = ($timeoutresponse ? $timeoutresponse : getDolGlobalInt('MAIN_USE_RESPONSE_TIMEOUT', 30));
155
156 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
157 dol_syslog("getURLContent newtimeoutconnect=".$newtimeoutconnect." newtimeoutresponse=".$newtimeoutresponse, LOG_DEBUG, 0, '_curl');
158 }
159 if ($morelogsuffix) {
160 dol_syslog("getURLContent newtimeoutconnect=".$newtimeoutconnect." newtimeoutresponse=".$newtimeoutresponse, LOG_DEBUG, 0, $morelogsuffix);
161 }
162
163 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $newtimeoutconnect); // Timeout for connection
164 curl_setopt($ch, CURLOPT_TIMEOUT, $newtimeoutresponse); // Timeout for total time including connection
165
166 // limit size of downloaded files.
167 $maxsize = getDolGlobalInt('MAIN_SECURITY_MAXFILESIZE_DOWNLOADED');
168 if ($maxsize && defined('CURLOPT_MAXFILESIZE_LARGE')) {
169 curl_setopt($ch, CURLOPT_MAXFILESIZE_LARGE, $maxsize * 1024); // @phan-suppress-current-line PhanTypeMismatchArgumentNullableInternal
170 }
171 if ($maxsize && defined('CURLOPT_MAXFILESIZE')) {
172 curl_setopt($ch, CURLOPT_MAXFILESIZE, $maxsize * 1024);
173 }
174
175 //curl_setopt($ch, CURLOPT_SAFE_UPLOAD, true); // PHP 5.5
176 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // We want response
177
178 // Accept a compressed response (gzip, deflate, br... whatever this libcurl supports): libcurl decodes it, so 'content' is the
179 // plain body. Note that the response headers (HEAD, MAIN_CURL_GET_RESPONSE_HEADER) still show the Content-Encoding.
180 curl_setopt($ch, CURLOPT_ENCODING, '');
181
182 $responsebuffer = '';
183 $responsetoolarge = false;
184 if ($maxsize) {
185 // CURLOPT_MAXFILESIZE counts the bytes received on the wire, so a compressed response can decode into much more than the
186 // limit: the decoded bytes are counted too, and the transfer is aborted as soon as they exceed the limit.
192 $writefunction = function ($curl, $data) use (&$responsebuffer, &$responsetoolarge, $maxsize) {
193 if (strlen($responsebuffer) + strlen($data) > $maxsize * 1024) {
194 $responsetoolarge = true;
195 return 0; // Less than strlen($data): libcurl aborts the transfer with CURLE_WRITE_ERROR (23)
196 }
197 $responsebuffer .= $data;
198 return strlen($data);
199 };
200 curl_setopt($ch, CURLOPT_WRITEFUNCTION, $writefunction);
201 }
202 if ($postorget == 'POST') {
203 curl_setopt($ch, CURLOPT_POST, true); // POST
204 curl_setopt($ch, CURLOPT_POSTFIELDS, $param); // Setting param x=a&y=z as POST fields
205 } elseif ($postorget == 'POSTALREADYFORMATED') {
206 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); // HTTP request is 'POST' but param string is taken as it is
207 curl_setopt($ch, CURLOPT_POSTFIELDS, $param); // param = content of post, like a xml string
208 } elseif ($postorget == 'PUT') {
209 $array_param = array();
210 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); // HTTP request is 'PUT'
211 if (!is_array($param)) {
212 parse_str($param, $array_param);
213 } else {
214 dol_syslog("parameter param must be a string", LOG_WARNING);
215 $array_param = $param;
216 }
217 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($array_param)); // Setting param x=a&y=z as PUT fields
218 } elseif ($postorget == 'PUTALREADYFORMATED') {
219 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); // HTTP request is 'PUT'
220 curl_setopt($ch, CURLOPT_POSTFIELDS, $param); // param = content of post, like a xml string
221 } elseif ($postorget == 'PATCH') {
222 $array_param = array();
223 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); // RFC 5789
224 if (!is_array($param)) {
225 parse_str($param, $array_param);
226 } else {
227 dol_syslog("parameter param must be a string", LOG_WARNING);
228 $array_param = $param;
229 }
230 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($array_param));
231 } elseif ($postorget == 'PATCHALREADYFORMATED') {
232 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); // RFC 5789
233 curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
234 } elseif ($postorget == 'HEAD') {
235 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'HEAD'); // HTTP request is 'HEAD'
236 curl_setopt($ch, CURLOPT_NOBODY, true);
237 curl_setopt($ch, CURLOPT_HEADER, true);
238 } elseif ($postorget == 'DELETE') {
239 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); // POST
240 } else {
241 curl_setopt($ch, CURLOPT_POST, false); // GET
242 }
243
244 //if USE_PROXY constant set at begin of this method.
245 if ($USE_PROXY) {
246 dol_syslog("getURLContent set proxy to ".$PROXY_HOST.":".$PROXY_PORT." - ".$PROXY_USER.":".$PROXY_PASS);
247 //curl_setopt ($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); // Curl 7.10
248 curl_setopt($ch, CURLOPT_PROXY, $PROXY_HOST.":".$PROXY_PORT);
249 if ($PROXY_USER) {
250 curl_setopt($ch, CURLOPT_PROXYUSERPWD, $PROXY_USER.":".$PROXY_PASS);
251 }
252 }
253
254 if (is_array($otherCurlOptions)) {
255 // Options that would bypass the anti SSRF check done below on each hop (redirections are followed by hand so that every hop
256 // is checked, and the connection is pinned to the IP that was checked): they are ignored whatever the caller asks.
257 $forbiddencurloptions = [CURLOPT_FOLLOWLOCATION, CURLOPT_URL, CURLOPT_RESOLVE, CURLOPT_PROXY, CURLOPT_PROTOCOLS, CURLOPT_REDIR_PROTOCOLS];
258 foreach (['CURLOPT_CONNECT_TO', 'CURLOPT_UNIX_SOCKET_PATH', 'CURLOPT_ABSTRACT_UNIX_SOCKET', 'CURLOPT_PRE_PROXY'] as $constname) {
259 if (defined($constname)) {
260 $forbiddencurloptions[] = constant($constname);
261 }
262 }
263 foreach ($otherCurlOptions as $option => $value) {
264 if (in_array($option, $forbiddencurloptions, true)) {
265 dol_syslog("getURLContent curl option ".$option." can not be set with otherCurlOptions, ignored", LOG_WARNING);
266 continue;
267 }
268 curl_setopt($ch, $option, $value);
269 }
270 }
271
272 $newUrl = $url;
273 $maxRedirection = 5;
274 $info = array();
275 $response = '';
276
277 do {
278 if ($maxRedirection < 1) {
279 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
280 dol_syslog("getURLContent http_code=400 Maximum number of redirections reached", LOG_DEBUG, 0, '_curl');
281 }
282 return array('http_code' => 400, 'content' => 'Maximum number of redirections reached', 'curl_error_no' => 1, 'curl_error_msg' => 'Maximum number of redirections reached');
283 }
284
285 curl_setopt($ch, CURLOPT_URL, $newUrl);
286
287 // Parse $newUrl
288 $newUrlArray = parse_url($newUrl);
289 if (!is_array($newUrlArray) || (empty($newUrlArray['host']) && empty($newUrlArray['path']))) {
290 // parse_url() returns false on a malformed URL (like 'http:///path')
291 return array('http_code' => 400, 'content' => '', 'curl_error_no' => 1, 'curl_error_msg' => 'Bad URL '.$newUrl);
292 }
293 $hosttocheck = !empty($newUrlArray['host']) ? $newUrlArray['host'] : $newUrlArray['path'];
294 $hosttocheck = str_replace(array('[', ']'), '', $hosttocheck); // Remove brackets of IPv6
295
296 // Deny some reserved host names
297 if (in_array(strtolower($hosttocheck), array('metadata.google.internal'))) {
298 $info['http_code'] = 400;
299 $info['content'] = 'Error bad hostname '.$hosttocheck.' (Used by Google metadata). This value for hostname is not allowed.';
300 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
301 dol_syslog("getURLContent http_code=400 ".$info['content'], LOG_DEBUG, 0, '_curl');
302 }
303 return array('http_code' => 400, 'content' => $info['content'], 'curl_error_no' => 1, 'curl_error_msg' => $info['content']);
304 }
305
306 // Discard a host name that is a plain integer (decimal like 2130706433, hex like 0x7f000001, octal like 017700000001): the
307 // gethostbyname() fallback of resolveDns() would turn it into an IP (127.0.0.1 for these three) while it is not a host name.
308 // Only integers are refused: a single label host name made of hex letters, like 'db' or 'cafe', is a legitimate host name.
309 if (preg_match('/^(0x[0-9a-f]+|[0-9]+)$/i', $hosttocheck)) {
310 return array('http_code' => 400, 'content' => '', 'curl_error_no' => 1, 'curl_error_msg' => 'Host is a numeric address that is not allowed');
311 }
312
313 // Clean host name $hosttocheck to convert it into an IP $iptocheck
314 if (filter_var($hosttocheck, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
315 $iptocheck = $hosttocheck; // Already an IP (v4, or v6 without its brackets), nothing to resolve
316 } elseif (in_array($hosttocheck, array('localhost', 'localhost.domain'))) {
317 $iptocheck = '127.0.0.1';
318 } elseif (in_array($hosttocheck, array('ip6-localhost', 'ip6-loopback'))) {
319 $iptocheck = '::1';
320 } else {
321 // Resolve $hosttocheck to get the IP $iptocheck
322 $iptocheck = resolveDns($hosttocheck);
323 }
324
325 // Check $iptocheck is an IP (v4 or v6). resolveDns() returns the host name itself when the resolution failed.
326 if (!filter_var($iptocheck, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) { // This is not an IP
327 return array('http_code' => 400, 'content' => '', 'curl_error_no' => 1, 'curl_error_msg' => 'Host '.$hosttocheck.' can not be resolved into an IP');
328 }
329
330 if ($iptocheck) {
331 $tmpresult = isIPAllowed($iptocheck, $localurl);
332 if ($tmpresult) {
333 $info['http_code'] = 400;
334 $info['content'] = $tmpresult;
335 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
336 dol_syslog("getURLContent http_code=400 ".$info['content'], LOG_DEBUG, 0, '_curl');
337 }
338 return array('http_code' => 400, 'content' => $tmpresult, 'curl_error_no' => 1, 'curl_error_msg' => $tmpresult);
339 }
340 }
341
342 if ($iptocheck) {
343 // Set CURLOPT_CONNECT_TO so curl will not try another resolution that may give a different result. Possible only on PHP v7+
344 // Format is "host:port:ip:port". Port fields MUST use %s, not %d: an empty port (default 80/443) must stay empty so it matches "any port" and pins to the same port. With %d the empty string becomes 0 ("host:0:ip:0"), never matches the real port, and libcurl ignores the pin, re-opening a DNS-rebinding SSRF bypass.
345 if (defined('CURLOPT_CONNECT_TO')) {
346 // An IPv6 must be in brackets, like in a URL, else libcurl can not tell it from the port separators ("host:80:::1:80" fails with "No valid port number in connect to host string").
347 $ipforconnect = (strpos($iptocheck, ':') !== false) ? '['.$iptocheck.']' : $iptocheck;
348 $connect_to = array(sprintf("%s:%s:%s:%s", $newUrlArray['host'], empty($newUrlArray['port']) ? '' : $newUrlArray['port'], $ipforconnect, empty($newUrlArray['port']) ? '' : $newUrlArray['port']));
349 //var_dump($newUrlArray);
350 //var_dump($connect_to);
351 curl_setopt($ch, CURLOPT_CONNECT_TO, $connect_to);
352 }
353 }
354
355 // Moving these just before the curl_exec option really limits
356 // on windows PHP 7.4.
357 curl_setopt($ch, CURLOPT_PROTOCOLS, $protocols);
358 curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, $protocols);
359 /* CURLOPT_REDIR_PROTOCOLS_STR available from PHP 7.85.0
360 if (version_compare(PHP_VERSION, '8.3.0', '>=') && version_compare(curl_version()['version'], '7.85.0', '>=')) {
361 curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS_STR, implode(",", array_keys($redir_list)));
362 }
363 */
364
365 // Getting response from server
366 $responsebuffer = '';
367 $responsetoolarge = false;
368 $response = curl_exec($ch); // return false on error, result on success
369 if ($maxsize) {
370 // With a write function, curl_exec() returns true instead of the response: the response is what the function collected
371 $response = ($response === false || $responsetoolarge) ? false : $responsebuffer;
372 }
373
374 $info = curl_getinfo($ch); // Reading of request must be done after sending request
375 $http_code = $info['http_code'];
376
377 if ($followlocation && in_array($http_code, [301, 302, 303, 307, 308]) && !empty($info['redirect_url'])) {
378 $newUrl = $info['redirect_url'];
379 $maxRedirection--;
380
381 // Redirections are followed by hand, so that every hop goes through the anti SSRF check above, which means the options set
382 // before this loop are still there for the next hop. When libcurl follows a redirection itself, it does not send the
383 // Authorization header to another host and it turns a POST into a GET on 301/302/303: do the same here.
384 $currenthost = (is_array($newUrlArray) && !empty($newUrlArray['host'])) ? $newUrlArray['host'] : '';
385 $nexthost = (string) parse_url($newUrl, PHP_URL_HOST);
386 if (strtolower($nexthost) != strtolower($currenthost)) {
387 // The credentials were meant for the host that was called, not for the one it redirects to.
388 $addheaders = removeCredentialHeaders($addheaders);
389 curl_setopt($ch, CURLOPT_HTTPHEADER, $addheaders);
390 }
391 if (in_array($http_code, [301, 302, 303]) && !in_array($postorget, ['GET', 'HEAD'])) {
392 // Same as libcurl and browsers: the redirected request is a GET without the body of the POST/PUT/PATCH.
393 $postorget = 'GET';
394 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, null);
395 curl_setopt($ch, CURLOPT_HTTPGET, true);
396 }
397 continue;
398 }
399
400 $http_code = 0;
401 } while ($http_code); // Stop if http_code is 0
402
403 $request = curl_getinfo($ch, CURLINFO_HEADER_OUT); // Reading of request must be done after sending request
404
405 dol_syslog("getURLContent request without content body=".$request);
406 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
407 // This may contains binary data, so we don't output response by default.
408 dol_syslog("getURLContent request without body=".$request, LOG_DEBUG, 0, '_curl');
409 dol_syslog("getURLContent response=".$response, LOG_DEBUG, 0, '_curl');
410 }
411 if ($morelogsuffix) {
412 // This may contains binary data, so we don't output response by default.
413 dol_syslog("getURLContent request without body=".$request, LOG_DEBUG, 0, $morelogsuffix);
414 dol_syslog("getURLContent response=".$response, LOG_DEBUG, 0, $morelogsuffix);
415 }
416
417 dol_syslog("getURLContent response size=".strlen($response)); // This $response may contains binary data, so we don't output it
418
419 $rep = array();
420 if (curl_errno($ch)) {
421 // Add keys to $rep
422 if ($response) {
423 $rep['content'] = (string) $response;
424 } else {
425 $rep['content'] = '';
426 }
427
428 $rep['http_code'] = 0;
429 $rep['curl_error_no'] = curl_errno($ch);
430 $rep['curl_error_msg'] = curl_error($ch);
431 if ($responsetoolarge) {
432 // The transfer was aborted by our write function (error 23): report it like libcurl does for its own size check
433 $rep['curl_error_no'] = 63; // CURLE_FILESIZE_EXCEEDED
434 $rep['curl_error_msg'] = 'Maximum file size exceeded';
435 }
436
437 dol_syslog("getURLContent response array is ".implode(',', $rep));
438
439 if (getDolGlobalInt('MAIN_CURL_DEBUG')) {
440 dol_syslog("getURLContent curl_error_no=".$rep['curl_error_no']." curl_error_msg=".$rep['curl_error_msg'], LOG_DEBUG, 0, '_curl');
441 }
442 if ($morelogsuffix) {
443 dol_syslog("getURLContent curl_error_no=".$rep['curl_error_no']." curl_error_msg=".$rep['curl_error_msg'], LOG_DEBUG, 0, $morelogsuffix);
444 }
445 } else {
446 //$info = curl_getinfo($ch);
447
448 // Return all fields found into $info.
449 $rep = $info;
450 //$rep['header_size'] = $info['header_size'];
451 //$rep['http_code'] = $info['http_code'];
452 //$rep['content_type'] = $info['http_code'];
453
454 dol_syslog("getURLContent http_code=".$rep['http_code']);
455
456 // Add more keys to $rep
457 if ($response) {
458 $rep['content'] = (string) $response;
459 if ($postorget == 'HEAD' || getDolGlobalInt('MAIN_CURL_GET_RESPONSE_HEADER')) { // In this case, response contains header + body
460 $rep['header'] = substr($rep['content'], 0, intval($rep['header_size']));
461 $rep['content'] = substr($rep['content'], intval($rep['header_size']));
462 }
463 } else {
464 $rep['content'] = '';
465 }
466
467 $rep['curl_error_no'] = 0;
468 $rep['curl_error_msg'] = '';
469 }
470
471 //closing the curl
472 curl_close($ch);
473
474 // We must exclude phpstant wwarning, because all fields found in result of curl_getinfo may not be all defined into description of this method.
475 // @phpstan-ignore-next-line
476 return $rep;
477}
478
487function removeCredentialHeaders($headers)
488{
489 if (!is_array($headers)) {
490 return [];
491 }
492
493 $credentialheaders = ['authorization', 'proxy-authorization', 'cookie', 'x-api-key', 'api-key', 'apikey', 'x-auth-token', 'x-access-token', 'x-goog-api-key', 'dolapikey'];
494
495 $ret = [];
496 foreach ($headers as $header) {
497 $name = strtolower(trim((string) strstr((string) $header, ':', true)));
498 if (in_array($name, $credentialheaders)) {
499 continue;
500 }
501 $ret[] = $header;
502 }
503
504 return $ret;
505}
506
507
514function resolveDns($hosttocheck)
515{
516 $iptocheck = null;
517
518 // Resolve $hosttocheck to get the IP $iptocheck
519 if (function_exists('dns_get_record') && !getDolGlobalString('MAIN_DISABLE_DNS_GET_RECORD_FOR_IP_RESOLUTION')) {
520 try {
521 // A failed resolution (host not found, DNS server failure) raises a PHP warning, that we silence: the caller handles the "not resolved" case.
522 // The try/catch is still needed when an error handler converts warnings into exceptions (the @ does not prevent that).
523 $records = @dns_get_record($hosttocheck, DNS_A + DNS_AAAA);
524
525 if (!empty($records[0]) && is_array($records[0]) && !empty($records[0]['ip'])) { // We take the first one
526 $iptocheck = $records[0]['ip'];
527 } elseif (!empty($records[0]) && is_array($records[0]) && !empty($records[0]['ipv6'])) { // We take the first one
528 $iptocheck = $records[0]['ipv6'];
529 }
530 } catch (Exception $e) {
531 // Nothing done
532 }
533 } elseif (function_exists('gethostbyname')) { // resolve only ipv4
534 $iptocheck = gethostbyname($hosttocheck);
535 } else {
536 $iptocheck = $hosttocheck;
537 }
538
539 if ($iptocheck === null) {
540 $iptocheck = $hosttocheck;
541 }
542 return $iptocheck;
543}
544
545
553function isIPAllowed($iptocheck, $localurl)
554{
555 if ($localurl == 0) { // Only external url allowed (dangerous, may allow to get malware)
556 if (!filter_var($iptocheck, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
557 // Deny ips like 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8 et 240.0.0.0/4, ::1/128, ::/128, ::ffff:0:0/96, fe80::/10...
558 $errormsg = 'Error bad hostname IP (private or reserved range). Must be an external URL.';
559 return $errormsg;
560 }
561 if (!empty($_SERVER["SERVER_ADDR"]) && $iptocheck == $_SERVER["SERVER_ADDR"]) {
562 $errormsg = 'Error bad hostname IP (IP is a local IP). Must be an external URL.';
563 return $errormsg;
564 }
565 if (getDolGlobalString('MAIN_SECURITY_ANTI_SSRF_SERVER_IP') && in_array($iptocheck, explode(',', getDolGlobalString('MAIN_SECURITY_ANTI_SSRF_SERVER_IP')))) {
566 $errormsg = 'Error bad hostname IP (IP is a local IP defined into MAIN_SECURITY_SERVER_IP). Must be an external URL.';
567 return $errormsg;
568 }
569 }
570 if ($localurl == 1) { // Only local url allowed (dangerous, may allow to get metadata on server or make internal port scanning)
571 // Deny ips NOT like 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8 et 240.0.0.0/4, ::1/128, ::/128, ::ffff:0:0/96, fe80::/10...
572 if (filter_var($iptocheck, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
573 $errormsg = 'Error bad hostname '.$iptocheck.'. Must be a local URL.';
574 return $errormsg;
575 }
576 if (getDolGlobalString('MAIN_SECURITY_ANTI_SSRF_SERVER_IP') && !in_array($iptocheck, explode(',', getDolGlobalString('MAIN_SECURITY_ANTI_SSRF_SERVER_IP')))) {
577 $errormsg = 'Error bad hostname IP (IP is not a local IP defined into list MAIN_SECURITY_SERVER_IP). Must be a local URL in allowed list.';
578 return $errormsg;
579 }
580 }
581
582 // Common check on ip (local and external)
583 // See list on https://tagmerge.com/gist/a7b9d57ff8ec11d63642f8778609a0b8
584 // Not evasive url that ar enot IP are excluded by test on IP v4/v6 validity.
585 // The link-local ones are already refused above when only external URLs are allowed, but not when local URLs are ($localurl = 1
586 // or 2), and 168.63.129.16 is in the public address space anyway.
587 $arrayofmetadataserver = array(
588 '169.254.169.254' => 'AWS, GCP, Azure, OpenStack, DigitalOcean...',
589 'fd00:ec2::254' => 'AWS (IPv6)',
590 '169.254.170.2' => 'AWS ECS',
591 '168.63.129.16' => 'Azure',
592 '100.100.100.200' => 'Alibaba',
593 '192.0.0.192' => 'Oracle',
594 '192.80.8.124' => 'Packet',
595 '100.88.222.5' => 'Tencent cloud',
596 );
597 foreach ($arrayofmetadataserver as $ipofmetadataserver => $nameofmetadataserver) {
598 if ($iptocheck == $ipofmetadataserver) {
599 $errormsg = 'Error bad hostname IP (Used by '.$nameofmetadataserver.' metadata server). This IP is forbidden.';
600 return $errormsg;
601 }
602 }
603
604 return '';
605}
606
617function getDomainFromURL($url, $mode = 0)
618{
619 $arrayof2levetopdomain = array(
620 'co.at', 'or.at', 'gv.at',
621 'avocat.fr', 'aeroport.fr', 'veterinaire.fr',
622 'com.ng', 'gov.ng', 'gov.ua', 'com.ua', 'in.ua', 'org.ua', 'edu.ua', 'net.ua',
623 'net.uk', 'org.uk', 'gov.uk', 'co.uk',
624 'com.mx'
625 );
626
627 // Set if tld is on 2 levels
628 $tldon2level = 0;
629 $parts = array_reverse(explode('.', $url));
630 if (!empty($parts[1]) && in_array($parts[1].'.'.$parts[0], $arrayof2levetopdomain)) {
631 $tldon2level = 1;
632 }
633
634 if ($tldon2level && $mode > 0) {
635 $mode++;
636 }
637
638 $tmpdomain = preg_replace('/^https?:\/\/[^:]+:[^@]+@/i', '', $url); // Remove http(s)://login@pass in https://login@pass:mydomain.com/path, so we now got mydomain.com/path
639 $tmpdomain = preg_replace('/^https?:\/\//i', '', $tmpdomain); // Remove http(s)://
640 $tmpdomain = preg_replace('/\/.*$/i', '', $tmpdomain); // Remove part after /
641 $tmpdomain = preg_replace('/^[^@]+@/i', '', $tmpdomain); // Remove part1@ in part1@part2 (for emails)
642 if ($mode == 3) {
643 $tmpdomain = preg_replace('/^.*\.([^\.]+)\.([^\.]+)\.([^\.]+)\.([^\.]+)$/', '\1.\2.\3.\4', $tmpdomain);
644 } elseif ($mode == 2) {
645 $tmpdomain = preg_replace('/^.*\.([^\.]+)\.([^\.]+)\.([^\.]+)$/', '\1.\2.\3', $tmpdomain); // Remove part 'www.' before 'abc.mydomain.com'
646 } elseif ($mode == 1) {
647 $tmpdomain = preg_replace('/^.*\.([^\.]+)\.([^\.]+)$/', '\1.\2', $tmpdomain); // Remove part 'www.abc.' before 'mydomain.com'
648 }
649
650 if (empty($mode)) {
651 if ($tldon2level) {
652 $tmpdomain = preg_replace('/^.*\.([^\.]+)\.([^\.]+)\.([^\.]+)$/', '\1.\2.\3', $tmpdomain); // Remove part 'www.abc.' before 'mydomain.com'
653 $tmpdomain = preg_replace('/\.[^\.]+\.[^\.]+$/', '', $tmpdomain); // Remove TLD (.com.mx, .co.uk, ...)
654 } else {
655 $tmpdomain = preg_replace('/^.*\.([^\.]+)\.([^\.]+)$/', '\1.\2', $tmpdomain); // Remove part 'www.abc.' before 'mydomain.com'
656 $tmpdomain = preg_replace('/\.[^\.]+$/', '', $tmpdomain); // Remove TLD (.com, .net, ...)
657 }
658 }
659
660 return $tmpdomain;
661}
662
673function getRootURLFromURL($url)
674{
675 return preg_replace('/^([a-z]*:\/\/[^\/]*).*/i', '$1', $url);
676}
677
684function removeHtmlComment($content)
685{
686 $content = preg_replace('/<!--[^\-]+-->/', '', $content);
687 return $content;
688}
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getDomainFromURL($url, $mode=0)
Function get second level domain name.
isIPAllowed($iptocheck, $localurl)
Is IP allowed.
getRootURLFromURL($url)
Function root url from a long url For example: https://www.abc.mydomain.com/dir/page....
resolveDns($hosttocheck)
Resolve a hostname into its IP.
removeHtmlComment($content)
Function to remove comments into HTML content.
removeCredentialHeaders($headers)
Remove, from a list of HTTP request headers, the ones that carry a credential (Authorization,...
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0, $otherCurlOptions=array(), $morelogsuffix='')
Function to get a content from an URL (use proxy if proxy defined).