config['site-health']['min_probe_delay']){ run_site_probe($entry['id'], $entry); } } } //If it does not exist. else{ //Add it and make sure it is ready for probing. q( "INSERT INTO `site-health` (`base_url`, `dt_first_noticed`) VALUES ('%s', NOW())", dbesc($site) ); //And in case we should probe now, do so. if($check_health){ $result = q( "SELECT * FROM `site-health` WHERE `base_url`= '%s' ORDER BY `id` ASC LIMIT 1", dbesc($site) ); if(!empty($result) && isset($result[0])){ $entry = $result[0]; run_site_probe($result[0]['id'], $entry); } } } //Give other scripts the site health. return isset($entry) ? $entry : false; }} //Extracts the site from a given URL. if(! function_exists('parse_site_from_url')){ function parse_site_from_url($url) { //Currently a simple implementation, but may improve over time. #TODO: support subdirectories? $urlMeta = parse_url($url); return $urlMeta['scheme'].'://'.$urlMeta['host']; }} //Performs a ping to the given site ID //You may need to notice the site first before you know it's ID. //TODO: Probe server location using IP address or using the info the friendica server provides (preferred). // If IP needs to be used only provide country information. //TODO: Check SSLLabs Grade // Check needs to be asynchronous, meaning that the check at SSLLabs will be initiated in one run while // the results must be fetched later. It might be good to mark sites, where a check has been inititated // f.e. using the ssl_grade field. In the next run, results of these sites could be fetched. if(! function_exists('run_site_probe')){ function run_site_probe($id, &$entry_out) { global $a; //Get the site information from the DB, based on the ID. $result = q( "SELECT * FROM `site-health` WHERE `id`= %u ORDER BY `id` ASC LIMIT 1", intval($id) ); //Abort the probe if site is not known. if(!$result || !isset($result[0])){ logger('Unknown site-health ID being probed: '.$id); throw new \Exception('Unknown site-health ID being probed: '.$id); } //Shortcut. $entry = $result[0]; $base_url = $entry['base_url']; $probe_location = $base_url.'/friendica/json'; //Prepare the CURL call. $handle = curl_init(); $options = array( //Timeouts CURLOPT_TIMEOUT => max($a->config['site-health']['probe_timeout'], 1), //Minimum of 1 second timeout. CURLOPT_CONNECTTIMEOUT => 1, //Redirecting CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 8, //SSL CURLOPT_SSL_VERIFYPEER => true, // CURLOPT_VERBOSE => true, // CURLOPT_CERTINFO => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, //Basic request CURLOPT_USERAGENT => 'friendica-directory-probe-1.0', CURLOPT_RETURNTRANSFER => true, CURLOPT_URL => $probe_location ); curl_setopt_array($handle, $options); //Probe the site. $probe_start = microtime(true); $probe_data = curl_exec($handle); $probe_end = microtime(true); //Check for SSL problems. $curl_statuscode = curl_errno($handle); $sslcert_issues = in_array($curl_statuscode, array( 60, //Could not authenticate certificate with known CA's 83 //Issuer check failed )); //When it's the certificate that doesn't work. if($sslcert_issues){ //Probe again, without strict SSL. $options[CURLOPT_SSL_VERIFYPEER] = false; //Replace the handle. curl_close($handle); $handle = curl_init(); curl_setopt_array($handle, $options); //Probe. $probe_start = microtime(true); $probe_data = curl_exec($handle); $probe_end = microtime(true); //Store new status. $curl_statuscode = curl_errno($handle); } //Gather more meta. $time = round(($probe_end - $probe_start) * 1000); $status = curl_getinfo($handle, CURLINFO_HTTP_CODE); $type = curl_getinfo($handle, CURLINFO_CONTENT_TYPE); $info = curl_getinfo($handle); //Done with CURL now. curl_close($handle); #TODO: if the site redirects elsewhere, notice this site and record an issue. $effective_base_url = parse_site_from_url($info['url']); $wrong_base_url = $effective_base_url !== $entry['base_url']; try{ $data = json_decode($probe_data); }catch(\Exception $ex){ $data = false; } $parse_failed = !$data; $parsedDataQuery = ''; logger('Effective Base URL: ' . $effective_base_url); if($wrong_base_url){ $parsedDataQuery .= sprintf( "`effective_base_url` = '%s',", dbesc($effective_base_url) ); }else{ $parsedDataQuery .= "`effective_base_url` = NULL,"; } if(!$parse_failed){ $given_base_url_match = $data->url == $base_url; //Record the probe speed in a probes table. q( "INSERT INTO `site-probe` (`site_health_id`, `dt_performed`, `request_time`)". "VALUES (%u, NOW(), %u)", $entry['id'], $time ); //Update any health calculations or otherwise processed data. $parsedDataQuery .= sprintf( "`dt_last_seen` = NOW(), `name` = '%s', `version` = '%s', `plugins` = '%s', `reg_policy` = '%s', `info` = '%s', `admin_name` = '%s', `admin_profile` = '%s', ", dbesc($data->site_name), dbesc($data->version), dbesc(implode("\r\n", $data->plugins)), dbesc($data->register_policy), dbesc($data->info), dbesc($data->admin->name), dbesc($data->admin->profile) ); //Did we use HTTPS? $urlMeta = parse_url($probe_location); if($urlMeta['scheme'] == 'https'){ $parsedDataQuery .= sprintf("`ssl_state` = b'%u',", $sslcert_issues ? '0' : '1'); } else { $parsedDataQuery .= "`ssl_state` = NULL,"; } //Do we have a no scrape supporting node? :D if(isset($data->no_scrape_url)){ $parsedDataQuery .= sprintf("`no_scrape_url` = '%s',", dbesc($data->no_scrape_url)); } } //Get the new health. $version = $parse_failed ? '' : $data->version; $health = health_score_after_probe($entry['health_score'], !$parse_failed, $time, $version, $sslcert_issues); //Update the health. q("UPDATE `site-health` SET `health_score` = '%d', $parsedDataQuery `dt_last_probed` = NOW() WHERE `id` = %d LIMIT 1", $health, $entry['id'] ); //Get the site information from the DB, based on the ID. $result = q( "SELECT * FROM `site-health` WHERE `id`= %u ORDER BY `id` ASC LIMIT 1", $entry['id'] ); //Return updated entry data. if($result && isset($result[0])){ $entry_out = $result[0]; } }} //Determines the new health score after a probe has been executed. if(! function_exists('health_score_after_probe')){ function health_score_after_probe($current, $probe_success, $time=null, $version=null, $ssl_issues=null) { //Probe failed, costs you 30 points. if(!$probe_success) return max($current-30, -100); //A good probe gives you 20 points. $current += 20; //Speed scoring. if(intval($time) > 0){ //Pentaly / bonus points. if ($time > 800) $current -= 10; //Bad speed. elseif ($time > 400) $current -= 5; //Still not good. elseif ($time > 250) $current += 0; //This is normal. elseif ($time > 120) $current += 5; //Good speed. else $current += 10; //Excellent speed. //Cap for bad speeds. if ($time > 800) $current = min(40, $current); elseif ($time > 400) $current = min(60, $current); } //Version check. if(!empty($version)){ $versionParts = explode('.', $version); //Older than 3.x.x? //Your score can not go above 30 health. if(intval($versionParts[0]) < 3){ $current = min($current, 30); } //Older than 3.3.x? elseif(intval($versionParts[1] < 3)){ $current -= 5; //Somewhat outdated. } #TODO: See if this needs to be more dynamic. #TODO: See if this is a proper indicator of health. } //SSL problems? That's a big deal. if($ssl_issues === true){ $current -= 10; } //Don't go beyond +100 or -100. return max(min(100, $current), -100); }} //Changes a score into a name. Used for classes and such. if(! function_exists('health_score_to_name')){ function health_score_to_name($score) { if ($score < -50) return 'very-bad'; elseif ($score < 0) return 'bad'; elseif ($score < 30) return 'neutral'; elseif ($score < 50) return 'ok'; elseif ($score < 80) return 'good'; else return 'perfect'; }}