> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ include_once('db.php'); function rpc_curlinit($domain, $rpc) { $curl=curl_init('https://'.$domain.'/rpc/'.$rpc); curl_setopt_array($curl, Array( CURLOPT_RETURNTRANSFER=>true, CURLOPT_FOLLOWLOCATION=>true, CURLOPT_MAXREDIRS=>10, CURLOPT_HTTPHEADER=>Array('X-Thingshare-node: '.DOMAIN), // Let other nodes know who we are in case they want to federate back, and for signature verification CURLOPT_USERAGENT=>'Thingshare RPC')); return $curl; } function rpc_cooldown_check($domain) { global $db; $domain=mysqli_real_escape_string($db, $domain); $timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s')); mysqli_query($db, 'delete from cooldown where end<"'.$timestamp.'"'); // Clear old cooldowns $res=mysqli_query($db, 'select domain from cooldown where domain="'.$domain.'"'); return (mysqli_fetch_row($res)===null); } function rpc_cooldown($domain) { global $db; $domain=mysqli_real_escape_string($db, $domain); $timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s', time()+3600)); // One hour sounds good, not too long for temporarily unavailable nodes and not too short for "flood" targets mysqli_query($db, 'insert into cooldown(domain, end) values("'.$domain.'", "'.$timestamp.'")'); } function rpc_cache($domain, $rpc, $content) { if($domain==DOMAIN){return;} // Don't cache self (or is there a good reason to? other than testing) global $db; $timestamp=mysqli_real_escape_string($db, date('Y-m-d H:i:s')); $hash=hash(HASH, $domain.'/'.$rpc); $content=mysqli_real_escape_string($db, $content); mysqli_query($db, 'delete from rpccache where hash="'.$hash.'"'); // Erase duplicate (avoids primary key duplication) mysqli_query($db, 'insert into rpccache(hash, timestamp, cache) values("'.$hash.'", "'.$timestamp.'", "'.$content.'")'); } function rpc_getcache($domain, $rpc, $fallback=false) { if($domain==DOMAIN){return false;} // Don't cache self (or is there a good reason to? other than testing) global $db; $limit=mysqli_real_escape_string($db, date('Y-m-d H:i:s', time()-3600)); // TODO: Configurable cache time limit mysqli_query($db, 'delete from rpccache where timestamp<"'.$limit.'"'); $hash=hash(HASH, $domain.'/'.$rpc); $q='select cache from rpccache where hash="'.$hash.'"'; if(!$fallback) { $limit=mysqli_real_escape_string($db, date('Y-m-d H:i:s', time()-600)); // TODO: Configurable cache time limit $q.=' and timestamp>"'.$limit.'"'; } // else{print('Trying to use the cache as fallback
');} $res=mysqli_query($db, $q); if($res=mysqli_fetch_row($res)) { // print('Cache hit ('.$domain.'/'.$rpc.')
'); return $res[0]; } // print('Cache miss ('.$domain.'/'.$rpc.')
'); return false; } function rpc_get($domain, $rpc) { // Check whether the domain is in cooldown first if(!rpc_cooldown_check($domain)) { // Attempt to fall back on the cache (without the soft time limit) if($obj=rpc_getcache($domain, $rpc, true)){return json_decode($obj, true);} return Array('error'=>'Domain is in cooldown'); } // Check for cached response first if($obj=rpc_getcache($domain, $rpc)){return json_decode($obj, true);} $curl=rpc_curlinit($domain, $rpc); $data=curl_exec($curl); curl_close($curl); $obj=json_decode($data, true); if($obj===null) // Unexpected response, set cooldown to prevent flooding of non-thingshare domains { rpc_cooldown($domain); // Attempt to fall back on the cache (without the soft time limit) if($obj=rpc_getcache($domain, $rpc, true)){return json_decode($obj, true);} return Array('error'=>'Unexpected response, putting domain in cooldown'); } rpc_cache($domain, $rpc, $data); return $obj; } function rpc_search($domains, $rpc) // TODO: Is there a more appropriate name for this function? { $multi=curl_multi_init(); $responses=Array(); $curls=Array(); // Set up each request foreach($domains as $domain) { if($cache=rpc_getcache($domain, $rpc)) { $responses[$domain]=json_decode($cache, true); continue; } if(!rpc_cooldown_check($domain)) { $responses[$domain]=Array('error'=>'Domain is in cooldown'); $cache=rpc_getcache($domain, $rpc, true); // Attempt to use cache as fallback if($cache){$responses[$domain]=json_decode($cache, true);} continue; } $curl=rpc_curlinit($domain, $rpc); curl_setopt($curl, CURLOPT_TIMEOUT, 2); // TODO: Make this configurable? maybe use TIMEOUT_MS? curl_multi_add_handle($multi, $curl); $curls[$domain]=$curl; } // Run requests in parallel while(($status=curl_multi_exec($multi, $running))==CURLM_OK && $running) { curl_multi_select(); // Wait for activity } // Handle responses foreach($domains as $domain) { if(!isset($curls[$domain])){continue;} // Already handled, cache or cooldown $curl=$curls[$domain]; $content=curl_multi_getcontent($curl); $obj=json_decode($content, true); if($obj===null) // Unexpected response, set cooldown { rpc_cooldown($domain); $obj=Array('error'=>'Unexpected response, putting domain in cooldown'); $cache=rpc_getcache($domain, $rpc, true); // Attempt to use cache as fallback if($cache){$obj=json_decode($cache, true);} }else{ rpc_cache($domain, $rpc, $content); } $responses[$domain]=$obj; curl_multi_remove_handle($multi, $curl); } curl_multi_close($multi); return $responses; } function rpc_post($domain, $rpc, $data) // For comments, reports, etc. { // Check whether the domain is in cooldown first if(!rpc_cooldown_check($domain)) { return Array('error'=>'Domain is in cooldown'); } // Encode $data to string if it isn't already if(is_array($data)){$data=json_encode($data);} // Sign content and bundle it up with signature and hash algorithm used openssl_sign($data, $sig, getoption('rpckey'), 'sha512'); $obj=Array( 'data'=>$data, 'signature'=>base64_encode($sig), 'algorithm'=>'sha512'); // Make the request $curl=rpc_curlinit($domain, $rpc); curl_setopt_array($curl, Array( CURLOPT_POST=>true, CURLOPT_SAFE_UPLOAD=>true, CURLOPT_POSTFIELDS=>$obj)); $data=curl_exec($curl); curl_close($curl); $obj=json_decode($data, true); if($obj===null) // Unexpected response, set cooldown to prevent flooding of non-thingshare domains { rpc_cooldown($domain); return Array('error'=>'Unexpected response, putting domain in cooldown'); } return $obj; } function rpc_verifypost(&$peer) { // Basic check if(!isset($_POST['data']) || !isset($_POST['signature']) || !isset($_POST['algorithm']) || !isset($_SERVER['HTTP_X_THINGSHARE_NODE'])) { die('{"error":"Missing data, signature, algorithm, or Thingshare node identifier header"}'); } $peer=strtolower($_SERVER['HTTP_X_THINGSHARE_NODE']); // TODO: Check algorithm against a whitelist? Might be good to avoid known weak hash algorithms and obscure algorithms which have avoided scrutiny // Get the public key $key=rpc_get($peer, 'rpckey'); if(isset($key['error']) || !isset($key['public'])) { die('{"error":"Failed to fetch public key for verification","suberror":'.json_encode($key).'}'); } // Verify signature $key=$key['public']; $data=$_POST['data']; $sig=base64_decode($_POST['signature']); $algo=$_POST['algorithm']; if(!openssl_verify($data, $sig, $key, $algo)) { die('{"error":"Signature check failed"}'); } $obj=json_decode($data, true); if($obj===null) { die('{"error":"Failed to decode data"}'); } return $obj; } ?>