dirroot.'/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); $MNET = new mnet_environment(); $MNET->init(); /** * 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, $MNET; // 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 = get_record('mnet_application', 'name', 'moodle'); } $rq = xmlrpc_encode_request('system/keyswap', array($CFG->wwwroot, $MNET->public_key, $application->name), array("encoding" => "utf-8")); $ch = curl_init($uri . $application->xmlrpc_server_url); curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_USERAGENT, 'Moodle'); curl_setopt($ch, CURLOPT_POSTFIELDS, $rq); curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/xml charset=UTF-8")); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); $res = xmlrpc_decode(curl_exec($ch)); // check for curl errors $curlerrno = curl_errno($ch); if ($curlerrno!=0) { debugging("Request for $uri failed with curl error $curlerrno"); } // check HTTP error code $info = curl_getinfo($ch); if (!empty($info['http_code']) and ($info['http_code'] != 200)) { debugging("Request for $uri failed with HTTP code ".$info['http_code']); } curl_close($ch); 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 (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, $MNET; $digest = sha1($message); // 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); // TODO: On failure? $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) { global $MNET; // Generate a key resource from the remote_certificate text string $publickey = openssl_get_publickey($remote_certificate); if ( gettype($publickey) != 'resource' ) { // 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)); $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; static $keypair = null; if (!is_null($keypair)) return $keypair; if ($result = get_field('config_plugins', 'value', 'plugin', 'mnet', 'name', 'openssl')) { list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result); $keypair['privatekey'] = openssl_pkey_get_private($keypair['keypair_PEM']); $keypair['publickey'] = openssl_pkey_get_public($keypair['certificate']); 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; // check if lifetime has been overriden if (!empty($CFG->mnetkeylifetime)) { $days = $CFG->mnetkeylifetime; } $host = strtolower($CFG->wwwroot); $host = ereg_replace("^http(s)?://",'',$host); $break = strpos($host.'/' , '/'); $host = substr($host, 0, $break); if ($result = get_record_select('course'," id ='".SITEID."' ")) { $organization = $result->fullname; } else { $organization = 'None'; } $keypair = array(); $country = 'NZ'; $province = 'Wellington'; $locality = 'Wellington'; $email = $CFG->noreplyaddress; 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" => $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] = 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)); $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 { $new_key = openssl_pkey_new(); $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']); openssl_x509_free($selfSignedCert); // 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 */); } openssl_pkey_free($new_key); unset($new_key); // Free up the resource return $keypair; } /** * Check that an IP address falls within the given network/mask * ok for export * * @param string $address Dotted quad * @param string $network Dotted quad * @param string $mask A number, e.g. 16, 24, 32 * @return bool */ function ip_in_range($address, $network, $mask) { $lnetwork = ip2long($network); $laddress = ip2long($address); $binnet = str_pad( decbin($lnetwork),32,"0","STR_PAD_LEFT" ); $firstpart = substr($binnet,0,$mask); $binip = str_pad( decbin($laddress),32,"0","STR_PAD_LEFT" ); $firstip = substr($binip,0,$mask); return(strcmp($firstpart,$firstip)==0); } /** * Check that a given function (or method) in an include file has been designated * ok for export * * @param string $includefile The path to the include file * @param string $functionname The name of the function (or method) to * execute * @param mixed $class A class name, or false if we're just testing * a function * @return int Zero (RPC_OK) if all ok - appropriate * constant otherwise */ function mnet_permit_rpc_call($includefile, $functionname, $class=false) { global $CFG, $MNET_REMOTE_CLIENT; if (file_exists($CFG->dirroot . $includefile)) { include_once $CFG->dirroot . $includefile; // $callprefix matches the rpc convention // of not having a leading slash $callprefix = preg_replace('!^/!', '', $includefile); } else { return RPC_NOSUCHFILE; } if ($functionname != clean_param($functionname, PARAM_PATH)) { // Under attack? // Todo: Should really return a much more BROKEN! response return RPC_FORBIDDENMETHOD; } $id_list = $MNET_REMOTE_CLIENT->id; if (!empty($CFG->mnet_all_hosts_id)) { $id_list .= ', '.$CFG->mnet_all_hosts_id; } // TODO: change to left-join so we can disambiguate: // 1. method doesn't exist // 2. method exists but is prohibited $sql = " SELECT count(r.id) FROM {$CFG->prefix}mnet_host2service h2s, {$CFG->prefix}mnet_service2rpc s2r, {$CFG->prefix}mnet_rpc r WHERE h2s.serviceid = s2r.serviceid AND s2r.rpcid = r.id AND r.xmlrpc_path = '$callprefix/$functionname' AND h2s.hostid in ($id_list) AND h2s.publish = '1'"; $permission = count_records_sql($sql); if (!$permission && 'dangerous' != $CFG->mnet_dispatcher_mode) { return RPC_FORBIDDENMETHOD; } // WE'RE LOOKING AT A CLASS/METHOD if (false != $class) { if (!class_exists($class)) { // Generate error response - unable to locate class return RPC_NOSUCHCLASS; } $object = new $class(); if (!method_exists($object, $functionname)) { // Generate error response - unable to locate method return RPC_NOSUCHMETHOD; } if (!method_exists($object, 'mnet_publishes')) { // Generate error response - the class doesn't publish // *any* methods, because it doesn't have an mnet_publishes // method return RPC_FORBIDDENMETHOD; } // Get the list of published services - initialise method array $servicelist = $object->mnet_publishes(); $methodapproved = false; // If the method is in the list of approved methods, set the // methodapproved flag to true and break foreach($servicelist as $service) { if (in_array($functionname, $service['methods'])) { $methodapproved = true; break; } } if (!$methodapproved) { return RPC_FORBIDDENMETHOD; } // Stash the object so we can call the method on it later $MNET_REMOTE_CLIENT->object_to_call($object); // WE'RE LOOKING AT A FUNCTION } else { if (!function_exists($functionname)) { // Generate error response - unable to locate function return RPC_NOSUCHFUNCTION; } } return RPC_OK; } function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) { $mnethost = get_record('mnet_host', 'id', $mnet_host_id); if ($aclrecord = get_record('mnet_sso_access_control', 'username', $username, 'mnet_host_id', $mnet_host_id)) { // update $aclrecord->accessctrl = $accessctrl; if (update_record('mnet_sso_access_control', $aclrecord)) { add_to_log(SITEID, 'admin/mnet', 'update', 'admin/mnet/access_control.php', "SSO ACL: $accessctrl user '$username' from {$mnethost->name}"); } else { print_error('failedaclwrite', 'mnet', '', $username); return false; } } else { // insert $aclrecord->username = $username; $aclrecord->accessctrl = $accessctrl; $aclrecord->mnet_host_id = $mnet_host_id; if ($id = insert_record('mnet_sso_access_control', $aclrecord)) { add_to_log(SITEID, 'admin/mnet', 'add', 'admin/mnet/access_control.php', "SSO ACL: $accessctrl user '$username' from {$mnethost->name}"); } else { print_error('failedaclwrite', 'mnet', '', $username); return false; } } return true; } function mnet_get_peer_host ($mnethostid) { static $hosts; if (!isset($hosts[$mnethostid])) { $host = get_record('mnet_host', '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. * * uses global MNETIDPJUMPURL the url which user should initially be directed to * MNETIDPJUMPURL 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 MNETIDPJUMPURL 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 ($url) { global $MNETIDPJUMPURL; global $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 $wwwrootparts = parse_url($CFG->wwwroot); if (!empty($wwwrootparts['path']) and strpos($path, $wwwrootparts['path']) === 0) { $path = substr($path, strlen($wwwrootparts['path'])); } $localpart .= $path; } if (isset($urlparts['query'])) { $localpart .= '?'.$urlparts['query']; } if (isset($urlparts['fragment'])) { $localpart .= '#'.$urlparts['fragment']; } } $indirecturl = $MNETIDPJUMPURL . 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; } ?>