ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç environment.php000064400000014046152266401670007637 0ustar00get_record('mnet_host', array('id'=>$CFG->mnet_localhost_id))) { return false; } $temparr = get_object_vars($hostobject); foreach($temparr as $key => $value) { $this->$key = $value; } unset($hostobject, $temparr); // Unless this is an install/upgrade, generate the SSL keys. if (empty($this->public_key)) { $this->get_keypair(); } // We need to set up a record that represents 'all hosts'. Any rights // granted to this host will be conferred on all hosts. if (empty($CFG->mnet_all_hosts_id) ) { $hostobject = new stdClass(); $hostobject->wwwroot = ''; $hostobject->ip_address = ''; $hostobject->public_key = ''; $hostobject->public_key_expires = 0; $hostobject->last_connect_time = 0; $hostobject->last_log_id = 0; $hostobject->deleted = 0; $hostobject->name = 'All Hosts'; $hostobject->id = $DB->insert_record('mnet_host',$hostobject); set_config('mnet_all_hosts_id', $hostobject->id); $CFG->mnet_all_hosts_id = $hostobject->id; unset($hostobject); } } function get_keypair() { global $DB, $CFG; // We don't generate keys on install/upgrade because we want the USER // record to have an email address, city and country already. if (during_initial_install()) return true; if ($CFG->mnet_dispatcher_mode == 'off') return true; if (!extension_loaded("openssl")) return true; if (!empty($this->keypair)) return true; $this->keypair = array(); $keypair = get_config('mnet', 'openssl'); if (!empty($keypair)) { // Explode/Implode is faster than Unserialize/Serialize list($this->keypair['certificate'], $this->keypair['keypair_PEM']) = explode('@@@@@@@@', $keypair); } if ($this->public_key_expires <= time()) { // Key generation/rotation // 1. Archive the current key (if there is one). $result = get_config('mnet', 'openssl_history'); if(empty($result)) { set_config('openssl_history', serialize(array()), 'mnet'); $openssl_history = array(); } else { $openssl_history = unserialize($result); } if(count($this->keypair)) { $this->keypair['expires'] = $this->public_key_expires; array_unshift($openssl_history, $this->keypair); } // 2. How many old keys do we want to keep? Use array_slice to get // rid of any we don't want $openssl_generations = get_config('mnet', 'openssl_generations'); if(empty($openssl_generations)) { set_config('openssl_generations', 3, 'mnet'); $openssl_generations = 3; } if(count($openssl_history) > $openssl_generations) { $openssl_history = array_slice($openssl_history, 0, $openssl_generations); } set_config('openssl_history', serialize($openssl_history), 'mnet'); // 3. Generate fresh keys $this->replace_keys(); } return true; } function replace_keys() { global $DB, $CFG; $keypair = mnet_generate_keypair(); if (empty($keypair)) { error_log('Can not generate keypair, sorry'); return; } $this->keypair = array(); $this->keypair = $keypair; $this->public_key = $this->keypair['certificate']; $details = openssl_x509_parse($this->public_key); $this->public_key_expires = $details['validTo_time_t']; $this->wwwroot = $CFG->wwwroot; if (empty($_SERVER['SERVER_ADDR'])) { // SERVER_ADDR is only returned by Apache-like webservers $my_hostname = mnet_get_hostname_from_uri($CFG->wwwroot); $my_ip = gethostbyname($my_hostname); // Returns unmodified hostname on failure. DOH! if ($my_ip == $my_hostname) { $this->ip_address = 'UNKNOWN'; } else { $this->ip_address = $my_ip; } } else { $this->ip_address = $_SERVER['SERVER_ADDR']; } set_config('openssl', implode('@@@@@@@@', $this->keypair), 'mnet'); $DB->update_record('mnet_host', $this); error_log('New public key has been generated. It expires ' . date('Y/m/d h:i:s', $this->public_key_expires)); } function get_private_key() { if (empty($this->keypair)) $this->get_keypair(); return openssl_pkey_get_private($this->keypair['keypair_PEM']); } function get_public_key() { if (!isset($this->keypair)) $this->get_keypair(); return openssl_pkey_get_public($this->keypair['certificate']); } } lib.php000064400000104027152266401670006040 0ustar00dirroot.'/mnet/xmlrpc/xmlparser.php'; require_once $CFG->dirroot.'/mnet/peer.php'; require_once $CFG->dirroot.'/mnet/environment.php'; /// CONSTANTS /////////////////////////////////////////////////////////// define('RPC_OK', 0); define('RPC_NOSUCHFILE', 1); define('RPC_NOSUCHCLASS', 2); define('RPC_NOSUCHFUNCTION', 3); define('RPC_FORBIDDENFUNCTION', 4); define('RPC_NOSUCHMETHOD', 5); define('RPC_FORBIDDENMETHOD', 6); /** * Strip extraneous detail from a URL or URI and return the hostname * * @param string $uri The URI of a file on the remote computer, optionally * including its http:// prefix like * http://www.example.com/index.html * @return string Just the hostname */ function mnet_get_hostname_from_uri($uri = null) { $count = preg_match("@^(?:http[s]?://)?([A-Z0-9\-\.]+).*@i", $uri, $matches); if ($count > 0) return $matches[1]; return false; } /** * Get the remote machine's SSL Cert * * @param string $uri The URI of a file on the remote computer, including * its http:// or https:// prefix * @return string A PEM formatted SSL Certificate. */ function mnet_get_public_key($uri, $application=null) { global $CFG, $DB; $mnet = get_mnet_environment(); // The key may be cached in the mnet_set_public_key function... // check this first $key = mnet_set_public_key($uri); if ($key != false) { return $key; } if (empty($application)) { $application = $DB->get_record('mnet_application', array('name'=>'moodle')); } $params = [ new \PhpXmlRpc\Value($CFG->wwwroot), new \PhpXmlRpc\Value($mnet->public_key), new \PhpXmlRpc\Value($application->name), ]; $request = new \PhpXmlRpc\Request('system/keyswap', $params); // Let's create a client to handle the request and the response easily. $client = new \PhpXmlRpc\Client($uri . $application->xmlrpc_server_url); $client->setOption('use_curl', \PhpXmlRpc\Client::USE_CURL_ALWAYS); $client->setOption('user_agent', 'Moodle'); $client->return_type = 'xmlrpcvals'; // This (keyswap) is not encrypted, so we can expect proper xmlrpc in this case. $client->setOption('request_charset_encoding', 'utf-8'); $client->setOption('accepted_charset_encodings', ['utf-8']); // TODO: Link this to DEBUG DEVELOPER or with MNET debugging... // $client->setdebug(1); // See a good number of complete requests and responses. $client->setOption('verifyhost', 0); $client->setOption('verifypeer', false); // TODO: It's curious that this service (keyswap) that needs // a custom client, different from mnet_xmlrpc_client, because // this is not encrypted / signed, does support proxies and the // general one does not. Worth analysing if the support below // should be added to it. // Some curl options need to be set apart, accumulate them here. $extracurloptions = []; // Check for proxy. if (!empty($CFG->proxyhost) && !is_proxybypass($uri)) { // SOCKS supported in PHP5 only. if (!empty($CFG->proxytype) && ($CFG->proxytype == 'SOCKS5')) { if (defined('CURLPROXY_SOCKS5')) { $extracurloptions[CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5; } else { throw new \moodle_exception( 'socksnotsupported', 'mnet'); } } $extracurloptions[CURLOPT_HTTPPROXYTUNNEL] = false; // Configure proxy host, port, user, pass and auth. $client->setProxy( $CFG->proxyhost, empty($CFG->proxyport) ? 0 : $CFG->proxyport, empty($CFG->proxyuser) ? '' : $CFG->proxyuser, empty($CFG->proxypassword) ? '' : $CFG->proxypassword, defined('CURLOPT_PROXYAUTH') ? CURLAUTH_BASIC | CURLAUTH_NTLM : 1); } // Finally, add the extra curl options we may have accumulated. $client->setCurlOptions($extracurloptions); $response = $client->send($request, 60); // Check curl / xmlrpc errors. if ($response->faultCode()) { debugging("Request for $uri failed with error {$response->faultCode()}: {$response->faultString()}"); return false; } // Check HTTP error code. $status = $response->httpResponse()['status_code']; if (!empty($status) && ($status != 200)) { debugging("Request for $uri failed with HTTP code " . $status); return false; } // Get the peer actual public key from the response. $res = $response->value()->scalarval(); if (!is_array($res)) { // ! error $public_certificate = $res; $credentials=array(); if (strlen(trim($public_certificate))) { $credentials = openssl_x509_parse($public_certificate); $host = $credentials['subject']['CN']; if (array_key_exists( 'subjectAltName', $credentials['subject'])) { $host = $credentials['subject']['subjectAltName']; } if (strpos($uri, $host) !== false) { mnet_set_public_key($uri, $public_certificate); return $public_certificate; } else { debugging("Request for $uri returned public key for different URI - $host"); } } else { debugging("Request for $uri returned empty response"); } } else { debugging( "Request for $uri returned unexpected result"); } return false; } /** * Store a URI's public key in a static variable, or retrieve the key for a URI * * @param string $uri The URI of a file on the remote computer, including its * https:// prefix * @param mixed $key A public key to store in the array OR null. If the key * is null, the function will return the previously stored * key for the supplied URI, should it exist. * @return mixed A public key OR true/false. */ function mnet_set_public_key($uri, $key = null) { static $keyarray = array(); if (isset($keyarray[$uri]) && empty($key)) { return $keyarray[$uri]; } elseif (!empty($key)) { $keyarray[$uri] = $key; return true; } return false; } /** * Sign a message and return it in an XML-Signature document * * This function can sign any content, but it was written to provide a system of * signing XML-RPC request and response messages. The message will be base64 * encoded, so it does not need to be text. * * We compute the SHA1 digest of the message. * We compute a signature on that digest with our private key. * We link to the public key that can be used to verify our signature. * We base64 the message data. * We identify our wwwroot - this must match our certificate's CN * * The XML-RPC document will be parceled inside an XML-SIG document, which holds * the base64_encoded XML as an object, the SHA1 digest of that document, and a * signature of that document using the local private key. This signature will * uniquely identify the RPC document as having come from this server. * * See the {@Link http://www.w3.org/TR/xmldsig-core/ XML-DSig spec} at the W3c * site * * @param string $message The data you want to sign * @param resource $privatekey The private key to sign the response with * @return string An XML-DSig document */ function mnet_sign_message($message, $privatekey = null) { global $CFG; $digest = sha1($message); $mnet = get_mnet_environment(); // If the user hasn't supplied a private key (for example, one of our older, // expired private keys, we get the current default private key and use that. if ($privatekey == null) { $privatekey = $mnet->get_private_key(); } // The '$sig' value below is returned by reference. // We initialize it first to stop my IDE from complaining. $sig = ''; $bool = openssl_sign($message, $sig, $privatekey); // Avoid passing null values to base64_encode. if ($bool === false) { throw new \moodle_exception('opensslsignerror'); } $message = ' '.$digest.' '.base64_encode($sig).' '.base64_encode($message).' '.$mnet->wwwroot.' '.time().' '; return $message; } /** * Encrypt a message and return it in an XML-Encrypted document * * This function can encrypt any content, but it was written to provide a system * of encrypting XML-RPC request and response messages. The message will be * base64 encoded, so it does not need to be text - binary data should work. * * We compute the SHA1 digest of the message. * We compute a signature on that digest with our private key. * We link to the public key that can be used to verify our signature. * We base64 the message data. * We identify our wwwroot - this must match our certificate's CN * * The XML-RPC document will be parceled inside an XML-SIG document, which holds * the base64_encoded XML as an object, the SHA1 digest of that document, and a * signature of that document using the local private key. This signature will * uniquely identify the RPC document as having come from this server. * * See the {@Link http://www.w3.org/TR/xmlenc-core/ XML-ENC spec} at the W3c * site * * @param string $message The data you want to sign * @param string $remote_certificate Peer's certificate in PEM format * @return string An XML-ENC document */ function mnet_encrypt_message($message, $remote_certificate) { $mnet = get_mnet_environment(); // Generate a key resource from the remote_certificate text string $publickey = openssl_get_publickey($remote_certificate); if ($publickey === false) { // Remote certificate is faulty. return false; } // Initialize vars $encryptedstring = ''; $symmetric_keys = array(); // passed by ref -> &$encryptedstring &$symmetric_keys $bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey), 'RC4'); // Avoid passing null values to base64_encode. if ($bool === false) { throw new \moodle_exception('opensslsealerror'); } $message = $encryptedstring; $symmetrickey = array_pop($symmetric_keys); $message = ' XMLENC '.base64_encode($message).' SSLKEY '.base64_encode($symmetrickey).' XMLENC '.$mnet->wwwroot.' '; return $message; } /** * Get your SSL keys from the database, or create them (if they don't exist yet) * * Get your SSL keys from the database, or (if they don't exist yet) call * mnet_generate_keypair to create them * * @param string $string The text you want to sign * @return string The signature over that text */ function mnet_get_keypair() { global $CFG, $DB; static $keypair = null; if (!is_null($keypair)) return $keypair; if ($result = get_config('mnet', 'openssl')) { list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result); return $keypair; } else { $keypair = mnet_generate_keypair(); return $keypair; } } /** * Generate public/private keys and store in the config table * * Use the distinguished name provided to create a CSR, and then sign that CSR * with the same credentials. Store the keypair you create in the config table. * If a distinguished name is not provided, create one using the fullname of * 'the course with ID 1' as your organization name, and your hostname (as * detailed in $CFG->wwwroot). * * @param array $dn The distinguished name of the server * @return string The signature over that text */ function mnet_generate_keypair($dn = null, $days=28) { global $CFG, $USER, $DB; // check if lifetime has been overriden if (!empty($CFG->mnetkeylifetime)) { $days = $CFG->mnetkeylifetime; } $host = strtolower($CFG->wwwroot); $host = preg_replace("~^http(s)?://~",'',$host); $break = strpos($host.'/' , '/'); $host = substr($host, 0, $break); $site = get_site(); $organization = $site->fullname; $keypair = array(); $country = 'NZ'; $province = 'Wellington'; $locality = 'Wellington'; $email = !empty($CFG->noreplyaddress) ? $CFG->noreplyaddress : 'noreply@'.$_SERVER['HTTP_HOST']; if(!empty($USER->country)) { $country = $USER->country; } if(!empty($USER->city)) { $province = $USER->city; $locality = $USER->city; } if(!empty($USER->email)) { $email = $USER->email; } if (is_null($dn)) { $dn = array( "countryName" => $country, "stateOrProvinceName" => $province, "localityName" => $locality, "organizationName" => $organization, "organizationalUnitName" => 'Moodle', "commonName" => substr($CFG->wwwroot, 0, 64), "subjectAltName" => $CFG->wwwroot, "emailAddress" => $email ); } $dnlimits = array( 'countryName' => 2, 'stateOrProvinceName' => 128, 'localityName' => 128, 'organizationName' => 64, 'organizationalUnitName' => 64, 'commonName' => 64, 'emailAddress' => 128 ); foreach ($dnlimits as $key => $length) { $dn[$key] = core_text::substr($dn[$key], 0, $length); } // ensure we remove trailing slashes $dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]); if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs $new_key = openssl_pkey_new(array("config" => $CFG->opensslcnf)); } else { $new_key = openssl_pkey_new(); } if ($new_key === false) { // can not generate keys - missing openssl.cnf?? return null; } if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs $csr_rsc = openssl_csr_new($dn, $new_key, array("config" => $CFG->opensslcnf)); $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days, array("config" => $CFG->opensslcnf)); } else { $csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048)); $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days); } unset($csr_rsc); // Free up the resource // We export our self-signed certificate to a string. openssl_x509_export($selfSignedCert, $keypair['certificate']); // Export your public/private key pair as a PEM encoded string. You // can protect it with an optional passphrase if you wish. if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'], null, array("config" => $CFG->opensslcnf)); } else { $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */); } unset($new_key); // Free up the resource return $keypair; } function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) { global $DB; $mnethost = $DB->get_record('mnet_host', array('id'=>$mnet_host_id)); if ($aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnet_host_id))) { // Update. $aclrecord->accessctrl = $accessctrl; $DB->update_record('mnet_sso_access_control', $aclrecord); // Trigger access control updated event. $params = array( 'objectid' => $aclrecord->id, 'context' => context_system::instance(), 'other' => array( 'username' => $username, 'hostname' => $mnethost->name, 'accessctrl' => $accessctrl ) ); $event = \core\event\mnet_access_control_updated::create($params); $event->add_record_snapshot('mnet_host', $mnethost); $event->trigger(); } else { // Insert. $aclrecord = new stdClass(); $aclrecord->username = $username; $aclrecord->accessctrl = $accessctrl; $aclrecord->mnet_host_id = $mnet_host_id; $aclrecord->id = $DB->insert_record('mnet_sso_access_control', $aclrecord); // Trigger access control created event. $params = array( 'objectid' => $aclrecord->id, 'context' => context_system::instance(), 'other' => array( 'username' => $username, 'hostname' => $mnethost->name, 'accessctrl' => $accessctrl ) ); $event = \core\event\mnet_access_control_created::create($params); $event->add_record_snapshot('mnet_host', $mnethost); $event->trigger(); } return true; } function mnet_get_peer_host ($mnethostid) { global $DB; static $hosts; if (!isset($hosts[$mnethostid])) { $host = $DB->get_record('mnet_host', array('id' => $mnethostid)); $hosts[$mnethostid] = $host; } return $hosts[$mnethostid]; } /** * Inline function to modify a url string so that mnet users are requested to * log in at their mnet identity provider (if they are not already logged in) * before ultimately being directed to the original url. * * @param string $jumpurl the url which user should initially be directed to. * This is a URL associated with a moodle networking peer when it * is fulfiling a role as an identity provider (IDP). Different urls for * different peers, the jumpurl is formed partly from the IDP's webroot, and * partly from a predefined local path within that webwroot. * The result of the user hitting this jump url is that they will be asked * to login (at their identity provider (if they aren't already)), mnet * will prepare the necessary authentication information, then redirect * them back to somewhere at the content provider(CP) moodle (this moodle) * @param array $url array with 2 elements * 0 - context the url was taken from, possibly just the url, possibly href="url" * 1 - the destination url * @return string the url the remote user should be supplied with. */ function mnet_sso_apply_indirection ($jumpurl, $url) { global $USER, $CFG; $localpart=''; $urlparts = parse_url($url[1]); if($urlparts) { if (isset($urlparts['path'])) { $path = $urlparts['path']; // if our wwwroot has a path component, need to strip that path from beginning of the // 'localpart' to make it relative to moodle's wwwroot $wwwrootpath = parse_url($CFG->wwwroot, PHP_URL_PATH); if (!empty($wwwrootpath) && strpos($path, $wwwrootpath) === 0) { $path = substr($path, strlen($wwwrootpath)); } $localpart .= $path; } if (isset($urlparts['query'])) { $localpart .= '?'.$urlparts['query']; } if (isset($urlparts['fragment'])) { $localpart .= '#'.$urlparts['fragment']; } } $indirecturl = $jumpurl . urlencode($localpart); //If we matched on more than just a url (ie an html link), return the url to an href format if ($url[0] != $url[1]) { $indirecturl = 'href="'.$indirecturl.'"'; } return $indirecturl; } function mnet_get_app_jumppath ($applicationid) { global $DB; static $appjumppaths; if (!isset($appjumppaths[$applicationid])) { $ssojumpurl = $DB->get_field('mnet_application', 'sso_jump_url', array('id' => $applicationid)); $appjumppaths[$applicationid] = $ssojumpurl; } return $appjumppaths[$applicationid]; } /** * Output debug information about mnet. this will go to the error_log. * * @param mixed $debugdata this can be a string, or array or object. * @param int $debuglevel optional , defaults to 1. bump up for very noisy debug info */ function mnet_debug($debugdata, $debuglevel=1) { global $CFG; $setlevel = get_config('', 'mnet_rpcdebug'); if (empty($setlevel) || $setlevel < $debuglevel) { return; } if (is_object($debugdata)) { $debugdata = (array)$debugdata; } if (is_array($debugdata)) { mnet_debug('DUMPING ARRAY'); foreach ($debugdata as $key => $value) { mnet_debug("$key: $value"); } mnet_debug('END DUMPING ARRAY'); return; } $prefix = 'MNET DEBUG '; if (defined('MNET_SERVER')) { $prefix .= " (server $CFG->wwwroot"; if ($peer = get_mnet_remote_client() && !empty($peer->wwwroot)) { $prefix .= ", remote peer " . $peer->wwwroot; } $prefix .= ')'; } else { $prefix .= " (client $CFG->wwwroot) "; } error_log("$prefix $debugdata"); } /** * Return an array of information about all moodle's profile fields * which ones are optional, which ones are forced. * This is used as the basis of providing lists of profile fields to the administrator * to pick which fields to import/export over MNET * * @return array(forced => array, optional => array) */ function mnet_profile_field_options() { global $DB; static $info; if (!empty($info)) { return $info; } $excludes = array( 'id', // makes no sense 'mnethostid', // makes no sense 'timecreated', // will be set to relative to the host anyway 'timemodified', // will be set to relative to the host anyway 'auth', // going to be set to 'mnet' 'deleted', // we should never get deleted users sent over, but don't send this anyway 'confirmed', // unconfirmed users can't log in to their home site, all remote users considered confirmed 'password', // no password for mnet users 'theme', // handled separately 'lastip', // will be set to relative to the host anyway ); // these are the ones that user_not_fully_set_up will complain about // and also special case ones $forced = array( 'username', 'email', 'firstname', 'lastname', 'auth', 'wwwroot', 'session.gc_lifetime', '_mnet_userpicture_timemodified', '_mnet_userpicture_mimetype', ); // these are the ones we used to send/receive (pre 2.0) $legacy = array( 'username', 'email', 'auth', 'deleted', 'firstname', 'lastname', 'city', 'country', 'lang', 'timezone', 'description', 'mailformat', 'maildigest', 'maildisplay', 'htmleditor', 'wwwroot', 'picture', ); // get a random user record from the database to pull the fields off $randomuser = $DB->get_record('user', array(), '*', IGNORE_MULTIPLE); foreach ($randomuser as $key => $discard) { if (in_array($key, $excludes) || in_array($key, $forced)) { continue; } $fields[$key] = $key; } $info = array( 'forced' => $forced, 'optional' => $fields, 'legacy' => $legacy, ); return $info; } /** * Returns information about MNet peers * * @param bool $withdeleted should the deleted peers be returned too * @return array */ function mnet_get_hosts($withdeleted = false) { global $CFG, $DB; $sql = "SELECT h.id, h.deleted, h.wwwroot, h.ip_address, h.name, h.public_key, h.public_key_expires, h.transport, h.portno, h.last_connect_time, h.last_log_id, h.applicationid, a.name as app_name, a.display_name as app_display_name, a.xmlrpc_server_url FROM {mnet_host} h JOIN {mnet_application} a ON h.applicationid = a.id WHERE h.id <> ?"; if (!$withdeleted) { $sql .= " AND h.deleted = 0"; } $sql .= " ORDER BY h.deleted, h.name, h.id"; return $DB->get_records_sql($sql, array($CFG->mnet_localhost_id)); } /** * return an array information about services enabled for the given peer. * in two modes, fulldata or very basic data. * * @param mnet_peer $mnet_peer the peer to get information abut * @param boolean $fulldata whether to just return which services are published/subscribed, or more information (defaults to full) * * @return array If $fulldata is false, an array is returned like: * publish => array( * serviceid => boolean, * serviceid => boolean, * ), * subscribe => array( * serviceid => boolean, * serviceid => boolean, * ) * If $fulldata is true, an array is returned like: * servicename => array( * apiversion => array( * name => string * offer => boolean * apiversion => int * plugintype => string * pluginname => string * hostsubscribes => boolean * hostpublishes => boolean * ), * ) */ function mnet_get_service_info(mnet_peer $mnet_peer, $fulldata=true) { global $CFG, $DB; $requestkey = (!empty($fulldata) ? 'fulldata' : 'mydata'); static $cache = array(); if (array_key_exists($mnet_peer->id, $cache)) { return $cache[$mnet_peer->id][$requestkey]; } $id_list = $mnet_peer->id; if (!empty($CFG->mnet_all_hosts_id)) { $id_list .= ', '.$CFG->mnet_all_hosts_id; } $concat = $DB->sql_concat('COALESCE(h2s.id,0) ', ' \'-\' ', ' svc.id', '\'-\'', 'r.plugintype', '\'-\'', 'r.pluginname'); $query = " SELECT DISTINCT $concat as id, svc.id as serviceid, svc.name, svc.offer, svc.apiversion, r.plugintype, r.pluginname, h2s.hostid, h2s.publish, h2s.subscribe FROM {mnet_service2rpc} s2r, {mnet_rpc} r, {mnet_service} svc LEFT JOIN {mnet_host2service} h2s ON h2s.hostid in ($id_list) AND h2s.serviceid = svc.id WHERE svc.offer = '1' AND s2r.serviceid = svc.id AND s2r.rpcid = r.id ORDER BY svc.name ASC"; $resultset = $DB->get_records_sql($query); if (is_array($resultset)) { $resultset = array_values($resultset); } else { $resultset = array(); } require_once $CFG->dirroot.'/mnet/xmlrpc/client.php'; $remoteservices = array(); if ($mnet_peer->id != $CFG->mnet_all_hosts_id) { // Create a new request object $mnet_request = new mnet_xmlrpc_client(); // Tell it the path to the method that we want to execute $mnet_request->set_method('system/listServices'); $mnet_request->send($mnet_peer); if (is_array($mnet_request->response)) { foreach($mnet_request->response as $service) { $remoteservices[$service['name']][$service['apiversion']] = $service; } } } $myservices = array(); $mydata = array(); foreach($resultset as $result) { $result->hostpublishes = false; $result->hostsubscribes = false; if (isset($remoteservices[$result->name][$result->apiversion])) { if ($remoteservices[$result->name][$result->apiversion]['publish'] == 1) { $result->hostpublishes = true; } if ($remoteservices[$result->name][$result->apiversion]['subscribe'] == 1) { $result->hostsubscribes = true; } } if (empty($myservices[$result->name][$result->apiversion])) { $myservices[$result->name][$result->apiversion] = array('serviceid' => $result->serviceid, 'name' => $result->name, 'offer' => $result->offer, 'apiversion' => $result->apiversion, 'plugintype' => $result->plugintype, 'pluginname' => $result->pluginname, 'hostsubscribes' => $result->hostsubscribes, 'hostpublishes' => $result->hostpublishes ); } // allhosts_publish allows us to tell the admin that even though he // is disabling a service, it's still available to the host because // he's also publishing it to 'all hosts' if ($result->hostid == $CFG->mnet_all_hosts_id && $CFG->mnet_all_hosts_id != $mnet_peer->id) { $myservices[$result->name][$result->apiversion]['allhosts_publish'] = $result->publish; $myservices[$result->name][$result->apiversion]['allhosts_subscribe'] = $result->subscribe; } elseif (!empty($result->hostid)) { $myservices[$result->name][$result->apiversion]['I_publish'] = $result->publish; $myservices[$result->name][$result->apiversion]['I_subscribe'] = $result->subscribe; } $mydata['publish'][$result->serviceid] = $result->publish; $mydata['subscribe'][$result->serviceid] = $result->subscribe; } $cache[$mnet_peer->id]['fulldata'] = $myservices; $cache[$mnet_peer->id]['mydata'] = $mydata; return $cache[$mnet_peer->id][$requestkey]; } /** * return an array of the profile fields to send * with user information to the given mnet host. * * @param mnet_peer $peer the peer to send the information to * * @return array (like 'username', 'firstname', etc) */ function mnet_fields_to_send(mnet_peer $peer) { return _mnet_field_helper($peer, 'export'); } /** * return an array of the profile fields to import * from the given host, when creating/updating user accounts * * @param mnet_peer $peer the peer we're getting the information from * * @return array (like 'username', 'firstname', etc) */ function mnet_fields_to_import(mnet_peer $peer) { return _mnet_field_helper($peer, 'import'); } /** * helper for {@see mnet_fields_to_import} and {@mnet_fields_to_send} * * @access private * * @param mnet_peer $peer the peer object * @param string $key 'import' or 'export' * * @return array (like 'username', 'firstname', etc) */ function _mnet_field_helper(mnet_peer $peer, $key) { $tmp = mnet_profile_field_options(); $defaults = explode(',', get_config('moodle', 'mnetprofile' . $key . 'fields')); if ('1' === get_config('mnet', 'host' . $peer->id . $key . 'default')) { return array_merge($tmp['forced'], $defaults); } $hostsettings = get_config('mnet', 'host' . $peer->id . $key . 'fields'); if (false === $hostsettings) { return array_merge($tmp['forced'], $defaults); } return array_merge($tmp['forced'], explode(',', $hostsettings)); } /** * given a user object (or array) and a list of allowed fields, * strip out all the fields that should not be included. * This can be used both for outgoing data and incoming data. * * @param mixed $user array or object representing a database record * @param array $fields an array of allowed fields (usually from mnet_fields_to_{send,import} * * @return mixed array or object, depending what type of $user object was passed (datatype is respected) */ function mnet_strip_user($user, $fields) { if (is_object($user)) { $user = (array)$user; $wasobject = true; // so we can cast back before we return } foreach ($user as $key => $value) { if (!in_array($key, $fields)) { unset($user[$key]); } } if (!empty($wasobject)) { $user = (object)$user; } return $user; } classes/privacy/provider.php000064400000027260152266401670012241 0ustar00. /** * Privacy Subsystem implementation for core_mnet. * * @package core_mnet * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_mnet\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy Subsystem for core_mnet implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\subsystem\provider { /** * Returns meta data about this system. * * @param collection $collection The initialised item collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $sessionfields = [ 'userid' => 'privacy:metadata:mnet_session:userid', 'username' => 'privacy:metadata:mnet_session:username', 'token' => 'privacy:metadata:mnet_session:token', 'mnethostid' => 'privacy:metadata:mnet_session:mnethostid', 'useragent' => 'privacy:metadata:mnet_session:useragent', 'expires' => 'privacy:metadata:mnet_session:expires', ]; $collection->add_database_table('mnet_session', $sessionfields, 'privacy:metadata:mnet_session'); $logfields = [ 'hostid' => 'privacy:metadata:mnet_log:hostid', 'remoteid' => 'privacy:metadata:mnet_log:remoteid', 'time' => 'privacy:metadata:mnet_log:time', 'userid' => 'privacy:metadata:mnet_log:userid', 'ip' => 'privacy:metadata:mnet_log:ip', 'course' => 'privacy:metadata:mnet_log:course', 'coursename' => 'privacy:metadata:mnet_log:coursename', 'module' => 'privacy:metadata:mnet_log:module', 'cmid' => 'privacy:metadata:mnet_log:cmid', 'action' => 'privacy:metadata:mnet_log:action', 'url' => 'privacy:metadata:mnet_log:url', 'info' => 'privacy:metadata:mnet_log:info', ]; $collection->add_database_table('mnet_log', $logfields, 'privacy:metadata:mnet_log'); $externalfields = [ 'address' => 'privacy:metadata:mnet_external:address', 'alternatename' => 'privacy:metadata:mnet_external:alternatename', 'autosubscribe' => 'privacy:metadata:mnet_external:autosubscribe', 'calendartype' => 'privacy:metadata:mnet_external:calendartype', 'city' => 'privacy:metadata:mnet_external:city', 'country' => 'privacy:metadata:mnet_external:country', 'currentlogin' => 'privacy:metadata:mnet_external:currentlogin', 'department' => 'privacy:metadata:mnet_external:department', 'description' => 'privacy:metadata:mnet_external:description', 'email' => 'privacy:metadata:mnet_external:email', 'emailstop' => 'privacy:metadata:mnet_external:emailstop', 'firstaccess' => 'privacy:metadata:mnet_external:firstaccess', 'firstname' => 'privacy:metadata:mnet_external:firstname', 'firstnamephonetic' => 'privacy:metadata:mnet_external:firstnamephonetic', 'id' => 'privacy:metadata:mnet_external:id', 'idnumber' => 'privacy:metadata:mnet_external:idnumber', 'imagealt' => 'privacy:metadata:mnet_external:imagealt', 'institution' => 'privacy:metadata:mnet_external:institution', 'lang' => 'privacy:metadata:mnet_external:lang', 'lastaccess' => 'privacy:metadata:mnet_external:lastaccess', 'lastlogin' => 'privacy:metadata:mnet_external:lastlogin', 'lastname' => 'privacy:metadata:mnet_external:lastname', 'lastnamephonetic' => 'privacy:metadata:mnet_external:lastnamephonetic', 'maildigest' => 'privacy:metadata:mnet_external:maildigest', 'maildisplay' => 'privacy:metadata:mnet_external:maildisplay', 'middlename' => 'privacy:metadata:mnet_external:middlename', 'phone1' => 'privacy:metadata:mnet_external:phone1', 'pnone2' => 'privacy:metadata:mnet_external:phone2', 'picture' => 'privacy:metadata:mnet_external:picture', 'policyagreed' => 'privacy:metadata:mnet_external:policyagreed', 'suspended' => 'privacy:metadata:mnet_external:suspended', 'timezone' => 'privacy:metadata:mnet_external:timezone', 'trackforums' => 'privacy:metadata:mnet_external:trackforums', 'trustbitmask' => 'privacy:metadata:mnet_external:trustbitmask', 'username' => 'privacy:metadata:mnet_external:username', ]; $collection->add_external_location_link('moodle', $externalfields, 'privacy:metadata:external:moodle'); $collection->add_external_location_link('mahara', $externalfields, 'privacy:metadata:external:mahara'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { $sql = "SELECT ctx.id FROM {mnet_log} ml JOIN {context} ctx ON ctx.instanceid = ml.userid AND ctx.contextlevel = :contextlevel WHERE ml.userid = :userid"; $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist): void { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT userid FROM {mnet_log} WHERE userid = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts, using the supplied exporter instance. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist): void { global $DB; $context = \context_user::instance($contextlist->get_user()->id); $sql = "SELECT ml.id, mh.wwwroot, mh.name, ml.remoteid, ml.time, ml.userid, ml.ip, ml.course, ml.coursename, ml.module, ml.cmid, ml.action, ml.url, ml.info FROM {mnet_log} ml JOIN {mnet_host} mh ON mh.id = ml.hostid WHERE ml.userid = :userid ORDER BY mh.name, ml.coursename"; $params = ['userid' => $contextlist->get_user()->id]; $data = []; $lastcourseid = null; $logentries = $DB->get_recordset_sql($sql, $params); foreach ($logentries as $logentry) { $item = (object) [ 'time' => transform::datetime($logentry->time), 'remoteid' => $logentry->remoteid, 'ip' => $logentry->ip, 'course' => $logentry->course, 'coursename' => format_string($logentry->coursename), 'module' => $logentry->module, 'cmid' => $logentry->cmid, 'action' => $logentry->action, 'url' => $logentry->url, 'info' => format_string($logentry->info), ]; $item->externalhost = ($logentry->name == '') ? preg_replace('#^https?://#', '', $logentry->wwwroot) : preg_replace('#^https?://#', '', $logentry->name); if ($lastcourseid && $lastcourseid != $logentry->course) { $path = [get_string('mnet', 'core_mnet'), $data[0]->externalhost, $data[0]->coursename]; writer::with_context($context)->export_data($path, (object) $data); $data = []; } $data[] = $item; $lastcourseid = $logentry->course; } $logentries->close(); $path = [get_string('mnet', 'core_mnet'), $item->externalhost, $item->coursename]; writer::with_context($context)->export_data($path, (object) $data); } /** * Delete all personal data for all users in the specified context. * * @param \context $context Context to delete data from. */ public static function delete_data_for_all_users_in_context(\context $context): void { global $DB; if ($context->contextlevel != CONTEXT_USER) { return; } $DB->delete_records('mnet_log', ['userid' => $context->instanceid]); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist): void { global $DB; $context = $userlist->get_context(); if ($context instanceof \context_user) { $DB->delete_records('mnet_log', ['userid' => $context->instanceid]); } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist): void { global $DB; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel != CONTEXT_USER) { continue; } if ($context->instanceid == $userid) { // Because we only use user contexts the instance ID is the user ID. $DB->delete_records('mnet_log', ['userid' => $context->instanceid]); } } } } peer.php000064400000026250152266401670006226 0ustar00libdir . '/filelib.php'); // download_file_content() used here class mnet_peer { /** No SSL verification. */ const SSL_NONE = 0; /** SSL verification for host. */ const SSL_HOST = 1; /** SSL verification for host and peer. */ const SSL_HOST_AND_PEER = 2; var $id = 0; var $wwwroot = ''; var $ip_address = ''; var $name = ''; var $public_key = ''; var $public_key_expires = 0; var $last_connect_time = 0; var $last_log_id = 0; var $force_theme = 0; var $theme = ''; var $applicationid = 1; // Default of 1 == Moodle var $keypair = array(); var $error = array(); var $bootstrapped = false; // set when the object is populated /** @var int $sslverification The level of SSL verification to apply. */ public $sslverification = self::SSL_HOST_AND_PEER; /** @var int deleted status. */ public $deleted; /** @var stdClass data from mnet_application table in DB. */ public $application; /** * Current SSL public key * * MNet need to compare the remote machine's SSL Cert and the public key to warn users of any mismatch. * The property is the remote machine's SSL Cert. * * @see admin/mnet/peers.php * @var string */ public $currentkey; /* * Fetch information about a peer identified by wwwroot * If information does not preexist in db, collect it together based on * supplied information * * @param string $wwwroot - address of peer whose details we want * @param string $pubkey - to use if we add a record to db for new peer * @param int $application - table id - what kind of peer are we talking to * @return bool - indication of success or failure */ function bootstrap($wwwroot, $pubkey, $application) { global $DB; if (substr($wwwroot, -1, 1) == '/') { $wwwroot = substr($wwwroot, 0, -1); } // If a peer record already exists for this address, // load that info and return if ($this->set_wwwroot($wwwroot)) { return true; } $hostname = mnet_get_hostname_from_uri($wwwroot); // Get the IP address for that host - if this fails, it will return the hostname string $ip_address = gethostbyname($hostname); // Couldn't find the IP address? if ($ip_address === $hostname && !preg_match('/^\d+\.\d+\.\d+.\d+$/',$hostname)) { throw new moodle_exception('noaddressforhost', 'mnet', '', $hostname); } $this->name = $wwwroot; // TODO: In reality, this will be prohibitively slow... need another // default - maybe blank string $homepage = download_file_content($wwwroot); if (!empty($homepage)) { $count = preg_match("@(.*)@siU", $homepage, $matches); if ($count > 0) { $this->name = $matches[1]; } } $this->wwwroot = $wwwroot; $this->ip_address = $ip_address; $this->deleted = 0; $this->application = $DB->get_record('mnet_application', array('name'=>$application)); if (empty($this->application)) { $this->application = $DB->get_record('mnet_application', array('name'=>'moodle')); } $this->applicationid = $this->application->id; if(empty($pubkey)) { $this->public_key = clean_param(mnet_get_public_key($this->wwwroot, $this->application), PARAM_PEM); } else { $this->public_key = clean_param($pubkey, PARAM_PEM); } $this->public_key_expires = $this->check_common_name($this->public_key); $this->last_connect_time = 0; $this->last_log_id = 0; if ($this->public_key_expires == false) { $this->public_key == ''; return false; } $this->bootstrapped = true; return true; } /* * Delete mnet peer * the peer is marked as deleted in the database * we delete current sessions. * @return bool - success */ function delete() { global $DB; if ($this->deleted) { return true; } $this->delete_all_sessions(); $this->deleted = 1; return $this->commit(); } function count_live_sessions() { global $DB; $obj = $this->delete_expired_sessions(); return $DB->count_records('mnet_session', array('mnethostid'=>$this->id)); } function delete_expired_sessions() { global $DB; $now = time(); return $DB->delete_records_select('mnet_session', " mnethostid = ? AND expires < ? ", array($this->id, $now)); } function delete_all_sessions() { global $CFG, $DB; // TODO: Expires each PHP session individually $sessions = $DB->get_records('mnet_session', array('mnethostid'=>$this->id)); if (count($sessions) > 0 && file_exists($CFG->dirroot.'/auth/mnet/auth.php')) { require_once($CFG->dirroot.'/auth/mnet/auth.php'); $auth = new auth_plugin_mnet(); $auth->end_local_sessions($sessions); } $deletereturn = $DB->delete_records('mnet_session', array('mnethostid'=>$this->id)); return true; } function check_common_name($key) { $credentials = $this->check_credentials($key); return $credentials['validTo_time_t']; } function check_credentials($key) { $credentials = openssl_x509_parse($key); if ($credentials == false) { $this->error[] = array('code' => 3, 'text' => get_string("nonmatchingcert", 'mnet', array('subject' => '','host' => ''))); return false; } elseif (array_key_exists('subjectAltName', $credentials['subject']) && $credentials['subject']['subjectAltName'] != $this->wwwroot) { $a['subject'] = $credentials['subject']['subjectAltName']; $a['host'] = $this->wwwroot; $this->error[] = array('code' => 5, 'text' => get_string("nonmatchingcert", 'mnet', $a)); return false; } else if ($credentials['subject']['CN'] !== substr($this->wwwroot, 0, 64)) { $a['subject'] = $credentials['subject']['CN']; $a['host'] = $this->wwwroot; $this->error[] = array('code' => 4, 'text' => get_string("nonmatchingcert", 'mnet', $a)); return false; } else { if (array_key_exists('subjectAltName', $credentials['subject'])) { $credentials['wwwroot'] = $credentials['subject']['subjectAltName']; } else { $credentials['wwwroot'] = $credentials['subject']['CN']; } return $credentials; } } function commit() { global $DB; $obj = new stdClass(); $obj->wwwroot = $this->wwwroot; $obj->ip_address = $this->ip_address; $obj->name = $this->name; $obj->public_key = $this->public_key; $obj->public_key_expires = $this->public_key_expires; $obj->deleted = $this->deleted; $obj->last_connect_time = $this->last_connect_time; $obj->last_log_id = $this->last_log_id; $obj->force_theme = $this->force_theme; $obj->theme = $this->theme; $obj->applicationid = $this->applicationid; $obj->sslverification = $this->sslverification; if (isset($this->id) && $this->id > 0) { $obj->id = $this->id; return $DB->update_record('mnet_host', $obj); } else { $this->id = $DB->insert_record('mnet_host', $obj); return $this->id > 0; } } function touch() { $this->last_connect_time = time(); $this->commit(); } function set_name($newname) { if (is_string($newname) && strlen($newname <= 80)) { $this->name = $newname; return true; } return false; } function set_applicationid($applicationid) { if (is_numeric($applicationid) && $applicationid == intval($applicationid)) { $this->applicationid = $applicationid; return true; } return false; } /** * Load information from db about an mnet peer into this object's properties * * @param string $wwwroot - address of peer whose details we want to load * @return bool - indication of success or failure */ function set_wwwroot($wwwroot) { global $CFG, $DB; $hostinfo = $DB->get_record('mnet_host', array('wwwroot'=>$wwwroot)); if ($hostinfo != false) { $this->populate($hostinfo); return true; } return false; } function set_id($id) { global $CFG, $DB; if (clean_param($id, PARAM_INT) != $id) { $this->error[] = ['code' => 1, 'text' => 'Your id ('.$id.') is not legal']; return false; } $sql = " SELECT h.* FROM {mnet_host} h WHERE h.id = ?"; if ($hostinfo = $DB->get_record_sql($sql, array($id))) { $this->populate($hostinfo); return true; } return false; } /** * Several methods can be used to get an 'mnet_host' record. They all then * send it to this private method to populate this object's attributes. * * @param object $hostinfo A database record from the mnet_host table * @return void */ function populate($hostinfo) { global $DB; $this->id = $hostinfo->id; $this->wwwroot = $hostinfo->wwwroot; $this->ip_address = $hostinfo->ip_address; $this->name = $hostinfo->name; $this->deleted = $hostinfo->deleted; $this->public_key = $hostinfo->public_key; $this->public_key_expires = $hostinfo->public_key_expires; $this->last_connect_time = $hostinfo->last_connect_time; $this->last_log_id = $hostinfo->last_log_id; $this->force_theme = $hostinfo->force_theme; $this->theme = $hostinfo->theme; $this->applicationid = $hostinfo->applicationid; $this->sslverification = $hostinfo->sslverification; $this->application = $DB->get_record('mnet_application', array('id'=>$this->applicationid)); $this->bootstrapped = true; } /** * @deprecated since Moodle 4.3 */ #[\core\attribute\deprecated(null, since: '4.3', mdl: 'MDL-77341', final: true)] public function get_public_key(): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } } publickey.php000064400000001005152266401670007251 0ustar00dirroot.'/mnet/lib.php'; if ($CFG->mnet_dispatcher_mode === 'off') { throw new \moodle_exception('mnetdisabled', 'mnet'); } header("Content-type: text/plain; charset=utf-8"); $keypair = mnet_get_keypair(); echo $keypair['certificate']; UPGRADING.md000064400000000301152266401670006411 0ustar00# core_mnet (subsystem) Upgrade notes ## 5.0 ### Removed - Remove deprecated mnet_peer::get_public_key() For more information see [MDL-78304](https://tracker.moodle.org/browse/MDL-78304) xmlrpc/server.php000064400000010177152266401670010107 0ustar00dirroot.'/mnet/lib.php'; require_once $CFG->dirroot.'/mnet/remote_client.php'; require_once $CFG->dirroot.'/mnet/xmlrpc/serverlib.php'; if ($CFG->mnet_dispatcher_mode === 'off') { throw new \moodle_exception('mnetdisabled', 'mnet'); } // Content type for output is not html: header('Content-type: text/xml; charset=utf-8'); $rawpostdata = file_get_contents("php://input"); mnet_debug("RAW POST DATA", 2); mnet_debug($rawpostdata, 2); if (!isset($_SERVER)) { exit(mnet_server_fault(712, get_string('phperror', 'mnet'))); } // New global variable which ONLY gets set in this server page, so you know that // if you've been called by a remote Moodle, this should be set: $remoteclient = new mnet_remote_client(); set_mnet_remote_client($remoteclient); try { $plaintextmessage = mnet_server_strip_encryption($rawpostdata); $xmlrpcrequest = mnet_server_strip_signature($plaintextmessage); } catch (Exception $e) { mnet_debug('encryption strip exception thrown: ' . $e->getMessage()); exit(mnet_server_fault($e->getCode(), $e->getMessage(), $e->a)); } mnet_debug('XMLRPC Payload', 2); mnet_debug($xmlrpcrequest, 2); if($remoteclient->pushkey == true) { // The peer used one of our older public keys, we will return a // signed/encrypted error message containing our new public key // Sign message with our old key, and encrypt to the peer's private key. mnet_debug('sending back new key'); exit(mnet_server_fault_xml(7025, $mnet->public_key, $remoteclient->useprivatekey)); } // Have a peek at what the request would be if we were to process it $encoder = new \PhpXmlRpc\Encoder(); $orequest = $encoder->decodeXML($xmlrpcrequest); // First, to internal. $method = $orequest->method(); // We just need the method. mnet_debug("incoming mnet request $method"); // One of three conditions need to be met before we continue processing this request: // 1. Request is properly encrypted and signed // 2. Request is for a keyswap (we don't mind enencrypted or unsigned requests for a public key) // 3. Request is properly signed and we're happy with it being unencrypted if ((($remoteclient->request_was_encrypted == true) && ($remoteclient->signatureok == true)) || (($method == 'system.keyswap') || ($method == 'system/keyswap')) || (($remoteclient->signatureok == true) && ($remoteclient->plaintext_is_ok() == true))) { try { // main dispatch call. will echo the response directly mnet_server_dispatch($xmlrpcrequest); mnet_debug('exiting cleanly'); exit; } catch (Exception $e) { mnet_debug('dispatch exception thrown: ' . $e->getMessage()); exit(mnet_server_fault($e->getCode(), $e->getMessage(), $e->a)); } } // if we get to here, something is wrong // so detect a few common cases and send appropriate errors if (($remoteclient->request_was_encrypted == false) && ($remoteclient->plaintext_is_ok() == false)) { mnet_debug('non encrypted request'); exit(mnet_server_fault(7021, get_string('forbidden-transport', 'mnet'))); } if ($remoteclient->request_was_signed == false) { // Request was not signed mnet_debug('non signed request'); exit(mnet_server_fault(711, get_string('verifysignature-error', 'mnet'))); } if ($remoteclient->signatureok == false) { // We were unable to verify the signature mnet_debug('non verified signature'); exit(mnet_server_fault(710, get_string('verifysignature-invalid', 'mnet'))); } mnet_debug('unknown error'); exit(mnet_server_fault(7000, get_string('unknownerror', 'mnet'))); xmlrpc/serverlib.php000064400000065165152266401670010605 0ustar00parse($rawpostdata); $mnet = get_mnet_environment(); if (!$crypt_parser->payload_encrypted) { return $rawpostdata; } // Make sure we know who we're talking to $host_record_exists = $remoteclient->set_wwwroot($crypt_parser->remote_wwwroot); if (false == $host_record_exists) { throw new mnet_server_exception(7020, 'wrong-wwwroot', $crypt_parser->remote_wwwroot); } // This key is symmetric, and is itself encrypted. Can be decrypted using our private key $key = array_pop($crypt_parser->cipher); // This data is symmetrically encrypted, can be decrypted using the above key $data = array_pop($crypt_parser->cipher); $crypt_parser->free_resource(); $payload = ''; // Initialize payload var // &$payload $isOpen = openssl_open(base64_decode($data), $payload, base64_decode($key), $mnet->get_private_key(), 'RC4'); if ($isOpen) { $remoteclient->was_encrypted(); return $payload; } // Decryption failed... let's try our archived keys $openssl_history = get_config('mnet', 'openssl_history'); if(empty($openssl_history)) { $openssl_history = array(); set_config('openssl_history', serialize($openssl_history), 'mnet'); } else { $openssl_history = unserialize($openssl_history); } foreach($openssl_history as $keyset) { $keyresource = openssl_pkey_get_private($keyset['keypair_PEM']); $isOpen = openssl_open(base64_decode($data), $payload, base64_decode($key), $keyresource, 'RC4'); if ($isOpen) { // It's an older code, sir, but it checks out $remoteclient->was_encrypted(); $remoteclient->encrypted_to($keyresource); $remoteclient->set_pushkey(); return $payload; } } //If after all that we still couldn't decrypt the message, error out. throw new mnet_server_exception(7023, 'encryption-invalid'); } /* Strip signature envelope (if present), try to verify any signature using our record of remote peer's public key. * * @param string $plaintextmessage XML envelope containing XMLRPC request and signature * * @return string XMLRPC request */ function mnet_server_strip_signature($plaintextmessage) { $remoteclient = get_mnet_remote_client(); $sig_parser = new mnet_encxml_parser(); $sig_parser->parse($plaintextmessage); if ($sig_parser->signature == '') { return $plaintextmessage; } // Record that the request was signed in some way $remoteclient->was_signed(); // Load any information we have about this mnet peer $remoteclient->set_wwwroot($sig_parser->remote_wwwroot); $payload = base64_decode($sig_parser->data_object); $signature = base64_decode($sig_parser->signature); $certificate = $remoteclient->public_key; // If we don't have any certificate for the host, don't try to check the signature // Just return the parsed request if ($certificate == false) { return $payload; } // Does the signature match the data and the public cert? $signature_verified = openssl_verify($payload, $signature, $certificate); if ($signature_verified == 0) { // $signature was not generated for $payload using $certificate // Get the key the remote peer is currently publishing: $currkey = mnet_get_public_key($remoteclient->wwwroot, $remoteclient->application); // If the key the remote peer is currently publishing is different to $certificate if($currkey != $certificate) { // if pushkey is already set, it means the request was encrypted to an old key // in mnet_server_strip_encryption. // if we call refresh_key() here before pushing out our new key, // and the other site ALSO has a new key, // we'll get into an infinite keyswap loop // so push just bail here, and push out the new key. // the next request will get through to refresh_key if ($remoteclient->pushkey) { return false; } // Try and get the server's new key through trusted means $remoteclient->refresh_key(); // If we did manage to re-key, try to verify the signature again using the new public key. $certificate = $remoteclient->public_key; $signature_verified = openssl_verify($payload, $signature, $certificate); } } if ($signature_verified == 1) { $remoteclient->signature_verified(); $remoteclient->touch(); } $sig_parser->free_resource(); return $payload; } /** * Return the proper XML-RPC content to report an error in the local language. * * @param int $code The ID code of the error message * @param string $text The full string of the error message (get_string will not be called) * @param string $param The $a param for the error message in the lang file * @return string $text The text of the error message */ function mnet_server_fault($code, $text, $param = null) { if (!is_numeric($code)) { $code = 0; } $code = intval($code); return mnet_server_fault_xml($code, $text); } /** * Return the proper XML-RPC content to report an error. * * @param int $code The ID code of the error message * @param string $text The error message * @param resource $privatekey The private key that should be used to sign the response * @return string $text The XML text of the error message */ function mnet_server_fault_xml($code, $text, $privatekey = null) { global $CFG; // Replace illegal XML chars - is this already in a lib somewhere? $text = str_replace(array('<','>','&','"',"'"), array('<','>','&','"','''), $text); $return = mnet_server_prepare_response(' faultCode '.$code.' faultString '.$text.' ', $privatekey); if ($code != 7025) { // new key responses mnet_debug("XMLRPC Error Response $code: $text"); //mnet_debug($return); } return $return; } /** * Package a response in any required envelope, and return it to the client * * @param string $response The XMLRPC response string * @param resource $privatekey The private key to sign the response with * @return string The encoded response string */ function mnet_server_prepare_response($response, $privatekey = null) { $remoteclient = get_mnet_remote_client(); if ($remoteclient->request_was_signed) { $response = mnet_sign_message($response, $privatekey); } if ($remoteclient->request_was_encrypted) { $response = mnet_encrypt_message($response, $remoteclient->public_key); } return $response; } /** * If security checks are passed, dispatch the request to the function/method * * The config variable 'mnet_dispatcher_mode' can be: * strict: Only execute functions that are in specific files * off: The default - don't execute anything * * @param string $payload The XML-RPC request * * @throws mnet_server_exception * * @return No return val - just echo the response */ function mnet_server_dispatch($payload) { global $CFG, $DB; $remoteclient = get_mnet_remote_client(); // Decode the request to method + params. $method = null; $params = null; $encoder = new \PhpXmlRpc\Encoder(); $orequest = $encoder->decodeXML($payload); // First, to internal PhpXmlRpc\Response structure. if ($orequest && $orequest instanceof \PhpXmlRpc\Request) { $method = $orequest->method(); $numparams = $orequest->getNumParams(); for ($i = 0; $i < $numparams; $i++) { $params[] = $encoder->decode($orequest->getParam($i)); } } // $method is something like: "mod/forum/lib.php/forum_add_instance" // $params is an array of parameters. A parameter might itself be an array. // Check that the method name consists of allowed characters only. // The method name must not begin with a / - avoid absolute paths // A dot character . is only allowed in the filename, i.e. something.php if (0 == preg_match("@^[A-Za-z0-9]+/[A-Za-z0-9/_\.-]+(\.php/)?[A-Za-z0-9_-]+$@",$method)) { throw new mnet_server_exception(713, 'nosuchfunction'); } if(preg_match("/^system\./", $method)) { $callstack = explode('.', $method); } else { $callstack = explode('/', $method); // callstack will look like array('mod', 'forum', 'lib.php', 'forum_add_instance'); } /** * What has the site administrator chosen as his dispatcher setting? * strict: Only execute functions that are in specific files * off: The default - don't execute anything */ ////////////////////////////////////// OFF if (!isset($CFG->mnet_dispatcher_mode) ) { set_config('mnet_dispatcher_mode', 'off'); throw new mnet_server_exception(704, 'nosuchservice'); } elseif ('off' == $CFG->mnet_dispatcher_mode) { throw new mnet_server_exception(704, 'nosuchservice'); ////////////////////////////////////// SYSTEM METHODS } elseif ($callstack[0] == 'system') { $functionname = $callstack[1]; $xmlrpcserver = new \PhpXmlRpc\Server(); $xmlrpcserver->functions_parameters_type = 'epivals'; $xmlrpcserver->compress_response = false; // register all the system methods $systemmethods = array('listMethods', 'methodSignature', 'methodHelp', 'listServices', 'listFiles', 'retrieveFile', 'keyswap'); foreach ($systemmethods as $m) { // I'm adding the canonical xmlrpc references here, however we've // already forbidden that the period (.) should be allowed in the call // stack, so if someone tries to access our XMLRPC in the normal way, // they'll already have received a RPC server fault message. // Maybe we should allow an easement so that regular XMLRPC clients can // call our system methods, and find out what we have to offer? $handler = 'mnet_system'; if ($m == 'keyswap') { $handler = 'mnet_keyswap'; } if ($method == 'system.' . $m || $method == 'system/' . $m) { $xmlrpcserver->add_to_map($method, $handler); $xmlrpcserver->user_data = $remoteclient; $response = $xmlrpcserver->service($payload, true); $response = mnet_server_prepare_response($response); echo $response; return; } } throw new mnet_server_exception(7018, 'nosuchfunction'); //////////////////////////////////// NORMAL PLUGIN DISPATCHER } else { // anything else comes from some sort of plugin if ($rpcrecord = $DB->get_record('mnet_rpc', array('xmlrpcpath' => $method))) { $response = mnet_server_invoke_plugin_method($method, $callstack, $rpcrecord, $payload); $response = mnet_server_prepare_response($response); echo $response; return; // if the rpc record isn't found, check to see if dangerous mode is on ////////////////////////////////////// DANGEROUS } else if ('dangerous' == $CFG->mnet_dispatcher_mode && $remoteclient->plaintext_is_ok()) { $functionname = array_pop($callstack); $filename = clean_param(implode('/',$callstack), PARAM_PATH); if (0 == preg_match("/php$/", $filename)) { // Filename doesn't end in 'php'; possible attack? // Generate error response - unable to locate function throw new mnet_server_exception(7012, 'nosuchfunction'); } // The call stack holds the path to any include file $includefile = $CFG->dirroot.'/'.$filename; $response = mnet_server_invoke_dangerous_method($includefile, $functionname, $method, $payload); echo $response; return; } } throw new mnet_server_exception(7012, 'nosuchfunction'); } /** * Execute the system functions - mostly for introspection * * @param string $method XMLRPC method name, e.g. system.listMethods * @param array $params Array of parameters from the XMLRPC request * @param string $hostinfo Hostinfo object from the mnet_host table * * @throws mnet_server_exception * * @return mixed Response data - any kind of PHP variable */ function mnet_system($method, $params, $hostinfo) { global $CFG, $DB; if (empty($hostinfo)) return array(); $id_list = $hostinfo->id; if (!empty($CFG->mnet_all_hosts_id)) { $id_list .= ', '.$CFG->mnet_all_hosts_id; } if ('system.listMethods' == $method || 'system/listMethods' == $method) { $query = ' SELECT DISTINCT rpc.functionname, rpc.xmlrpcpath FROM {mnet_host2service} h2s JOIN {mnet_service2rpc} s2r ON h2s.serviceid = s2r.serviceid JOIN {mnet_rpc} rpc ON s2r.rpcid = rpc.id JOIN {mnet_service} svc ON svc.id = s2r.serviceid WHERE h2s.hostid in ('.$id_list .') AND h2s.publish = 1 AND rpc.enabled = 1 ' . ((count($params) > 0) ? 'AND svc.name = ? ' : '') . ' ORDER BY rpc.xmlrpcpath ASC'; if (count($params) > 0) { $params = array($params[0]); } $methods = array(); foreach ($DB->get_records_sql($query, $params) as $result) { $methods[] = $result->xmlrpcpath; } return $methods; } elseif (in_array($method, array('system.methodSignature', 'system/methodSignature', 'system.methodHelp', 'system/methodHelp'))) { $query = ' SELECT DISTINCT rpc.functionname, rpc.help, rpc.profile FROM {mnet_host2service} h2s, {mnet_service2rpc} s2r, {mnet_rpc} rpc WHERE rpc.xmlrpcpath = ? AND s2r.rpcid = rpc.id AND h2s.publish = 1 AND rpc.enabled = 1 AND h2s.serviceid = s2r.serviceid AND h2s.hostid in ('.$id_list .')'; $params = array($params[0]); if (!$result = $DB->get_record_sql($query, $params)) { return false; } if (strpos($method, 'methodSignature') !== false) { return unserialize($result->profile); } return $result->help; } elseif ('system.listServices' == $method || 'system/listServices' == $method) { $query = ' SELECT DISTINCT s.id, s.name, s.apiversion, h2s.publish, h2s.subscribe FROM {mnet_host2service} h2s, {mnet_service} s WHERE h2s.serviceid = s.id AND (h2s.publish = 1 OR h2s.subscribe = 1) AND h2s.hostid in ('.$id_list .') ORDER BY s.name ASC'; $params = array(); $result = $DB->get_records_sql($query, $params); $services = array(); if (is_array($result)) { foreach($result as $service) { $services[] = array('name' => $service->name, 'apiversion' => $service->apiversion, 'publish' => $service->publish, 'subscribe' => $service->subscribe); } } return $services; } throw new mnet_server_exception(7019, 'nosuchfunction'); } /** * Invoke a normal style plugin method * This will verify permissions first. * * @param string $method the full xmlrpc method that was called eg auth/mnet/auth.php/user_authorise * @param array $callstack the exploded callstack * @param stdclass $rpcrecord the record from mnet_rpc * * @return mixed the response from the invoked method */ function mnet_server_invoke_plugin_method($method, $callstack, $rpcrecord, $payload) { mnet_verify_permissions($rpcrecord); // will throw exceptions mnet_setup_dummy_method($method, $callstack, $rpcrecord); $methodname = array_pop($callstack); $xmlrpcserver = new \PhpXmlRpc\Server(); $xmlrpcserver->functions_parameters_type = 'epivals'; $xmlrpcserver->compress_response = false; $xmlrpcserver->add_to_map($method, 'mnet_server_dummy_method'); $xmlrpcserver->user_data = $methodname; $response = $xmlrpcserver->service($payload, true); return $response; } /** * Initialize the object (if necessary), execute the method or function, and * return the response * * @param string $includefile The file that contains the object definition * @param string $methodname The name of the method to execute * @param string $method The full path to the method * @param string $payload The XML-RPC request payload * @param string $class The name of the class to instantiate (or false) * * @throws mnet_server_exception * * @return string The XML-RPC response */ function mnet_server_invoke_dangerous_method($includefile, $methodname, $method, $payload) { if (file_exists($CFG->dirroot . $includefile)) { require_once $CFG->dirroot . $includefile; // $callprefix matches the rpc convention // of not having a leading slash $callprefix = preg_replace('!^/!', '', $includefile); } else { throw new mnet_server_exception(705, "nosuchfile"); } if ($functionname != clean_param($functionname, PARAM_PATH)) { throw new mnet_server_exception(7012, "nosuchfunction"); } if (!function_exists($functionname)) { throw new mnet_server_exception(7012, "nosuchfunction"); } $xmlrpcserver = new \PhpXmlRpc\Server(); $xmlrpcserver->functions_parameters_type = 'epivals'; $xmlrpcserver->compress_response = false; $xmlrpcserver->add_to_map($method, 'mnet_server_dummy_method'); $xmlrpcserver->user_data = $methodname; $response = $xmlrpcserver->service($payload, true); return $response; } /** * Accepts a public key from a new remote host and returns the public key for * this host. If 'register all hosts' is turned on, it will bootstrap a record * for the remote host in the mnet_host table (if it's not already there) * * @param string $function XML-RPC requires this but we don't... discard! * @param array $params Array of parameters * $params[0] is the remote wwwroot * $params[1] is the remote public key * @return string The XML-RPC response */ function mnet_keyswap($function, $params) { global $CFG; $return = array(); $mnet = get_mnet_environment(); if (!empty($CFG->mnet_register_allhosts)) { $mnet_peer = new mnet_peer(); list($wwwroot, $pubkey, $application) = $params; $keyok = $mnet_peer->bootstrap($wwwroot, $pubkey, $application); if ($keyok) { $mnet_peer->commit(); } } return $mnet->public_key; } /** * Verify that the requested xmlrpc method can be called * This just checks the method exists in the rpc table and is enabled. * * @param stdclass $rpcrecord the record from mnet_rpc * * @throws mnet_server_exception */ function mnet_verify_permissions($rpcrecord) { global $CFG, $DB; $remoteclient = get_mnet_remote_client(); $id_list = $remoteclient->id; if (!empty($CFG->mnet_all_hosts_id)) { $id_list .= ', '.$CFG->mnet_all_hosts_id; } // TODO: Change this to avoid the first column duplicate debugging, keeping current behaviour 100%. $sql = "SELECT r.*, h2s.publish FROM {mnet_rpc} r JOIN {mnet_service2rpc} s2r ON s2r.rpcid = r.id LEFT JOIN {mnet_host2service} h2s ON h2s.serviceid = s2r.serviceid WHERE r.id = ? AND h2s.hostid in ($id_list)"; $params = array($rpcrecord->id); if (!$permission = $DB->get_record_sql($sql, $params)) { throw new mnet_server_exception(7012, "nosuchfunction"); } else if (!$permission->publish || !$permission->enabled) { throw new mnet_server_exception(707, "nosuchfunction"); } } /** * Figure out exactly what needs to be called and stashes it in $remoteclient * Does some further verification that the method is callable * * @param string $method the full xmlrpc method that was called eg auth/mnet/auth.php/user_authorise * @param array $callstack the exploded callstack * @param stdclass $rpcrecord the record from mnet_rpc * * @throws mnet_server_exception */ function mnet_setup_dummy_method($method, $callstack, $rpcrecord) { global $CFG; $remoteclient = get_mnet_remote_client(); // verify that the callpath in the stack matches our records // callstack will look like array('mod', 'forum', 'lib.php', 'forum_add_instance'); $path = core_component::get_plugin_directory($rpcrecord->plugintype, $rpcrecord->pluginname); $path = substr($path, strlen($CFG->dirroot)+1); // this is a bit hacky and fragile, it is not guaranteed that plugins are in dirroot array_pop($callstack); $providedpath = implode('/', $callstack); if ($providedpath != $path . '/' . $rpcrecord->filename) { throw new mnet_server_exception(705, "nosuchfile"); } if (!file_exists($CFG->dirroot . '/' . $providedpath)) { throw new mnet_server_exception(705, "nosuchfile"); } require_once($CFG->dirroot . '/' . $providedpath); if (!empty($rpcrecord->classname)) { if (!class_exists($rpcrecord->classname)) { throw new mnet_server_exception(708, 'nosuchclass'); } if (!$rpcrecord->static) { try { $object = new $rpcrecord->classname; } catch (Exception $e) { throw new mnet_server_exception(709, "classerror"); } if (!is_callable(array($object, $rpcrecord->functionname))) { throw new mnet_server_exception(706, "nosuchfunction"); } $remoteclient->object_to_call($object); } else { if (!is_callable(array($rpcrecord->classname, $rpcrecord->functionname))) { throw new mnet_server_exception(706, "nosuchfunction"); } $remoteclient->static_location($rpcrecord->classname); } } } /** * Dummy function for the XML-RPC dispatcher - use to call a method on an object * or to call a function * * Translate XML-RPC's strange function call syntax into a more straightforward * PHP-friendly alternative. This dummy function will be called by the * dispatcher, and can be used to call a method on an object, or just a function * * The methodName argument (eg. mnet/testlib/mnet_concatenate_strings) * is ignored. * * @throws mnet_server_exception * * @param string $methodname We discard this - see 'functionname' * @param array $argsarray Each element is an argument to the real * function * @param string $functionname The name of the PHP function you want to call * @return mixed The return value will be that of the real * function, whatever it may be. */ function mnet_server_dummy_method($methodname, $argsarray, $functionname) { $remoteclient = get_mnet_remote_client(); try { if (is_object($remoteclient->object_to_call)) { return @call_user_func_array(array($remoteclient->object_to_call,$functionname), $argsarray); } else if (!empty($remoteclient->static_location)) { return @call_user_func_array(array($remoteclient->static_location, $functionname), $argsarray); } else { return @call_user_func_array($functionname, $argsarray); } } catch (Exception $e) { exit(mnet_server_fault($e->getCode(), $e->getMessage())); } } /** * mnet server exception. extends moodle_exception, but takes slightly different arguments. * and unlike the rest of moodle, the actual int error code is used. * this exception should only be used during an xmlrpc server request, ie, not for client requests. */ class mnet_server_exception extends moodle_exception { /** * @param int $intcode the numerical error associated with this fault. this is not the string errorcode * @param string $langkey the error message in full (get_string will not be used) * @param string $module the language module, defaults to 'mnet' * @param mixed $a params for get_string */ public function __construct($intcode, $languagekey, $module='mnet', $a=null) { parent::__construct($languagekey, $module, '', $a); $this->code = $intcode; } } xmlrpc/client.php000064400000041372152266401670010060 0ustar00dirroot.'/mnet/lib.php'; /** * Class representing an XMLRPC request against a remote machine */ class mnet_xmlrpc_client { var $method = ''; var $params = array(); var $timeout = 60; var $error = array(); var $response = ''; var $mnet = null; /** * Constructor */ public function __construct() { // make sure we've got this set up before we try and do anything else $this->mnet = get_mnet_environment(); } /** * Allow users to override the default timeout * @param int $timeout Request timeout in seconds * $return bool True if param is an integer or integer string */ public function set_timeout($timeout) { if (!is_integer($timeout)) { if (is_numeric($timeout)) { $this->timeout = (integer)$timeout; return true; } return false; } $this->timeout = $timeout; return true; } /** * Set the path to the method or function we want to execute on the remote * machine. Examples: * mod/scorm/functionname * auth/mnet/methodname * In the case of auth and enrolment plugins, an object will be created and * the method on that object will be called */ public function set_method($xmlrpcpath) { if (is_string($xmlrpcpath)) { $this->method = $xmlrpcpath; $this->params = array(); return true; } $this->method = ''; $this->params = array(); return false; } /** * Add a parameter to the array of parameters. * * @param string $argument A transport ID, as defined in lib.php * @param string $type The argument type, can be one of: * i4 * i8 * int * double * string * boolean * datetime | dateTime.iso8601 * base64 * null * array * struct * @return bool True on success */ public function add_param($argument, $type = 'string') { // Convert any use of the old 'datetime' to the correct 'dateTime.iso8601' one. $type = ($type === 'datetime' ? 'dateTime.iso8601' : $type); // BC fix, if some argument is array and comes as string, change type to array (sequentials) // or struct (associative). // This is the behavior of the encode_request() method from the xmlrpc extension. // Note that uses in core have been fixed, but there may be others using that. if (is_array($argument) && $type === 'string') { if (array_keys($argument) === range(0, count($argument) - 1)) { $type = 'array'; } else { $type = 'struct'; } mnet_debug('Incorrect ' . $type . ' param passed as string in mnet_xmlrpc_client->add_param(): ' . json_encode($argument)); } if (!isset(\PhpXmlRpc\Value::$xmlrpcTypes[$type])) { // Arrived here, still erong type? Let's stop. return false; } // If we are array or struct, we need to ensure that, recursively, all the elements are proper values. // or serialize, used later on send() won't work with them. Encoder::encode() provides us with that. if ($type === 'array' || $type === 'struct') { $encoder = new \PhpXmlRpc\Encoder(); $this->params[] = $encoder->encode($argument); } else { // Normal scalar case. $this->params[] = new \PhpXmlRpc\Value($argument, $type); } return true; } /** * Send the request to the server - decode and return the response * * @param object $mnet_peer A mnet_peer object with details of the * remote host we're connecting to * @param bool $rekey The rekey attribute stops us from * getting into a loop. * @return mixed A PHP variable, as returned by the */ public function send($mnet_peer, bool $rekey = false) { global $CFG, $DB; if (!$this->permission_to_call($mnet_peer)) { mnet_debug("tried and wasn't allowed to call a method on $mnet_peer->wwwroot"); return false; } $request = new \PhpXmlRpc\Request($this->method, $this->params); $requesttext = $request->serialize('utf-8'); $signedrequest = mnet_sign_message($requesttext); $encryptedrequest = mnet_encrypt_message($signedrequest, $mnet_peer->public_key); $client = $this->prepare_http_request($mnet_peer); $timestamp_send = time(); mnet_debug("about to send the xmlrpc request"); $response = $client->send($encryptedrequest, $this->timeout); mnet_debug("managed to complete a xmlrpc request"); $timestamp_receive = time(); if ($response->faultCode()) { $this->error[] = $response->faultCode() .':'. $response->faultString(); return false; } $rawresponse = trim($response->value()); // Because MNet responses ARE NOT valid xmlrpc, don't try any PhpXmlRpc facility. $mnet_peer->touch(); $crypt_parser = new mnet_encxml_parser(); $crypt_parser->parse($rawresponse); // If we couldn't parse the message, or it doesn't seem to have encrypted contents, // give the most specific error msg available & return if (!$crypt_parser->payload_encrypted) { if (! empty($crypt_parser->remoteerror)) { $this->error[] = '4: remote server error: ' . $crypt_parser->remoteerror; } else if (! empty($crypt_parser->error)) { $crypt_parser_error = $crypt_parser->error[0]; $message = '3:XML Parse error in payload: '.$crypt_parser_error['string']."\n"; if (array_key_exists('lineno', $crypt_parser_error)) { $message .= 'At line number: '.$crypt_parser_error['lineno']."\n"; } if (array_key_exists('line', $crypt_parser_error)) { $message .= 'Which reads: '.$crypt_parser_error['line']."\n"; } $this->error[] = $message; } else { $this->error[] = '1:Payload not encrypted '; } $crypt_parser->free_resource(); return false; } $key = array_pop($crypt_parser->cipher); $data = array_pop($crypt_parser->cipher); $crypt_parser->free_resource(); // Initialize payload var $decryptedenvelope = ''; // &$decryptedenvelope $isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), $this->mnet->get_private_key(), 'RC4'); if (!$isOpen) { // Decryption failed... let's try our archived keys $openssl_history = get_config('mnet', 'openssl_history'); if(empty($openssl_history)) { $openssl_history = array(); set_config('openssl_history', serialize($openssl_history), 'mnet'); } else { $openssl_history = unserialize($openssl_history); } foreach($openssl_history as $keyset) { $keyresource = openssl_pkey_get_private($keyset['keypair_PEM']); $isOpen = openssl_open(base64_decode($data), $decryptedenvelope, base64_decode($key), $keyresource, 'RC4'); if ($isOpen) { // It's an older code, sir, but it checks out break; } } } if (!$isOpen) { trigger_error("None of our keys could open the payload from host {$mnet_peer->wwwroot} with id {$mnet_peer->id}."); $this->error[] = '3:No key match'; return false; } if (strpos(substr($decryptedenvelope, 0, 100), '')) { $sig_parser = new mnet_encxml_parser(); $sig_parser->parse($decryptedenvelope); } else { $this->error[] = '2:Payload not signed: ' . $decryptedenvelope; return false; } // Margin of error is the time it took the request to complete. $margin_of_error = $timestamp_receive - $timestamp_send; // Guess the time gap between sending the request and the remote machine // executing the time() function. Marginally better than nothing. $hysteresis = ($margin_of_error) / 2; $remote_timestamp = $sig_parser->remote_timestamp - $hysteresis; $time_offset = $remote_timestamp - $timestamp_send; if ($time_offset > 0) { $threshold = get_config('mnet', 'drift_threshold'); if(empty($threshold)) { // We decided 15 seconds was a pretty good arbitrary threshold // for time-drift between servers, but you can customize this in // the config_plugins table. It's not advised though. set_config('drift_threshold', 15, 'mnet'); $threshold = 15; } if ($time_offset > $threshold) { $this->error[] = '6:Time gap with '.$mnet_peer->name.' ('.$time_offset.' seconds) is greater than the permitted maximum of '.$threshold.' seconds'; return false; } } $xmlrpcresponse = base64_decode($sig_parser->data_object); // Let's convert the xmlrpc back to PHP structure. $response = null; $encoder = new \PhpXmlRpc\Encoder(); $oresponse = $encoder->decodeXML($xmlrpcresponse); // First, to internal PhpXmlRpc\Response structure. if ($oresponse instanceof \PhpXmlRpc\Response) { // Special handling of fault responses (because value() doesn't handle them properly). if ($oresponse->faultCode()) { $response = ['faultCode' => $oresponse->faultCode(), 'faultString' => $oresponse->faultString()]; } else { $response = $encoder->decode($oresponse->value()); // Normal Response conversion to PHP. } } else { // Maybe this is just a param, let's convert it too. $response = $encoder->decode($oresponse); } $this->response = $response; // xmlrpc errors are pushed onto the $this->error stack if (is_array($this->response) && array_key_exists('faultCode', $this->response)) { // The faultCode 7025 means we tried to connect with an old SSL key // The faultString is the new key - let's save it and try again // The rekey attribute stops us from getting into a loop if($this->response['faultCode'] == 7025 && empty($rekey)) { mnet_debug('recieved an old-key fault, so trying to get the new key and update our records'); // If the new certificate doesn't come thru clean_param() unmolested, error out if($this->response['faultString'] != clean_param($this->response['faultString'], PARAM_PEM)) { $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; } $record = new stdClass(); $record->id = $mnet_peer->id; $record->public_key = $this->response['faultString']; $details = openssl_x509_parse($record->public_key); if(!isset($details['validTo_time_t'])) { $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; } $record->public_key_expires = $details['validTo_time_t']; $DB->update_record('mnet_host', $record); // Create a new peer object populated with the new info & try re-sending the request $rekeyed_mnet_peer = new mnet_peer(); $rekeyed_mnet_peer->set_id($record->id); return $this->send($rekeyed_mnet_peer, true); // Re-send mnet_peer with the new key. } if (!empty($CFG->mnet_rpcdebug)) { if (get_string_manager()->string_exists('error'.$this->response['faultCode'], 'mnet')) { $guidance = get_string('error'.$this->response['faultCode'], 'mnet'); } else { $guidance = ''; } } else { $guidance = ''; } $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString'] ."\n".$guidance; } // ok, it's signed, but is it signed with the right certificate ? // do this *after* we check for an out of date key $verified = openssl_verify($xmlrpcresponse, base64_decode($sig_parser->signature), $mnet_peer->public_key); if ($verified != 1) { $this->error[] = 'Invalid signature'; } return empty($this->error); } /** * Check that we are permitted to call method on specified peer * * @param object $mnet_peer A mnet_peer object with details of the remote host we're connecting to * @return bool True if we permit calls to method on specified peer, False otherwise. */ public function permission_to_call($mnet_peer) { global $DB, $CFG, $USER; // Executing any system method is permitted. $system_methods = array('system/listMethods', 'system/methodSignature', 'system/methodHelp', 'system/listServices'); if (in_array($this->method, $system_methods) ) { return true; } $hostids = array($mnet_peer->id); if (!empty($CFG->mnet_all_hosts_id)) { $hostids[] = $CFG->mnet_all_hosts_id; } // At this point, we don't care if the remote host implements the // method we're trying to call. We just want to know that: // 1. The method belongs to some service, as far as OUR host knows // 2. We are allowed to subscribe to that service on this mnet_peer list($hostidsql, $hostidparams) = $DB->get_in_or_equal($hostids); $sql = "SELECT r.id FROM {mnet_remote_rpc} r INNER JOIN {mnet_remote_service2rpc} s2r ON s2r.rpcid = r.id INNER JOIN {mnet_host2service} h2s ON h2s.serviceid = s2r.serviceid WHERE r.xmlrpcpath = ? AND h2s.subscribe = ? AND h2s.hostid $hostidsql"; $params = array($this->method, 1); $params = array_merge($params, $hostidparams); if ($DB->record_exists_sql($sql, $params)) { return true; } $this->error[] = '7:User with ID '. $USER->id . ' attempted to call unauthorised method '. $this->method.' on host '. $mnet_peer->wwwroot; return false; } /** * Generate a \PhpXmlRpc\Client handle and prepare it for sending to an mnet host * * @param object $mnet_peer A mnet_peer object with details of the remote host the request will be sent to * @return \PhpXmlRpc\Client handle - the almost-ready-to-send http request */ public function prepare_http_request($mnet_peer) { $uri = $mnet_peer->wwwroot . $mnet_peer->application->xmlrpc_server_url; // Instantiate the xmlrpc client to be used for the client request // and configure it the way we want. $client = new \PhpXmlRpc\Client($uri); $client->setUseCurl(\PhpXmlRpc\Client::USE_CURL_ALWAYS); $client->setUserAgent('Moodle'); $client->return_type = 'xml'; // Because MNet responses ARE NOT valid xmlrpc, don't try any validation. // TODO: Link this to DEBUG DEVELOPER or with MNET debugging... // $client->setdebug(1); // See a good number of complete requests and responses. $verifyhost = 0; $verifypeer = false; if ($mnet_peer->sslverification == mnet_peer::SSL_HOST_AND_PEER) { $verifyhost = 2; $verifypeer = true; } else if ($mnet_peer->sslverification == mnet_peer::SSL_HOST) { $verifyhost = 2; } $client->setSSLVerifyHost($verifyhost); $client->setSSLVerifyPeer($verifypeer); return $client; } } xmlrpc/xmlparser.php000064400000030102152266401670010604 0ustar00initialise(); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function mnet_encxml_parser() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Set default element handlers and initialise properties to empty. * * @return bool True */ function initialise() { $this->parser = xml_parser_create(); xml_set_element_handler($this->parser, [$this, "start_element"], [$this, "end_element"]); xml_set_character_data_handler($this->parser, [$this, "discard_data"]); $this->tag_number = 0; // Just a unique ID for each tag $this->digest = ''; $this->remote_timestamp = ''; $this->remote_wwwroot = ''; $this->signature = ''; $this->data_object = ''; $this->key_URI = ''; $this->payload_encrypted = false; $this->cipher = array(); $this->error = array(); $this->remoteerror = null; $this->errorstarted = false; return true; } /** * Parse a block of XML text * * The XML Text will be an XML-RPC request which is wrapped in an XML doc * with a signature from the sender. This envelope may be encrypted and * delivered within another XML envelope with a symmetric key. The parser * should first decrypt this XML, and then place the XML-RPC request into * the data_object property, and the signature into the signature property. * * See the W3C's {@link http://www.w3.org/TR/xmlenc-core/ XML Encryption Syntax and Processing} * and {@link http://www.w3.org/TR/2001/PR-xmldsig-core-20010820/ XML-Signature Syntax and Processing} * guidelines for more detail on the XML. * * -----XML-Envelope--------------------------------- * | | * | Symmetric-key-------------------------- | * | |_____________________________________| | * | | * | Encrypted data------------------------- | * | | | | * | | -XML-Envelope------------------ | | * | | | | | | * | | | --Signature------------- | | | * | | | |______________________| | | | * | | | | | | * | | | --Signed-Payload-------- | | | * | | | | | | | | * | | | | XML-RPC Request | | | | * | | | |______________________| | | | * | | | | | | * | | |_____________________________| | | * | |_____________________________________| | * | | * |________________________________________________| * * @param string $data The XML that you want to parse * @return bool True on success - false on failure */ function parse($data) { $p = xml_parse($this->parser, $data); if ($p == 0) { // Parse failed $errcode = xml_get_error_code($this->parser); $errstring = xml_error_string($errcode); $lineno = xml_get_current_line_number($this->parser); if ($lineno !== false) { $error = array('lineno' => $lineno); $lineno--; // Line numbering starts at 1. while ($lineno > 0) { $data = strstr($data, "\n"); $lineno--; } $data .= "\n"; // In case there's only one line (no newline) $line = substr($data, 0, strpos($data, "\n")); $error['code'] = $errcode; $error['string'] = $errstring; $error['line'] = $line; $this->error[] = $error; } else { $this->error[] = array('code' => $errcode, 'string' => $errstring); } } if (!empty($this->remoteerror)) { return false; } if (count($this->cipher) > 0) { $this->cipher = array_values($this->cipher); $this->payload_encrypted = true; } return (bool)$p; } /** * Destroy the parser and free up any related resource. */ function free_resource() { $free = xml_parser_free($this->parser); } /** * Set the character-data handler to the right function for each element * * For each tag (element) name, this function switches the character-data * handler to the function that handles that element. Note that character * data is referred to the handler in blocks of 1024 bytes. * * @param mixed $parser The XML parser * @param string $name The name of the tag, e.g. method_call * @param array $attrs The tag's attributes (if any exist). * @return bool True */ function start_element($parser, $name, $attrs) { $this->tag_number++; $handler = 'discard_data'; switch(strtoupper($name)) { case 'DIGESTVALUE': $handler = 'parse_digest'; break; case 'SIGNATUREVALUE': $handler = 'parse_signature'; break; case 'OBJECT': $handler = 'parse_object'; break; case 'RETRIEVALMETHOD': $this->key_URI = $attrs['URI']; break; case 'TIMESTAMP': $handler = 'parse_timestamp'; break; case 'WWWROOT': $handler = 'parse_wwwroot'; break; case 'CIPHERVALUE': $this->cipher[$this->tag_number] = ''; $handler = 'parse_cipher'; break; case 'FAULT': $handler = 'parse_fault'; default: break; } xml_set_character_data_handler($this->parser, [$this, $handler]); return true; } /** * Add the next chunk of character data to the remote_timestamp string * * @param mixed $parser The XML parser * @param string $data The content of the current tag (1024 byte chunk) * @return bool True */ function parse_timestamp($parser, $data) { $this->remote_timestamp .= $data; return true; } /** * Add the next chunk of character data to the cipher string for that tag * * The XML parser calls the character-data handler with 1024-character * chunks of data. This means that the handler may be called several times * for a single tag, so we use the concatenate operator (.) to build the * tag content into a string. * We should not encounter more than one of each tag type, except for the * cipher tag. We will often see two of those. We prevent the content of * these two tags being concatenated together by counting each tag, and * using its 'number' as the key to an array of ciphers. * * @param mixed $parser The XML parser * @param string $data The content of the current tag (1024 byte chunk) * @return bool True */ function parse_cipher($parser, $data) { $this->cipher[$this->tag_number] .= $data; return true; } /** * Add the next chunk of character data to the remote_wwwroot string * * @param mixed $parser The XML parser * @param string $data The content of the current tag (1024 byte chunk) * @return bool True */ function parse_wwwroot($parser, $data) { $this->remote_wwwroot .= $data; return true; } /** * Add the next chunk of character data to the digest string * * @param mixed $parser The XML parser * @param string $data The content of the current tag (1024 byte chunk) * @return bool True */ function parse_digest($parser, $data) { $this->digest .= $data; return true; } /** * Add the next chunk of character data to the signature string * * @param mixed $parser The XML parser * @param string $data The content of the current tag (1024 byte chunk) * @return bool True */ function parse_signature($parser, $data) { $this->signature .= $data; return true; } /** * Add the next chunk of character data to the data_object string * * @param mixed $parser The XML parser * @param string $data The content of the current tag (1024 byte chunk) * @return bool True */ function parse_object($parser, $data) { $this->data_object .= $data; return true; } /** * Discard the next chunk of character data * * This is used for tags that we're not interested in. * * @param mixed $parser The XML parser * @param string $data The content of the current tag (1024 byte chunk) * @return bool True */ function discard_data($parser, $data) { if (!$this->errorstarted) { // Not interested return true; } $data = trim($data); if (isset($this->errorstarted->faultstringstarted) && !empty($data)) { $this->remoteerror .= ', message: ' . $data; } else if (isset($this->errorstarted->faultcodestarted)) { $this->remoteerror = 'code: ' . $data; unset($this->errorstarted->faultcodestarted); } else if ($data == 'faultCode') { $this->errorstarted->faultcodestarted = true; } else if ($data == 'faultString') { $this->errorstarted->faultstringstarted = true; } return true; } function parse_fault($parser, $data) { $this->errorstarted = new StdClass; return true; } /** * Switch the character-data handler to ignore the next chunk of data * * @param mixed $parser The XML parser * @param string $name The name of the tag, e.g. method_call * @return bool True */ function end_element($parser, $name) { $ok = xml_set_character_data_handler($this->parser, [$this, "discard_data"]); return true; } } tests/privacy/provider_test.php000064400000027740152266401670013010 0ustar00. /** * Privacy test for the mnet subsystems * * @package core_mnet * @category test * @copyright 2018 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_mnet\privacy; defined('MOODLE_INTERNAL') || die(); use core_mnet\privacy\provider; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\transform; use core_privacy\local\request\approved_userlist; /** * Privacy test for the mnet subsystem * * @package core_mnet * @category test * @copyright 2018 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Set up method. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); } /** * Check that a user context is returned if there is any user data for this user. */ public function test_get_contexts_for_userid(): void { global $DB; $user = $this->getDataGenerator()->create_user(); $this->assertEmpty(provider::get_contexts_for_userid($user->id)); // Insert mnet_log record. $logrecord = new \stdClass(); $logrecord->hostid = ''; $logrecord->remoteid = 65; $logrecord->time = time(); $logrecord->userid = $user->id; $DB->insert_record('mnet_log', $logrecord); $contextlist = provider::get_contexts_for_userid($user->id); // Check that we only get back one context. $this->assertCount(1, $contextlist); // Check that a context is returned is the expected. $usercontext = \context_user::instance($user->id); $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]); } /** * Test that user data is exported correctly. */ public function test_export_user_data(): void { global $DB; $user = $this->getDataGenerator()->create_user(); // Insert mnet_host record. $hostrecord = new \stdClass(); $hostrecord->wwwroot = 'https://external.moodle.com'; $hostrecord->name = 'External Moodle'; $hostrecord->public_key = '-----BEGIN CERTIFICATE-----'; $hostid = $DB->insert_record('mnet_host', $hostrecord); // Insert mnet_log record. $logrecord = new \stdClass(); $logrecord->hostid = $hostid; $logrecord->remoteid = 65; $logrecord->time = time(); $logrecord->userid = $user->id; $logrecord->course = 3; $logrecord->coursename = 'test course'; $DB->insert_record('mnet_log', $logrecord); $usercontext = \context_user::instance($user->id); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data()); $approvedlist = new approved_contextlist($user, 'core_mnet', [$usercontext->id]); provider::export_user_data($approvedlist); $data = (array)$writer->get_data([get_string('mnet', 'core_mnet'), $hostrecord->name, $logrecord->coursename]); $this->assertEquals($logrecord->remoteid, reset($data)->remoteid); $this->assertEquals(transform::datetime($logrecord->time), reset($data)->time); } /** * Test deleting all user data for a specific context. */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $user1 = $this->getDataGenerator()->create_user(); // Insert mnet_log record. $logrecord1 = new \stdClass(); $logrecord1->hostid = ''; $logrecord1->remoteid = 65; $logrecord1->time = time(); $logrecord1->userid = $user1->id; $DB->insert_record('mnet_log', $logrecord1); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); // Insert mnet_log record. $logrecord2 = new \stdClass(); $logrecord2->hostid = ''; $logrecord2->remoteid = 65; $logrecord2->time = time(); $logrecord2->userid = $user2->id; $DB->insert_record('mnet_log', $logrecord2); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', []); // There should be two. $this->assertCount(2, $mnetlogrecords); // Delete everything for the first user context. provider::delete_data_for_all_users_in_context($user1context); // Get all user1 mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', ['userid' => $user1->id]); $this->assertCount(0, $mnetlogrecords); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', []); // There should be one (user2). $this->assertCount(1, $mnetlogrecords); } /** * This should work identical to the above test. */ public function test_delete_data_for_user(): void { global $DB; $user1 = $this->getDataGenerator()->create_user(); // Insert mnet_log record. $logrecord1 = new \stdClass(); $logrecord1->hostid = ''; $logrecord1->remoteid = 65; $logrecord1->time = time(); $logrecord1->userid = $user1->id; $DB->insert_record('mnet_log', $logrecord1); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); // Insert mnet_log record. $logrecord2 = new \stdClass(); $logrecord2->hostid = ''; $logrecord2->remoteid = 65; $logrecord2->time = time(); $logrecord2->userid = $user2->id; $DB->insert_record('mnet_log', $logrecord2); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', []); // There should be two. $this->assertCount(2, $mnetlogrecords); // Delete everything for the first user. $approvedlist = new approved_contextlist($user1, 'core_mnet', [$user1context->id]); provider::delete_data_for_user($approvedlist); // Get all user1 mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', ['userid' => $user1->id]); $this->assertCount(0, $mnetlogrecords); // Get all mnet log records. $mnetlogrecords = $DB->get_records('mnet_log', []); // There should be one (user2). $this->assertCount(1, $mnetlogrecords); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context(): void { global $DB; $this->resetAfterTest(); $component = 'core_mnet'; // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); // Insert mnet_log record. $logrecord = new \stdClass(); $logrecord->hostid = ''; $logrecord->remoteid = 65; $logrecord->time = time(); $logrecord->userid = $user->id; $DB->insert_record('mnet_log', $logrecord); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users(): void { global $DB; $this->resetAfterTest(); $component = 'core_mnet'; // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); // Insert mnet_log record. $logrecord1 = new \stdClass(); $logrecord1->hostid = ''; $logrecord1->remoteid = 65; $logrecord1->time = time(); $logrecord1->userid = $user1->id; $DB->insert_record('mnet_log', $logrecord1); // Insert mnet_log record. $logrecord2 = new \stdClass(); $logrecord2->hostid = ''; $logrecord2->remoteid = 65; $logrecord2->time = time(); $logrecord2->userid = $user2->id; $DB->insert_record('mnet_log', $logrecord2); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); } } tests/event/events_test.php000064400000006716152266401670012126 0ustar00. /** * Events tests. * * @package core_mnet * @category test * @copyright 2013 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_mnet\event; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mnet/lib.php'); final class events_test extends \advanced_testcase { /** @var stdClass the mnet host we are using to test */ protected $mnethost; /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { global $DB; parent::setUp(); $this->resetAfterTest(); // Add a mnet host. $this->mnethost = new \stdClass(); $this->mnethost->name = 'A mnet host'; $this->mnethost->public_key = 'A random public key!'; $this->mnethost->id = $DB->insert_record('mnet_host', $this->mnethost); } /** * Test the mnet access control created event. */ public function test_mnet_access_control_created(): void { // Trigger and capture the event. $sink = $this->redirectEvents(); mnet_update_sso_access_control('username', $this->mnethost->id, 'enabled'); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\mnet_access_control_created', $event); $this->assertEquals(\context_system::instance(), $event->get_context()); $this->assertEventContextNotUsed($event); $url = new \moodle_url('/admin/mnet/access_control.php'); $this->assertEquals($url, $event->get_url()); } /** * Test the mnet access control updated event. */ public function test_mnet_access_control_updated(): void { global $DB; // Create a mnet access control. $mnetaccesscontrol = new \stdClass(); $mnetaccesscontrol->username = 'username'; $mnetaccesscontrol->mnet_host_id = $this->mnethost->id; $mnetaccesscontrol->accessctrl = 'enabled'; $mnetaccesscontrol->id = $DB->insert_record('mnet_sso_access_control', $mnetaccesscontrol); // Trigger and capture the event. $sink = $this->redirectEvents(); mnet_update_sso_access_control('username', $this->mnethost->id, 'enabled'); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\core\event\mnet_access_control_updated', $event); $this->assertEquals(\context_system::instance(), $event->get_context()); $this->assertEventContextNotUsed($event); $url = new \moodle_url('/admin/mnet/access_control.php'); $this->assertEquals($url, $event->get_url()); } } remote_client.php000064400000006057152266401670010127 0ustar00request_was_encrypted = true; } /* Record private key to use in pushkey response * Called when we have decrypted a request using an old (but still acceptable) keypair * @param $keyresource the private key we should use to sign the response. */ function encrypted_to($keyresource) { $this->useprivatekey = $keyresource; } function set_pushkey() { $this->pushkey = true; } function was_signed() { $this->request_was_signed = true; } function signature_verified() { $this->signatureok = true; } function object_to_call($object) { $this->object_to_call = $object; } function static_location($location) { $this->static_location = $location; } function plaintext_is_ok() { global $CFG; $trusted_hosts = explode(',', get_config('mnet', 'mnet_trusted_hosts')); foreach($trusted_hosts as $host) { if (address_in_subnet(getremoteaddr(), $host)) { return true; } } return false; } function refresh_key() { mnet_debug("remote client refreshing key"); global $CFG; // set up an RPC request require_once $CFG->dirroot.'/mnet/xmlrpc/client.php'; $mnetrequest = new mnet_xmlrpc_client(); // Use any method - listServices is pretty lightweight. $mnetrequest->set_method('system/listServices'); // Do RPC call and store response if ($mnetrequest->send($this) === true) { mnet_debug("refresh key request complete"); // Ok - we actually don't care about the result $temp = new mnet_peer(); $temp->set_id($this->id); if($this->public_key != $temp->public_key) { $newkey = clean_param($temp->public_key, PARAM_PEM); if(!empty($newkey)) { $this->public_key = $newkey; return true; } } } return false; } }