moodle/mnet/lib.php
mjollnir_ 2520258128 Merged Donal's changes to MNET to allow moodle to also network with mahara.
changelog follows

    [MNET-manual] added version.php, install.xml and upgrade.php patches
    Route remote users back to their home server without going through jump.php and land.php
    Display app-specific strings in the user view
    Display the application icon in the Remote Host block
    Hide the 'logs' tab if the application under review is not Moodle
    Hide the 'logs' tab if the application under review is not Moodle
    Update user record to note that picture == 1 once a picture has been
    transferred.
    Change 2 to uri - is this fluid?
    Add application paramter to bootstrap function
    Find the application
    Workaround for PHP5.2.2 bug: http://bugs.php.net/bug.php?id=41293
    $HTTP_RAW_POST_DATA was not being populated
    Ensure we get an application for our Peer
    Update the URI to use for MNET
    The default 'wantsurl' should be empty
    Use the appropriate 'land' url for the remote application
    Add hidden form elements for 'application'
    Add awareness of new Application concept
    Add awareness of new Application concept
    Add awareness of new Application concept
    Add awareness of new Application concept
    Add awareness of new Application concept
    Add awareness of new Application concept
    Add awareness of new Application concept
2007-07-14 03:00:12 +00:00

498 lines
18 KiB
PHP

<?php
/**
* Library functions for mnet
*
* @author Donal McMullan donal@catalyst.net.nz
* @version 0.0.1
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package mnet
*/
require_once $CFG->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));
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;
}
}
}
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
* @return string An XML-DSig document
*/
function mnet_sign_message($message) {
global $CFG, $MNET;
$digest = sha1($message);
$sig = $MNET->sign_message($message);
$message = '<?xml version="1.0" encoding="iso-8859-1"?>
<signedMessage>
<Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
<Reference URI="#XMLRPC-MSG">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>'.$digest.'</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>'.base64_encode($sig).'</SignatureValue>
<KeyInfo>
<RetrievalMethod URI="'.$CFG->wwwroot.'/mnet/publickey.php"/>
</KeyInfo>
</Signature>
<object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
<wwwroot>'.$MNET->wwwroot.'</wwwroot>
<timestamp>'.time().'</timestamp>
</signedMessage>';
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 = '<?xml version="1.0" encoding="iso-8859-1"?>
<encryptedMessage>
<EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
<ds:KeyName>XMLENC</ds:KeyName>
</ds:KeyInfo>
<CipherData>
<CipherValue>'.base64_encode($message).'</CipherValue>
</CipherData>
</EncryptedData>
<EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:KeyName>SSLKEY</ds:KeyName>
</ds:KeyInfo>
<CipherData>
<CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
</CipherData>
<ReferenceList>
<DataReference URI="#ED"/>
</ReferenceList>
<CarriedKeyName>XMLENC</CarriedKeyName>
</EncryptedKey>
<wwwroot>'.$MNET->wwwroot.'</wwwroot>
</encryptedMessage>';
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')) {
$keypair = explode('@@@@@@@@', $keypair);
$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;
$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
);
}
// ensure we remove trailing slashes
$dn["commonName"] = preg_replace(':/$:', '', $dn["commonName"]);
$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.
$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'";
$permissionobj = record_exists_sql($sql);
if ($permissionobj === false) {
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: $access user '$username' from {$mnethost->name}");
} else {
error(get_string('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: $access user '$username' from {$mnethost->name}");
} else {
error(get_string('failedaclwrite','mnet', $username));
return false;
}
}
return true;
}
?>