Differences Between: [Versions 310 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
1 <?php 2 /** 3 * An XML-RPC client 4 * 5 * @author Donal McMullan donal@catalyst.net.nz 6 * @version 0.0.1 7 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 8 * @package mnet 9 */ 10 11 require_once $CFG->dirroot.'/mnet/lib.php'; 12 13 /** 14 * Class representing an XMLRPC request against a remote machine 15 */ 16 class mnet_xmlrpc_client { 17 18 var $method = ''; 19 var $params = array(); 20 var $timeout = 60; 21 var $error = array(); 22 var $response = ''; 23 var $mnet = null; 24 25 /** 26 * Constructor 27 */ 28 public function __construct() { 29 // make sure we've got this set up before we try and do anything else 30 $this->mnet = get_mnet_environment(); 31 } 32 33 /** 34 * Old syntax of class constructor. Deprecated in PHP7. 35 * 36 * @deprecated since Moodle 3.1 37 */ 38 public function mnet_xmlrpc_client() { 39 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); 40 self::__construct(); 41 } 42 43 /** 44 * Allow users to override the default timeout 45 * @param int $timeout Request timeout in seconds 46 * $return bool True if param is an integer or integer string 47 */ 48 function set_timeout($timeout) { 49 if (!is_integer($timeout)) { 50 if (is_numeric($timeout)) { 51 $this->timeout = (integer)$timeout; 52 return true; 53 } 54 return false; 55 } 56 $this->timeout = $timeout; 57 return true; 58 } 59 60 /** 61 * Set the path to the method or function we want to execute on the remote 62 * machine. Examples: 63 * mod/scorm/functionname 64 * auth/mnet/methodname 65 * In the case of auth and enrolment plugins, an object will be created and 66 * the method on that object will be called 67 */ 68 function set_method($xmlrpcpath) { 69 if (is_string($xmlrpcpath)) { 70 $this->method = $xmlrpcpath; 71 $this->params = array(); 72 return true; 73 } 74 $this->method = ''; 75 $this->params = array(); 76 return false; 77 } 78 79 /** 80 * Add a parameter to the array of parameters. 81 * 82 * @param string $argument A transport ID, as defined in lib.php 83 * @param string $type The argument type, can be one of: 84 * none 85 * empty 86 * base64 87 * boolean 88 * datetime 89 * double 90 * int 91 * string 92 * array 93 * struct 94 * In its weakly-typed wisdom, PHP will (currently) 95 * ignore everything except datetime and base64 96 * @return bool True on success 97 */ 98 function add_param($argument, $type = 'string') { 99 100 $allowed_types = array('none', 101 'empty', 102 'base64', 103 'boolean', 104 'datetime', 105 'double', 106 'int', 107 'i4', 108 'string', 109 'array', 110 'struct'); 111 if (!in_array($type, $allowed_types)) { 112 return false; 113 } 114 115 if ($type != 'datetime' && $type != 'base64') { 116 $this->params[] = $argument; 117 return true; 118 } 119 120 // Note weirdness - The type of $argument gets changed to an object with 121 // value and type properties. 122 // bool xmlrpc_set_type ( string &value, string type ) 123 xmlrpc_set_type($argument, $type); 124 $this->params[] = $argument; 125 return true; 126 } 127 128 /** 129 * Send the request to the server - decode and return the response 130 * 131 * @param object $mnet_peer A mnet_peer object with details of the 132 * remote host we're connecting to 133 * @return mixed A PHP variable, as returned by the 134 * remote function 135 */ 136 function send($mnet_peer) { 137 global $CFG, $DB; 138 139 140 if (!$this->permission_to_call($mnet_peer)) { 141 mnet_debug("tried and wasn't allowed to call a method on $mnet_peer->wwwroot"); 142 return false; 143 } 144 145 $this->requesttext = xmlrpc_encode_request($this->method, $this->params, array("encoding" => "utf-8", "escaping" => "markup")); 146 $this->signedrequest = mnet_sign_message($this->requesttext); 147 $this->encryptedrequest = mnet_encrypt_message($this->signedrequest, $mnet_peer->public_key); 148 149 $httprequest = $this->prepare_http_request($mnet_peer); 150 curl_setopt($httprequest, CURLOPT_POSTFIELDS, $this->encryptedrequest); 151 152 $timestamp_send = time(); 153 mnet_debug("about to send the curl request"); 154 $this->rawresponse = curl_exec($httprequest); 155 mnet_debug("managed to complete a curl request"); 156 $timestamp_receive = time(); 157 158 if ($this->rawresponse === false) { 159 $this->error[] = curl_errno($httprequest) .':'. curl_error($httprequest); 160 return false; 161 } 162 curl_close($httprequest); 163 164 $this->rawresponse = trim($this->rawresponse); 165 166 $mnet_peer->touch(); 167 168 $crypt_parser = new mnet_encxml_parser(); 169 $crypt_parser->parse($this->rawresponse); 170 171 // If we couldn't parse the message, or it doesn't seem to have encrypted contents, 172 // give the most specific error msg available & return 173 if (!$crypt_parser->payload_encrypted) { 174 if (! empty($crypt_parser->remoteerror)) { 175 $this->error[] = '4: remote server error: ' . $crypt_parser->remoteerror; 176 } else if (! empty($crypt_parser->error)) { 177 $crypt_parser_error = $crypt_parser->error[0]; 178 179 $message = '3:XML Parse error in payload: '.$crypt_parser_error['string']."\n"; 180 if (array_key_exists('lineno', $crypt_parser_error)) { 181 $message .= 'At line number: '.$crypt_parser_error['lineno']."\n"; 182 } 183 if (array_key_exists('line', $crypt_parser_error)) { 184 $message .= 'Which reads: '.$crypt_parser_error['line']."\n"; 185 } 186 $this->error[] = $message; 187 } else { 188 $this->error[] = '1:Payload not encrypted '; 189 } 190 191 $crypt_parser->free_resource(); 192 return false; 193 } 194 195 $key = array_pop($crypt_parser->cipher); 196 $data = array_pop($crypt_parser->cipher); 197 198 $crypt_parser->free_resource(); 199 200 // Initialize payload var 201 $decryptedenvelope = ''; 202 203 // &$decryptedenvelope 204 $isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), 205 $this->mnet->get_private_key(), 'RC4'); 206 207 if (!$isOpen) { 208 // Decryption failed... let's try our archived keys 209 $openssl_history = get_config('mnet', 'openssl_history'); 210 if(empty($openssl_history)) { 211 $openssl_history = array(); 212 set_config('openssl_history', serialize($openssl_history), 'mnet'); 213 } else { 214 $openssl_history = unserialize($openssl_history); 215 } 216 foreach($openssl_history as $keyset) { 217 $keyresource = openssl_pkey_get_private($keyset['keypair_PEM']); 218 $isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), $keyresource, 'RC4'); 219 if ($isOpen) { 220 // It's an older code, sir, but it checks out 221 break; 222 } 223 } 224 } 225 226 if (!$isOpen) { 227 trigger_error("None of our keys could open the payload from host {$mnet_peer->wwwroot} with id {$mnet_peer->id}."); 228 $this->error[] = '3:No key match'; 229 return false; 230 } 231 232 if (strpos(substr($decryptedenvelope, 0, 100), '<signedMessage>')) { 233 $sig_parser = new mnet_encxml_parser(); 234 $sig_parser->parse($decryptedenvelope); 235 } else { 236 $this->error[] = '2:Payload not signed: ' . $decryptedenvelope; 237 return false; 238 } 239 240 // Margin of error is the time it took the request to complete. 241 $margin_of_error = $timestamp_receive - $timestamp_send; 242 243 // Guess the time gap between sending the request and the remote machine 244 // executing the time() function. Marginally better than nothing. 245 $hysteresis = ($margin_of_error) / 2; 246 247 $remote_timestamp = $sig_parser->remote_timestamp - $hysteresis; 248 $time_offset = $remote_timestamp - $timestamp_send; 249 if ($time_offset > 0) { 250 $threshold = get_config('mnet', 'drift_threshold'); 251 if(empty($threshold)) { 252 // We decided 15 seconds was a pretty good arbitrary threshold 253 // for time-drift between servers, but you can customize this in 254 // the config_plugins table. It's not advised though. 255 set_config('drift_threshold', 15, 'mnet'); 256 $threshold = 15; 257 } 258 if ($time_offset > $threshold) { 259 $this->error[] = '6:Time gap with '.$mnet_peer->name.' ('.$time_offset.' seconds) is greater than the permitted maximum of '.$threshold.' seconds'; 260 return false; 261 } 262 } 263 264 $this->xmlrpcresponse = base64_decode($sig_parser->data_object); 265 $this->response = xmlrpc_decode($this->xmlrpcresponse); 266 267 // xmlrpc errors are pushed onto the $this->error stack 268 if (is_array($this->response) && array_key_exists('faultCode', $this->response)) { 269 // The faultCode 7025 means we tried to connect with an old SSL key 270 // The faultString is the new key - let's save it and try again 271 // The re_key attribute stops us from getting into a loop 272 if($this->response['faultCode'] == 7025 && empty($mnet_peer->re_key)) { 273 mnet_debug('recieved an old-key fault, so trying to get the new key and update our records'); 274 // If the new certificate doesn't come thru clean_param() unmolested, error out 275 if($this->response['faultString'] != clean_param($this->response['faultString'], PARAM_PEM)) { 276 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; 277 } 278 $record = new stdClass(); 279 $record->id = $mnet_peer->id; 280 $record->public_key = $this->response['faultString']; 281 $details = openssl_x509_parse($record->public_key); 282 if(!isset($details['validTo_time_t'])) { 283 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; 284 } 285 $record->public_key_expires = $details['validTo_time_t']; 286 $DB->update_record('mnet_host', $record); 287 288 // Create a new peer object populated with the new info & try re-sending the request 289 $rekeyed_mnet_peer = new mnet_peer(); 290 $rekeyed_mnet_peer->set_id($record->id); 291 $rekeyed_mnet_peer->re_key = true; 292 return $this->send($rekeyed_mnet_peer); 293 } 294 if (!empty($CFG->mnet_rpcdebug)) { 295 if (get_string_manager()->string_exists('error'.$this->response['faultCode'], 'mnet')) { 296 $guidance = get_string('error'.$this->response['faultCode'], 'mnet'); 297 } else { 298 $guidance = ''; 299 } 300 } else { 301 $guidance = ''; 302 } 303 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString'] ."\n".$guidance; 304 } 305 306 // ok, it's signed, but is it signed with the right certificate ? 307 // do this *after* we check for an out of date key 308 $verified = openssl_verify($this->xmlrpcresponse, base64_decode($sig_parser->signature), $mnet_peer->public_key); 309 if ($verified != 1) { 310 $this->error[] = 'Invalid signature'; 311 } 312 313 return empty($this->error); 314 } 315 316 /** 317 * Check that we are permitted to call method on specified peer 318 * 319 * @param object $mnet_peer A mnet_peer object with details of the remote host we're connecting to 320 * @return bool True if we permit calls to method on specified peer, False otherwise. 321 */ 322 323 function permission_to_call($mnet_peer) { 324 global $DB, $CFG, $USER; 325 326 // Executing any system method is permitted. 327 $system_methods = array('system/listMethods', 'system/methodSignature', 'system/methodHelp', 'system/listServices'); 328 if (in_array($this->method, $system_methods) ) { 329 return true; 330 } 331 332 $hostids = array($mnet_peer->id); 333 if (!empty($CFG->mnet_all_hosts_id)) { 334 $hostids[] = $CFG->mnet_all_hosts_id; 335 } 336 // At this point, we don't care if the remote host implements the 337 // method we're trying to call. We just want to know that: 338 // 1. The method belongs to some service, as far as OUR host knows 339 // 2. We are allowed to subscribe to that service on this mnet_peer 340 341 list($hostidsql, $hostidparams) = $DB->get_in_or_equal($hostids); 342 343 $sql = "SELECT r.id 344 FROM {mnet_remote_rpc} r 345 INNER JOIN {mnet_remote_service2rpc} s2r ON s2r.rpcid = r.id 346 INNER JOIN {mnet_host2service} h2s ON h2s.serviceid = s2r.serviceid 347 WHERE r.xmlrpcpath = ? 348 AND h2s.subscribe = ? 349 AND h2s.hostid $hostidsql"; 350 351 $params = array($this->method, 1); 352 $params = array_merge($params, $hostidparams); 353 354 if ($DB->record_exists_sql($sql, $params)) { 355 return true; 356 } 357 358 $this->error[] = '7:User with ID '. $USER->id . 359 ' attempted to call unauthorised method '. 360 $this->method.' on host '. 361 $mnet_peer->wwwroot; 362 return false; 363 } 364 365 /** 366 * Generate a curl handle and prepare it for sending to an mnet host 367 * 368 * @param object $mnet_peer A mnet_peer object with details of the remote host the request will be sent to 369 * @return cURL handle - the almost-ready-to-send http request 370 */ 371 function prepare_http_request ($mnet_peer) { 372 $this->uri = $mnet_peer->wwwroot . $mnet_peer->application->xmlrpc_server_url; 373 374 // Initialize request the target URL 375 $httprequest = curl_init($this->uri); 376 curl_setopt($httprequest, CURLOPT_TIMEOUT, $this->timeout); 377 curl_setopt($httprequest, CURLOPT_RETURNTRANSFER, true); 378 curl_setopt($httprequest, CURLOPT_POST, true); 379 curl_setopt($httprequest, CURLOPT_USERAGENT, 'Moodle'); 380 curl_setopt($httprequest, CURLOPT_HTTPHEADER, array("Content-Type: text/xml charset=UTF-8")); 381 382 $verifyhost = 0; 383 $verifypeer = false; 384 if ($mnet_peer->sslverification == mnet_peer::SSL_HOST_AND_PEER) { 385 $verifyhost = 2; 386 $verifypeer = true; 387 } else if ($mnet_peer->sslverification == mnet_peer::SSL_HOST) { 388 $verifyhost = 2; 389 } 390 curl_setopt($httprequest, CURLOPT_SSL_VERIFYHOST, $verifyhost); 391 curl_setopt($httprequest, CURLOPT_SSL_VERIFYPEER, $verifypeer); 392 return $httprequest; 393 } 394 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body