]> git.mxchange.org Git - friendica.git/blob - src/Util/Network.php
92b57784986307a0d52242c6e4852f38a20ea2a1
[friendica.git] / src / Util / Network.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Util;
23
24 use Friendica\Core\Hook;
25 use Friendica\Core\Logger;
26 use Friendica\DI;
27 use Friendica\Model\Contact;
28 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
29 use Friendica\Network\HTTPException\NotModifiedException;
30 use GuzzleHttp\Psr7\Uri;
31
32 class Network
33 {
34
35         /**
36          * Return raw post data from a post request
37          *
38          * @return string post data
39          */
40         public static function postdata()
41         {
42                 return file_get_contents('php://input');
43         }
44
45         /**
46          * Check URL to see if it's real
47          *
48          * Take a URL from the wild, prepend http:// if necessary
49          * and check DNS to see if it's real (or check if is a valid IP address)
50          *
51          * @param string $url The URL to be validated
52          * @return string|boolean The actual working URL, false else
53          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
54          */
55         public static function isUrlValid(string $url)
56         {
57                 if (DI::config()->get('system', 'disable_url_validation')) {
58                         return $url;
59                 }
60
61                 // no naked subdomains (allow localhost for tests)
62                 if (strpos($url, '.') === false && strpos($url, '/localhost/') === false) {
63                         return false;
64                 }
65
66                 if (substr($url, 0, 4) != 'http') {
67                         $url = 'http://' . $url;
68                 }
69
70                 $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
71                 $host = parse_url($url, PHP_URL_HOST);
72
73                 if (empty($host) || !(@dns_get_record($host . '.', DNS_A + DNS_AAAA + DNS_CNAME) || filter_var($host, FILTER_VALIDATE_IP))) {
74                         return false;
75                 }
76
77                 // Check if the certificate is valid for this hostname
78                 if (parse_url($url, PHP_URL_SCHEME) == 'https') {
79                         $port = parse_url($url, PHP_URL_PORT) ?? 443;
80
81                         $context = stream_context_create(["ssl" => ['capture_peer_cert' => true]]);
82
83                         $resource = @stream_socket_client('ssl://' . $host . ':' . $port, $errno, $errstr, $xrd_timeout, STREAM_CLIENT_CONNECT, $context);
84                         if (empty($resource)) {
85                                 Logger::notice('Invalid certificate', ['host' => $host]);
86                                 return false;
87                         }
88
89                         $cert = stream_context_get_params($resource);
90                         if (empty($cert)) {
91                                 Logger::notice('Invalid certificate params', ['host' => $host]);
92                                 return false;
93                         }
94
95                         $certinfo = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);
96                         if (empty($certinfo)) {
97                                 Logger::notice('Invalid certificate information', ['host' => $host]);
98                                 return false;
99                         }
100
101                         $valid_from = date(DATE_RFC2822,$certinfo['validFrom_time_t']);
102                         $valid_to   = date(DATE_RFC2822,$certinfo['validTo_time_t']);
103
104                         if ($certinfo['validFrom_time_t'] > time()) {
105                                 Logger::notice('Certificate validity starts after current date', ['host' => $host, 'from' => $valid_from, 'to' => $valid_to]);
106                                 return false;
107                         }
108
109                         if ($certinfo['validTo_time_t'] < time()) {
110                                 Logger::notice('Certificate validity ends before current date', ['host' => $host, 'from' => $valid_from, 'to' => $valid_to]);
111                                 return false;
112                         }
113                 }
114                 if (in_array(parse_url($url, PHP_URL_SCHEME), ['https', 'http'])) {
115                         if (!ParseUrl::getContentType($url, HttpClientAccept::DEFAULT, $xrd_timeout)) {
116                                 Logger::notice('Url not reachable', ['host' => $host, 'url' => $url]);
117                                 return false;
118                         }
119                 }
120
121                 return $url;
122         }
123
124         /**
125          * Checks that email is an actual resolvable internet address
126          *
127          * @param string $addr The email address
128          * @return boolean True if it's a valid email address, false if it's not
129          */
130         public static function isEmailDomainValid(string $addr)
131         {
132                 if (DI::config()->get('system', 'disable_email_validation')) {
133                         return true;
134                 }
135
136                 if (! strpos($addr, '@')) {
137                         return false;
138                 }
139
140                 $h = substr($addr, strpos($addr, '@') + 1);
141
142                 // Concerning the @ see here: https://stackoverflow.com/questions/36280957/dns-get-record-a-temporary-server-error-occurred
143                 if ($h && (@dns_get_record($h, DNS_A + DNS_AAAA + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP))) {
144                         return true;
145                 }
146                 if ($h && @dns_get_record($h, DNS_CNAME + DNS_MX)) {
147                         return true;
148                 }
149                 return false;
150         }
151
152         /**
153          * Check if URL is allowed
154          *
155          * Check $url against our list of allowed sites,
156          * wildcards allowed. If allowed_sites is unset return true;
157          *
158          * @param string $url URL which get tested
159          * @return boolean True if url is allowed otherwise return false
160          */
161         public static function isUrlAllowed(string $url)
162         {
163                 $h = @parse_url($url);
164
165                 if (! $h) {
166                         return false;
167                 }
168
169                 $str_allowed = DI::config()->get('system', 'allowed_sites');
170                 if (! $str_allowed) {
171                         return true;
172                 }
173
174                 $found = false;
175
176                 $host = strtolower($h['host']);
177
178                 // always allow our own site
179                 if ($host == strtolower($_SERVER['SERVER_NAME'])) {
180                         return true;
181                 }
182
183                 $fnmatch = function_exists('fnmatch');
184                 $allowed = explode(',', $str_allowed);
185
186                 if (count($allowed)) {
187                         foreach ($allowed as $a) {
188                                 $pat = strtolower(trim($a));
189                                 if (($fnmatch && fnmatch($pat, $host)) || ($pat == $host)) {
190                                         $found = true;
191                                         break;
192                                 }
193                         }
194                 }
195                 return $found;
196         }
197
198         /**
199          * Checks if the provided url domain is on the domain blocklist.
200          * Returns true if it is or malformed URL, false if not.
201          *
202          * @param string $url The url to check the domain from
203          *
204          * @return boolean
205          */
206         public static function isUrlBlocked(string $url)
207         {
208                 $host = @parse_url($url, PHP_URL_HOST);
209                 if (!$host) {
210                         return false;
211                 }
212
213                 $domain_blocklist = DI::config()->get('system', 'blocklist', []);
214                 if (!$domain_blocklist) {
215                         return false;
216                 }
217
218                 foreach ($domain_blocklist as $domain_block) {
219                         if (fnmatch(strtolower($domain_block['domain']), strtolower($host))) {
220                                 return true;
221                         }
222                 }
223
224                 return false;
225         }
226
227         /**
228          * Checks if the provided url is on the list of domains where redirects are blocked.
229          * Returns true if it is or malformed URL, false if not.
230          *
231          * @param string $url The url to check the domain from
232          *
233          * @return boolean
234          */
235         public static function isRedirectBlocked(string $url)
236         {
237                 $host = @parse_url($url, PHP_URL_HOST);
238                 if (!$host) {
239                         return false;
240                 }
241
242                 $no_redirect_list = DI::config()->get('system', 'no_redirect_list', []);
243                 if (!$no_redirect_list) {
244                         return false;
245                 }
246
247                 foreach ($no_redirect_list as $no_redirect) {
248                         if (fnmatch(strtolower($no_redirect), strtolower($host))) {
249                                 return true;
250                         }
251                 }
252
253                 return false;
254         }
255
256         /**
257          * Check if email address is allowed to register here.
258          *
259          * Compare against our list (wildcards allowed).
260          *
261          * @param  string $email email address
262          * @return boolean False if not allowed, true if allowed
263          *                       or if allowed list is not configured
264          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
265          */
266         public static function isEmailDomainAllowed(string $email)
267         {
268                 $domain = strtolower(substr($email, strpos($email, '@') + 1));
269                 if (!$domain) {
270                         return false;
271                 }
272
273                 $str_allowed = DI::config()->get('system', 'allowed_email', '');
274                 if (empty($str_allowed)) {
275                         return true;
276                 }
277
278                 $allowed = explode(',', $str_allowed);
279
280                 return self::isDomainAllowed($domain, $allowed);
281         }
282
283         /**
284          * Checks for the existence of a domain in a domain list
285          *
286          * @param string $domain
287          * @param array  $domain_list
288          * @return boolean
289          */
290         public static function isDomainAllowed(string $domain, array $domain_list)
291         {
292                 $found = false;
293
294                 foreach ($domain_list as $item) {
295                         $pat = strtolower(trim($item));
296                         if (fnmatch($pat, $domain) || ($pat == $domain)) {
297                                 $found = true;
298                                 break;
299                         }
300                 }
301
302                 return $found;
303         }
304
305         public static function lookupAvatarByEmail(string $email)
306         {
307                 $avatar['size'] = 300;
308                 $avatar['email'] = $email;
309                 $avatar['url'] = '';
310                 $avatar['success'] = false;
311
312                 Hook::callAll('avatar_lookup', $avatar);
313
314                 if (! $avatar['success']) {
315                         $avatar['url'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO;
316                 }
317
318                 Logger::info('Avatar: ' . $avatar['email'] . ' ' . $avatar['url']);
319                 return $avatar['url'];
320         }
321
322         /**
323          * Remove Google Analytics and other tracking platforms params from URL
324          *
325          * @param string $url Any user-submitted URL that may contain tracking params
326          * @return string The same URL stripped of tracking parameters
327          */
328         public static function stripTrackingQueryParams(string $url)
329         {
330                 $urldata = parse_url($url);
331                 if (!empty($urldata["query"])) {
332                         $query = $urldata["query"];
333                         parse_str($query, $querydata);
334
335                         if (is_array($querydata)) {
336                                 foreach ($querydata as $param => $value) {
337                                         if (in_array(
338                                                 $param,
339                                                 [
340                                                         "utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign",
341                                                         "wt_mc", "pk_campaign", "pk_kwd", "mc_cid", "mc_eid",
342                                                         "fb_action_ids", "fb_action_types", "fb_ref",
343                                                         "awesm", "wtrid",
344                                                         "woo_campaign", "woo_source", "woo_medium", "woo_content", "woo_term"]
345                                                 )
346                                         ) {
347                                                 $pair = $param . "=" . urlencode($value);
348                                                 $url = str_replace($pair, "", $url);
349
350                                                 // Second try: if the url isn't encoded completely
351                                                 $pair = $param . "=" . str_replace(" ", "+", $value);
352                                                 $url = str_replace($pair, "", $url);
353
354                                                 // Third try: Maybey the url isn't encoded at all
355                                                 $pair = $param . "=" . $value;
356                                                 $url = str_replace($pair, "", $url);
357
358                                                 $url = str_replace(["?&", "&&"], ["?", ""], $url);
359                                         }
360                                 }
361                         }
362
363                         if (substr($url, -1, 1) == "?") {
364                                 $url = substr($url, 0, -1);
365                         }
366                 }
367
368                 return $url;
369         }
370
371         /**
372          * Add a missing base path (scheme and host) to a given url
373          *
374          * @param string $url
375          * @param string $basepath
376          * @return string url
377          */
378         public static function addBasePath(string $url, string $basepath)
379         {
380                 if (!empty(parse_url($url, PHP_URL_SCHEME)) || empty(parse_url($basepath, PHP_URL_SCHEME)) || empty($url) || empty(parse_url($url))) {
381                         return $url;
382                 }
383
384                 $base = ['scheme' => parse_url($basepath, PHP_URL_SCHEME),
385                         'host' => parse_url($basepath, PHP_URL_HOST)];
386
387                 $parts = array_merge($base, parse_url('/' . ltrim($url, '/')));
388                 return self::unparseURL($parts);
389         }
390
391         /**
392          * Find the matching part between two url
393          *
394          * @param string $url1
395          * @param string $url2
396          * @return string The matching part
397          */
398         public static function getUrlMatch(string $url1, string $url2)
399         {
400                 if (($url1 == "") || ($url2 == "")) {
401                         return "";
402                 }
403
404                 $url1 = Strings::normaliseLink($url1);
405                 $url2 = Strings::normaliseLink($url2);
406
407                 $parts1 = parse_url($url1);
408                 $parts2 = parse_url($url2);
409
410                 if (!isset($parts1["host"]) || !isset($parts2["host"])) {
411                         return "";
412                 }
413
414                 if (empty($parts1["scheme"])) {
415                         $parts1["scheme"] = '';
416                 }
417                 if (empty($parts2["scheme"])) {
418                         $parts2["scheme"] = '';
419                 }
420
421                 if ($parts1["scheme"] != $parts2["scheme"]) {
422                         return "";
423                 }
424
425                 if (empty($parts1["host"])) {
426                         $parts1["host"] = '';
427                 }
428                 if (empty($parts2["host"])) {
429                         $parts2["host"] = '';
430                 }
431
432                 if ($parts1["host"] != $parts2["host"]) {
433                         return "";
434                 }
435
436                 if (empty($parts1["port"])) {
437                         $parts1["port"] = '';
438                 }
439                 if (empty($parts2["port"])) {
440                         $parts2["port"] = '';
441                 }
442
443                 if ($parts1["port"] != $parts2["port"]) {
444                         return "";
445                 }
446
447                 $match = $parts1["scheme"]."://".$parts1["host"];
448
449                 if ($parts1["port"]) {
450                         $match .= ":".$parts1["port"];
451                 }
452
453                 if (empty($parts1["path"])) {
454                         $parts1["path"] = '';
455                 }
456                 if (empty($parts2["path"])) {
457                         $parts2["path"] = '';
458                 }
459
460                 $pathparts1 = explode("/", $parts1["path"]);
461                 $pathparts2 = explode("/", $parts2["path"]);
462
463                 $i = 0;
464                 $path = "";
465                 do {
466                         $path1 = $pathparts1[$i] ?? '';
467                         $path2 = $pathparts2[$i] ?? '';
468
469                         if ($path1 == $path2) {
470                                 $path .= $path1."/";
471                         }
472                 } while (($path1 == $path2) && ($i++ <= count($pathparts1)));
473
474                 $match .= $path;
475
476                 return Strings::normaliseLink($match);
477         }
478
479         /**
480          * Glue url parts together
481          *
482          * @param array $parsed URL parts
483          *
484          * @return string The glued URL.
485          * @deprecated since version 2021.12, use GuzzleHttp\Psr7\Uri::fromParts($parts) instead
486          */
487         public static function unparseURL(array $parsed)
488         {
489                 $get = function ($key) use ($parsed) {
490                         return isset($parsed[$key]) ? $parsed[$key] : null;
491                 };
492
493                 $pass      = $get('pass');
494                 $user      = $get('user');
495                 $userinfo  = $pass !== null ? "$user:$pass" : $user;
496                 $port      = $get('port');
497                 $scheme    = $get('scheme');
498                 $query     = $get('query');
499                 $fragment  = $get('fragment');
500                 $authority = ($userinfo !== null ? $userinfo."@" : '') .
501                                                 $get('host') .
502                                                 ($port ? ":$port" : '');
503
504                 return  (strlen($scheme) ? $scheme.":" : '') .
505                         (strlen($authority) ? "//".$authority : '') .
506                         $get('path') .
507                         (strlen($query) ? "?".$query : '') .
508                         (strlen($fragment) ? "#".$fragment : '');
509         }
510
511         /**
512          * Convert an URI to an IDN compatible URI
513          *
514          * @param string $uri
515          * @return string
516          */
517         public static function convertToIdn(string $uri): string
518         {
519                 $parts = parse_url($uri);
520                 if (!empty($parts['scheme']) && !empty($parts['host'])) {
521                         $parts['host'] = idn_to_ascii($parts['host']);
522                         $uri = Uri::fromParts($parts);
523                 } else {
524                         $parts = explode('@', $uri);
525                         if (count($parts) == 2) {
526                                 $uri = $parts[0] . '@' . idn_to_ascii($parts[1]);
527                         } else {
528                                 $uri = idn_to_ascii($uri);
529                         }
530                 }
531
532                 return $uri;
533         }
534
535         /**
536          * Switch the scheme of an url between http and https
537          *
538          * @param string $url URL
539          *
540          * @return string switched URL
541          */
542         public static function switchScheme(string $url)
543         {
544                 $scheme = parse_url($url, PHP_URL_SCHEME);
545                 if (empty($scheme)) {
546                         return $url;
547                 }
548
549                 if ($scheme === 'http') {
550                         $url = str_replace('http://', 'https://', $url);
551                 } elseif ($scheme === 'https') {
552                         $url = str_replace('https://', 'http://', $url);
553                 }
554
555                 return $url;
556         }
557
558         /**
559          * Adds query string parameters to the provided URI. Replace the value of existing keys.
560          *
561          * @param string $path
562          * @param array  $additionalParams Associative array of parameters
563          * @return string
564          */
565         public static function appendQueryParam(string $path, array $additionalParams)
566         {
567                 $parsed = parse_url($path);
568
569                 $params = [];
570                 if (!empty($parsed['query'])) {
571                         parse_str($parsed['query'], $params);
572                 }
573
574                 $params = array_merge($params, $additionalParams);
575
576                 $parsed['query'] = http_build_query($params);
577
578                 return self::unparseURL($parsed);
579         }
580
581         /**
582          * Generates ETag and Last-Modified response headers and checks them against
583          * If-None-Match and If-Modified-Since request headers if present.
584          *
585          * Blocking function, sends 304 headers and exits if check passes.
586          *
587          * @param string $etag          The page etag
588          * @param string $last_modified The page last modification UTC date
589          * @throws \Exception
590          */
591         public static function checkEtagModified(string $etag, string $last_modified)
592         {
593                 $last_modified = DateTimeFormat::utc($last_modified, 'D, d M Y H:i:s') . ' GMT';
594
595                 /**
596                  * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
597                  */
598                 $if_none_match     = filter_input(INPUT_SERVER, 'HTTP_IF_NONE_MATCH');
599                 $if_modified_since = filter_input(INPUT_SERVER, 'HTTP_IF_MODIFIED_SINCE');
600                 $flag_not_modified = null;
601                 if ($if_none_match) {
602                         $result = [];
603                         preg_match('/^(?:W\/")?([^"]+)"?$/i', $etag, $result);
604                         $etagTrimmed = $result[1];
605                         // Lazy exact ETag match, could check weak/strong ETags
606                         $flag_not_modified = $if_none_match == '*' || strpos($if_none_match, $etagTrimmed) !== false;
607                 }
608
609                 if ($if_modified_since && (!$if_none_match || $flag_not_modified)) {
610                         // Lazy exact Last-Modified match, could check If-Modified-Since validity
611                         $flag_not_modified = $if_modified_since == $last_modified;
612                 }
613
614                 header('Etag: ' . $etag);
615                 header('Last-Modified: ' . $last_modified);
616
617                 if ($flag_not_modified) {
618                         throw new NotModifiedException();
619                 }
620         }
621
622         /**
623          * Check if the given URL is a local link
624          *
625          * @param string $url
626          * @return bool
627          */
628         public static function isLocalLink(string $url)
629         {
630                 return (strpos(Strings::normaliseLink($url), Strings::normaliseLink(DI::baseUrl())) !== false);
631         }
632
633         /**
634          * Check if the given URL is a valid HTTP/HTTPS URL
635          *
636          * @param string $url
637          * @return bool
638          */
639         public static function isValidHttpUrl(string $url)
640         {
641                 $scheme = parse_url($url, PHP_URL_SCHEME);
642                 return !empty($scheme) && in_array($scheme, ['http', 'https']) && parse_url($url, PHP_URL_HOST);
643         }
644 }