3 * Library functions for mnet
5 * @author Donal McMullan donal@catalyst.net.nz
7 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
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 ///////////////////////////////////////////////////////////
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();
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];
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) {
50 // The key may be cached in the mnet_set_public_key function...
52 $key = mnet_set_public_key($uri);
57 $rq = xmlrpc_encode_request('system/keyswap', array($CFG->wwwroot
, $MNET->public_key
), array("encoding" => "utf-8"));
58 $ch = curl_init($uri.'/mnet/xmlrpc/server.php');
60 curl_setopt($ch, CURLOPT_TIMEOUT
, 60);
61 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
62 curl_setopt($ch, CURLOPT_POST
, true);
63 curl_setopt($ch, CURLOPT_USERAGENT
, 'Moodle');
64 curl_setopt($ch, CURLOPT_POSTFIELDS
, $rq);
65 curl_setopt($ch, CURLOPT_HTTPHEADER
, array("Content-Type: text/xml charset=UTF-8"));
66 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER
, false);
67 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST
, 0);
69 $res = xmlrpc_decode(curl_exec($ch));
72 if (!is_array($res)) { // ! error
73 $public_certificate = $res;
75 if (strlen(trim($public_certificate))) {
76 $credentials = openssl_x509_parse($public_certificate);
77 $host = $credentials['subject']['CN'];
78 if (strpos($uri, $host) !== false) {
79 mnet_set_public_key($uri, $public_certificate);
80 return $public_certificate;
88 * Store a URI's public key in a static variable, or retrieve the key for a URI
90 * @param string $uri The URI of a file on the remote computer, including its
92 * @param mixed $key A public key to store in the array OR null. If the key
93 * is null, the function will return the previously stored
94 * key for the supplied URI, should it exist.
95 * @return mixed A public key OR true/false.
97 function mnet_set_public_key($uri, $key = null) {
98 static $keyarray = array();
99 if (isset($keyarray[$uri]) && empty($key)) {
100 return $keyarray[$uri];
101 } elseif (!empty($key)) {
102 $keyarray[$uri] = $key;
109 * Sign a message and return it in an XML-Signature document
111 * This function can sign any content, but it was written to provide a system of
112 * signing XML-RPC request and response messages. The message will be base64
113 * encoded, so it does not need to be text.
115 * We compute the SHA1 digest of the message.
116 * We compute a signature on that digest with our private key.
117 * We link to the public key that can be used to verify our signature.
118 * We base64 the message data.
119 * We identify our wwwroot - this must match our certificate's CN
121 * The XML-RPC document will be parceled inside an XML-SIG document, which holds
122 * the base64_encoded XML as an object, the SHA1 digest of that document, and a
123 * signature of that document using the local private key. This signature will
124 * uniquely identify the RPC document as having come from this server.
126 * See the {@Link http://www.w3.org/TR/xmldsig-core/ XML-DSig spec} at the W3c
129 * @param string $message The data you want to sign
130 * @param resource $privatekey The private key to sign the response with
131 * @return string An XML-DSig document
133 function mnet_sign_message($message, $privatekey = null) {
135 $digest = sha1($message);
137 // If the user hasn't supplied a private key (for example, one of our older,
138 // expired private keys, we get the current default private key and use that.
139 if ($privatekey == null) {
140 $privatekey = $MNET->get_private_key();
143 // The '$sig' value below is returned by reference.
144 // We initialize it first to stop my IDE from complaining.
146 $bool = openssl_sign($message, $sig, $privatekey); // TODO: On failure?
148 $message = '<?xml version="1.0" encoding="iso-8859-1"?>
150 <Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
152 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
153 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
154 <Reference URI="#XMLRPC-MSG">
155 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
156 <DigestValue>'.$digest.'</DigestValue>
159 <SignatureValue>'.base64_encode($sig).'</SignatureValue>
161 <RetrievalMethod URI="'.$CFG->wwwroot
.'/mnet/publickey.php"/>
164 <object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
165 <wwwroot>'.$MNET->wwwroot
.'</wwwroot>
166 <timestamp>'.time().'</timestamp>
172 * Encrypt a message and return it in an XML-Encrypted document
174 * This function can encrypt any content, but it was written to provide a system
175 * of encrypting XML-RPC request and response messages. The message will be
176 * base64 encoded, so it does not need to be text - binary data should work.
178 * We compute the SHA1 digest of the message.
179 * We compute a signature on that digest with our private key.
180 * We link to the public key that can be used to verify our signature.
181 * We base64 the message data.
182 * We identify our wwwroot - this must match our certificate's CN
184 * The XML-RPC document will be parceled inside an XML-SIG document, which holds
185 * the base64_encoded XML as an object, the SHA1 digest of that document, and a
186 * signature of that document using the local private key. This signature will
187 * uniquely identify the RPC document as having come from this server.
189 * See the {@Link http://www.w3.org/TR/xmlenc-core/ XML-ENC spec} at the W3c
192 * @param string $message The data you want to sign
193 * @param string $remote_certificate Peer's certificate in PEM format
194 * @return string An XML-ENC document
196 function mnet_encrypt_message($message, $remote_certificate) {
199 // Generate a key resource from the remote_certificate text string
200 $publickey = openssl_get_publickey($remote_certificate);
202 if ( gettype($publickey) != 'resource' ) {
203 // Remote certificate is faulty.
208 $encryptedstring = '';
209 $symmetric_keys = array();
211 // passed by ref -> &$encryptedstring &$symmetric_keys
212 $bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));
213 $message = $encryptedstring;
214 $symmetrickey = array_pop($symmetric_keys);
216 $message = '<?xml version="1.0" encoding="iso-8859-1"?>
218 <EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
219 <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
220 <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
221 <ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
222 <ds:KeyName>XMLENC</ds:KeyName>
225 <CipherValue>'.base64_encode($message).'</CipherValue>
228 <EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
229 <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
230 <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
231 <ds:KeyName>SSLKEY</ds:KeyName>
234 <CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
237 <DataReference URI="#ED"/>
239 <CarriedKeyName>XMLENC</CarriedKeyName>
241 <wwwroot>'.$MNET->wwwroot
.'</wwwroot>
242 </encryptedMessage>';
247 * Get your SSL keys from the database, or create them (if they don't exist yet)
249 * Get your SSL keys from the database, or (if they don't exist yet) call
250 * mnet_generate_keypair to create them
252 * @param string $string The text you want to sign
253 * @return string The signature over that text
255 function mnet_get_keypair() {
257 static $keypair = null;
258 if (!is_null($keypair)) return $keypair;
259 if ($result = get_field('config_plugins', 'value', 'plugin', 'mnet', 'name', 'openssl')) {
260 list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result);
261 $keypair['privatekey'] = openssl_pkey_get_private($keypair['keypair_PEM']);
262 $keypair['publickey'] = openssl_pkey_get_public($keypair['certificate']);
265 $keypair = mnet_generate_keypair();
271 * Generate public/private keys and store in the config table
273 * Use the distinguished name provided to create a CSR, and then sign that CSR
274 * with the same credentials. Store the keypair you create in the config table.
275 * If a distinguished name is not provided, create one using the fullname of
276 * 'the course with ID 1' as your organization name, and your hostname (as
277 * detailed in $CFG->wwwroot).
279 * @param array $dn The distinguished name of the server
280 * @return string The signature over that text
282 function mnet_generate_keypair($dn = null, $days=28) {
284 $host = strtolower($CFG->wwwroot
);
285 $host = ereg_replace("^http(s)?://",'',$host);
286 $break = strpos($host.'/' , '/');
287 $host = substr($host, 0, $break);
289 if ($result = get_record_select('course'," id ='".SITEID
."' ")) {
290 $organization = $result->fullname
;
292 $organization = 'None';
298 $province = 'Wellington';
299 $locality = 'Wellington';
300 $email = $CFG->noreplyaddress
;
302 if(!empty($USER->country
)) {
303 $country = $USER->country
;
305 if(!empty($USER->city
)) {
306 $province = $USER->city
;
307 $locality = $USER->city
;
309 if(!empty($USER->email
)) {
310 $email = $USER->email
;
315 "countryName" => $country,
316 "stateOrProvinceName" => $province,
317 "localityName" => $locality,
318 "organizationName" => $organization,
319 "organizationalUnitName" => 'Moodle',
320 "commonName" => $CFG->wwwroot
,
321 "emailAddress" => $email
325 // ensure we remove trailing slashes
326 $dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
328 $new_key = openssl_pkey_new();
329 $csr_rsc = openssl_csr_new($dn, $new_key, array('private_key_bits',2048));
330 $selfSignedCert = openssl_csr_sign($csr_rsc, null, $new_key, $days);
331 unset($csr_rsc); // Free up the resource
333 // We export our self-signed certificate to a string.
334 openssl_x509_export($selfSignedCert, $keypair['certificate']);
335 openssl_x509_free($selfSignedCert);
337 // Export your public/private key pair as a PEM encoded string. You
338 // can protect it with an optional passphrase if you wish.
339 $export = openssl_pkey_export($new_key, $keypair['keypair_PEM'] /* , $passphrase */);
340 openssl_pkey_free($new_key);
341 unset($new_key); // Free up the resource
347 * Check that an IP address falls within the given network/mask
350 * @param string $address Dotted quad
351 * @param string $network Dotted quad
352 * @param string $mask A number, e.g. 16, 24, 32
355 function ip_in_range($address, $network, $mask) {
356 $lnetwork = ip2long($network);
357 $laddress = ip2long($address);
359 $binnet = str_pad( decbin($lnetwork),32,"0","STR_PAD_LEFT" );
360 $firstpart = substr($binnet,0,$mask);
362 $binip = str_pad( decbin($laddress),32,"0","STR_PAD_LEFT" );
363 $firstip = substr($binip,0,$mask);
364 return(strcmp($firstpart,$firstip)==0);
368 * Check that a given function (or method) in an include file has been designated
371 * @param string $includefile The path to the include file
372 * @param string $functionname The name of the function (or method) to
374 * @param mixed $class A class name, or false if we're just testing
376 * @return int Zero (RPC_OK) if all ok - appropriate
379 function mnet_permit_rpc_call($includefile, $functionname, $class=false) {
380 global $CFG, $MNET_REMOTE_CLIENT;
382 if (file_exists($CFG->dirroot
. $includefile)) {
383 include_once $CFG->dirroot
. $includefile;
384 // $callprefix matches the rpc convention
385 // of not having a leading slash
386 $callprefix = preg_replace('!^/!', '', $includefile);
388 return RPC_NOSUCHFILE
;
391 if ($functionname != clean_param($functionname, PARAM_PATH
)) {
393 // Todo: Should really return a much more BROKEN! response
394 return RPC_FORBIDDENMETHOD
;
397 $id_list = $MNET_REMOTE_CLIENT->id
;
398 if (!empty($CFG->mnet_all_hosts_id
)) {
399 $id_list .= ', '.$CFG->mnet_all_hosts_id
;
402 // TODO: change to left-join so we can disambiguate:
403 // 1. method doesn't exist
404 // 2. method exists but is prohibited
409 {$CFG->prefix}mnet_host2service h2s,
410 {$CFG->prefix}mnet_service2rpc s2r,
411 {$CFG->prefix}mnet_rpc r
413 h2s.serviceid = s2r.serviceid AND
415 r.xmlrpc_path = '$callprefix/$functionname' AND
416 h2s.hostid in ($id_list) AND
419 $permission = count_records_sql($sql);
421 if (!$permission && 'dangerous' != $CFG->mnet_dispatcher_mode
) {
422 return RPC_FORBIDDENMETHOD
;
425 // WE'RE LOOKING AT A CLASS/METHOD
426 if (false != $class) {
427 if (!class_exists($class)) {
428 // Generate error response - unable to locate class
429 return RPC_NOSUCHCLASS
;
432 $object = new $class();
434 if (!method_exists($object, $functionname)) {
435 // Generate error response - unable to locate method
436 return RPC_NOSUCHMETHOD
;
439 if (!method_exists($object, 'mnet_publishes')) {
440 // Generate error response - the class doesn't publish
441 // *any* methods, because it doesn't have an mnet_publishes
443 return RPC_FORBIDDENMETHOD
;
446 // Get the list of published services - initialise method array
447 $servicelist = $object->mnet_publishes();
448 $methodapproved = false;
450 // If the method is in the list of approved methods, set the
451 // methodapproved flag to true and break
452 foreach($servicelist as $service) {
453 if (in_array($functionname, $service['methods'])) {
454 $methodapproved = true;
459 if (!$methodapproved) {
460 return RPC_FORBIDDENMETHOD
;
463 // Stash the object so we can call the method on it later
464 $MNET_REMOTE_CLIENT->object_to_call($object);
465 // WE'RE LOOKING AT A FUNCTION
467 if (!function_exists($functionname)) {
468 // Generate error response - unable to locate function
469 return RPC_NOSUCHFUNCTION
;
477 function mnet_update_sso_access_control($username, $mnet_host_id, $accessctrl) {
478 $mnethost = get_record('mnet_host', 'id', $mnet_host_id);
479 if ($aclrecord = get_record('mnet_sso_access_control', 'username', $username, 'mnet_host_id', $mnet_host_id)) {
481 $aclrecord->accessctrl
= $accessctrl;
482 if (update_record('mnet_sso_access_control', $aclrecord)) {
483 add_to_log(SITEID
, 'admin/mnet', 'update', 'admin/mnet/access_control.php',
484 "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
486 error(get_string('failedaclwrite','mnet', $username));
491 $aclrecord->username
= $username;
492 $aclrecord->accessctrl
= $accessctrl;
493 $aclrecord->mnet_host_id
= $mnet_host_id;
494 if ($id = insert_record('mnet_sso_access_control', $aclrecord)) {
495 add_to_log(SITEID
, 'admin/mnet', 'add', 'admin/mnet/access_control.php',
496 "SSO ACL: $accessctrl user '$username' from {$mnethost->name}");
498 error(get_string('failedaclwrite','mnet', $username));