From: martinlanghoff Date: Thu, 4 Jan 2007 02:33:51 +0000 (+0000) Subject: mnet: core libraries and admin pages X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=71558f8502644f9a1ea7640e35a87dd0885f41c9;p=moodle.git mnet: core libraries and admin pages --- diff --git a/admin/mnet/MethodTable.php b/admin/mnet/MethodTable.php new file mode 100644 index 0000000000..e5e4b390f8 --- /dev/null +++ b/admin/mnet/MethodTable.php @@ -0,0 +1,583 @@ +methodTable = MethodTable::create($this); + * @author Christophe Herreman + * @since 05/01/2005 + * @version $id$ + * + * Special contributions by Allessandro Crugnola and Ted Milker + */ + +if (!defined('T_ML_COMMENT')) { + define('T_ML_COMMENT', T_COMMENT); +} else { + define('T_DOC_COMMENT', T_ML_COMMENT); +} + +/** + * Return string from start of haystack to first occurance of needle, or whole + * haystack, if needle does not occur + * + * @access public + * @param $haystack(String) Haystack to search in + * @param $needle(String) Needle to look for + */ +function strrstr($haystack, $needle) +{ + return substr($haystack, 0, strpos($haystack.$needle,$needle)); +} + +/** + * Return substring of haystack from end of needle onwards, or FALSE + * + * @access public + * @param $haystack(String) Haystack to search in + * @param $needle(String) Needle to look for + */ +function strstrafter($haystack, $needle) +{ + return substr(strstr($haystack, $needle), strlen($needle)); +} + +class MethodTable +{ + /** + * Constructor. + * + * Since this class should only be accessed through the static create() method + * this constructor should be made private. Unfortunately, this is not possible + * in PHP4. + * + * @access private + */ + function MethodTable(){ + } + + + /** + * Creates the methodTable for a passed class. + * + * @static + * @access public + * @param $sourcePath(String) The path to the file you want to parse + * @param $containsClass(Bool) True if the file is a class definition (optional) + */ + function create($sourcePath, $containsClass = false){ + + $methodTable = array(); + if(!file_exists($sourcePath)) + { + return false; + } + + $source = file_get_contents($sourcePath); + $tokens = (array)token_get_all($source); + + $waitingForOpenParenthesis = false; + $waitingForFunction = false; + $waitingForClassName = false; + $bufferingArgs = false; + $argBuffer = ""; + $lastFunction = ""; + $lastFunctionComment = ""; + $lastComment = ""; + $classMethods = array(); + $realClassName = ""; + + if($containsClass) { + $openBraces = -10000; + } + else + { + $openBraces = 1; + } + + $waitingForEndEncapsedString = false; + foreach($tokens as $token) + { + if (is_string($token)) { + if($token == '{') + { + $openBraces++; + } + if($token == '}') + { + if($waitingForEndEncapsedString) + { + $waitingForEndEncapsedString = false; + } + else + { + $lastComment = ''; + $openBraces--; + + if($openBraces == 0) + { + break; + } + } + } + elseif($waitingForOpenParenthesis && $token == '(') + { + $bufferingArgs = true; + $argBuffer = ""; + $waitingForOpenParenthesis = false; + } + elseif($bufferingArgs) + { + if($token != ')') + { + $argBuffer .= $token; + } + else + { + if($lastFunction != $realClassName) + { + $classMethods[] = array("name" => $lastFunction, + "comment" => $lastFunctionComment, + "args" => $argBuffer); + + $bufferingArgs = false; + $argBuffer = ""; + $lastFunction = ""; + $lastFunctionComment = ""; + } + } + + } + } else { + // token array + list($id, $text) = $token; + + if($bufferingArgs) + { + $argBuffer .= $text; + } + switch ($id) + { + + case T_COMMENT: + case T_ML_COMMENT: // we've defined this + case T_DOC_COMMENT: // and this + // no action on comments + $lastComment = $text; + break; + case T_FUNCTION: + if($openBraces >= 1) + { + $waitingForFunction = true; + } + break; + case T_STRING: + if($waitingForFunction) + { + $waitingForFunction = false; + $waitingForOpenParenthesis = true; + $lastFunction = $text; + $lastFunctionComment = $lastComment; + $lastComment = ""; + } + if($waitingForClassName) + { + $waitingForClassName = false; + $realClassName = $text; + } + break; + case T_CLASS: + $openBraces = 0; + $waitingForClassName = true; + break; + case T_CURLY_OPEN: + case T_DOLLAR_OPEN_CURLY_BRACES: + $waitingForEndEncapsedString = true; + break; + } + } + } + + foreach ($classMethods as $key => $value) { + $methodSignature = $value['args']; + $methodName = $value['name']; + $methodComment = $value['comment']; + + $description = MethodTable::getMethodDescription($methodComment) . " " . MethodTable::getMethodCommentAttribute($methodComment, "desc"); + $description = trim($description); + $access = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "access"); + $roles = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "roles"); + $instance = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "instance"); + $returns = MethodTable::getMethodReturnValue($methodComment); + $pagesize = MethodTable::getMethodCommentAttributeFirstWord($methodComment, "pagesize"); + $params = MethodTable::getMethodCommentArguments($methodComment); + + + //description, arguments, access, [roles, [instance, [returns, [pagesize]]]] + $methodTable[$methodName] = array(); + //$methodTable[$methodName]["signature"] = $methodSignature; //debug purposes + $methodTable[$methodName]["description"] = ($description == "") ? "No description given." : $description; + $methodTable[$methodName]["arguments"] = MethodTable::getMethodArguments($methodSignature, $params); + $methodTable[$methodName]["access"] = ($access == "") ? "private" : $access; + + if($roles != "") $methodTable[$methodName]["roles"] = $roles; + if($instance != "") $methodTable[$methodName]["instance"] = $instance; + if($returns != "") $methodTable[$methodName]["returns"] = $returns; + if($pagesize != "") $methodTable[$methodName]["pagesize"] = $pagesize; + } + + return $methodTable; + + } + + /** + * + */ + function getMethodCommentServices($comment) + { + $pieces = explode('@service', $comment); + $args = array(); + if(is_array($pieces) && count($pieces) > 1) + { + for($i = 0; $i < count($pieces) - 1; $i++) + { + $ps = strrstr($pieces[$i + 1], '@'); + $ps = strrstr($ps, '*/'); + $args[] = MethodTable::cleanComment($ps); + } + } + return $args; + } + + + /** + * + */ + function getMethodCommentArguments($comment) + { + $pieces = explode('@param', $comment); + $args = array(); + if(is_array($pieces) && count($pieces) > 1) + { + for($i = 0; $i < count($pieces) - 1; $i++) + { + $ps = strrstr($pieces[$i + 1], '@'); + $ps = strrstr($ps, '*/'); + $args[] = MethodTable::cleanComment($ps); + } + } + return $args; + } + + + /** + * Returns the description from the comment. + * The description is(are) the first line(s) in the comment. + * + * @static + * @private + * @param $comment(String) The method's comment. + */ + function getMethodDescription($comment){ + $comment = MethodTable::cleanComment(strrstr($comment, "@")); + return trim($comment); + } + + + /** + * Returns the value of a comment attribute. + * + * @static + * @private + * @param $comment(String) The method's comment. + * @param $attribute(String) The name of the attribute to get its value from. + */ + function getMethodCommentAttribute($comment, $attribute){ + $pieces = strstrafter($comment, '@' . $attribute); + if($pieces !== FALSE) + { + $pieces = strrstr($pieces, '@'); + $pieces = strrstr($pieces, '*/'); + return MethodTable::cleanComment($pieces); + } + return ""; + } + + /** + * Returns the value of a comment attribute. + * + * @static + * @private + * @param $comment(String) The method's comment. + * @param $attribute(String) The name of the attribute to get its value from. + */ + function getMethodCommentAttributeFirstLine($comment, $attribute){ + $pieces = strstrafter($comment, '@' . $attribute); + if($pieces !== FALSE) + { + $pieces = strrstr($pieces, '@'); + $pieces = strrstr($pieces, "*"); + $pieces = strrstr($pieces, "/"); + $pieces = strrstr($pieces, "-"); + $pieces = strrstr($pieces, "\n"); + $pieces = strrstr($pieces, "\r"); + $pieces = strrstr($pieces, '*/'); + return MethodTable::cleanComment($pieces); + } + return ""; + } + + /** + * Returns the value of a comment attribute. + * + * @static + * @private + * @param $comment(String) The method's comment. + * @param $attribute(String) The name of the attribute to get its value from. + */ + function getMethodReturnValue($comment){ + $result = array('type' => 'void', 'description' => ''); + $pieces = strstrafter($comment, '@returns'); + if(FALSE == $pieces) $pieces = strstrafter($comment, '@return'); + if($pieces !== FALSE) + { + $pieces = strrstr($pieces, '@'); + $pieces = strrstr($pieces, "*"); + $pieces = strrstr($pieces, "/"); + $pieces = strrstr($pieces, "-"); + $pieces = strrstr($pieces, "\n"); + $pieces = strrstr($pieces, "\r"); + $pieces = strrstr($pieces, '*/'); + $pieces = trim(MethodTable::cleanComment($pieces)); + @list($result['type'], $result['description']) = explode(' ', $pieces, 2); + $result['type'] = MethodTable::standardizeType($result['type']); + } + return $result; + } + + function getMethodCommentAttributeFirstWord($comment, $attribute){ + $pieces = strstrafter($comment, '@' . $attribute); + if($pieces !== FALSE) + { + $val = MethodTable::cleanComment($pieces); + return trim(strrstr($val, ' ')); + } + return ""; + } + + /** + * Returns an array with the arguments of a method. + * + * @static + * @access private + * @param $methodSignature(String) The method's signature; + */ + function getMethodArguments($methodSignature, $commentParams){ + if(strlen($methodSignature) == 0){ + //no arguments, return an empty array + $result = array(); + }else{ + //clean the arguments before returning them + $result = MethodTable::cleanArguments(explode(",", $methodSignature), $commentParams); + } + + return $result; + } + + /** + * Cleans the function or method's return value. + * + * @static + * @access private + * @param $value(String) The "dirty" value. + */ + function cleanReturnValue($value){ + $result = array(); + $value = trim($value); + + list($result['type'], $result['description']) = explode(' ', $value, 2); + + $result['type'] = MethodTable::standardizeType($result['type']); + + return $result; + } + + + /** + * Takes a string and returns the XMLRPC type that most closely matches it. + * + * @static + * @access private + * @param $type(String) The given type string. + */ + function standardizeType($type) { + $type = strtolower($type); + if('str' == $type || 'string' == $type) return 'string'; + if('int' == $type || 'integer' == $type) return 'int'; + if('bool' == $type || 'boolean' == $type) return 'boolean'; + + // Note that object is not a valid XMLRPC type + if('object' == $type || 'class' == $type) return 'object'; + if('float' == $type || 'dbl' == $type || 'double' == $type || 'flt' == $type) return 'double'; + + // Note that null is not a valid XMLRPC type. The null type can have + // only one value - null. + if('null' == $type) return 'null'; + + // Note that mixed is not a valid XMLRPC type + if('mixed' == $type) return 'mixed'; + if('array' == $type || 'arr' == $type) return 'array'; + if('assoc' == $type || 'struct' == $type) return 'struct'; + + // Note that this is not a valid XMLRPC type. As references cannot be + // serialized or exported, there is no way this could be XML-RPCed. + if('reference' == $type || 'ref' == $type) return 'reference'; + return 'string'; + } + + /** + * Cleans the arguments array. + * This method removes all whitespaces and the leading "$" sign from each argument + * in the array. + * + * @static + * @access private + * @param $args(Array) The "dirty" array with arguments. + */ + function cleanArguments($args, $commentParams){ + $result = array(); + + if(!is_array($args)) return array(); + + foreach($args as $index => $arg){ + $arg = strrstr(str_replace(array('$','&$'), array('','&'), $arg), '='); + if(!isset($commentParams[$index])) + { + $result[] = trim($arg); + } + else + { + $start = trim($arg); + $end = trim(str_replace('$', '', $commentParams[$index])); + + // Suppress Notice of 'Undefined offset' with @ + @list($word0, $word1, $tail) = preg_split("/[\s]+/", $end, 3); + $word0 = strtolower($word0); + $word1 = strtolower($word1); + + $wordBase0 = ereg_replace('^[&$]+','',$word0); + $wordBase1 = ereg_replace('^[&$]+','',$word1); + $startBase = strtolower(ereg_replace('^[&$]+','',$start)); + + if ($wordBase0 == $startBase) { + $type = str_replace(array('(',')'),'', $word1); + } elseif($wordBase1 == $startBase) { + $type = str_replace(array('(',')'),'', $word0); + } elseif( ereg('(^[&$]+)|(\()([a-z0-9]+)(\)$)', $word0, $regs) ) { + $tail = str_ireplace($word0, '', $end); + $type = $regs[3]; + } else { + // default to string + $type = 'string'; + } + + $type = MethodTable::standardizeType($type); +/* + if($type == 'str') { + $type = 'string'; + } elseif($type == 'int' || $type == 'integer') { + $type = 'int'; + } elseif($type == 'bool' || $type == 'boolean') { + $type = 'boolean'; + } elseif($type == 'object' || $type == 'class') { + // Note that this is not a valid XMLRPC type + $type = 'object'; + } elseif($type == 'float' || $type == 'dbl' || $type == 'double' || $type == 'flt') { + $type = 'double'; + } elseif($type == 'null') { + // Note that this is not a valid XMLRPC type + // The null type can have only one value - null. Why would + // that be an argument to a function? Just in case: + $type = 'null'; + } elseif($type == 'mixed') { + // Note that this is not a valid XMLRPC type + $type = 'mixed'; + } elseif($type == 'array' || $type == 'arr') { + $type = 'array'; + } elseif($type == 'assoc') { + $type = 'struct'; + } elseif($type == 'reference' || $type == 'ref') { + // Note that this is not a valid XMLRPC type + // As references cannot be serialized or exported, there is + // no way this could be XML-RPCed. + $type = 'reference'; + } else { + $type = 'string'; + } +*/ + $result[] = array('type' => $type, 'description' => $start . ' - ' . $tail); + } + } + + return $result; + } + + + /** + * Cleans the comment string by removing all comment start and end characters. + * + * @static + * @private + * @param $comment(String) The method's comment. + */ + function cleanComment($comment){ + $comment = str_replace("/**", "", $comment); + $comment = str_replace("*/", "", $comment); + $comment = str_replace("*", "", $comment); + $comment = str_replace("\n", "\\n", trim($comment)); + $comment = eregi_replace("[\r\t\n ]+", " ", trim($comment)); + $comment = str_replace("\"", "\\\"", $comment); + return $comment; + } + + /** + * + */ + function showCode($methodTable){ + + if(!is_array($methodTable)) $methodTable = array(); + + foreach($methodTable as $methodName=>$methodProps){ + $result .= "\n\t\"" . $methodName . "\" => array("; + + foreach($methodProps as $key=>$value){ + $result .= "\n\t\t\"" . $key . "\" => "; + + if($key=="arguments"){ + $result .= "array("; + for($i=0; $i diff --git a/admin/mnet/access_control.php b/admin/mnet/access_control.php new file mode 100644 index 0000000000..7c9e83780e --- /dev/null +++ b/admin/mnet/access_control.php @@ -0,0 +1,208 @@ +libdir.'/adminlib.php'); +include_once($CFG->dirroot.'/mnet/lib.php'); + +$sort = optional_param('sort', 'username', PARAM_ALPHA); +$dir = optional_param('dir', 'ASC', PARAM_ALPHA); +$page = optional_param('page', 0, PARAM_INT); +$perpage = optional_param('perpage', 30, PARAM_INT); +$action = trim(strtolower(optional_param('action', '', PARAM_ALPHA))); + +require_login(); +$adminroot = admin_get_root(); +admin_externalpage_setup('ssoaccesscontrol', $adminroot); +admin_externalpage_print_header($adminroot); + +$sitecontext = get_context_instance(CONTEXT_SYSTEM, SITEID); +$sesskey = sesskey(); +$formerror = array(); + +// grab the mnet hosts and remove the localhost +$mnethosts = get_records_menu('mnet_host', '', '', 'name', 'id, name'); +if (array_key_exists($CFG->mnet_localhost_id, $mnethosts)) { + unset($mnethosts[$CFG->mnet_localhost_id]); +} + + + +// process actions +if (!empty($action) and confirm_sesskey()) { + + // boot if insufficient permission + if (!has_capability('moodle/user:delete', $sitecontext)) { + error('You are not permitted to modify the MNET access control list.'); + } + + // fetch the record in question + $id = required_param('id', PARAM_INT); + if (!$idrec = get_record('mnet_sso_access_control', 'id', $id)) { + error('Record does not exist.', '/admin/mnet/access_control.php'); + } + + switch ($action) { + + case "delete": + delete_records('mnet_sso_access_control', 'id', $id); + notify("SSO ACL: delete record for user '{$idrec->username}' from {$mnethosts[$idrec->mnet_host_id]}."); + break; + + case "acl": + + // require the access parameter, and it must be 'allow' or 'deny' + $access = trim(strtolower(required_param('access', PARAM_ALPHA))); + if ($access != 'allow' and $access != 'deny') { + error('Invalid access parameter.', '/admin/mnet/access_control.php'); + } + + if (mnet_update_sso_access_control($idrec->username, $idrec->mnet_host_id, $access)) { + notify("SSO ACL: $access user '{$idrec->username}' from {$mnethosts[$idrec->mnet_host_id]}"); + } + break; + + default: + error('Invalid action parameter.', '/admin/mnet/access_control.php'); + } + redirect('access_control.php', get_string('changessaved')); +} + + + +// process the form results +if ($form = data_submitted() and confirm_sesskey()) { + + // check permissions and verify form input + if (!has_capability('moodle/user:delete', $sitecontext)) { + error('You are not permitted to modify the MNET access control list.', '/admin/mnet/access_control.php'); + } + if (empty($form->username)) { + $formerror['username'] = 'Please enter a username, or a list of usernames separated by commas.'; + } + if (empty($form->mnet_host_id)) { + $formerror['mnet_host_id'] = 'Please select a remote Moodle host.'; + } + if (empty($form->access)) { + $formerror['access'] = 'Please select an access level from the list.'; + } + + // process if there are no errors + if (count($formerror) == 0) { + + // username can be a comma separated list + $usernames = explode(',', $form->username); + + foreach ($usernames as $username) { + $username = trim(moodle_strtolower($username)); + if (!empty($username)) { + if (mnet_update_sso_access_control($username, $form->mnet_host_id, $form->access)) { + notify("SSO ACL: $form->access user '$username' from {$mnethosts[$form->mnet_host_id]}"); + } + } + } + redirect('access_control.php', get_string('changessaved')); + } +} + + + +// output the ACL table +$columns = array("username", "mnet_host_id", "access", "delete"); +$headings = array(); +$string = array('username' => get_string('username'), + 'mnet_host_id' => get_string('remotehost', 'mnet'), + 'access' => get_string('accesslevel', 'mnet'), + 'delete' => get_string('delete')); +foreach ($columns as $column) { + if ($sort != $column) { + $columnicon = ""; + $columndir = "ASC"; + } else { + $columndir = $dir == "ASC" ? "DESC" : "ASC"; + $columnicon = $dir == "ASC" ? "down" : "up"; + $columnicon = " pixpath/t/$columnicon.gif\" alt=\"\" />"; + } + $headings[$column] = "".$string[$column]."$columnicon"; +} +$headings['delete'] = ''; +$acl = get_records('mnet_sso_access_control', '', '', "$sort $dir", '*'); //, $page * $perpage, $perpage); +$aclcount = count_records('mnet_sso_access_control'); + +if (!$acl) { + print_heading('No entries in the SSO access control list'); + $table = NULL; +} else { + $table->head = $headings; + $table->align = array('left', 'left', 'center'); + $table->width = "95%"; + foreach ($acl as $aclrecord) { + if ($aclrecord->access == 'allow') { + $accesscolumn = get_string('allow', 'mnet') + . " (id}&action=acl&access=deny&sesskey={$USER->sesskey}\">" + . get_string('deny', 'mnet') . ")"; + } else { + $accesscolumn = get_string('deny', 'mnet') + . " (id}&action=acl&access=allow&sesskey={$USER->sesskey}\">" + . get_string('allow', 'mnet') . ")"; + } + $deletecolumn = "id}&action=delete&sesskey={$USER->sesskey}\">" + . get_string('delete') . ""; + $table->data[] = array ($aclrecord->username, $aclrecord->mnet_host_id, $accesscolumn, $deletecolumn); + } +} + +if (!empty($table)) { + print_table($table); + echo '

 

'; + print_paging_bar($aclcount, $page, $perpage, "?sort=$sort&dir=$dir&perpage=$perpage&"); +} + + + +// output the add form +print_simple_box_start('center','90%','','20'); + +?> +
+
+ + * '; +} +echo ''; + +// choose a remote host +echo " " . get_string('remotehost', 'mnet') . ":\n"; +if (!empty($formerror['mnet_host_id'])) { + echo ' * '; +} +choose_from_menu($mnethosts, 'mnet_host_id'); + +// choose an access level +echo " " . get_string('accesslevel', 'mnet') . ":\n"; +if (!empty($formerror['access'])) { + echo ' * '; +} +$accessmenu['allow'] = get_string('allow', 'mnet'); +$accessmenu['deny'] = get_string('deny', 'mnet'); +choose_from_menu($accessmenu, 'access'); + +// submit button +echo ''; +echo "
\n"; + +// print errors +foreach ($formerror as $error) { + echo "
$error"; +} + +print_simple_box_end(); +admin_externalpage_print_footer($adminroot); + +?> diff --git a/admin/mnet/adminlib.php b/admin/mnet/adminlib.php new file mode 100644 index 0000000000..6d3bddbf85 --- /dev/null +++ b/admin/mnet/adminlib.php @@ -0,0 +1,177 @@ +dirroot.'/admin/mnet/MethodTable.php'; +error_reporting(E_ALL); +/** + * Parse a file to find out what functions/methods exist in it, and add entries + * for the remote-call-enabled functions to the database. + * + * The path to a file, e.g. auth/mnet/auth.php can be thought of as + * type/parentname/docname + * + * @param string $type mod, auth or enrol + * @param string $parentname Implementation of type, e.g. 'mnet' in the + * case of auth/mnet/auth.php + * @return bool True on success, else false + */ +function mnet_get_functions($type, $parentname) { + global $CFG; + $dataobject = new stdClass(); + $docname = $type.'.php'; + $publishes = array(); + if ('mod' == $type) { + $docname = 'rpclib.php'; + $relname = '/mod/'.$parentname.'/'.$docname; + $filename = $CFG->dirroot.$relname; + if (file_exists($filename)) include $filename; + $mnet_publishes = $parentname.'_mnet_publishes'; + if (function_exists($mnet_publishes)) { + (array)$publishes = $mnet_publishes(); + } + } else { + // auth or enrol + $relname = '/'.$type.'/'.$parentname.'/'.$docname; + $filename = $CFG->dirroot.$relname; + if (file_exists($filename)) include $filename; + $class = $type.($type=='enrol'? 'ment':'').'_plugin_'.$parentname; + if (class_exists($class)) { + $object = new $class(); + if (method_exists($object, 'mnet_publishes')) { + (array)$publishes = $object->mnet_publishes(); + } + } + } + + $methodServiceArray = array(); + foreach($publishes as $service) { + if (is_array($service['methods'])) { + foreach($service['methods'] as $methodname) { + $methodServiceArray[$methodname][] = $service; + } + } + } + + // Disable functions that don't exist (any more) in the source + // Should these be deleted? What about their permissions records? + $rpcrecords = get_records_select('mnet_rpc', ' parent=\''.$parentname.'\' AND parent_type=\''.$type.'\' ', 'function_name ASC '); + if (!empty($rpcrecords)) { + foreach($rpcrecords as $rpc) { + if (!array_key_exists($rpc->function_name, $methodServiceArray)) { + $rpc->enabled = 0; + update_record('mnet_rpc', $rpc); + } + } + } + + if (!file_exists($filename)) return false; + + $functions = (array)MethodTable::create($filename,false); + + foreach($functions as $functionname => $details) { + if (array_key_exists($functionname, $methodServiceArray)) { + $dataobject->function_name = $functionname; + $dataobject->xmlrpc_path = $type.'/'.$parentname.'/'.$docname.'/'.$functionname; + $dataobject->parent_type = $type; + $dataobject->parent = $parentname; + $dataobject->enabled = '0'; + $dataobject->help = addslashes($details['description']); + + $profile = $details['arguments']; + if (!isset($details['returns'])) { + array_unshift($profile, array('type' => 'void', 'description' => 'No return value')); + } else { + array_unshift($profile, $details['returns']); + } + $dataobject->profile = serialize($profile); + + if ($record_exists = get_record('mnet_rpc', 'xmlrpc_path', $dataobject->xmlrpc_path)) { + $dataobject->id = $record_exists->id; + $dataobject->enabled = $record_exists->enabled; + update_record('mnet_rpc', $dataobject); + } else { + $dataobject->id = insert_record('mnet_rpc', $dataobject, true); + } + + foreach($methodServiceArray[$functionname] as $service) { + $serviceobj = get_record('mnet_service', 'name', $service['name']); + if (false == $serviceobj) { + $serviceobj = new stdClass(); + $serviceobj->name = $service['name']; + $serviceobj->apiversion = $service['apiversion']; + $serviceobj->offer = 1; + $serviceobj->id = insert_record('mnet_service', $serviceobj, true); + } + + if (false == get_record('mnet_service2rpc', 'rpcid', $dataobject->id, 'serviceid', $serviceobj->id)) { + $obj = new stdClass(); + $obj->rpcid = $dataobject->id; + $obj->serviceid = $serviceobj->id; + insert_record('mnet_service2rpc', $obj, true); + } + } + } + } + return true; +} + +function upgrade_RPC_functions($returnurl) { + global $CFG; + + $basedir = $CFG->dirroot.'/mod'; + if (file_exists($basedir) && filetype($basedir) == 'dir') { + $dirhandle = opendir($basedir); + while (false !== ($dir = readdir($dirhandle))) { + $firstchar = substr($dir, 0, 1); + if ($firstchar == '.' or $dir == 'CVS' or $dir == '_vti_cnf') { + continue; + } + if (filetype($basedir .'/'. $dir) != 'dir') { + continue; + } + + mnet_get_functions('mod', $dir); + + } + } + + $basedir = $CFG->dirroot.'/auth'; + if (file_exists($basedir) && filetype($basedir) == 'dir') { + $dirhandle = opendir($basedir); + while (false !== ($dir = readdir($dirhandle))) { + $firstchar = substr($dir, 0, 1); + if ($firstchar == '.' or $dir == 'CVS' or $dir == '_vti_cnf') { + continue; + } + if (filetype($basedir .'/'. $dir) != 'dir') { + continue; + } + + mnet_get_functions('auth', $dir); + } + } + + $basedir = $CFG->dirroot.'/enrol'; + if (file_exists($basedir) && filetype($basedir) == 'dir') { + $dirhandle = opendir($basedir); + while (false !== ($dir = readdir($dirhandle))) { + $firstchar = substr($dir, 0, 1); + if ($firstchar == '.' or $dir == 'CVS' or $dir == '_vti_cnf') { + continue; + } + if (filetype($basedir .'/'. $dir) != 'dir') { + continue; + } + + mnet_get_functions('enrol', $dir); + } + } +} +?> diff --git a/admin/mnet/delete.html b/admin/mnet/delete.html new file mode 100644 index 0000000000..25fc5e49ba --- /dev/null +++ b/admin/mnet/delete.html @@ -0,0 +1,52 @@ +shortname: $strmnetsettings", "$site->fullname", + ''.$stradministration.' -> '. + ''.get_string('mnetsettings', 'mnet').' -> Delete host'); + +print_heading(get_string('mnetsettings', 'mnet')); +?> +
+ + + + +
+ + + + + 0): + ?> + + + + + + + + + + + +
Deleting a Server
The following warnings were received:
+ '; ?> +
Are you sure you want to delete the server: "name; ?>"?
+
+ + + + +
+
+
+ +
+
+
+
+ diff --git a/admin/mnet/delete.php b/admin/mnet/delete.php new file mode 100644 index 0000000000..14c517b5e2 --- /dev/null +++ b/admin/mnet/delete.php @@ -0,0 +1,52 @@ +dirroot.'/mnet/lib.php'); + $stradministration = get_string('administration'); + $strconfiguration = get_string('configuration'); + $strmnetsettings = get_string('mnetsettings', 'mnet'); + $strmnetservices = get_string('mnetservices', 'mnet'); + $strmnetlog = get_string('mnetlog', 'mnet'); + $strmnetedithost = get_string('reviewhostdetails', 'mnet'); + require_login(); + + if (!isadmin()) { + error('Only administrators can use this page!'); + } + + $context = get_context_instance(CONTEXT_SYSTEM, SITEID); + + require_capability('moodle/site:config', $context, $USER->id, true, "nopermissions"); + + if (!$site = get_site()) { + error('Site isn\'t defined!'); + } + +/// Initialize variables. + + // Step must be one of: + // input Parse the details of a new host and fetch its public key + // commit Save our changes (to a new OR existing host) + $step = optional_param('step', 'verify', PARAM_ALPHA); + $hostid = required_param('hostid', PARAM_INT); + $warn = array(); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + redirect('index.php', "The delete function requires a POST request.",7); + } + + if ('verify' == $step) { + $mnet_peer = new mnet_peer(); + $mnet_peer->set_id($hostid); + $live_users = $mnet_peer->count_live_sessions(); + if ($live_users > 0) { + $warn[] = get_string('usersareonline', 'mnet', $live_users); + } + include('delete.html'); + } elseif ('delete' == $step) { + $mnet_peer = new mnet_peer(); + $mnet_peer->set_id($hostid); + $mnet_peer->delete(); + redirect('peers.php', 'Ok - host deleted', 5); + } +?> diff --git a/admin/mnet/index.html b/admin/mnet/index.html new file mode 100644 index 0000000000..6b0359f627 --- /dev/null +++ b/admin/mnet/index.html @@ -0,0 +1,33 @@ + +
+
+ + + + +
+ + + + + + + + + + + + +
Public Key:
Networking: + mnet_dispatcher_mode)? 'checked="true"' : '' ?> />
+ mnet_dispatcher_mode)? 'checked="true"' : '' ?> />
+ +
+
+
+
+ \ No newline at end of file diff --git a/admin/mnet/index.php b/admin/mnet/index.php new file mode 100644 index 0000000000..5620141f25 --- /dev/null +++ b/admin/mnet/index.php @@ -0,0 +1,41 @@ +libdir.'/adminlib.php'); + include_once($CFG->dirroot.'/mnet/lib.php'); + + require_login(); + $adminroot = admin_get_root(); + admin_externalpage_setup('net', $adminroot); + + $context = get_context_instance(CONTEXT_SYSTEM, SITEID); + + require_capability('moodle/site:config', $context, $USER->id, true, "nopermissions"); + + if (!$site = get_site()) { + error('Site isn\'t defined!'); + } + + if (!function_exists('curl_init') ) { + error('PHP Curl library is not installed'); + } + + $keypair = unserialize($CFG->openssl); + + if (!isset($CFG->mnet_dispatcher_mode)) set_config('mnet_dispatcher_mode', 'off'); + +/// If data submitted, process and store + if (($form = data_submitted()) && confirm_sesskey()) { + if (in_array($form->mode, array("off", "strict", "promiscuous"))) { + if (set_config('mnet_dispatcher_mode', $form->mode)) { + redirect('index.php', get_string('changessaved')); + } else { + error('Invalid action parameter.', 'index.php'); + } + } + } + $hosts = get_records_select('mnet_host', " id != '{$CFG->mnet_localhost_id}' AND deleted = '0' ",'wwwroot ASC' ); + include('./index.html'); +?> diff --git a/admin/mnet/mnet_log.php b/admin/mnet/mnet_log.php new file mode 100644 index 0000000000..f80b880ce9 --- /dev/null +++ b/admin/mnet/mnet_log.php @@ -0,0 +1,63 @@ +dirroot.'/mnet/lib.php'); + + require_login(); + + if (!isadmin()) { + error('Only administrators can use this page!'); + } + + if (!$site = get_site()) { + error('Site isn\'t defined!'); + } + $hostid = optional_param('hostid', NULL, PARAM_INT); + $stradministration = get_string('administration'); + $strconfiguration = get_string('configuration'); + $strmnetsettings = get_string('mnetsettings', 'mnet'); + $strmnetservices = get_string('mnetservices', 'mnet'); + $strmnetlog = get_string('mnetlog', 'mnet'); + $strmnetedithost = get_string('reviewhostdetails', 'mnet'); + $strmneteditservices = get_string('reviewhostservices', 'mnet'); + +print_header("$site->shortname: $strmnetsettings", "$site->fullname", + ''.$stradministration.' -> '. + ''.get_string('mnetsettings', 'mnet').' -> '.get_string('hostsettings', 'mnet')); + +print_heading(get_string('mnetsettings', 'mnet')); +$tabs[] = new tabobject('mnetdetails', 'index.php?step=update&hostid='.$hostid, $strmnetedithost, $strmnetedithost, false); +$tabs[] = new tabobject('mnetservices', 'mnet_services.php?step=list&hostid='.$hostid, $strmnetservices, $strmnetservices, false); +$tabs[] = new tabobject('mnetlog', 'mnet_log.php?step=list&hostid='.$hostid, $strmnetlog, $strmnetlog, false); +print_tabs(array($tabs), 'mnetlog'); +print_simple_box_start("center", ""); + +?> + + + + + + + + + + +'; +print_simple_box_end(); +print_simple_box_end(); + +?> + + + diff --git a/admin/mnet/mnet_review.html b/admin/mnet/mnet_review.html new file mode 100644 index 0000000000..afc221c644 --- /dev/null +++ b/admin/mnet/mnet_review.html @@ -0,0 +1,127 @@ +wwwroot. + '/course/report/log/index.php?chooselog=1&showusers=1&showcourses=1&host_course='.$mnet_peer->id. + '%2F1&user='.'0'. + '&date=0'. + '&modid=&modaction=0&logformat=showashtml'; + +admin_externalpage_print_header($adminroot); + +if (isset($mnet_peer->id) && $mnet_peer->id > 0) { + $tabs[] = new tabobject('mnetdetails', 'peers.php?step=update&hostid='.$mnet_peer->id, $strmnetedithost, $strmnetedithost, false); + $tabs[] = new tabobject('mnetservices', 'mnet_services.php?step=list&hostid='.$mnet_peer->id, $strmnetservices, $strmnetservices, false); + $tabs[] = new tabobject('mnetlog', $logurl, $strmnetlog, $strmnetlog, false); +} else { + $tabs[] = new tabobject('mnetdetails', '#', $strmnetedithost, $strmnetedithost, false); +} +print_tabs(array($tabs), 'mnetdetails'); + +print_simple_box_start("center", ""); +?> + + + + + +
+ Activity Logs +
+
+
+ + + + + + + + + +public_key)) $mnet_peer->public_key = ''; +?> + + +deleted) && $mnet_peer->deleted > 0) { + $key = mnet_get_public_key($mnet_peer->wwwroot); + $mnet_peer->public_key = clean_param($key, PARAM_PEM); +?> + + + + + +id) && $mnet_peer->id > 0): +?> + + + + +transport) && $mnet_peer->transport > 0): +?> + + + + +deleted) && $mnet_peer->deleted > 0): +?> + + + + +ip_address) && '' != $mnet_peer->ip_address): +?> + + + + + + + + + + +'; +print_simple_box_end(); +admin_externalpage_print_footer($adminroot); +?> diff --git a/admin/mnet/mnet_review_allhosts.html b/admin/mnet/mnet_review_allhosts.html new file mode 100644 index 0000000000..e43399d29c --- /dev/null +++ b/admin/mnet/mnet_review_allhosts.html @@ -0,0 +1,22 @@ +id, $strmnetedithost, $strmnetedithost, false); +$tabs[] = new tabobject('mnetservices', 'mnet_services.php?step=list&hostid='.$mnet_peer->id, $strmnetservices, $strmnetservices, false); +print_tabs(array($tabs), 'mnetdetails'); + +print_simple_box_start("center", ""); +?> +
:
:
'; + helpbutton("publickey", get_string('publickey', 'mnet'), "mnet", true, true); + ?> + public_key)) { + notice(get_string('invalidpubkey', 'mnet')); + } + ?> +
+ public_key)) { + p(get_string('invalidpubkey', 'mnet')); + } + ?> +
+    public_key; ?>
+    
:last_connect_time == 0)? get_string('never','mnet') : date('H:i:s d/m/Y',$mnet_peer->last_connect_time);?>
:transport);?>
: + mnet_dispatcher_mode)? 'checked="true"' : '' ?> /> No - select this option to re-enable this server.
+ mnet_dispatcher_mode)? 'checked="true"' : '' ?> /> Yes
+
'; + helpbutton("ipaddress", get_string('ipaddress', 'mnet'), "mnet", true, true); + ?>:ip_address; ?>
" />
+ + + + + + + +
:name; ?>
:
+ diff --git a/admin/mnet/mnet_services.html b/admin/mnet/mnet_services.html new file mode 100644 index 0000000000..e230f66aae --- /dev/null +++ b/admin/mnet/mnet_services.html @@ -0,0 +1,84 @@ +wwwroot. + '/course/report/log/index.php?chooselog=1&showusers=1&showcourses=1&host_course='.$mnet_peer->id. + '%2F1&user='.'0'. + '&date=0'. + '&modid=&modaction=0&logformat=showashtml'; + +$tabs[] = new tabobject('mnetdetails', 'peers.php?step=update&hostid='.$mnet_peer->id, $strmnetedithost, $strmnetedithost, false); +$tabs[] = new tabobject('mnetservices', 'mnet_services.php?step=list&hostid='.$mnet_peer->id, $strmnetservices, $strmnetservices, false); +if ($mnet_peer->id != $CFG->mnet_all_hosts_id) $tabs[] = new tabobject('mnetlog', $logurl, $strmnetlog, $strmnetlog, false); +print_tabs(array($tabs), 'mnetservices'); +print_simple_box_start("center", ""); + +?> + + + + + + + $versions): + $version = current($versions); +?> + + + + + + + + + + + + + + + + + +'; +print_simple_box_end(); +print_simple_box_end(); + +echo ' + +'; +admin_externalpage_print_footer($adminroot); +?> diff --git a/admin/mnet/mnet_services.php b/admin/mnet/mnet_services.php new file mode 100644 index 0000000000..9f131f0a4a --- /dev/null +++ b/admin/mnet/mnet_services.php @@ -0,0 +1,174 @@ +libdir.'/adminlib.php'); + include_once($CFG->dirroot.'/mnet/lib.php'); + + require_login(); + $adminroot = admin_get_root(); + admin_externalpage_setup('mnetpeers', $adminroot); + + $context = get_context_instance(CONTEXT_SYSTEM, SITEID); + + require_capability('moodle/site:config', $context, $USER->id, true, "nopermissions"); + + if (!$site = get_site()) { + error('Site isn\'t defined!'); + } + +/// Initialize variables. + + // Step must be one of: + // input Parse the details of a new host and fetch its public key + // commit Save our changes (to a new OR existing host) + // force Go ahead with something we've been warned is strange + $step = optional_param('step', NULL, PARAM_ALPHA); + $hostid = optional_param('hostid', NULL, PARAM_INT); + $nocertstring = ''; + $nocertmatch = ''; + $badcert = ''; + $certerror = ''; + $noipmatch = ''; + $stradministration = get_string('administration'); + $strconfiguration = get_string('configuration'); + $strmnetsettings = get_string('mnetsettings', 'mnet'); + $strmnetservices = get_string('mnetservices', 'mnet'); + $strmnetlog = get_string('mnetlog', 'mnet'); + $strmnetedithost = get_string('reviewhostdetails', 'mnet'); + $strmneteditservices = get_string('reviewhostservices', 'mnet'); + + $mnet_peer = new mnet_peer(); + + if (($form = data_submitted()) && confirm_sesskey()) { + $mnet_peer->set_id($hostid); + $treevals = array(); + foreach($_POST['exists'] as $key => $value) { + $host2service = get_record('mnet_host2service', 'hostid', $_POST['hostid'], 'serviceid', $key); + $publish = (isset($_POST['publish'][$key]) && $_POST['publish'][$key] == 'on')? 1 : 0; + $subscribe = (isset($_POST['subscribe'][$key]) && $_POST['subscribe'][$key] == 'on')? 1 : 0; + + if (false == $host2service && ($publish == 1 || $subscribe == 1)) { + $host2service = new stdClass(); + $host2service->hostid = $_POST['hostid']; + $host2service->serviceid = $key; + + $host2service->publish = $publish; + $host2service->subscribe = $subscribe; + + $host2service->id = insert_record('mnet_host2service', $host2service); + } elseif ($host2service->publish != $publish || $host2service->subscribe != $subscribe) { + $host2service->publish = $publish; + $host2service->subscribe = $subscribe; + $tf = update_record('mnet_host2service', $host2service); + } + } + } + + if (is_int($hostid)) { + if (0 == $mnet_peer->id) $mnet_peer->set_id($hostid); + $mnet_peer->nextstep = 'verify'; + + $id_list = $mnet_peer->id; + if (!empty($CFG->mnet_all_hosts_id)) { + $id_list .= ', '.$CFG->mnet_all_hosts_id; + } + + $query = " + SELECT DISTINCT + h2s.id, + svc.id as serviceid, + svc.name, + svc.offer, + svc.apiversion, + r.parent_type, + r.parent, + h2s.hostid, + h2s.publish, + h2s.subscribe + FROM + {$CFG->prefix}mnet_service2rpc s2r, + {$CFG->prefix}mnet_rpc r, + {$CFG->prefix}mnet_service svc + LEFT JOIN + {$CFG->prefix}mnet_host2service h2s + ON + h2s.hostid in ($id_list) AND + h2s.serviceid = svc.id + WHERE + svc.offer = '1' AND + s2r.serviceid = svc.id AND + s2r.rpcid = r.id + ORDER BY + svc.name ASC"; + + $resultset = get_records_sql($query); + + if (is_array($resultset)) { + $resultset = array_values($resultset); + } else { + $resultset = array(); + } + + require_once $CFG->dirroot.'/mnet/xmlrpc/client.php'; + + $remoteservices = array(); + if ($hostid != $CFG->mnet_all_hosts_id) { + // Create a new request object + $mnet_request = new mnet_xmlrpc_client(); + + // Tell it the path to the method that we want to execute + $mnet_request->set_method('system/listServices'); + $mnet_request->send($mnet_peer); + if (is_array($mnet_request->response)) { + foreach($mnet_request->response as $service) { + $remoteservices[$service['name']][$service['apiversion']] = $service; + } + } + } + + $myservices = array(); + foreach($resultset as $result) { + $result->hostpublishes = false; + $result->hostsubscribes = false; + if (isset($remoteservices[$result->name][$result->apiversion])) { + if ($remoteservices[$result->name][$result->apiversion]['publish'] == 1) { + $result->hostpublishes = true; + } + if ($remoteservices[$result->name][$result->apiversion]['subscribe'] == 1) { + $result->hostsubscribes = true; + } + } + + if (empty($myservices[$result->name][$result->apiversion])) { + $myservices[$result->name][$result->apiversion] = array('serviceid' => $result->serviceid, + 'name' => $result->name, + 'offer' => $result->offer, + 'apiversion' => $result->apiversion, + 'parent_type' => $result->parent_type, + 'parent' => $result->parent, + 'hostsubscribes' => $result->hostsubscribes, + 'hostpublishes' => $result->hostpublishes + ); + } + + // allhosts_publish allows us to tell the admin that even though he + // is disabling a service, it's still available to the host because + // he's also publishing it to 'all hosts' + if ($result->hostid == $CFG->mnet_all_hosts_id && $CFG->mnet_all_hosts_id != $mnet_peer->id) { + $myservices[$result->name][$result->apiversion]['allhosts_publish'] = $result->publish; + $myservices[$result->name][$result->apiversion]['allhosts_subscribe'] = $result->subscribe; + } elseif (!empty($result->hostid)) { + $myservices[$result->name][$result->apiversion]['I_publish'] = $result->publish; + $myservices[$result->name][$result->apiversion]['I_subscribe'] = $result->subscribe; + } + + } + + } else { + redirect('peers.php', get_string('nohostid','mnet'), '5'); + exit; + } + + include('./mnet_services.html'); +?> diff --git a/admin/mnet/peers.html b/admin/mnet/peers.html new file mode 100644 index 0000000000..90c40443e6 --- /dev/null +++ b/admin/mnet/peers.html @@ -0,0 +1,84 @@ + +
+
+ + +
+

name); ?>

+

name); ?>

+ 1) { + $versionstring = '('.get_string('version','mnet') .' '.$version['apiversion'].')'; + } else { + $versionstring = ''; + } + + echo $breakstring; +?> + + /> Publish name).'">√ '; if (!empty($version['allhosts_publish'])) print_string("enabled_for_all",'mnet',!empty($version['I_publish'])); ?>
+ /> Subscribe name).'">√ '; if (!empty($version['allhosts_subscribe'])) print_string("enabled_for_all",'mnet',!empty($version['I_subscribe'])); ?>
+'; + endforeach; +?> +
+
+
+
+
" />
+
+
+ + + + + + + + + + + + + +
mnet_register_allhosts)) echo 'checked '; ?>/>
+ + + + + + + + +last_connect_time == 0) { + $last_connect = get_string('never'); + } else { + $last_connect = date('H:i:s d/m/Y', $host->last_connect_time); + } + +?> + + + + + + + + + + + + + + + + + + + + + + + + +
name; ?>wwwroot; ?>id != $CFG->mnet_all_hosts_id) echo $last_connect; ?> +id != $CFG->mnet_all_hosts_id): ?> +
+ + + +
+ +
 
+ + + +
+ + \ No newline at end of file diff --git a/admin/mnet/peers.php b/admin/mnet/peers.php new file mode 100644 index 0000000000..52c3793b6a --- /dev/null +++ b/admin/mnet/peers.php @@ -0,0 +1,125 @@ +libdir.'/adminlib.php'); +include_once($CFG->dirroot.'/mnet/lib.php'); + +require_login(); +$adminroot = admin_get_root(); +admin_externalpage_setup('mnetpeers', $adminroot); + +$context = get_context_instance(CONTEXT_SYSTEM, SITEID); + +require_capability('moodle/site:config', $context, $USER->id, true, "nopermissions"); + +if (!$site = get_site()) { + error('Site isn\'t defined!'); +} + +if (!function_exists('curl_init') ) { + error('PHP Curl library is not installed'); +} + +/// Initialize variables. + +// Step must be one of: +// input Parse the details of a new host and fetch its public key +// commit Save our changes (to a new OR existing host) +$step = optional_param('step', NULL, PARAM_ALPHA); +$hostid = optional_param('hostid', NULL, PARAM_INT); + +// Fetch some strings for the HTML templates +$strmnetservices = get_string('mnetservices', 'mnet'); +$strmnetlog = get_string('mnetlog', 'mnet'); +$strmnetedithost = get_string('reviewhostdetails', 'mnet'); + +$keypair = unserialize($CFG->openssl); + +if (!isset($CFG->mnet_dispatcher_mode)) set_config('mnet_dispatcher_mode', 'off'); + +/// If data submitted, process and store +if (($form = data_submitted()) && confirm_sesskey()) { + + if (!empty($form->updateregisterall)) { + if (!empty($form->registerallhosts)) { + set_config('mnet_register_allhosts',1); + } else { + set_config('mnet_register_allhosts',0); + } + redirect('peers.php', get_string('changessaved')); + } else { + + $mnet_peer = new mnet_peer(); + + if (!empty($form->id)) { + $form->id = clean_param($form->id, PARAM_INT); + $mnet_peer->set_id($form->id); + } else { + // PARAM_URL requires a genuine TLD (I think) This breaks my testing + $temp_wwwroot = $form->wwwroot; //clean_param($form->wwwroot, PARAM_URL); + if ($temp_wwwroot !== $form->wwwroot) { + trigger_error("We now parse the wwwroot with PARAM_URL"); + error('Invalid URL parameter.', 'peers.php'); + } + unset($temp_wwwroot); + $mnet_peer->bootstrap($form->wwwroot); + } + + if (isset($form->name) && $form->name != $mnet_peer->name) { + $form->name = clean_param($form->name, PARAM_NOTAGS); + $mnet_peer->set_name($form->name); + } + + if (isset($form->deleted) && ($form->deleted == '0' || $form->deleted == '1')) { + $mnet_peer->deleted = $form->deleted; + } + + if (isset($form->public_key)) { + $form->public_key = clean_param($form->public_key, PARAM_PEM); + if (empty($form->public_key)) { + // Public key was not in a correct format + } else { + $oldkey = $mnet_peer->public_key; + $mnet_peer->public_key = $form->public_key; + $mnet_peer->public_key_expires = $mnet_peer->check_common_name($form->public_key); + if ($mnet_peer->public_key_expires == false) { + $mnet_peer->public_key == $oldkey; + } + } + } + + // PREVENT DUPLICATE RECORDS /////////////////////////////////////////// + if ('input' == $form->step) { + if ( isset($mnet_peer->id) && $mnet_peer->id > 0 ) { + error(get_string("hostexists ".$mnet_peer->id, 'mnet', $mnet_peer->id),'peers.php?step=update&hostid='.$mnet_peer->id); + } + } + + if ('input' == $form->step) { + include('./mnet_review.html'); + } elseif ('commit' == $form->step) { + $bool = $mnet_peer->commit(); + if ($bool) { + redirect('peers.php?step=update&hostid='.$mnet_peer->id, get_string('changessaved')); + } else { + error('Invalid action parameter.', 'index.php'); + } + } + } +} elseif (is_int($hostid)) { + $mnet_peer = new mnet_peer(); + $mnet_peer->set_id($hostid); + $form = new stdClass(); + if ($hostid != $CFG->mnet_all_hosts_id) { + include('./mnet_review.html'); + } else { + include('./mnet_review_allhosts.html'); + } +} else { + $hosts = get_records_select('mnet_host', " id != '{$CFG->mnet_localhost_id}' AND deleted = '0' ",'wwwroot ASC' ); + if (empty($hosts)) $hosts = array(); + include('./peers.html'); +} +?> \ No newline at end of file diff --git a/admin/mnet/trustedhosts.html b/admin/mnet/trustedhosts.html new file mode 100644 index 0000000000..34dc636d46 --- /dev/null +++ b/admin/mnet/trustedhosts.html @@ -0,0 +1,61 @@ + +
+ + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + + + + + + + + + +
+ '; + print_string('validated_by', 'mnet', $validated_by); + } else { + print_string('not_in_range', 'mnet', $test_ip_address); + } + ?> +
+ + + +
+
+ \ No newline at end of file diff --git a/admin/mnet/trustedhosts.php b/admin/mnet/trustedhosts.php new file mode 100644 index 0000000000..861fc9d3b2 --- /dev/null +++ b/admin/mnet/trustedhosts.php @@ -0,0 +1,62 @@ +libdir.'/adminlib.php'); + include_once($CFG->dirroot.'/mnet/lib.php'); + + require_login(); + $adminroot = admin_get_root(); + admin_externalpage_setup('trustedhosts', $adminroot); + + $context = get_context_instance(CONTEXT_SYSTEM, SITEID); + + require_capability('moodle/site:config', $context, $USER->id, true, "nopermissions"); + + if (!$site = get_site()) { + error('Site isn\'t defined!'); + } + + $trusted_hosts = '';//array(); + $old_trusted_hosts = get_config('mnet', 'mnet_trusted_hosts'); + + $test_ip_address = optional_param('testipaddress', NULL, PARAM_HOST); + $in_range = false; + if (!empty($test_ip_address)) { + foreach(explode(',', $old_trusted_hosts->value) as $host) { + list($network, $mask) = explode('/', $host.'/'); + if (empty($network)) continue; + if (strlen($mask) == 0) $mask = 32; + + if (ip_in_range($test_ip_address, $network, $mask)) { + $in_range = true; + $validated_by = $network.'/'.$mask; + break; + } + } + } + + /// If data submitted, process and store + if (($form = data_submitted()) && confirm_sesskey()) { + echo 'a'; + $hostlist = preg_split("/[\s,]+/", $form->hostlist); + foreach($hostlist as $host) { + list($address, $mask) = explode('/', $host.'/'); + if (empty($address)) continue; + if (strlen($mask) == 0) $mask = 32; + $trusted_hosts .= trim($address).'/'.trim($mask)."\n"; + unset($address, $mask); + } + set_config('mnet_trusted_hosts', str_replace("\n", ',', $trusted_hosts), 'mnet'); + } elseif (!empty($old_trusted_hosts->value)) { + foreach(explode(',', $old_trusted_hosts->value) as $host) { + list($address, $mask) = explode('/', $host.'/'); + if (empty($address)) continue; + if (strlen($mask) == 0) $mask = 32; + $trusted_hosts .= trim($address).'/'.trim($mask)."\n"; + unset($address, $mask); + } + } + + include('./trustedhosts.html'); +?> diff --git a/mnet/environment.php b/mnet/environment.php new file mode 100644 index 0000000000..a3b620cef9 --- /dev/null +++ b/mnet/environment.php @@ -0,0 +1,118 @@ +mnet_localhost_id) ) { + $this->get_keypair(); + + $hostobject = new stdClass(); + $hostobject->wwwroot = $CFG->wwwroot; + $hostobject->ip_address = $_SERVER['SERVER_ADDR']; + $hostobject->public_key = $this->keypair['certificate']; + $hostobject->public_key_expires = ''; + $hostobject->last_connect_time = '0'; + $hostobject->last_log_id = '0'; + $hostobject->deleted = 0; + + $this->id = insert_record('mnet_host',$hostobject, true); + + $temparr = (array)get_object_vars($hostobject); + + foreach($temparr as $key => $value) { + $this->$key = $value; + } + + unset($temparr, $hostobject); + + set_config('mnet_localhost_id', $this->id); + $CFG->mnet_localhost_id = $this->id; + } else { + $hostobject = get_record('mnet_host','id', $CFG->mnet_localhost_id); + $temparr = (array)get_object_vars($hostobject); + + foreach($temparr as $key => $value) { + $this->$key = $value; + } + + unset($temparr, $hostobject); + } + + // We need to set up a record that represents 'all hosts'. Any rights + // granted to this host will be conferred on all hosts. + if (empty($CFG->mnet_all_hosts_id) ) { + $hostobject = new stdClass(); + $hostobject->wwwroot = ''; + $hostobject->ip_address = ''; + $hostobject->public_key = ''; + $hostobject->public_key_expires = ''; + $hostobject->last_connect_time = '0'; + $hostobject->last_log_id = '0'; + $hostobject->deleted = 0; + $hostobject->name = 'All Hosts'; + + $hostobject->id = insert_record('mnet_host',$hostobject, true); + set_config('mnet_all_hosts_id', $hostobject->id); + $CFG->mnet_all_hosts_id = $hostobject->id; + unset($hostobject); + } + } + + function get_keypair() { + if (!empty($this->keypair)) return true; + if ($result = get_record_select('config', " name = 'openssl'")) { + $this->keypair = unserialize($result->value); + $this->keypair['privatekey'] = openssl_pkey_get_private($this->keypair['keypair_PEM']); + $this->keypair['publickey'] = openssl_pkey_get_public($this->keypair['certificate']); + } else { + $this->keypair = mnet_generate_keypair(); + } + return true; + } + + function get_private_key() { + if (empty($this->keypair)) $this->get_keypair(); + if (isset($this->keypair['privatekey'])) return $this->keypair['privatekey']; + $this->keypair['privatekey'] = openssl_pkey_get_private($this->keypair['keypair_PEM']); + return $this->keypair['privatekey']; + } + + function get_public_key() { + if (!isset($this->keypair)) $this->get_keypair(); + if (isset($this->keypair['publickey'])) return $this->keypair['publickey']; + $this->keypair['publickey'] = openssl_pkey_get_public($this->keypair['certificate']); + return $this->keypair['publickey']; + } + + /** + * Note that the openssl_sign function computes the sha1 hash, and then + * signs the hash. + */ + function sign_message($message) { + $bool = openssl_sign($message, $signature, $this->get_private_key()); + return $signature; + } +} + +?> diff --git a/mnet/lib.php b/mnet/lib.php new file mode 100644 index 0000000000..b078db7383 --- /dev/null +++ b/mnet/lib.php @@ -0,0 +1,491 @@ +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 + * @param bool $verify True if the SSL Cert must be signed by a trusted 3rd + * party. [TODO! NOT IMPLEMENTED] + * @return string A PEM formatted SSL Certificate. + */ +function mnet_get_public_key($uri, $verify=false) { + global $CFG; + // 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; + } + + $rq = xmlrpc_encode_request('system/keyswap', $CFG->wwwroot); + $ch = curl_init($uri.'/mnet/xmlrpc/server.php'); + + 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")); + + $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 = ' + + + + + + + + '.$digest.' + + + '.base64_encode($sig).' + + + + + '.base64_encode($message).' + '.$MNET->wwwroot.' + '; + 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 = ' + + + + + + XMLENC + + + '.base64_encode($message).' + + + + + + SSLKEY + + + '.base64_encode($symmetrickey).' + + + + + XMLENC + + '.$MNET->wwwroot.' + '; + 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_record_select('config', " name = 'openssl'")) { + $keypair = unserialize($result->value); + $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) { + global $CFG; + $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 ='1' ")) { + $organization = $result->fullname; + } else { + $organization = 'None'; + } + + $keypair = array(); + // TODO: fix this with a redirect, form, etc. + + if (is_null($dn)) { + $dn = array( + "countryName" => 'NZ', + "stateOrProvinceName" => 'Wellington', + "localityName" => 'Wellington', + "organizationName" => $organization, + "organizationalUnitName" => 'Moodle', + "commonName" => $CFG->wwwroot, + "emailAddress" => $CFG->noreplyaddress + ); + } + + $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, 365); + + // You'll want to keep your certificate signing request, so we'll + // export that to a property - csr_txt. + openssl_csr_export($csr_rsc, $csr_txt); + unset($csr_rsc); // Free up the resource + + // We export our self-signed certificate to a string as well. + 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 + + $record = new stdClass(); + $record->name = 'openssl'; + // Normally we would just serialize the object, but that's not + // working, nor behaving as the docs suggest it should. Casting the + // object to an array and serializing the array works fine. + $record->value = serialize($keypair); + + insert_record('config', $record); + // unset $record + + $keypair['privatekey'] = openssl_pkey_get_private($keypair['keypair_PEM']); + $keypair['publickey'] = openssl_pkey_get_public($keypair['certificate']); + + 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, $access) { + $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->access = $access; + 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("Failed to write to the MNET access control list for user '$username'."); + return false; + } + } else { + // insert + $aclrecord->username = $username; + $aclrecord->access = $access; + $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("Failed to write to the MNET access control list for user '$username'."); + return false; + } + } + return true; +} +?> diff --git a/mnet/peer.php b/mnet/peer.php new file mode 100644 index 0000000000..0f0628b8b3 --- /dev/null +++ b/mnet/peer.php @@ -0,0 +1,228 @@ +set_wwwroot($wwwroot) ) { + $hostname = mnet_get_hostname_from_uri($wwwroot); + + // Get the IP address for that host - if this fails, it will + // return the hostname string + $ip_address = gethostbyname($hostname); + + // Couldn't find the IP address? + if ($ip_address === $hostname && !preg_match('/^\d+\.\d+\.\d+.\d+$/',$hostname)) { + $this->error[] = array('code' => 2, 'text' => get_string("noaddressforhost", 'mnet')); + return false; + } + + $this->name = $wwwroot; + + // TODO: In reality, this will be prohibitively slow... need another + // default - maybe blank string + $homepage = file_get_contents($wwwroot); + if (!empty($homepage)) { + $count = preg_match("@(.*)@siU", $homepage, $matches); + if ($count > 0) { + $this->name = $matches[1]; + } + } + + if (substr($wwwroot, 0, -1) == '/') { + $wwwroot = substr($wwwroot, 0, -1); + } + + $this->wwwroot = $wwwroot; + $this->ip_address = $ip_address; + $this->deleted = 0; + $this->public_key = clean_param(mnet_get_public_key($this->wwwroot), PARAM_PEM); + $this->public_key_expires = $this->check_common_name($this->public_key); + $this->last_connect_time = 0; + $this->last_log_id = 0; + if ($this->public_key_expires == false) { + $this->public_key == ''; + return false; + } + } + + return true; + } + + function delete() { + if ($this->deleted) return true; + + $users = count_records('user','mnethostid', $this->id); + if ($users > 0) { + $this->deleted = 1; + } + + $actions = count_records('mnet_log','hostid', $this->id); + if ($actions > 0) { + $this->deleted = 1; + } + + $obj = delete_records('mnet_rpc2host', 'host_id', $this->id); + + $this->delete_all_sessions(); + + // If we don't have any activity records for which the mnet_host table + // provides a foreign key, then we can delete the record. Otherwise, we + // just mark it as deleted. + if (0 == $this->deleted) { + delete_records('mnet_host', "id", $this->id); + } else { + $this->commit(); + } + } + + function count_live_sessions() { + $obj = $this->delete_expired_sessions(); + return count_records('mnet_session','mnethostid', $this->id); + } + + function delete_expired_sessions() { + $now = time(); + return delete_records_select('mnet_session', " mnethostid = '{$this->id}' AND expires < '$now' "); + } + + function delete_all_sessions() { + global $CFG; + // TODO: Expires each PHP session individually + // $sessions = get_records('mnet_session', 'mnethostid', $this->id); + $sessions = get_records('mnet_session', 'mnethostid', $this->id); + + if (count($sessions) > 0 && file_exists($CFG->dirroot.'/auth/mnet/auth.php')) { + require_once($CFG->dirroot.'/auth/mnet/auth.php'); + $auth = new auth_plugin_mnet(); + $auth->end_local_sessions($sessions); + } + + $deletereturn = delete_records_select('mnet_session', " mnethostid = '{$this->id}'"); + return true; + } + + function check_common_name($key) { + $credentials = openssl_x509_parse($key); + if ($credentials == false) { + $this->error[] = array('code' => 3, 'text' => get_string("nonmatchingcert", 'mnet', array('',''))); + return false; + } elseif ($credentials['subject']['CN'] != $this->wwwroot) { + $a[] = $credentials['subject']['CN']; + $a[] = $this->wwwroot; + $this->error[] = array('code' => 4, 'text' => get_string("nonmatchingcert", 'mnet', $a)); + return false; + } else { + return $credentials['validTo_time_t']; + } + } + + function commit() { + $obj = new stdClass(); + + $obj->wwwroot = $this->wwwroot; + $obj->ip_address = $this->ip_address; + $obj->name = $this->name; + $obj->public_key = $this->public_key; + $obj->public_key_expires = $this->public_key_expires; + $obj->deleted = $this->deleted; + $obj->last_connect_time = $this->last_connect_time; + $obj->last_log_id = $this->last_log_id; + + if (isset($this->id) && $this->id > 0) { + $obj->id = $this->id; + return update_record('mnet_host', $obj); + } else { + $this->id = insert_record('mnet_host', $obj); + return $this->id > 0; + } + } + + function set_name($newname) { + if (is_string($newname) && strlen($newname <= 80)) { + $this->name = $newname; + return true; + } + return false; + } + + function set_wwwroot($wwwroot) { + global $CFG; + + $hostinfo = get_record('mnet_host', 'wwwroot', $wwwroot); + + if ($hostinfo != false) { + $this->populate($hostinfo); + return true; + } + return false; + } + + function set_id($id) { + global $CFG; + + if (clean_param($id, PARAM_INT) != $id) { + $this->errno[] = 1; + $this->errmsg[] = 'Your id ('.$id.') is not legal'; + return false; + } + + $sql = " + SELECT + h.* + FROM + {$CFG->prefix}mnet_host h + WHERE + h.id = '". $id ."'"; + + if ($hostinfo = get_record_sql($sql)) { + $this->populate($hostinfo); + return true; + } + return false; + } + + // PRIVATE METHOD + function populate($hostinfo) { + $this->id = $hostinfo->id; + $this->wwwroot = $hostinfo->wwwroot; + $this->ip_address = $hostinfo->ip_address; + $this->name = $hostinfo->name; + $this->deleted = $hostinfo->deleted; + $this->public_key = $hostinfo->public_key; + $this->public_key_expires = $hostinfo->public_key_expires; + $this->last_connect_time = $hostinfo->last_connect_time; + $this->last_log_id = $hostinfo->last_log_id; + } + + function get_public_key() { + if (isset($this->public_key_ref)) return $this->public_key_ref; + $this->public_key_ref = openssl_pkey_get_public($this->public_key); + return $this->public_key_ref; + } +} + +?> diff --git a/mnet/publickey.php b/mnet/publickey.php new file mode 100644 index 0000000000..51cff94111 --- /dev/null +++ b/mnet/publickey.php @@ -0,0 +1,16 @@ +dirroot.'/mnet/lib.php'; +header("Content-type: text/plain"); +$keypair = mnet_get_keypair(); +echo $keypair['certificate']; +?> diff --git a/mnet/remote_client.php b/mnet/remote_client.php new file mode 100644 index 0000000000..2cfcfcffc4 --- /dev/null +++ b/mnet/remote_client.php @@ -0,0 +1,50 @@ +request_was_encrypted = true; + } + + function was_signed() { + $this->request_was_signed = true; + } + + function object_to_call($object) { + $this->object_to_call = $object; + } + + function plaintext_is_ok() { + global $CFG; + + $trusted_hosts = explode(',', get_config('mnet', 'mnet_trusted_hosts')); + + foreach($trusted_hosts as $host) { + list($network, $mask) = explode('/', $host.'/'); + if (empty($network)) continue; + if (strlen($mask) == 0) $mask = 32; + + if (ip_in_range($_SERVER['REMOTE_ADDR'], $network, $mask)) { + return true; + } + } + + return false; + } +} +?> \ No newline at end of file diff --git a/mnet/rpclib.php b/mnet/rpclib.php new file mode 100644 index 0000000000..6cf8e7f83f --- /dev/null +++ b/mnet/rpclib.php @@ -0,0 +1,80 @@ +first = 'last'; + $this->last = 'first'; + } + + function augment_first($newval) { + $this->first = $this->first.$newval; + return $this->first; + } + + function augment_first_RPC_OK() { + return true; + } + + function mnet_concatenate_strings_RPC_OK() { + return true; + } + function mnet_concatenate_strings($string1='', $string2='', $string3='') { + return $string1.$string2.$string3; + } +} + +?> \ No newline at end of file diff --git a/mnet/testclient.php b/mnet/testclient.php new file mode 100644 index 0000000000..c3c2d4b487 --- /dev/null +++ b/mnet/testclient.php @@ -0,0 +1,146 @@ +dirroot.'/mnet/xmlrpc/client.php'; + +error_reporting(E_ALL); + +if (isset($_GET['func']) && is_numeric($_GET['func'])) { + $func = $_GET['func']; + + +// Some HTML sugar +echo ''; +?> + + +Moodle MNET Test Client +wwwroot; + +// Enter the complete path to the file that contains the function you want to +// call on the remote server. In our example the function is in +// mnet/testlib/ +// The function itself is added to that path to complete the $path_to_function +// variable +$path_to_function[0] = 'mnet/rpclib/mnet_concatenate_strings'; +$path_to_function[1] = 'mod/scorm/rpclib/scorm_add_floats'; +$path_to_function[2] = 'system/listMethods'; +$path_to_function[3] = 'system/methodSignature'; +$path_to_function[4] = 'system/methodHelp'; +$path_to_function[5] = 'system/listServices'; +$path_to_function[6] = 'system/listMethods'; +$path_to_function[7] = 'system/listMethods'; + +$paramArray[0] = array(array('some string, ', 'string'), +array('some other string, ', 'string'), +array('and a final string', 'string')); + +$paramArray[1] = array(array(5.3, 'string'), +array(7.1, 'string'), +array(8.25323, 'string')); + +$paramArray[2] = array(); + +$paramArray[3] = array(array('auth/mnet/auth/user_authorise', 'string')); + +$paramArray[4] = array(array('auth/mnet/auth/user_authorise', 'string')); + +$paramArray[5] = array(); + +$paramArray[6] = array(array('sso', 'string')); + +$paramArray[7] = array(array('concatenate', 'string')); + +echo 'Your local wwwroot appears to be '. $wwwroot .".
\n"; +echo "We will use this as the local and remote hosts.

\n"; +flush(); + +// mnet_peer pulls information about a remote host from the database. +$mnet_peer = new mnet_peer(); +$mnet_peer->set_wwwroot($wwwroot); + +echo "Your \$mnet_peer from the database looks like:
\n
";
+$h2 = get_object_vars($mnet_peer);
+while(list($key, $val) = each($h2)) {
+    if (!is_numeric($key)) echo ''.$key.': '. $val."\n";
+}
+echo "

It's ok if that info is not complete - the required field is:
\nwwwroot: {$mnet_peer->wwwroot}.

\n"; +flush(); + +// The transport id is one of: +// RPC_HTTPS_VERIFIED 1 +// RPC_HTTPS_SELF_SIGNED 2 +// RPC_HTTP_VERIFIED 3 +// RPC_HTTP_SELF_SIGNED 4 + +if (!$mnet_peer->transport) exit('No transport method is approved for this host in your DB table. Please enable a transport method and try again.'); +$t[1] = 'http2 (port 443 encrypted) with a verified certificate.'; +$t[2] = 'https (port 443 encrypted) with a self-signed certificate.'; +$t[4] = 'http (port 80 unencrypted) with a verified certificate.'; +$t[8] = 'http (port 80 unencrypted) with a self-signed certificate.'; +$t[16] = 'http (port 80 unencrypted) unencrypted with no certificate.'; + +echo 'Your transportid is '.$mnet_peer->transport.' which represents '.$t[$mnet_peer->transport]."

\n"; +flush(); + +// Create a new request object +$mnet_request = new mnet_xmlrpc_client(); + +// Tell it the path to the method that we want to execute +$mnet_request->set_method($path_to_function[$func]); +// Add parameters for your function. The mnet_concatenate_strings takes three +// parameters, like mnet_concatenate_strings($string1, $string2, $string3) +// PHP is weakly typed, so you can get away with calling most things strings, +// unless it's non-scalar (i.e. an array or object or something). +foreach($paramArray[$func] as $param) { + $mnet_request->add_param($param[0], $param[1]); +} + +if (count($mnet_request->params)) { + echo 'Your parameters are:
'; + while(list($key, $val) = each($mnet_request->params)) { + echo '   '.$key.': '. $val."
\n"; + } +} +flush(); + +// We send the request: +$mnet_request->send($mnet_peer); + +?> + +A var_dump of the decoded response:
response); ?>

+ +params)) { +?> + A var_dump of the parameters you sent:
params); ?>

+ +

+ Choose a function to call:
+ system/listMethods
+ system/methodSignature
+ system/methodHelp
+ listServices
+ system/listMethods(SSO)
+ system/listMethods(concatenate)
+ + diff --git a/mnet/xmlrpc/client.php b/mnet/xmlrpc/client.php new file mode 100644 index 0000000000..ccc8cd9aa2 --- /dev/null +++ b/mnet/xmlrpc/client.php @@ -0,0 +1,225 @@ +dirroot.'/mnet/lib.php'; + +/** + * Class representing an XMLRPC request against a remote machine + */ +class mnet_xmlrpc_client { + + var $method = ''; + var $params = array(); + var $timeout = 60; + var $error = array(); + var $response = ''; + + /** + * Constructor returns true + */ + function mnet_xmlrpc_client() { + return true; + } + + /** + * Allow users to override the default timeout + * @param int $timeout Request timeout in seconds + * $return bool True if param is an integer or integer string + */ + function set_timeout($timeout) { + if (!is_integer($timeout)) { + if (is_numeric($timeout)) { + $this->timeout = (integer($timeout)); + return true; + } + return false; + } + $this->timeout = $timeout; + return true; + } + + /** + * Set the path to the method or function we want to execute on the remote + * machine. Examples: + * mod/scorm/functionname + * auth/mnet/methodname + * In the case of auth and enrolment plugins, an object will be created and + * the method on that object will be called + */ + function set_method($xmlrpcpath) { + if (is_string($xmlrpcpath)) { + $this->method = $xmlrpcpath; + $this->params = array(); + return true; + } + $this->method = ''; + $this->params = array(); + return false; + } + + /** + * Add a parameter to the array of parameters. + * + * @param string $argument A transport ID, as defined in lib.php + * @param string $type The argument type, can be one of: + * none + * empty + * base64 + * boolean + * datetime + * double + * int + * string + * array + * struct + * In its weakly-typed wisdom, PHP will (currently) + * ignore everything except datetime and base64 + * @return bool True on success + */ + function add_param($argument, $type = 'string') { + + $allowed_types = array('none', + 'empty', + 'base64', + 'boolean', + 'datetime', + 'double', + 'int', + 'i4', + 'string', + 'array', + 'struct'); + if (!in_array($type, $allowed_types)) { + return false; + } + + if ($type != 'datetime' && $type != 'base64') { + $this->params[] = $argument; + return true; + } + + // Note weirdness - The type of $argument gets changed to an object with + // value and type properties. + // bool xmlrpc_set_type ( string &value, string type ) + xmlrpc_set_type($argument, $type); + $this->params[] = $argument; + return true; + } + + /** + * Send the request to the server - decode and return the response + * + * @param object $mnet_peer A mnet_peer object with details of the + * remote host we're connecting to + * @return mixed A PHP variable, as returned by the + * remote function + */ + function send($mnet_peer) { + global $CFG, $MNET; + + $this->uri = $mnet_peer->wwwroot. + '/mnet/xmlrpc/server.php'; + + // Initialize with the target URL + $ch = curl_init($this->uri); + + $system_methods = array('system/listMethods', 'system/methodSignature', 'system/methodHelp', 'system/listServices'); + + if (in_array($this->method, $system_methods) ) { + + // Executing any system method is permitted. + + } else { + + // Find methods that we subscribe to on this host + $sql = " + SELECT + * + FROM + {$CFG->prefix}mnet_rpc r, + {$CFG->prefix}mnet_service2rpc s2r, + {$CFG->prefix}mnet_host2service h2s + WHERE + r.xmlrpc_path = '{$this->method}' AND + s2r.rpcid = r.id AND + s2r.serviceid = h2s.serviceid AND + h2s.subscribe = '1'"; + + $permission = get_record_sql($sql); + if ($permission == false) { + // TODO: Handle attempt to call not-permitted method + echo '

'.$sql.'
'; + return false; + } + + } + $this->requesttext = xmlrpc_encode_request($this->method, $this->params); + $rq = $this->requesttext; + $rq = mnet_sign_message($this->requesttext); + $this->signedrequest = $rq; + $rq = mnet_encrypt_message($rq, $mnet_peer->public_key); + $this->encryptedrequest = $rq; + + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + 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")); + + $this->rawresponse = curl_exec($ch); + if ($this->rawresponse == false) { + $this->error[] = array(curl_errno($ch), curl_error($ch)); + } + + $crypt_parser = new mnet_encxml_parser(); + $crypt_parser->parse($this->rawresponse); + + if ($crypt_parser->payload_encrypted) { + + $key = array_pop($crypt_parser->cipher); + $data = array_pop($crypt_parser->cipher); + + $crypt_parser->free_resource(); + + // Initialize payload var + $payload = ''; + + // &$payload + $isOpen = openssl_open(base64_decode($data), $payload, base64_decode($key), $MNET->get_private_key()); + + if (!$isOpen) { + return false; + } + + if (strpos(substr($payload, 0, 100), '')) { + $sig_parser = new mnet_encxml_parser(); + $sig_parser->parse($payload); + } else { + return false; + } + + } else { + $crypt_parser->free_resource(); + return false; + } + + $this->xmlrpcresponse = base64_decode($sig_parser->data_object); + $this->response = xmlrpc_decode($this->xmlrpcresponse); + curl_close($ch); + + // xmlrpc errors are pushed onto the $this->error stack + if (isset($this->response['faultCode'])) { + $this->error[] = $this->response['faultCode'] . " : " . $this->response['faultString']; + } + return empty($this->error); + } +} +?> diff --git a/mnet/xmlrpc/server.php b/mnet/xmlrpc/server.php new file mode 100644 index 0000000000..cced068284 --- /dev/null +++ b/mnet/xmlrpc/server.php @@ -0,0 +1,669 @@ +dirroot.'/mnet/lib.php'; +require_once $CFG->dirroot.'/mnet/remote_client.php'; + +// Content type for output is not html: +header('Content-type: text/xml'); + +if (!empty($CFG->mnet_rpcdebug)) { + trigger_error("HTTP_RAW_POST_DATA"); + trigger_error($HTTP_RAW_POST_DATA); +} + +// New global variable which ONLY gets set in this server page, so you know that +// if you've been called by a remote Moodle, this should be set: +$MNET_REMOTE_CLIENT = new mnet_remote_client(); + +// Peek at the message to see if it's an XML-ENC document. If it is, note that +// the client connection was encrypted, and strip the xml-encryption and +// xml-signature wrappers from the XML-RPC payload +if (strpos(substr($HTTP_RAW_POST_DATA, 0, 100), '')) { + $MNET_REMOTE_CLIENT->was_encrypted(); +// Extract the XML-RPC payload from the XML-ENC and XML-SIG wrappers. + $payload = mnet_server_strip_wrappers($HTTP_RAW_POST_DATA); +} else { + $params = xmlrpc_decode_request($HTTP_RAW_POST_DATA, $method); + if ($method == 'system.keyswap' || + $method == 'system/keyswap') { + + // OK + + } elseif ($MNET_REMOTE_CLIENT->plaintext_is_ok() == false) { + exit(mnet_server_fault(7021, 'forbidden-transport')); + } + // Looks like plaintext is ok. It is assumed that a plaintext call: + // 1. Came from a trusted host on your local network + // 2. Is *not* from a Moodle - otherwise why skip encryption/signing? + // 3. Is free to execute ANY function in Moodle + // 4. Cannot execute any methods (as it can't instantiate a class first) + // To execute a method, you'll need to create a wrapper function that first + // instantiates the class, and then calls the method. + $payload = $HTTP_RAW_POST_DATA; +} + +if (!empty($CFG->mnet_rpcdebug)) { + trigger_error("XMLRPC Payload"); + trigger_error(print_r($payload,1)); +} + +// Parse and action the XML-RPC payload +$response = mnet_server_dispatch($payload); + +/** + * Strip the encryption (XML-ENC) and signature (XML-SIG) wrappers and return the XML-RPC payload + * + * IF COMMUNICATION TAKES PLACE OVER UNENCRYPTED HTTP: + * The payload will have been encrypted with a symmetric key. This key will + * itself have been encrypted using your public key. The key is decrypted using + * your private key, and then used to decrypt the XML payload. + * + * IF COMMUNICATION TAKES PLACE OVER UNENCRYPTED HTTP *OR* ENCRYPTED HTTPS: + * In either case, there will be an XML wrapper which contains your XML-RPC doc + * as an object element, a signature for that doc, and various standards- + * compliant info to aid in verifying the signature. + * + * This function parses the encryption wrapper, decrypts the contents, parses + * the signature wrapper, and if the signature matches the payload, it returns + * the payload, which should be an XML-RPC request. + * If there is an error, or the signatures don't match, it echoes an XML-RPC + * error and exits. + * + * See the W3C's {@link http://www.w3.org/TR/xmlenc-core/ XML Encryption Syntax and Processing} + * and {@link http://www.w3.org/TR/2001/PR-xmldsig-core-20010820/ XML-Signature Syntax and Processing} + * guidelines for more detail on the XML. + * + * -----XML-Envelope--------------------------------- + * | | + * | Encrypted-Symmetric-key---------------- | + * | |_____________________________________| | + * | | + * | Encrypted data------------------------- | + * | | | | + * | | -XML-Envelope------------------ | | + * | | | | | | + * | | | --Signature------------- | | | + * | | | |______________________| | | | + * | | | | | | + * | | | --Signed-Payload-------- | | | + * | | | | | | | | + * | | | | XML-RPC Request | | | | + * | | | |______________________| | | | + * | | | | | | + * | | |_____________________________| | | + * | |_____________________________________| | + * | | + * |________________________________________________| + * + * @uses $db + * @param string $HTTP_RAW_POST_DATA The XML that the client sent + * @return string The XMLRPC payload. + */ +function mnet_server_strip_wrappers($HTTP_RAW_POST_DATA) { + global $MNET, $MNET_REMOTE_CLIENT; + if (isset($_SERVER)) { + + $crypt_parser = new mnet_encxml_parser(); + $crypt_parser->parse($HTTP_RAW_POST_DATA); + + if ($crypt_parser->payload_encrypted) { + + $key = array_pop($crypt_parser->cipher); + $data = array_pop($crypt_parser->cipher); + + $crypt_parser->free_resource(); + + // Initialize payload var + $payload = ''; + + // &$payload + $isOpen = openssl_open(base64_decode($data), $payload, base64_decode($key), $MNET->get_private_key()); + + if (!$isOpen) { + exit(mnet_server_fault(7023, 'encryption-invalid')); + } + + if (strpos(substr($payload, 0, 100), '')) { + $MNET_REMOTE_CLIENT->was_signed(); + $sig_parser = new mnet_encxml_parser(); + $sig_parser->parse($payload); + } else { + exit(mnet_server_fault(7022, 'verifysignature-error')); + } + + } else { + exit(mnet_server_fault(7024, 'payload-not-encrypted')); + } + + unset($payload); + + $host_record_exists = $MNET_REMOTE_CLIENT->set_wwwroot($sig_parser->remote_wwwroot); + + if (false == $host_record_exists) { + exit(mnet_server_fault(7020, 'wrong-wwwroot', $sig_parser->remote_wwwroot)); + } elseif (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] != $MNET_REMOTE_CLIENT->ip_address) { + exit(mnet_server_fault(7017, 'wrong-ip')); + } + + /** + * Get the certificate (i.e. public key) from the remote server. + */ + $certificate = $MNET_REMOTE_CLIENT->public_key; + + if ($certificate == false) { + exit(mnet_server_fault(709, 'nosuchpublickey')); + } + + $payload = base64_decode($sig_parser->data_object); + + // Does the signature match the data and the public cert? + $signature_verified = openssl_verify($payload, base64_decode($sig_parser->signature), $certificate); + if ($signature_verified == 1) { + // Parse the XML + } elseif ($signature_verified == 0) { + exit(mnet_server_fault(710, 'verifysignature-invalid')); + } else { + exit(mnet_server_fault(711, 'verifysignature-error')); + } + + $sig_parser->free_resource(); + + return $payload; + } else { + exit(mnet_server_fault(712, "phperror")); + } +} + +/** + * Return the proper XML-RPC content to report an error. + * + * @param int $code The ID code of the error message + * @return string $text The text of the error message + */ +function mnet_server_fault($code, $text, $param = null) { + if (!is_numeric($code)) { + $code = 0; + } + $code = intval($code); + + $text = get_string($text, 'mnet', $param); + + // Replace illegal XML chars - is this already in a lib somewhere? + $text = str_replace(array('<','>','&','"',"'"), array('<','>','&','"','''), $text); + + $return = mnet_server_prepare_response(' + + + + + + faultCode + '.$code.' + + + faultString + '.$text.' + + + + + '); + return $return; +} + +/** + * Dummy function for the XML-RPC dispatcher - use to call a method on an object + * or to call a function + * + * Translate XML-RPC's strange function call syntax into a more straightforward + * PHP-friendly alternative. This dummy function will be called by the + * dispatcher, and can be used to call a method on an object, or just a function + * + * The methodName argument (eg. mnet/testlib/mnet_concatenate_strings) + * is ignored. + * + * @param string $methodname We discard this - see 'functionname' + * @param array $argsarray Each element is an argument to the real + * function + * @param string $functionname The name of the PHP function you want to call + * @return mixed The return value will be that of the real + * function, whateber it may be. + */ +function mnet_server_dummy_method($methodname, $argsarray, $functionname) { + global $MNET_REMOTE_CLIENT; + + if ($MNET_REMOTE_CLIENT->object_to_call == false) { + return @call_user_func_array($functionname, $argsarray); + } else { + return @call_user_method_array($functionname, $MNET_REMOTE_CLIENT->object_to_call, $argsarray); + } +} + +/** + * Package a response in any required envelope, and return it to the client + * + * @param string $response The XMLRPC response string + * @return string The encoded response string + */ +function mnet_server_prepare_response($response) { + global $MNET_REMOTE_CLIENT; + + if ($MNET_REMOTE_CLIENT->request_was_signed) { + $response = mnet_sign_message($response); + } + + if ($MNET_REMOTE_CLIENT->request_was_encrypted) { + $response = mnet_encrypt_message($response, $MNET_REMOTE_CLIENT->public_key); + } + + return $response; +} + +/** + * If security checks are passed, dispatch the request to the function/method + * + * The config variable 'mnet_dispatcher_mode' can be: + * strict: Only execute functions that are in specific files + * off: The default - don't execute anything + * + * @param string $payload The XML-RPC request + * @return No return val - just echo the response + */ +function mnet_server_dispatch($payload) { + global $CFG, $MNET_REMOTE_CLIENT; + // xmlrpc_decode_request returns an array of parameters, and the $method + // variable (which is passed by reference) is instantiated with the value from + // the methodName tag in the xml payload + // xmlrpc_decode_request($xml, &$method) + $params = xmlrpc_decode_request($payload, $method); + + // $method is something like: "mod/forum/lib/forum_add_instance" + // $params is an array of parameters. A parameter might itself be an array. + + // Whitelist characters that are permitted in a method name + // The method name must not begin with a / - avoid absolute paths + // A dot character . is only allowed in the filename, i.e. something.php + if (0 == preg_match("@^[A-Za-z0-9]+/[A-Za-z0-9/_-]+(\.php/)?[A-Za-z0-9_-]+$@",$method)) { + exit(mnet_server_fault(713, 'nosuchfunction')); + } + + $callstack = explode('/', $method); + // callstack will look like array('mod', 'forum', 'lib', 'forum_add_instance'); + + /** + * What has the site administrator chosen as his dispatcher setting? + * strict: Only execute functions that are in specific files + * off: The default - don't execute anything + */ + ////////////////////////////////////// OFF + if (!isset($CFG->mnet_dispatcher_mode) ) { + set_config('mnet_dispatcher_mode', 'off'); + exit(mnet_server_fault(704, 'nosuchservice')); + } elseif ('off' == $CFG->mnet_dispatcher_mode) { + exit(mnet_server_fault(704, 'nosuchservice')); + + ////////////////////////////////////// SYSTEM METHODS + } elseif ($callstack[0] == 'system') { + $functionname = $callstack[1]; + $xmlrpcserver = xmlrpc_server_create(); + + // I'm adding the canonical xmlrpc references here, however we've + // already forbidden that the period (.) should be allowed in the call + // stack, so if someone tries to access our XMLRPC in the normal way, + // they'll already have received a RPC server fault message. + + // Maybe we should allow an easement so that regular XMLRPC clients can + // call our system methods, and find out what we have to offer? + + xmlrpc_server_register_method($xmlrpcserver, 'system.listMethods', 'mnet_system'); + xmlrpc_server_register_method($xmlrpcserver, 'system/listMethods', 'mnet_system'); + + xmlrpc_server_register_method($xmlrpcserver, 'system.methodSignature', 'mnet_system'); + xmlrpc_server_register_method($xmlrpcserver, 'system/methodSignature', 'mnet_system'); + + xmlrpc_server_register_method($xmlrpcserver, 'system.methodHelp', 'mnet_system'); + xmlrpc_server_register_method($xmlrpcserver, 'system/methodHelp', 'mnet_system'); + + xmlrpc_server_register_method($xmlrpcserver, 'system.listServices', 'mnet_system'); + xmlrpc_server_register_method($xmlrpcserver, 'system/listServices', 'mnet_system'); + + xmlrpc_server_register_method($xmlrpcserver, 'system.keyswap', 'mnet_keyswap'); + xmlrpc_server_register_method($xmlrpcserver, 'system/keyswap', 'mnet_keyswap'); + + if ($method == 'system.listMethods' || + $method == 'system/listMethods' || + $method == 'system.methodSignature' || + $method == 'system/methodSignature' || + $method == 'system.methodHelp' || + $method == 'system/methodHelp' || + $method == 'system.listServices' || + $method == 'system/listServices' || + $method == 'system.keyswap' || + $method == 'system/keyswap') { + + $response = xmlrpc_server_call_method($xmlrpcserver, $payload, $MNET_REMOTE_CLIENT); + $response = mnet_server_prepare_response($response); + } else { + exit(mnet_server_fault(7018, 'nosuchfunction')); + } + + xmlrpc_server_destroy($xmlrpcserver); + echo $response; + ////////////////////////////////////// STRICT AUTH + } elseif ($callstack[0] == 'auth') { + + // Break out the callstack into its elements + list($base, $plugin, $filename, $methodname) = $callstack; + + // We refuse to include anything that is not auth.php + if ($filename == 'auth.php' && is_enabled_auth($plugin)) { + $authclass = 'auth_plugin_'.$plugin; + $includefile = '/auth/'.$plugin.'/auth.php'; + $response = mnet_server_invoke_method($includefile, $methodname, $method, $payload, $authclass); + $response = mnet_server_prepare_response($response); + echo $response; + } else { + // Generate error response - unable to locate function + exit(mnet_server_fault(702, 'nosuchfunction')); + } + + ////////////////////////////////////// STRICT ENROL + } elseif ($callstack[0] == 'enrol') { + + // Break out the callstack into its elements + list($base, $plugin, $filename, $methodname) = $callstack; + + if ($filename == 'enrol.php' && is_enabled_enrol($plugin)) { + $enrolclass = 'enrolment_plugin_'.$plugin; + $includefile = '/enrol/'.$plugin.'/enrol.php'; + $response = mnet_server_invoke_method($includefile, $methodname, $method, $payload, $enrolclass); + $response = mnet_server_prepare_response($response); + echo $response; + } else { + // Generate error response - unable to locate function + exit(mnet_server_fault(703, 'nosuchfunction')); + } + + ////////////////////////////////////// STRICT MOD/* + } elseif ($callstack[0] == 'mod' || 'promiscuous' == $CFG->mnet_dispatcher_mode) { + list($base, $module, $filename, $functionname) = $callstack; + + ////////////////////////////////////// STRICT MOD/* + if ($base == 'mod' && $filename == 'rpclib.php') { + $includefile = '/mod/'.$module.'/rpclib.php'; + $response = mnet_server_invoke_method($includefile, $functionname, $method, $payload); + $response = mnet_server_prepare_response($response); + echo $response; + + ////////////////////////////////////// PROMISCUOUS + } elseif ('promiscuous' == $CFG->mnet_dispatcher_mode && $MNET_REMOTE_CLIENT->plaintext_is_ok()) { + + $functionname = array_pop($callstack); + $filename = array_pop($callstack); + + if ($MNET_REMOTE_CLIENT->plaintext_is_ok()) { + + // The call stack holds the path to any include file + $includefile = $CFG->dirroot.'/'.implode('/',$callstack).'/'.$filename.'.php'; + + $response = mnet_server_invoke_function($includefile, $functionname, $method, $payload); + echo $response; + } + + } else { + // Generate error response - unable to locate function + exit(mnet_server_fault(7012, 'nosuchfunction')); + } + + } else { + // Generate error response - unable to locate function + exit(mnet_server_fault(7012, 'nosuchfunction')); + } +} + +/** + * Execute the system functions - mostly for introspection + * + * @param string $method XMLRPC method name, e.g. system.listMethods + * @param array $params Array of parameters from the XMLRPC request + * @param string $hostinfo Hostinfo object from the mnet_host table + * @return mixed Response data - any kind of PHP variable + */ +function mnet_system($method, $params, $hostinfo) { + global $CFG; + + if (empty($hostinfo)) return array(); + + $id_list = $hostinfo->id; + if (!empty($CFG->mnet_all_hosts_id)) { + $id_list .= ', '.$CFG->mnet_all_hosts_id; + } + + if ('system.listMethods' == $method || 'system/listMethods' == $method) { + if (count($params) == 0) { + $query = ' + SELECT DISTINCT + rpc.function_name, + rpc.xmlrpc_path, + rpc.enabled, + rpc.help, + rpc.profile + FROM + '.$CFG->prefix.'mnet_host2service h2s, + '.$CFG->prefix.'mnet_service2rpc s2r, + '.$CFG->prefix.'mnet_rpc rpc + WHERE + s2r.rpcid = rpc.id AND + h2s.serviceid = s2r.serviceid AND + h2s.hostid in ('.$id_list .') + ORDER BY + rpc.xmlrpc_path ASC'; + + } else { + $query = ' + SELECT DISTINCT + rpc.function_name, + rpc.xmlrpc_path, + rpc.enabled, + rpc.help, + rpc.profile + FROM + '.$CFG->prefix.'mnet_host2service h2s, + '.$CFG->prefix.'mnet_service2rpc s2r, + '.$CFG->prefix.'mnet_service svc, + '.$CFG->prefix.'mnet_rpc rpc + WHERE + s2r.rpcid = rpc.id AND + h2s.serviceid = s2r.serviceid AND + h2s.hostid in ('.$id_list .') AND + svc.id = h2s.serviceid AND + svc.name = \''.$params[0].'\' + ORDER BY + rpc.xmlrpc_path ASC'; + + } + $resultset = array_values(get_records_sql($query)); + $methods = array(); + foreach($resultset as $result) { + $methods[] = $result->xmlrpc_path; + } + return $methods; + } elseif ('system.methodSignature' == $method || 'system/methodSignature' == $method) { + $query = ' + SELECT DISTINCT + rpc.function_name, + rpc.xmlrpc_path, + rpc.enabled, + rpc.help, + rpc.profile + FROM + '.$CFG->prefix.'mnet_host2service h2s, + '.$CFG->prefix.'mnet_service2rpc s2r, + '.$CFG->prefix.'mnet_rpc rpc + WHERE + rpc.xmlrpc_path = \''.$params[0].'\' AND + s2r.rpcid = rpc.id AND + h2s.serviceid = s2r.serviceid AND + h2s.hostid in ('.$id_list .')'; + + $result = get_records_sql($query); + $methodsigs = array(); + + if (is_array($result)) { + foreach($result as $method) { + $methodsigs[] = unserialize($method->profile); + } + } + + return $methodsigs; + } elseif ('system.methodHelp' == $method || 'system/methodHelp' == $method) { + $query = ' + SELECT DISTINCT + rpc.function_name, + rpc.xmlrpc_path, + rpc.enabled, + rpc.help, + rpc.profile + FROM + '.$CFG->prefix.'mnet_host2service h2s, + '.$CFG->prefix.'mnet_service2rpc s2r, + '.$CFG->prefix.'mnet_rpc rpc + WHERE + rpc.xmlrpc_path = \''.$params[0].'\' AND + s2r.rpcid = rpc.id AND + h2s.serviceid = s2r.serviceid AND + h2s.hostid in ('.$id_list .')'; + + $result = get_record_sql($query); + + if (is_object($result)) { + return $result->help; + } + } elseif ('system.listServices' == $method || 'system/listServices' == $method) { + $query = ' + SELECT DISTINCT + s.id, + s.name, + s.apiversion, + h2s.publish, + h2s.subscribe + FROM + '.$CFG->prefix.'mnet_host2service h2s, + '.$CFG->prefix.'mnet_service s + WHERE + h2s.serviceid = s.id AND + h2s.hostid in ('.$id_list .') + ORDER BY + s.name ASC'; + + $result = get_records_sql($query); + $services = array(); + + if (is_array($result)) { + foreach($result as $service) { + $services[] = array('name' => $service->name, + 'apiversion' => $service->apiversion, + 'publish' => $service->publish, + 'subscribe' => $service->subscribe); + } + } + + return $services; + } + exit(mnet_server_fault(7019, 'nosuchfunction')); +} + +/** + * Initialize the object (if necessary), execute the method or function, and + * return the response + * + * @param string $includefile The file that contains the object definition + * @param string $methodname The name of the method to execute + * @param string $method The full path to the method + * @param string $payload The XML-RPC request payload + * @param string $class The name of the class to instantiate (or false) + * @return string The XML-RPC response + */ +function mnet_server_invoke_method($includefile, $methodname, $method, $payload, $class=false) { + + $permission = mnet_permit_rpc_call($includefile, $methodname, $class); + + if (RPC_NOSUCHFILE == $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(705, 'nosuchfile', $includefile)); + } + + if (RPC_NOSUCHFUNCTION == $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(706, 'nosuchfunction')); + } + + if (RPC_FORBIDDENFUNCTION == $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(707, 'forbidden-function')); + } + + if (RPC_NOSUCHCLASS == $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(7013, 'nosuchfunction')); + } + + if (RPC_NOSUCHMETHOD == $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(7014, 'nosuchmethod')); + } + + if (RPC_NOSUCHFUNCTION == $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(7014, 'nosuchmethod')); + } + + if (RPC_FORBIDDENMETHOD == $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(7015, 'nosuchfunction')); + } + + if (0 < $permission) { + // Generate error response - unable to locate function + exit(mnet_server_fault(7019, 'unknownerror')); + } + + if (RPC_OK == $permission) { + $xmlrpcserver = xmlrpc_server_create(); + $bool = xmlrpc_server_register_method($xmlrpcserver, $method, 'mnet_server_dummy_method'); + $response = xmlrpc_server_call_method($xmlrpcserver, $payload, $methodname); + $bool = xmlrpc_server_destroy($xmlrpcserver); + return $response; + } +} + +function mnet_keyswap($function, $params) { + global $CFG; + $return = array(); + + if (!empty($CFG->mnet_register_allhosts)) { + $mnet_peer = new mnet_peer(); + $keyok = $mnet_peer->bootstrap($params[0]); + if ($keyok) { + $mnet_peer->commit(); + } + } + $keypair = mnet_get_keypair(); + return $keypair['certificate']; +} +?> diff --git a/mnet/xmlrpc/xmlparser.php b/mnet/xmlrpc/xmlparser.php new file mode 100644 index 0000000000..c1af97df1c --- /dev/null +++ b/mnet/xmlrpc/xmlparser.php @@ -0,0 +1,242 @@ +initialise(); + } + + /** + * Set default element handlers and initialise properties to empty. + * + * @return bool True + */ + function initialise() { + $this->parser = xml_parser_create(); + xml_set_object($this->parser, $this); + + xml_set_element_handler($this->parser, "start_element", "end_element"); + xml_set_character_data_handler($this->parser, "discard_data"); + + $this->tag_number = 0; // Just a unique ID for each tag + $this->digest = ''; + $this->remote_wwwroot = ''; + $this->signature = ''; + $this->data_object = ''; + $this->key_URI = ''; + $this->payload_encrypted = false; + $this->cipher = array(); + return true; + } + + /** + * Parse a block of XML text + * + * The XML Text will be an XML-RPC request which is wrapped in an XML doc + * with a signature from the sender. This envelope may be encrypted and + * delivered within another XML envelope with a symmetric key. The parser + * should first decrypt this XML, and then place the XML-RPC request into + * the data_object property, and the signature into the signature property. + * + * See the W3C's {@link http://www.w3.org/TR/xmlenc-core/ XML Encryption Syntax and Processing} + * and {@link http://www.w3.org/TR/2001/PR-xmldsig-core-20010820/ XML-Signature Syntax and Processing} + * guidelines for more detail on the XML. + * + * -----XML-Envelope--------------------------------- + * | | + * | Symmetric-key-------------------------- | + * | |_____________________________________| | + * | | + * | Encrypted data------------------------- | + * | | | | + * | | -XML-Envelope------------------ | | + * | | | | | | + * | | | --Signature------------- | | | + * | | | |______________________| | | | + * | | | | | | + * | | | --Signed-Payload-------- | | | + * | | | | | | | | + * | | | | XML-RPC Request | | | | + * | | | |______________________| | | | + * | | | | | | + * | | |_____________________________| | | + * | |_____________________________________| | + * | | + * |________________________________________________| + * + * @uses $MNET + * @param string $data The XML that you want to parse + * @return bool True on success - false on failure + */ + function parse($data) { + global $MNET, $MNET_REMOTE_CLIENT; + + $p = xml_parse($this->parser, $data); + + if (count($this->cipher) > 0) { + $this->payload_encrypted = true; + } + + return (bool)$p; + } + + /** + * Destroy the parser and free up any related resource. + */ + function free_resource() { + $free = xml_parser_free($this->parser); + } + + /** + * Set the character-data handler to the right function for each element + * + * For each tag (element) name, this function switches the character-data + * handler to the function that handles that element. Note that character + * data is referred to the handler in blocks of 1024 bytes. + * + * @param mixed $parser The XML parser + * @param string $name The name of the tag, e.g. method_call + * @param array $attrs The tag's attributes (if any exist). + * @return bool True + */ + function start_element($parser, $name, $attrs) { + $this->tag_number++; + $handler = 'discard_data'; + switch(strtoupper($name)) { + case 'DIGESTVALUE': + $handler = 'parse_digest'; + break; + case 'SIGNATUREVALUE': + $handler = 'parse_signature'; + break; + case 'OBJECT': + $handler = 'parse_object'; + break; + case 'RETRIEVALMETHOD': + $this->key_URI = $attrs['URI']; + break; + case 'WWWROOT': + $handler = 'parse_wwwroot'; + break; + case 'CIPHERVALUE': + $this->cipher[$this->tag_number] = ''; + $handler = 'parse_cipher'; + break; + default: + break; + } + xml_set_character_data_handler($this->parser, $handler); + return true; + } + + /** + * Add the next chunk of character data to the cipher string for that tag + * + * The XML parser calls the character-data handler with 1024-character + * chunks of data. This means that the handler may be called several times + * for a single tag, so we use the concatenate operator (.) to build the + * tag content into a string. + * We should not encounter more than one of each tag type, except for the + * cipher tag. We will often see two of those. We prevent the content of + * these two tags being concatenated together by counting each tag, and + * using its 'number' as the key to an array of ciphers. + * + * @param mixed $parser The XML parser + * @param string $data The content of the current tag (1024 byte chunk) + * @return bool True + */ + function parse_cipher($parser, $data) { + $this->cipher[$this->tag_number] .= $data; + return true; + } + + /** + * Add the next chunk of character data to the remote_wwwroot string + * + * @param mixed $parser The XML parser + * @param string $data The content of the current tag (1024 byte chunk) + * @return bool True + */ + function parse_wwwroot($parser, $data) { + $this->remote_wwwroot .= $data; + return true; + } + + /** + * Add the next chunk of character data to the digest string + * + * @param mixed $parser The XML parser + * @param string $data The content of the current tag (1024 byte chunk) + * @return bool True + */ + function parse_digest($parser, $data) { + $this->digest .= $data; + return true; + } + + /** + * Add the next chunk of character data to the signature string + * + * @param mixed $parser The XML parser + * @param string $data The content of the current tag (1024 byte chunk) + * @return bool True + */ + function parse_signature($parser, $data) { + $this->signature .= $data; + return true; + } + + /** + * Add the next chunk of character data to the data_object string + * + * @param mixed $parser The XML parser + * @param string $data The content of the current tag (1024 byte chunk) + * @return bool True + */ + function parse_object($parser, $data) { + $this->data_object .= $data; + return true; + } + + /** + * Discard the next chunk of character data + * + * This is used for tags that we're not interested in. + * + * @param mixed $parser The XML parser + * @param string $data The content of the current tag (1024 byte chunk) + * @return bool True + */ + function discard_data($parser, $data) { + // Not interested + return true; + } + + /** + * Switch the character-data handler to ignore the next chunk of data + * + * @param mixed $parser The XML parser + * @param string $name The name of the tag, e.g. method_call + * @return bool True + */ + function end_element($parser, $name) { + $ok = xml_set_character_data_handler($this->parser, "discard_data"); + return true; + } +} +?>