|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00011 require_once $CFG->dirroot.'/mnet/lib.php'; 00012 00016 class mnet_xmlrpc_client { 00017 00018 var $method = ''; 00019 var $params = array(); 00020 var $timeout = 60; 00021 var $error = array(); 00022 var $response = ''; 00023 var $mnet = null; 00024 00028 function mnet_xmlrpc_client() { 00029 // make sure we've got this set up before we try and do anything else 00030 $this->mnet = get_mnet_environment(); 00031 return true; 00032 } 00033 00039 function set_timeout($timeout) { 00040 if (!is_integer($timeout)) { 00041 if (is_numeric($timeout)) { 00042 $this->timeout = (integer)$timeout; 00043 return true; 00044 } 00045 return false; 00046 } 00047 $this->timeout = $timeout; 00048 return true; 00049 } 00050 00059 function set_method($xmlrpcpath) { 00060 if (is_string($xmlrpcpath)) { 00061 $this->method = $xmlrpcpath; 00062 $this->params = array(); 00063 return true; 00064 } 00065 $this->method = ''; 00066 $this->params = array(); 00067 return false; 00068 } 00069 00089 function add_param($argument, $type = 'string') { 00090 00091 $allowed_types = array('none', 00092 'empty', 00093 'base64', 00094 'boolean', 00095 'datetime', 00096 'double', 00097 'int', 00098 'i4', 00099 'string', 00100 'array', 00101 'struct'); 00102 if (!in_array($type, $allowed_types)) { 00103 return false; 00104 } 00105 00106 if ($type != 'datetime' && $type != 'base64') { 00107 $this->params[] = $argument; 00108 return true; 00109 } 00110 00111 // Note weirdness - The type of $argument gets changed to an object with 00112 // value and type properties. 00113 // bool xmlrpc_set_type ( string &value, string type ) 00114 xmlrpc_set_type($argument, $type); 00115 $this->params[] = $argument; 00116 return true; 00117 } 00118 00127 function send($mnet_peer) { 00128 global $CFG, $DB; 00129 00130 00131 if (!$this->permission_to_call($mnet_peer)) { 00132 mnet_debug("tried and wasn't allowed to call a method on $mnet_peer->wwwroot"); 00133 return false; 00134 } 00135 00136 $this->requesttext = xmlrpc_encode_request($this->method, $this->params, array("encoding" => "utf-8", "escaping" => "markup")); 00137 $this->signedrequest = mnet_sign_message($this->requesttext); 00138 $this->encryptedrequest = mnet_encrypt_message($this->signedrequest, $mnet_peer->public_key); 00139 00140 $httprequest = $this->prepare_http_request($mnet_peer); 00141 curl_setopt($httprequest, CURLOPT_POSTFIELDS, $this->encryptedrequest); 00142 00143 $timestamp_send = time(); 00144 mnet_debug("about to send the curl request"); 00145 $this->rawresponse = curl_exec($httprequest); 00146 mnet_debug("managed to complete a curl request"); 00147 $timestamp_receive = time(); 00148 00149 if ($this->rawresponse === false) { 00150 $this->error[] = curl_errno($httprequest) .':'. curl_error($httprequest); 00151 return false; 00152 } 00153 curl_close($httprequest); 00154 00155 $this->rawresponse = trim($this->rawresponse); 00156 00157 $mnet_peer->touch(); 00158 00159 $crypt_parser = new mnet_encxml_parser(); 00160 $crypt_parser->parse($this->rawresponse); 00161 00162 // If we couldn't parse the message, or it doesn't seem to have encrypted contents, 00163 // give the most specific error msg available & return 00164 if (!$crypt_parser->payload_encrypted) { 00165 if (! empty($crypt_parser->remoteerror)) { 00166 $this->error[] = '4: remote server error: ' . $crypt_parser->remoteerror; 00167 } else if (! empty($crypt_parser->error)) { 00168 $crypt_parser_error = $crypt_parser->error[0]; 00169 00170 $message = '3:XML Parse error in payload: '.$crypt_parser_error['string']."\n"; 00171 if (array_key_exists('lineno', $crypt_parser_error)) { 00172 $message .= 'At line number: '.$crypt_parser_error['lineno']."\n"; 00173 } 00174 if (array_key_exists('line', $crypt_parser_error)) { 00175 $message .= 'Which reads: '.$crypt_parser_error['line']."\n"; 00176 } 00177 $this->error[] = $message; 00178 } else { 00179 $this->error[] = '1:Payload not encrypted '; 00180 } 00181 00182 $crypt_parser->free_resource(); 00183 return false; 00184 } 00185 00186 $key = array_pop($crypt_parser->cipher); 00187 $data = array_pop($crypt_parser->cipher); 00188 00189 $crypt_parser->free_resource(); 00190 00191 // Initialize payload var 00192 $decryptedenvelope = ''; 00193 00194 // &$decryptedenvelope 00195 $isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), $this->mnet->get_private_key()); 00196 00197 if (!$isOpen) { 00198 // Decryption failed... let's try our archived keys 00199 $openssl_history = get_config('mnet', 'openssl_history'); 00200 if(empty($openssl_history)) { 00201 $openssl_history = array(); 00202 set_config('openssl_history', serialize($openssl_history), 'mnet'); 00203 } else { 00204 $openssl_history = unserialize($openssl_history); 00205 } 00206 foreach($openssl_history as $keyset) { 00207 $keyresource = openssl_pkey_get_private($keyset['keypair_PEM']); 00208 $isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), $keyresource); 00209 if ($isOpen) { 00210 // It's an older code, sir, but it checks out 00211 break; 00212 } 00213 } 00214 } 00215 00216 if (!$isOpen) { 00217 trigger_error("None of our keys could open the payload from host {$mnet_peer->wwwroot} with id {$mnet_peer->id}."); 00218 $this->error[] = '3:No key match'; 00219 return false; 00220 } 00221 00222 if (strpos(substr($decryptedenvelope, 0, 100), '<signedMessage>')) { 00223 $sig_parser = new mnet_encxml_parser(); 00224 $sig_parser->parse($decryptedenvelope); 00225 } else { 00226 $this->error[] = '2:Payload not signed: ' . $decryptedenvelope; 00227 return false; 00228 } 00229 00230 // Margin of error is the time it took the request to complete. 00231 $margin_of_error = $timestamp_receive - $timestamp_send; 00232 00233 // Guess the time gap between sending the request and the remote machine 00234 // executing the time() function. Marginally better than nothing. 00235 $hysteresis = ($margin_of_error) / 2; 00236 00237 $remote_timestamp = $sig_parser->remote_timestamp - $hysteresis; 00238 $time_offset = $remote_timestamp - $timestamp_send; 00239 if ($time_offset > 0) { 00240 $threshold = get_config('mnet', 'drift_threshold'); 00241 if(empty($threshold)) { 00242 // We decided 15 seconds was a pretty good arbitrary threshold 00243 // for time-drift between servers, but you can customize this in 00244 // the config_plugins table. It's not advised though. 00245 set_config('drift_threshold', 15, 'mnet'); 00246 $threshold = 15; 00247 } 00248 if ($time_offset > $threshold) { 00249 $this->error[] = '6:Time gap with '.$mnet_peer->name.' ('.$time_offset.' seconds) is greater than the permitted maximum of '.$threshold.' seconds'; 00250 return false; 00251 } 00252 } 00253 00254 $this->xmlrpcresponse = base64_decode($sig_parser->data_object); 00255 $this->response = xmlrpc_decode($this->xmlrpcresponse); 00256 00257 // xmlrpc errors are pushed onto the $this->error stack 00258 if (is_array($this->response) && array_key_exists('faultCode', $this->response)) { 00259 // The faultCode 7025 means we tried to connect with an old SSL key 00260 // The faultString is the new key - let's save it and try again 00261 // The re_key attribute stops us from getting into a loop 00262 if($this->response['faultCode'] == 7025 && empty($mnet_peer->re_key)) { 00263 mnet_debug('recieved an old-key fault, so trying to get the new key and update our records'); 00264 // If the new certificate doesn't come thru clean_param() unmolested, error out 00265 if($this->response['faultString'] != clean_param($this->response['faultString'], PARAM_PEM)) { 00266 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; 00267 } 00268 $record = new stdClass(); 00269 $record->id = $mnet_peer->id; 00270 $record->public_key = $this->response['faultString']; 00271 $details = openssl_x509_parse($record->public_key); 00272 if(!isset($details['validTo_time_t'])) { 00273 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; 00274 } 00275 $record->public_key_expires = $details['validTo_time_t']; 00276 $DB->update_record('mnet_host', $record); 00277 00278 // Create a new peer object populated with the new info & try re-sending the request 00279 $rekeyed_mnet_peer = new mnet_peer(); 00280 $rekeyed_mnet_peer->set_id($record->id); 00281 $rekeyed_mnet_peer->re_key = true; 00282 return $this->send($rekeyed_mnet_peer); 00283 } 00284 if (!empty($CFG->mnet_rpcdebug)) { 00285 if (get_string_manager()->string_exists('error'.$this->response['faultCode'], 'mnet')) { 00286 $guidance = get_string('error'.$this->response['faultCode'], 'mnet'); 00287 } else { 00288 $guidance = ''; 00289 } 00290 } else { 00291 $guidance = ''; 00292 } 00293 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString'] ."\n".$guidance; 00294 } 00295 00296 // ok, it's signed, but is it signed with the right certificate ? 00297 // do this *after* we check for an out of date key 00298 $verified = openssl_verify($this->xmlrpcresponse, base64_decode($sig_parser->signature), $mnet_peer->public_key); 00299 if ($verified != 1) { 00300 $this->error[] = 'Invalid signature'; 00301 } 00302 00303 return empty($this->error); 00304 } 00305 00313 function permission_to_call($mnet_peer) { 00314 global $DB, $CFG, $USER; 00315 00316 // Executing any system method is permitted. 00317 $system_methods = array('system/listMethods', 'system/methodSignature', 'system/methodHelp', 'system/listServices'); 00318 if (in_array($this->method, $system_methods) ) { 00319 return true; 00320 } 00321 00322 $hostids = array($mnet_peer->id); 00323 if (!empty($CFG->mnet_all_hosts_id)) { 00324 $hostids[] = $CFG->mnet_all_hosts_id; 00325 } 00326 // At this point, we don't care if the remote host implements the 00327 // method we're trying to call. We just want to know that: 00328 // 1. The method belongs to some service, as far as OUR host knows 00329 // 2. We are allowed to subscribe to that service on this mnet_peer 00330 00331 list($hostidsql, $hostidparams) = $DB->get_in_or_equal($hostids); 00332 00333 $sql = "SELECT r.id 00334 FROM {mnet_remote_rpc} r 00335 INNER JOIN {mnet_remote_service2rpc} s2r ON s2r.rpcid = r.id 00336 INNER JOIN {mnet_host2service} h2s ON h2s.serviceid = s2r.serviceid 00337 WHERE r.xmlrpcpath = ? 00338 AND h2s.subscribe = ? 00339 AND h2s.hostid $hostidsql"; 00340 00341 $params = array($this->method, 1); 00342 $params = array_merge($params, $hostidparams); 00343 00344 if ($DB->record_exists_sql($sql, $params)) { 00345 return true; 00346 } 00347 00348 $this->error[] = '7:User with ID '. $USER->id . 00349 ' attempted to call unauthorised method '. 00350 $this->method.' on host '. 00351 $mnet_peer->wwwroot; 00352 return false; 00353 } 00354 00361 function prepare_http_request ($mnet_peer) { 00362 $this->uri = $mnet_peer->wwwroot . $mnet_peer->application->xmlrpc_server_url; 00363 00364 // Initialize request the target URL 00365 $httprequest = curl_init($this->uri); 00366 curl_setopt($httprequest, CURLOPT_TIMEOUT, $this->timeout); 00367 curl_setopt($httprequest, CURLOPT_RETURNTRANSFER, true); 00368 curl_setopt($httprequest, CURLOPT_POST, true); 00369 curl_setopt($httprequest, CURLOPT_USERAGENT, 'Moodle'); 00370 curl_setopt($httprequest, CURLOPT_HTTPHEADER, array("Content-Type: text/xml charset=UTF-8")); 00371 curl_setopt($httprequest, CURLOPT_SSL_VERIFYPEER, false); 00372 curl_setopt($httprequest, CURLOPT_SSL_VERIFYHOST, 0); 00373 return $httprequest; 00374 } 00375 }