admin MDL-20980 Touched up regex that determines weak password salts
[moodle.git] / mnet / lib.php
blob7a0478eef0ee3eb804ebce5abf440f759dc1db0e
1 <?php // $Id$
2 /**
3 * Library functions for mnet
5 * @author Donal McMullan donal@catalyst.net.nz
6 * @version 0.0.1
7 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
8 * @package mnet
9 */
10 require_once $CFG->dirroot.'/mnet/xmlrpc/xmlparser.php';
11 require_once $CFG->dirroot.'/mnet/peer.php';
12 require_once $CFG->dirroot.'/mnet/environment.php';
14 /// CONSTANTS ///////////////////////////////////////////////////////////
16 define('RPC_OK', 0);
17 define('RPC_NOSUCHFILE', 1);
18 define('RPC_NOSUCHCLASS', 2);
19 define('RPC_NOSUCHFUNCTION', 3);
20 define('RPC_FORBIDDENFUNCTION', 4);
21 define('RPC_NOSUCHMETHOD', 5);
22 define('RPC_FORBIDDENMETHOD', 6);
24 $MNET = new mnet_environment();
25 $MNET->init();
27 /**
28 * Strip extraneous detail from a URL or URI and return the hostname
30 * @param string $uri The URI of a file on the remote computer, optionally
31 * including its http:// prefix like
32 * http://www.example.com/index.html
33 * @return string Just the hostname
35 function mnet_get_hostname_from_uri($uri = null) {
36 $count = preg_match("@^(?:http[s]?://)?([A-Z0-9\-\.]+).*@i", $uri, $matches);
37 if ($count > 0) return $matches[1];
38 return false;
41 /**
42 * Get the remote machine's SSL Cert
44 * @param string $uri The URI of a file on the remote computer, including
45 * its http:// or https:// prefix
46 * @return string A PEM formatted SSL Certificate.
48 function mnet_get_public_key($uri, $application=null) {
49 global $CFG, $MNET;
50 // The key may be cached in the mnet_set_public_key function...
51 // check this first
52 $key = mnet_set_public_key($uri);
53 if ($key != false) {
54 return $key;
57 if (empty($application)) {
58 $application = get_record('mnet_application', 'name', 'moodle');
61 $rq = xmlrpc_encode_request('system/keyswap', array($CFG->wwwroot, $MNET->public_key, $application->name), array("encoding" => "utf-8"));
62 $ch = curl_init($uri . $application->xmlrpc_server_url);
64 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
65 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
66 curl_setopt($ch, CURLOPT_POST, true);
67 curl_setopt($ch, CURLOPT_USERAGENT, 'Moodle');
68 curl_setopt($ch, CURLOPT_POSTFIELDS, $rq);
69 curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/xml charset=UTF-8"));
70 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
71 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
73 $res = xmlrpc_decode(curl_exec($ch));
75 // check for curl errors
76 $curlerrno = curl_errno($ch);
77 if ($curlerrno!=0) {
78 debugging("Request for $uri failed with curl error $curlerrno");
81 // check HTTP error code
82 $info = curl_getinfo($ch);
83 if (!empty($info['http_code']) and ($info['http_code'] != 200)) {
84 debugging("Request for $uri failed with HTTP code ".$info['http_code']);
87 curl_close($ch);
89 if (!is_array($res)) { // ! error
90 $public_certificate = $res;
91 $credentials=array();
92 if (strlen(trim($public_certificate))) {
93 $credentials = openssl_x509_parse($public_certificate);
94 $host = $credentials['subject']['CN'];
95 if (strpos($uri, $host) !== false) {
96 mnet_set_public_key($uri, $public_certificate);
97 return $public_certificate;
99 else {
100 debugging("Request for $uri returned public key for different URI - $host");
103 else {
104 debugging("Request for $uri returned empty response");
107 else {
108 debugging( "Request for $uri returned unexpected result");
110 return false;
114 * Store a URI's public key in a static variable, or retrieve the key for a URI
116 * @param string $uri The URI of a file on the remote computer, including its
117 * https:// prefix
118 * @param mixed $key A public key to store in the array OR null. If the key
119 * is null, the function will return the previously stored
120 * key for the supplied URI, should it exist.
121 * @return mixed A public key OR true/false.
123 function mnet_set_public_key($uri, $key = null) {
124 static $keyarray = array();
125 if (isset($keyarray[$uri]) && empty($key)) {
126 return $keyarray[$uri];
127 } elseif (!empty($key)) {
128 $keyarray[$uri] = $key;
129 return true;
131 return false;
135 * Sign a message and return it in an XML-Signature document
137 * This function can sign any content, but it was written to provide a system of
138 * signing XML-RPC request and response messages. The message will be base64
139 * encoded, so it does not need to be text.
141 * We compute the SHA1 digest of the message.
142 * We compute a signature on that digest with our private key.
143 * We link to the public key that can be used to verify our signature.
144 * We base64 the message data.
145 * We identify our wwwroot - this must match our certificate's CN
147 * The XML-RPC document will be parceled inside an XML-SIG document, which holds
148 * the base64_encoded XML as an object, the SHA1 digest of that document, and a
149 * signature of that document using the local private key. This signature will
150 * uniquely identify the RPC document as having come from this server.
152 * See the {@Link http://www.w3.org/TR/xmldsig-core/ XML-DSig spec} at the W3c
153 * site
155 * @param string $message The data you want to sign
156 * @param resource $privatekey The private key to sign the response with
157 * @return string An XML-DSig document
159 function mnet_sign_message($message, $privatekey = null) {
160 global $CFG, $MNET;
161 $digest = sha1($message);
163 // If the user hasn't supplied a private key (for example, one of our older,
164 // expired private keys, we get the current default private key and use that.
165 if ($privatekey == null) {
166 $privatekey = $MNET->get_private_key();
169 // The '$sig' value below is returned by reference.
170 // We initialize it first to stop my IDE from complaining.
171 $sig = '';
172 $bool = openssl_sign($message, $sig, $privatekey); // TODO: On failure?
174 $message = '<?xml version="1.0" encoding="iso-8859-1"?>
175 <signedMessage>
176 <Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
177 <SignedInfo>
178 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
179 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
180 <Reference URI="#XMLRPC-MSG">
181 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
182 <DigestValue>'.$digest.'</DigestValue>
183 </Reference>
184 </SignedInfo>
185 <SignatureValue>'.base64_encode($sig).'</SignatureValue>
186 <KeyInfo>
187 <RetrievalMethod URI="'.$CFG->wwwroot.'/mnet/publickey.php"/>
188 </KeyInfo>
189 </Signature>
190 <object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
191 <wwwroot>'.$MNET->wwwroot.'</wwwroot>
192 <timestamp>'.time().'</timestamp>
193 </signedMessage>';
194 return $message;
198 * Encrypt a message and return it in an XML-Encrypted document
200 * This function can encrypt any content, but it was written to provide a system
201 * of encrypting XML-RPC request and response messages. The message will be
202 * base64 encoded, so it does not need to be text - binary data should work.
204 * We compute the SHA1 digest of the message.
205 * We compute a signature on that digest with our private key.
206 * We link to the public key that can be used to verify our signature.
207 * We base64 the message data.
208 * We identify our wwwroot - this must match our certificate's CN
210 * The XML-RPC document will be parceled inside an XML-SIG document, which holds
211 * the base64_encoded XML as an object, the SHA1 digest of that document, and a
212 * signature of that document using the local private key. This signature will
213 * uniquely identify the RPC document as having come from this server.
215 * See the {@Link http://www.w3.org/TR/xmlenc-core/ XML-ENC spec} at the W3c
216 * site
218 * @param string $message The data you want to sign
219 * @param string $remote_certificate Peer's certificate in PEM format
220 * @return string An XML-ENC document
222 function mnet_encrypt_message($message, $remote_certificate) {
223 global $MNET;
225 // Generate a key resource from the remote_certificate text string
226 $publickey = openssl_get_publickey($remote_certificate);
228 if ( gettype($publickey) != 'resource' ) {
229 // Remote certificate is faulty.
230 return false;
233 // Initialize vars
234 $encryptedstring = '';
235 $symmetric_keys = array();
237 // passed by ref -> &$encryptedstring &$symmetric_keys
238 $bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));
239 $message = $encryptedstring;
240 $symmetrickey = array_pop($symmetric_keys);
242 $message = '<?xml version="1.0" encoding="iso-8859-1"?>
243 <encryptedMessage>
244 <EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
245 <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
246 <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
247 <ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
248 <ds:KeyName>XMLENC</ds:KeyName>
249 </ds:KeyInfo>
250 <CipherData>
251 <CipherValue>'.base64_encode($message).'</CipherValue>
252 </CipherData>
253 </EncryptedData>
254 <EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
255 <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
256 <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
257 <ds:KeyName>SSLKEY</ds:KeyName>
258 </ds:KeyInfo>
259 <CipherData>
260 <CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
261 </CipherData>
262 <ReferenceList>
263 <DataReference URI="#ED"/>
264 </ReferenceList>
265 <CarriedKeyName>XMLENC</CarriedKeyName>
266 </EncryptedKey>
267 <wwwroot>'.$MNET->wwwroot.'</wwwroot>
268 </encryptedMessage>';
269 return $message;
273 * Get your SSL keys from the database, or create them (if they don't exist yet)
275 * Get your SSL keys from the database, or (if they don't exist yet) call
276 * mnet_generate_keypair to create them
278 * @param string $string The text you want to sign
279 * @return string The signature over that text
281 function mnet_get_keypair() {
282 global $CFG;
283 static $keypair = null;
284 if (!is_null($keypair)) return $keypair;
285 if ($result = get_field('config_plugins', 'value', 'plugin', 'mnet', 'name', 'openssl')) {
286 list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result);
287 $keypair['privatekey'] = openssl_pkey_get_private($keypair['keypair_PEM']);
288 $keypair['publickey'] = openssl_pkey_get_public($keypair['certificate']);
289 return $keypair;
290 } else {
291 $keypair = mnet_generate_keypair();
292 return $keypair;
297 * Generate public/private keys and store in the config table
299 * Use the distinguished name provided to create a CSR, and then sign that CSR
300 * with the same credentials. Store the keypair you create in the config table.
301 * If a distinguished name is not provided, create one using the fullname of
302 * 'the course with ID 1' as your organization name, and your hostname (as
303 * detailed in $CFG->wwwroot).
305 * @param array $dn The distinguished name of the server
306 * @return string The signature over that text
308 function mnet_generate_keypair($dn = null, $days=28) {
309 global $CFG, $USER;
311 // check if lifetime has been overriden
312 if (!empty($CFG->mnetkeylifetime)) {
313 $days = $CFG->mnetkeylifetime;
316 $host = strtolower($CFG->wwwroot);
317 $host = ereg_replace("^http(s)?://",'',$host);
318 $break = strpos($host.'/' , '/');
319 $host = substr($host, 0, $break);
321 if ($result = get_record_select('course'," id ='".SITEID."' ")) {
322 $organization = $result->fullname;
323 } else {
324 $organization = 'None';
327 $keypair = array();
329 $country = 'NZ';
330 $province = 'Wellington';
331 $locality = 'Wellington';
332 $email = $CFG->noreplyaddress;
334 if(!empty($USER->country)) {
335 $country = $USER->country;
337 if(!empty($USER->city)) {
338 $province = $USER->city;
339 $locality = $USER->city;
341 if(!empty($USER->email)) {
342 $email = $USER->email;
345 if (is_null($dn)) {
346 $dn = array(
347 "countryName" => $country,
348 "stateOrProvinceName" => $province,
349 "localityName" => $locality,
350 "organizationName" => $organization,
351 "organizationalUnitName" => 'Moodle',
352 "commonName" => $CFG->wwwroot,
353 "emailAddress" => $email
357 // ensure we remove trailing slashes
358 $dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
360 if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
361 $new_key = openssl_pkey_new(array("config" => $CFG->opensslcnf));
362 $csr_rsc = openssl_csr_new($dn, $new_key, array("config" => $CFG->opensslcnf));
363 $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days, array("config" => $CFG->opensslcnf));
364 } else {
365 $new_key = openssl_pkey_new();
366 $csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048));
367 $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days);
369 unset($csr_rsc); // Free up the resource
371 // We export our self-signed certificate to a string.
372 openssl_x509_export($selfSignedCert, $keypair['certificate']);
373 openssl_x509_free($selfSignedCert);
375 // Export your public/private key pair as a PEM encoded string. You
376 // can protect it with an optional passphrase if you wish.
377 if (!empty($CFG->opensslcnf)) { //allow specification of openssl.cnf especially for Windows installs
378 $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'], null, array("config" => $CFG->opensslcnf));
379 } else {
380 $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */);
382 openssl_pkey_free($new_key);
383 unset($new_key); // Free up the resource
385 return $keypair;
389 * Check that an IP address falls within the given network/mask
390 * ok for export
392 * @param string $address Dotted quad
393 * @param string $network Dotted quad
394 * @param string $mask A number, e.g. 16, 24, 32
395 * @return bool
397 function ip_in_range($address, $network, $mask) {
398 $lnetwork = ip2long($network);
399 $laddress = ip2long($address);
401 $binnet = str_pad( decbin($lnetwork),32,"0","STR_PAD_LEFT" );
402 $firstpart = substr($binnet,0,$mask);
404 $binip = str_pad( decbin($laddress),32,"0","STR_PAD_LEFT" );
405 $firstip = substr($binip,0,$mask);
406 return(strcmp($firstpart,$firstip)==0);
410 * Check that a given function (or method) in an include file has been designated
411 * ok for export
413 * @param string $includefile The path to the include file
414 * @param string $functionname The name of the function (or method) to
415 * execute
416 * @param mixed $class A class name, or false if we're just testing
417 * a function
418 * @return int Zero (RPC_OK) if all ok - appropriate
419 * constant otherwise
421 function mnet_permit_rpc_call($includefile, $functionname, $class=false) {
422 global $CFG, $MNET_REMOTE_CLIENT;
424 if (file_exists($CFG->dirroot . $includefile)) {
425 include_once $CFG->dirroot . $includefile;
426 // $callprefix matches the rpc convention
427 // of not having a leading slash
428 $callprefix = preg_replace('!^/!', '', $includefile);
429 } else {
430 return RPC_NOSUCHFILE;
433 if ($functionname != clean_param($functionname, PARAM_PATH)) {
434 // Under attack?
435 // Todo: Should really return a much more BROKEN! response
436 return RPC_FORBIDDENMETHOD;
439 $id_list = $MNET_REMOTE_CLIENT->id;
440 if (!empty($CFG->mnet_all_hosts_id)) {
441 $id_list .= ', '.$CFG->mnet_all_hosts_id;
444 // TODO: change to left-join so we can disambiguate:
445 // 1. method doesn't exist
446 // 2. method exists but is prohibited
447 $sql = "
448 SELECT
449 count(r.id)
450 FROM
451 {$CFG->prefix}mnet_host2service h2s,
452 {$CFG->prefix}mnet_service2rpc s2r,
453 {$CFG->prefix}mnet_rpc r
454 WHERE
455 h2s.serviceid = s2r.serviceid AND
456 s2r.rpcid = r.id AND
457 r.xmlrpc_path = '$callprefix/$functionname' AND
458 h2s.hostid in ($id_list) AND
459 h2s.publish = '1'";
461 $permission = count_records_sql($sql);
463 if (!$permission && 'dangerous' != $CFG->mnet_dispatcher_mode) {
464 return RPC_FORBIDDENMETHOD;
467 // WE'RE LOOKING AT A CLASS/METHOD
468 if (false != $class) {
469 if (!class_exists($class)) {
470 // Generate error response - unable to locate class
471 return RPC_NOSUCHCLASS;
474 $object = new $class();
476 if (!method_exists($object, $functionname)) {
477 // Generate error response - unable to locate method
478 return RPC_NOSUCHMETHOD;
481 if (!method_exists($object, 'mnet_publishes')) {
482 // Generate error response - the class doesn't publish
483 // *any* methods, because it doesn't have an mnet_publishes
484 // method
485 return RPC_FORBIDDENMETHOD;
488 // Get the list of published services - initialise method array
489 $servicelist = $object->mnet_publishes();
490 $methodapproved = false;
492 // If the method is in the list of approved methods, set the
493 // methodapproved flag to true and break
494 foreach($servicelist as $service) {
495 if (in_array($functionname, $service['methods'])) {
496 $methodapproved = true;
497 break;
501 if (!$methodapproved) {
502 return RPC_FORBIDDENMETHOD;
505 // Stash the object so we can call the method on it later
506 $MNET_REMOTE_CLIENT->object_to_call($object);
507 // WE'RE LOOKING AT A FUNCTION
508 } else {
509 if (!function_exists($functionname)) {
510 // Generate error response - unable to locate function
511 return RPC_NOSUCHFUNCTION;
516 return RPC_OK;
519 function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) {
520 $mnethost = get_record('mnet_host', 'id', $mnet_host_id);
521 if ($aclrecord = get_record('mnet_sso_access_control', 'username', $username, 'mnet_host_id', $mnet_host_id)) {
522 // update
523 $aclrecord->accessctrl = $accessctrl;
524 if (update_record('mnet_sso_access_control', $aclrecord)) {
525 add_to_log(SITEID, 'admin/mnet', 'update', 'admin/mnet/access_control.php',
526 "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
527 } else {
528 print_error('failedaclwrite', 'mnet', '', $username);
529 return false;
531 } else {
532 // insert
533 $aclrecord->username = $username;
534 $aclrecord->accessctrl = $accessctrl;
535 $aclrecord->mnet_host_id = $mnet_host_id;
536 if ($id = insert_record('mnet_sso_access_control', $aclrecord)) {
537 add_to_log(SITEID, 'admin/mnet', 'add', 'admin/mnet/access_control.php',
538 "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
539 } else {
540 print_error('failedaclwrite', 'mnet', '', $username);
541 return false;
544 return true;
547 function mnet_get_peer_host ($mnethostid) {
548 static $hosts;
549 if (!isset($hosts[$mnethostid])) {
550 $host = get_record('mnet_host', 'id', $mnethostid);
551 $hosts[$mnethostid] = $host;
553 return $hosts[$mnethostid];
557 * Inline function to modify a url string so that mnet users are requested to
558 * log in at their mnet identity provider (if they are not already logged in)
559 * before ultimately being directed to the original url.
561 * uses global MNETIDPJUMPURL the url which user should initially be directed to
562 * MNETIDPJUMPURL is a URL associated with a moodle networking peer when it
563 * is fulfiling a role as an identity provider (IDP). Different urls for
564 * different peers, the jumpurl is formed partly from the IDP's webroot, and
565 * partly from a predefined local path within that webwroot.
566 * The result of the user hitting MNETIDPJUMPURL is that they will be asked
567 * to login (at their identity provider (if they aren't already)), mnet
568 * will prepare the necessary authentication information, then redirect
569 * them back to somewhere at the content provider(CP) moodle (this moodle)
570 * @param array $url array with 2 elements
571 * 0 - context the url was taken from, possibly just the url, possibly href="url"
572 * 1 - the destination url
573 * @return string the url the remote user should be supplied with.
575 function mnet_sso_apply_indirection ($url) {
576 global $MNETIDPJUMPURL;
577 global $CFG;
579 $localpart='';
580 $urlparts = parse_url($url[1]);
581 if($urlparts) {
582 if (isset($urlparts['path'])) {
583 $path = $urlparts['path'];
584 // if our wwwroot has a path component, need to strip that path from beginning of the
585 // 'localpart' to make it relative to moodle's wwwroot
586 $wwwrootparts = parse_url($CFG->wwwroot);
587 if (!empty($wwwrootparts['path']) and strpos($path, $wwwrootparts['path']) === 0) {
588 $path = substr($path, strlen($wwwrootparts['path']));
590 $localpart .= $path;
592 if (isset($urlparts['query'])) {
593 $localpart .= '?'.$urlparts['query'];
595 if (isset($urlparts['fragment'])) {
596 $localpart .= '#'.$urlparts['fragment'];
599 $indirecturl = $MNETIDPJUMPURL . urlencode($localpart);
600 //If we matched on more than just a url (ie an html link), return the url to an href format
601 if ($url[0] != $url[1]) {
602 $indirecturl = 'href="'.$indirecturl.'"';
604 return $indirecturl;