>
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;
}
?>