mirror of
https://github.com/moodle/moodle.git
synced 2025-02-19 07:41:02 +01:00
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
498 lines
18 KiB
PHP
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;
|
|
}
|
|
?>
|