moodle/mnet/lib.php

498 lines
18 KiB
PHP

<?php // $Id$
/**
* 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')) {
list($keypair['certificate'], $keypair['keypair_PEM']) = explode('@@@@@@@@', $result);
$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 && 'dangerous' != $CFG->mnet_dispatcher_mode) {
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: $accessctrl 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: $accessctrl user '$username' from {$mnethost->name}");
} else {
error(get_string('failedaclwrite','mnet', $username));
return false;
}
}
return true;
}
?>