summaryrefslogblamecommitdiffstats
path: root/modules-available/dozmod/api.inc.php
blob: b5030cc5f732966ea7ec851568272c6686b290ff (plain) (tree)
1
2
3
4
5
6
7
8
9
10
     
                                                              




                                                                            
                                             

   
 
 

                                
                                        
                                        
 
 
 

                                                            
                                                                     
 

                                                                    
                                 
 
                                    

 
                                                   
 
                                               

 
                                                    
 
                                                



                                                                                            

 
                                     
 

                                                
 


                                           
                      
                                                                       

 
                                       
 

                                                


                                 

                                              
 





                                                
                                                                            
 
 

                                                                     
 

                                                                
                                  

                                  

 
   


                                                      
                              
   
                                                    
 





                                                                                                                  

                                  
 

                                       


                                                                                                 

                      

 

                                     








                                                          

                                                                                                                                  


























                                                                                                                                              
                                                        
                                                           
 


                                                                                  

                                                                                     
                                           

                                                                                                                              


                                                                                


                                                                                        
                                    




                                                          










                                                                



                                                                       
                             
                                                     

                                  
                                                                                  
         
                                                           







                                          

 
                                                        
 
                                                        

 
                                                        
 
                                                       
 
 
                                                                     
 
                                             
                                    

                                           
                             
                                                        

                                  
                                                                                  
         

                                                                                             
                                  
         
                         

 
                                           
                                                                     
 






                                                                                
                                               
                              





                                                                                                                     

                                         
                                                              
                                     
                               



                                                   


                                        

 
           

                                 
                                               
                                            
 
 
                                                     
 




                                                            
                                                              








                                                                               

 
 


                                                                 
                                 









                                                          
                                                                                  
                               
                                                      
 
                          
                                                                                 
 

                                                 

                                           
 
 

                                      
                             


 
                           
                                               
                                                              
 
                           

                                                   


                                          


                                                   
 
                         
<?php
/* small API server that acts as a proxy to the dozmod server.
* To reduce the number of requests and connections to dozmod-server, results
* gets cached into a file cache.
*
* Required Configuration:
* CONFIG_DOZMOD_EXPIRE: Expiration time in seconds for the cache
* CONFIG_DOZMOD_URL: URL to the dozmod server
*
**/


use JetBrains\PhpStorm\NoReturn;

if (!Module::isAvailable('locations')) {
	die('require locations module');
}


define('LIST_URL', CONFIG_DOZMOD_URL . '/vmchooser/list');
define('VMX_URL', CONFIG_DOZMOD_URL . '/vmchooser/lecture');
$availableRessources = ['list', 'netrules', 'metadata', 'imagemeta'];

/* BEGIN: A simple caching mechanism ---------------------------- */

function cache_hash($obj): string
{
	return md5(serialize($obj));
}

function cache_key_to_filename(string $key): string
{
	return "/tmp/bwlp-slxadmin-cache-$key";
}

function cache_put(string $key, string $value): void
{
	$filename = cache_key_to_filename($key);
	// Try to avoid another client concurrently accessing the cache seeing an empty file
	$tmp = $filename . '-' . mt_rand();
	file_put_contents($tmp, $value);
	rename($tmp, $filename);
}

function cache_has(string $key): bool
{
	$filename = cache_key_to_filename($key);
	$mtime = @filemtime($filename);

	if ($mtime === false) {
		return false; // cache miss
	}
	$now = time();
	return $now >= $mtime && $now - $mtime <= CONFIG_DOZMOD_EXPIRE;
}

function cache_get(string $key): string
{
	$filename = cache_key_to_filename($key);
	return file_get_contents($filename);
}

/* good for large binary files */
#[NoReturn]
function cache_get_passthru(string $key): void
{
	$filename = cache_key_to_filename($key);
	$fp = fopen($filename, "r");
	if ($fp) {
		fpassthru($fp);
		exit;
	}
	error_log('DMSD-cache: Cannot passthrough cache file ' . $filename);
}

/* END: Cache ---------------------------------------------------- */


/* this script requires 2 (3 with implicit client ip) parameters
*
* resource     = list,metadata,...
* lecture_uuid = client can choose
**/


/**
 * Takes raw lecture list xml, returns array of uuids.
 *
 * @param string $responseXML XML from dozmod server
 * @return array list of UUIDs
 */
function xmlToLectureIds(string $responseXML): array
{
	try {
		$xml = new SimpleXMLElement($responseXML);
	} catch (Exception $e) {
		EventLog::warning('Error parsing XML response data from DMSD: ' . $e->getMessage(), $responseXML);
		return [];
	}
	if (!isset($xml->eintrag))
		return [];

	$uuids = [];
	foreach ($xml->eintrag as $e) {
		if (isset($e->uuid) && isset($e->uuid['param']) && isset($e->uuid['param'][0])) {
			$uuids[] = strval($e->uuid['param'][0]);
		}
	}
	return $uuids;
}

#[NoReturn]
function sendExamModeMismatch(): void
{
	Header('Content-Type: text/xml; charset=utf-8');
	echo
	<<<BLA
	<settings>
		<eintrag>
			<image_name param="null"/>
			<priority param="100"/>
			<creator param="Ernie Esslingen"/>
			<short_description param="Prüfungsmodus geändert, bitte PC neustarten"/>
			<long_description param="Der Prüfungsmodus wurde ein- oder ausgeschaltet, bitte starten Sie den PC neu"/>
			<uuid param="exam-mode-warning"/>
			<virtualmachine param="exam-mode-warning"/>
			<os param="debian8"/>
			<virtualizer_name param="null"/>
			<os_name param="null"/>
			<for_location param="0"/>
			<is_template param="0"/>
		</eintrag>
		<eintrag>
			<image_name param="null"/>
			<priority param="200"/>
			<creator param="Ernie Esslingen"/>
			<short_description param="Exam mode changed, please reboot PC"/>
			<long_description param="Exam mode has been activated or deactivated since this PC was booted; please reboot the PC"/>
			<uuid param="exam-mode-warning"/>
			<virtualmachine param="exam-mode-warning"/>
			<os param="debian8"/>
			<virtualizer_name param="null"/>
			<os_name param="null"/>
			<for_location param="0"/>
			<is_template param="0"/>
		</eintrag>
	</settings>
BLA;
	exit(0);
}

/** Caching wrapper around _getLecturesForLocations() */
function getListForLocations(array $locationIds, bool $raw)
{
	/* if in any of the locations there is an exam active, consider the client
		 to be in "exam-mode" and only offer him exams (no lectures) */
	$key = 'lectures_' . cache_hash($locationIds);
	$examMode = Request::get('exams', 'normal-mode', 'string') !== 'normal-mode';
	$clientServerMismatch = false;
	if (Module::isAvailable('exams')) {
		// If we have the exam mode module, we can enforce a server side check and make sure it agrees with the client
		$serverExamMode = Exams::isInExamMode($locationIds);
		if ($raw) {
			$clientServerMismatch = ($serverExamMode !== $examMode);
		}
		$examMode = $serverExamMode;
	}
	// Only enforce exam mode validity check if the client requests the raw xml data
	if ($clientServerMismatch) {
		sendExamModeMismatch(); // does not return
	}
	// Proceed normally from here on
	if ($examMode) {
		$key .= '_exams';
	}
	$rawKey = $key . '_raw';
	if ($raw) {
		Header('Content-Type: text/xml; charset=utf-8');
		if (cache_has($rawKey)) {
			cache_get_passthru($rawKey);
		}
	} elseif (cache_has($key)) {
		return unserialize(cache_get($key));
	}
	// Not in cache
	$url = LIST_URL . "?locations=" . implode('%20', $locationIds);
	if ($examMode) {
		$url .= '&exams';
	}
	$t = microtime(true);
	$value = Download::asString($url, 60, $code);
	$t = microtime(true) - $t;
	if ($t > 5) {
		error_log("DMSD-cache: Download of lecture list took $t ($code)");
	}
	if ($value === false || $code < 200 || $code > 299)
		return false;
	cache_put($rawKey, $value);
	$list = xmlToLectureIds($value);
	cache_put($key, serialize($list));
	if ($raw) {
		die($value);
	}
	return $list;
}

function getLectureUuidsForLocations(array $locationIds)
{
	return getListForLocations($locationIds, false);
}

function outputLectureXmlForLocation(array $locationIds)
{
	return getListForLocations($locationIds, true);
}

function _getVmData(string $lecture_uuid, string $subResource = null)
{
	$url = VMX_URL . '/' . $lecture_uuid;
	if ($subResource !== null) {
		$url .= '/' . $subResource;
	}
	$t = microtime(true);
	$response = Download::asString($url, 60, $code);
	$t = microtime(true) - $t;
	if ($t > 5) {
		error_log("DMSD-cache: Download of $subResource took $t ($code)");
	}
	if ($code < 200 || $code > 299) {
		error_log("DMSD-cache: Return code $code, payload len " . strlen($response));
		return (int)$code;
	}
	return $response;
}

/** Caching wrapper around _getVmData() **/
function outputResource(string $lecture_uuid, string $resource): void
{
	if ($resource === 'metadata') {
		// HACK: config.tgz is compressed, don't use gzip output handler
		@ob_end_clean();
		Header('Content-Type: application/gzip');
	} else {
		Header('Content-Type: text/plain; charset=utf-8');
	}
	$key = $resource . '_' . $lecture_uuid;
	if (cache_has($key)) {
		if ($resource === 'metadata' || $resource === 'vmx') {
			// HACK HACK HACK: Update launch counter as it was cached,
			// otherwise dmsd would take care of increasing it...
			Database::exec("UPDATE sat.lecture SET usecount = usecount + 1 WHERE lectureid = :lectureid",
				['lectureid' => $lecture_uuid], true);
		}
		cache_get_passthru($key);
	} else {
		$value = _getVmData($lecture_uuid, $resource);
		if ($value === false)
			return;
		if (is_int($value)) {
			http_response_code($value);
			exit;
		}
		cache_put($key, $value);
		die($value);
	}
}

#[NoReturn]
function fatalDozmodUnreachable()
{
	Header('HTTP/1.1 504 Gateway Timeout');
	die('DMSD currently not available');
}

function readLectureParam(array $locationIds): string
{
	$lecture = Request::get('lecture', false, 'string');
	if ($lecture === false) {
		Header('HTTP/1.1 400 Bad Request');
		die('Missing lecture UUID');
	}
	$lectures = getLectureUuidsForLocations($locationIds);
	if ($lectures === false) {
		fatalDozmodUnreachable();
	}
	/* check that the user requests a lecture that he is allowed to have */
	if (!in_array($lecture, $lectures)) {
		Header('HTTP/1.1 403 Forbidden');
		die("You don't have permission to access this lecture");
	}
	return $lecture;
}


// in this context the lecture param is an image id (container), 
// just read and check if valid.
// TODO do we need to check if this is allowed?
function readImageParam(): string
{
	$image = Request::get('lecture', false, 'string');
		
	if ($image === false) {
		Header('HTTP/1.1 400 Bad Request');
		die('Missing IMAGE UUID');
	}
	return $image;
}

// -----------------------------------------------------------------------------//
/* request data, don't trust */
$resource = Request::get('resource', false, 'string');

if ($resource === false) {
	ErrorHandler::traceError("you have to specify the 'resource' parameter");
}

if (!in_array($resource, $availableRessources)) {
	Header('HTTP/1.1 400 Bad Request');
	die("unknown resource: $resource");
}

$ip = $_SERVER['REMOTE_ADDR'];
if (substr($ip, 0, 7) === '::ffff:') {
	$ip = substr($ip, 7);
}


/* lookup location id(s) */
$location_ids = Location::getFromIp($ip, true);
$location_ids = Location::getLocationRootChain($location_ids);

if ($resource === 'list') {
	outputLectureXmlForLocation($location_ids);
	// Won't return on success...
} elseif ($resource === 'imagemeta') {
	$image = readImageParam();
	outputResource($image, $resource);
} else {
	$lecture = readLectureParam($location_ids);
	outputResource($lecture, $resource);
}
fatalDozmodUnreachable();