MDL-11278 Admin settings page completed, implementation of settings in the gradebook...
[moodle-pu.git] / mnet / lib.php
blob4873697fb0712a5c4bef19642d937f6d9fbc31fc
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));
74 curl_close($ch);
76 if (!is_array($res)) { // ! error
77 $public_certificate = $res;
78 $credentials=array();
79 if (strlen(trim($public_certificate))) {
80 $credentials = openssl_x509_parse($public_certificate);
81 $host = $credentials['subject']['CN'];
82 if (strpos($uri, $host) !== false) {
83 mnet_set_public_key($uri, $public_certificate);
84 return $public_certificate;
88 return false;
91 /**
92 * Store a URI's public key in a static variable, or retrieve the key for a URI
94 * @param string $uri The URI of a file on the remote computer, including its
95 * https:// prefix
96 * @param mixed $key A public key to store in the array OR null. If the key
97 * is null, the function will return the previously stored
98 * key for the supplied URI, should it exist.
99 * @return mixed A public key OR true/false.
101 function mnet_set_public_key($uri, $key = null) {
102 static $keyarray = array();
103 if (isset($keyarray[$uri]) && empty($key)) {
104 return $keyarray[$uri];
105 } elseif (!empty($key)) {
106 $keyarray[$uri] = $key;
107 return true;
109 return false;
113 * Sign a message and return it in an XML-Signature document
115 * This function can sign any content, but it was written to provide a system of
116 * signing XML-RPC request and response messages. The message will be base64
117 * encoded, so it does not need to be text.
119 * We compute the SHA1 digest of the message.
120 * We compute a signature on that digest with our private key.
121 * We link to the public key that can be used to verify our signature.
122 * We base64 the message data.
123 * We identify our wwwroot - this must match our certificate's CN
125 * The XML-RPC document will be parceled inside an XML-SIG document, which holds
126 * the base64_encoded XML as an object, the SHA1 digest of that document, and a
127 * signature of that document using the local private key. This signature will
128 * uniquely identify the RPC document as having come from this server.
130 * See the {@Link http://www.w3.org/TR/xmldsig-core/ XML-DSig spec} at the W3c
131 * site
133 * @param string $message The data you want to sign
134 * @return string An XML-DSig document
136 function mnet_sign_message($message) {
137 global $CFG, $MNET;
138 $digest = sha1($message);
139 $sig = $MNET->sign_message($message);
141 $message = '<?xml version="1.0" encoding="iso-8859-1"?>
142 <signedMessage>
143 <Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
144 <SignedInfo>
145 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
146 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
147 <Reference URI="#XMLRPC-MSG">
148 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
149 <DigestValue>'.$digest.'</DigestValue>
150 </Reference>
151 </SignedInfo>
152 <SignatureValue>'.base64_encode($sig).'</SignatureValue>
153 <KeyInfo>
154 <RetrievalMethod URI="'.$CFG->wwwroot.'/mnet/publickey.php"/>
155 </KeyInfo>
156 </Signature>
157 <object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
158 <wwwroot>'.$MNET->wwwroot.'</wwwroot>
159 <timestamp>'.time().'</timestamp>
160 </signedMessage>';
161 return $message;
165 * Encrypt a message and return it in an XML-Encrypted document
167 * This function can encrypt any content, but it was written to provide a system
168 * of encrypting XML-RPC request and response messages. The message will be
169 * base64 encoded, so it does not need to be text - binary data should work.
171 * We compute the SHA1 digest of the message.
172 * We compute a signature on that digest with our private key.
173 * We link to the public key that can be used to verify our signature.
174 * We base64 the message data.
175 * We identify our wwwroot - this must match our certificate's CN
177 * The XML-RPC document will be parceled inside an XML-SIG document, which holds
178 * the base64_encoded XML as an object, the SHA1 digest of that document, and a
179 * signature of that document using the local private key. This signature will
180 * uniquely identify the RPC document as having come from this server.
182 * See the {@Link http://www.w3.org/TR/xmlenc-core/ XML-ENC spec} at the W3c
183 * site
185 * @param string $message The data you want to sign
186 * @param string $remote_certificate Peer's certificate in PEM format
187 * @return string An XML-ENC document
189 function mnet_encrypt_message($message, $remote_certificate) {
190 global $MNET;
192 // Generate a key resource from the remote_certificate text string
193 $publickey = openssl_get_publickey($remote_certificate);
195 if ( gettype($publickey) != 'resource' ) {
196 // Remote certificate is faulty.
197 return false;
200 // Initialize vars
201 $encryptedstring = '';
202 $symmetric_keys = array();
204 // passed by ref -> &$encryptedstring &$symmetric_keys
205 $bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));
206 $message = $encryptedstring;
207 $symmetrickey = array_pop($symmetric_keys);
209 $message = '<?xml version="1.0" encoding="iso-8859-1"?>
210 <encryptedMessage>
211 <EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
212 <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
213 <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
214 <ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
215 <ds:KeyName>XMLENC</ds:KeyName>
216 </ds:KeyInfo>
217 <CipherData>
218 <CipherValue>'.base64_encode($message).'</CipherValue>
219 </CipherData>
220 </EncryptedData>
221 <EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
222 <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
223 <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
224 <ds:KeyName>SSLKEY</ds:KeyName>
225 </ds:KeyInfo>
226 <CipherData>
227 <CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
228 </CipherData>
229 <ReferenceList>
230 <DataReference URI="#ED"/>
231 </ReferenceList>
232 <CarriedKeyName>XMLENC</CarriedKeyName>
233 </EncryptedKey>
234 <wwwroot>'.$MNET->wwwroot.'</wwwroot>
235 </encryptedMessage>';
236 return $message;
240 * Get your SSL keys from the database, or create them (if they don't exist yet)
242 * Get your SSL keys from the database, or (if they don't exist yet) call
243 * mnet_generate_keypair to create them
245 * @param string $string The text you want to sign
246 * @return string The signature over that text
248 function mnet_get_keypair() {
249 global $CFG;
250 static $keypair = null;
251 if (!is_null($keypair)) return $keypair;
252 if ($result = get_field('config_plugins', 'value', 'plugin', 'mnet', 'name', 'openssl')) {
253 $keypair = explode('@@@@@@@@', $keypair);
254 $keypair['privatekey'] = openssl_pkey_get_private($keypair['keypair_PEM']);
255 $keypair['publickey'] = openssl_pkey_get_public($keypair['certificate']);
256 return $keypair;
257 } else {
258 $keypair = mnet_generate_keypair();
259 return $keypair;
264 * Generate public/private keys and store in the config table
266 * Use the distinguished name provided to create a CSR, and then sign that CSR
267 * with the same credentials. Store the keypair you create in the config table.
268 * If a distinguished name is not provided, create one using the fullname of
269 * 'the course with ID 1' as your organization name, and your hostname (as
270 * detailed in $CFG->wwwroot).
272 * @param array $dn The distinguished name of the server
273 * @return string The signature over that text
275 function mnet_generate_keypair($dn = null, $days=28) {
276 global $CFG, $USER;
277 $host = strtolower($CFG->wwwroot);
278 $host = ereg_replace("^http(s)?://",'',$host);
279 $break = strpos($host.'/' , '/');
280 $host = substr($host, 0, $break);
282 if ($result = get_record_select('course'," id ='".SITEID."' ")) {
283 $organization = $result->fullname;
284 } else {
285 $organization = 'None';
288 $keypair = array();
290 $country = 'NZ';
291 $province = 'Wellington';
292 $locality = 'Wellington';
293 $email = $CFG->noreplyaddress;
295 if(!empty($USER->country)) {
296 $country = $USER->country;
298 if(!empty($USER->city)) {
299 $province = $USER->city;
300 $locality = $USER->city;
302 if(!empty($USER->email)) {
303 $email = $USER->email;
306 if (is_null($dn)) {
307 $dn = array(
308 "countryName" => $country,
309 "stateOrProvinceName" => $province,
310 "localityName" => $locality,
311 "organizationName" => $organization,
312 "organizationalUnitName" => 'Moodle',
313 "commonName" => $CFG->wwwroot,
314 "emailAddress" => $email
318 // ensure we remove trailing slashes
319 $dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
321 $new_key = openssl_pkey_new();
322 $csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048));
323 $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days);
324 unset($csr_rsc); // Free up the resource
326 // We export our self-signed certificate to a string.
327 openssl_x509_export($selfSignedCert, $keypair['certificate']);
328 openssl_x509_free($selfSignedCert);
330 // Export your public/private key pair as a PEM encoded string. You
331 // can protect it with an optional passphrase if you wish.
332 $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */);
333 openssl_pkey_free($new_key);
334 unset($new_key); // Free up the resource
336 return $keypair;
340 * Check that an IP address falls within the given network/mask
341 * ok for export
343 * @param string $address Dotted quad
344 * @param string $network Dotted quad
345 * @param string $mask A number, e.g. 16, 24, 32
346 * @return bool
348 function ip_in_range($address, $network, $mask) {
349 $lnetwork = ip2long($network);
350 $laddress = ip2long($address);
352 $binnet = str_pad( decbin($lnetwork),32,"0","STR_PAD_LEFT" );
353 $firstpart = substr($binnet,0,$mask);
355 $binip = str_pad( decbin($laddress),32,"0","STR_PAD_LEFT" );
356 $firstip = substr($binip,0,$mask);
357 return(strcmp($firstpart,$firstip)==0);
361 * Check that a given function (or method) in an include file has been designated
362 * ok for export
364 * @param string $includefile The path to the include file
365 * @param string $functionname The name of the function (or method) to
366 * execute
367 * @param mixed $class A class name, or false if we're just testing
368 * a function
369 * @return int Zero (RPC_OK) if all ok - appropriate
370 * constant otherwise
372 function mnet_permit_rpc_call($includefile, $functionname, $class=false) {
373 global $CFG, $MNET_REMOTE_CLIENT;
375 if (file_exists($CFG->dirroot . $includefile)) {
376 include_once $CFG->dirroot . $includefile;
377 // $callprefix matches the rpc convention
378 // of not having a leading slash
379 $callprefix = preg_replace('!^/!', '', $includefile);
380 } else {
381 return RPC_NOSUCHFILE;
384 if ($functionname != clean_param($functionname, PARAM_PATH)) {
385 // Under attack?
386 // Todo: Should really return a much more BROKEN! response
387 return RPC_FORBIDDENMETHOD;
390 $id_list = $MNET_REMOTE_CLIENT->id;
391 if (!empty($CFG->mnet_all_hosts_id)) {
392 $id_list .= ', '.$CFG->mnet_all_hosts_id;
395 // TODO: change to left-join so we can disambiguate:
396 // 1. method doesn't exist
397 // 2. method exists but is prohibited
398 $sql = "
399 SELECT
400 count(r.id)
401 FROM
402 {$CFG->prefix}mnet_host2service h2s,
403 {$CFG->prefix}mnet_service2rpc s2r,
404 {$CFG->prefix}mnet_rpc r
405 WHERE
406 h2s.serviceid = s2r.serviceid AND
407 s2r.rpcid = r.id AND
408 r.xmlrpc_path = '$callprefix/$functionname' AND
409 h2s.hostid in ($id_list) AND
410 h2s.publish = '1'";
412 $permissionobj = record_exists_sql($sql);
414 if ($permissionobj === false && 'dangerous' != $CFG->mnet_dispatcher_mode) {
415 return RPC_FORBIDDENMETHOD;
418 // WE'RE LOOKING AT A CLASS/METHOD
419 if (false != $class) {
420 if (!class_exists($class)) {
421 // Generate error response - unable to locate class
422 return RPC_NOSUCHCLASS;
425 $object = new $class();
427 if (!method_exists($object, $functionname)) {
428 // Generate error response - unable to locate method
429 return RPC_NOSUCHMETHOD;
432 if (!method_exists($object, 'mnet_publishes')) {
433 // Generate error response - the class doesn't publish
434 // *any* methods, because it doesn't have an mnet_publishes
435 // method
436 return RPC_FORBIDDENMETHOD;
439 // Get the list of published services - initialise method array
440 $servicelist = $object->mnet_publishes();
441 $methodapproved = false;
443 // If the method is in the list of approved methods, set the
444 // methodapproved flag to true and break
445 foreach($servicelist as $service) {
446 if (in_array($functionname, $service['methods'])) {
447 $methodapproved = true;
448 break;
452 if (!$methodapproved) {
453 return RPC_FORBIDDENMETHOD;
456 // Stash the object so we can call the method on it later
457 $MNET_REMOTE_CLIENT->object_to_call($object);
458 // WE'RE LOOKING AT A FUNCTION
459 } else {
460 if (!function_exists($functionname)) {
461 // Generate error response - unable to locate function
462 return RPC_NOSUCHFUNCTION;
467 return RPC_OK;
470 function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) {
471 $mnethost = get_record('mnet_host', 'id', $mnet_host_id);
472 if ($aclrecord = get_record('mnet_sso_access_control', 'username', $username, 'mnet_host_id', $mnet_host_id)) {
473 // update
474 $aclrecord->accessctrl = $accessctrl;
475 if (update_record('mnet_sso_access_control', $aclrecord)) {
476 add_to_log(SITEID, 'admin/mnet', 'update', 'admin/mnet/access_control.php',
477 "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
478 } else {
479 error(get_string('failedaclwrite','mnet', $username));
480 return false;
482 } else {
483 // insert
484 $aclrecord->username = $username;
485 $aclrecord->accessctrl = $accessctrl;
486 $aclrecord->mnet_host_id = $mnet_host_id;
487 if ($id = insert_record('mnet_sso_access_control', $aclrecord)) {
488 add_to_log(SITEID, 'admin/mnet', 'add', 'admin/mnet/access_control.php',
489 "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
490 } else {
491 error(get_string('failedaclwrite','mnet', $username));
492 return false;
495 return true;