<?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="Klausurmodus geändert, bitte PC neustarten"/>
<long_description param="Der Klausurmodus 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();