Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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), $this->mnet->get_private_key()); 205 206 if (!$isOpen) { 207 // Decryption failed... let's try our archived keys 208 $openssl_history = get_config('mnet', 'openssl_history'); 209 if(empty($openssl_history)) { 210 $openssl_history = array(); 211 set_config('openssl_history', serialize($openssl_history), 'mnet'); 212 } else { 213 $openssl_history = unserialize($openssl_history); 214 } 215 foreach($openssl_history as $keyset) { 216 $keyresource = openssl_pkey_get_private($keyset['keypair_PEM']); 217 $isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), $keyresource); 218 if ($isOpen) { 219 // It's an older code, sir, but it checks out 220 break; 221 } 222 } 223 } 224 225 if (!$isOpen) { 226 trigger_error("None of our keys could open the payload from host {$mnet_peer->wwwroot} with id {$mnet_peer->id}."); 227 $this->error[] = '3:No key match'; 228 return false; 229 } 230 231 if (strpos(substr($decryptedenvelope, 0, 100), '<signedMessage>')) { 232 $sig_parser = new mnet_encxml_parser(); 233 $sig_parser->parse($decryptedenvelope); 234 } else { 235 $this->error[] = '2:Payload not signed: ' . $decryptedenvelope; 236 return false; 237 } 238 239 // Margin of error is the time it took the request to complete. 240 $margin_of_error = $timestamp_receive - $timestamp_send; 241 242 // Guess the time gap between sending the request and the remote machine 243 // executing the time() function. Marginally better than nothing. 244 $hysteresis = ($margin_of_error) / 2; 245 246 $remote_timestamp = $sig_parser->remote_timestamp - $hysteresis; 247 $time_offset = $remote_timestamp - $timestamp_send; 248 if ($time_offset > 0) { 249 $threshold = get_config('mnet', 'drift_threshold'); 250 if(empty($threshold)) { 251 // We decided 15 seconds was a pretty good arbitrary threshold 252 // for time-drift between servers, but you can customize this in 253 // the config_plugins table. It's not advised though. 254 set_config('drift_threshold', 15, 'mnet'); 255 $threshold = 15; 256 } 257 if ($time_offset > $threshold) { 258 $this->error[] = '6:Time gap with '.$mnet_peer->name.' ('.$time_offset.' seconds) is greater than the permitted maximum of '.$threshold.' seconds'; 259 return false; 260 } 261 } 262 263 $this->xmlrpcresponse = base64_decode($sig_parser->data_object); 264 $this->response = xmlrpc_decode($this->xmlrpcresponse); 265 266 // xmlrpc errors are pushed onto the $this->error stack 267 if (is_array($this->response) && array_key_exists('faultCode', $this->response)) { 268 // The faultCode 7025 means we tried to connect with an old SSL key 269 // The faultString is the new key - let's save it and try again 270 // The re_key attribute stops us from getting into a loop 271 if($this->response['faultCode'] == 7025 && empty($mnet_peer->re_key)) { 272 mnet_debug('recieved an old-key fault, so trying to get the new key and update our records'); 273 // If the new certificate doesn't come thru clean_param() unmolested, error out 274 if($this->response['faultString'] != clean_param($this->response['faultString'], PARAM_PEM)) { 275 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; 276 } 277 $record = new stdClass(); 278 $record->id = $mnet_peer->id; 279 $record->public_key = $this->response['faultString']; 280 $details = openssl_x509_parse($record->public_key); 281 if(!isset($details['validTo_time_t'])) { 282 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; 283 } 284 $record->public_key_expires = $details['validTo_time_t']; 285 $DB->update_record('mnet_host', $record); 286 287 // Create a new peer object populated with the new info & try re-sending the request 288 $rekeyed_mnet_peer = new mnet_peer(); 289 $rekeyed_mnet_peer->set_id($record->id); 290 $rekeyed_mnet_peer->re_key = true; 291 return $this->send($rekeyed_mnet_peer); 292 } 293 if (!empty($CFG->mnet_rpcdebug)) { 294 if (get_string_manager()->string_exists('error'.$this->response['faultCode'], 'mnet')) { 295 $guidance = get_string('error'.$this->response['faultCode'], 'mnet'); 296 } else { 297 $guidance = ''; 298 } 299 } else { 300 $guidance = ''; 301 } 302 $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString'] ."\n".$guidance; 303 } 304 305 // ok, it's signed, but is it signed with the right certificate ? 306 // do this *after* we check for an out of date key 307 $verified = openssl_verify($this->xmlrpcresponse, base64_decode($sig_parser->signature), $mnet_peer->public_key); 308 if ($verified != 1) { 309 $this->error[] = 'Invalid signature'; 310 } 311 312 return empty($this->error); 313 } 314 315 /** 316 * Check that we are permitted to call method on specified peer 317 * 318 * @param object $mnet_peer A mnet_peer object with details of the remote host we're connecting to 319 * @return bool True if we permit calls to method on specified peer, False otherwise. 320 */ 321 322 function permission_to_call($mnet_peer) { 323 global $DB, $CFG, $USER; 324 325 // Executing any system method is permitted. 326 $system_methods = array('system/listMethods', 'system/methodSignature', 'system/methodHelp', 'system/listServices'); 327 if (in_array($this->method, $system_methods) ) { 328 return true; 329 } 330 331 $hostids = array($mnet_peer->id); 332 if (!empty($CFG->mnet_all_hosts_id)) { 333 $hostids[] = $CFG->mnet_all_hosts_id; 334 } 335 // At this point, we don't care if the remote host implements the 336 // method we're trying to call. We just want to know that: 337 // 1. The method belongs to some service, as far as OUR host knows 338 // 2. We are allowed to subscribe to that service on this mnet_peer 339 340 list($hostidsql, $hostidparams) = $DB->get_in_or_equal($hostids); 341 342 $sql = "SELECT r.id 343 FROM {mnet_remote_rpc} r 344 INNER JOIN {mnet_remote_service2rpc} s2r ON s2r.rpcid = r.id 345 INNER JOIN {mnet_host2service} h2s ON h2s.serviceid = s2r.serviceid 346 WHERE r.xmlrpcpath = ? 347 AND h2s.subscribe = ? 348 AND h2s.hostid $hostidsql"; 349 350 $params = array($this->method, 1); 351 $params = array_merge($params, $hostidparams); 352 353 if ($DB->record_exists_sql($sql, $params)) { 354 return true; 355 } 356 357 $this->error[] = '7:User with ID '. $USER->id . 358 ' attempted to call unauthorised method '. 359 $this->method.' on host '. 360 $mnet_peer->wwwroot; 361 return false; 362 } 363 364 /** 365 * Generate a curl handle and prepare it for sending to an mnet host 366 * 367 * @param object $mnet_peer A mnet_peer object with details of the remote host the request will be sent to 368 * @return cURL handle - the almost-ready-to-send http request 369 */ 370 function prepare_http_request ($mnet_peer) { 371 $this->uri = $mnet_peer->wwwroot . $mnet_peer->application->xmlrpc_server_url; 372 373 // Initialize request the target URL 374 $httprequest = curl_init($this->uri); 375 curl_setopt($httprequest, CURLOPT_TIMEOUT, $this->timeout); 376 curl_setopt($httprequest, CURLOPT_RETURNTRANSFER, true); 377 curl_setopt($httprequest, CURLOPT_POST, true); 378 curl_setopt($httprequest, CURLOPT_USERAGENT, 'Moodle'); 379 curl_setopt($httprequest, CURLOPT_HTTPHEADER, array("Content-Type: text/xml charset=UTF-8")); 380 381 $verifyhost = 0; 382 $verifypeer = false; 383 if ($mnet_peer->sslverification == mnet_peer::SSL_HOST_AND_PEER) { 384 $verifyhost = 2; 385 $verifypeer = true; 386 } else if ($mnet_peer->sslverification == mnet_peer::SSL_HOST) { 387 $verifyhost = 2; 388 } 389 curl_setopt($httprequest, CURLOPT_SSL_VERIFYHOST, $verifyhost); 390 curl_setopt($httprequest, CURLOPT_SSL_VERIFYPEER, $verifypeer); 391 return $httprequest; 392 } 393 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body