summaryrefslogtreecommitdiffstats
path: root/modules-available/remoteaccess
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/remoteaccess')
-rw-r--r--modules-available/remoteaccess/api.inc.php98
-rw-r--r--modules-available/remoteaccess/baseconfig/getconfig.inc.php50
-rw-r--r--modules-available/remoteaccess/config.json7
-rw-r--r--modules-available/remoteaccess/hooks/client-update.inc.php9
-rw-r--r--modules-available/remoteaccess/hooks/cron.inc.php3
-rw-r--r--modules-available/remoteaccess/inc/remoteaccess.inc.php117
-rw-r--r--modules-available/remoteaccess/install.inc.php80
-rw-r--r--modules-available/remoteaccess/lang/de/messages.json8
-rw-r--r--modules-available/remoteaccess/lang/de/module.json4
-rw-r--r--modules-available/remoteaccess/lang/de/permissions.json7
-rw-r--r--modules-available/remoteaccess/lang/de/template-tags.json27
-rw-r--r--modules-available/remoteaccess/lang/en/messages.json8
-rw-r--r--modules-available/remoteaccess/lang/en/module.json4
-rw-r--r--modules-available/remoteaccess/lang/en/permissions.json7
-rw-r--r--modules-available/remoteaccess/lang/en/template-tags.json27
-rw-r--r--modules-available/remoteaccess/page.inc.php196
-rw-r--r--modules-available/remoteaccess/permissions/permissions.json17
-rw-r--r--modules-available/remoteaccess/templates/edit-group.html56
-rw-r--r--modules-available/remoteaccess/templates/edit-settings.html142
19 files changed, 867 insertions, 0 deletions
diff --git a/modules-available/remoteaccess/api.inc.php b/modules-available/remoteaccess/api.inc.php
new file mode 100644
index 00000000..c558d126
--- /dev/null
+++ b/modules-available/remoteaccess/api.inc.php
@@ -0,0 +1,98 @@
+<?php
+
+$ip = $_SERVER['REMOTE_ADDR'];
+if (substr($ip, 0, 7) === '::ffff:') $ip = substr($ip, 7);
+
+$password = Request::post('password', false, 'string');
+if ($password !== false) {
+ $c = Database::queryFirst("SELECT machineuuid FROM machine
+ WHERE clientip = :ip
+ ORDER BY lastseen DESC
+ LIMIT 1", ['ip' => $ip]);
+ if ($c !== false) {
+ $vncport = Request::post('vncport', 5900, 'int');
+ Database::exec("INSERT INTO remoteaccess_machine (machineuuid, password, vncport)
+ VALUES (:uuid, :passwd, :vncport)
+ ON DUPLICATE KEY UPDATE
+ password = VALUES(password), vncport = VALUES(vncport)",
+ ['uuid' => $c['machineuuid'], 'passwd' => $password, 'vncport' => $vncport]);
+ }
+ exit;
+}
+
+$range = IpUtil::parseCidr(Property::get(RemoteAccess::PROP_ALLOWED_VNC_NET));
+if ($range === null) {
+ die('No allowed IP defined');
+}
+$iplong = ip2long($ip);
+if ($iplong < $range['start'] || $iplong > $range['end']) {
+ die('Access denied');
+}
+
+$headers = getallheaders();
+$version = false;
+if (!empty($headers['Bwlp-Plugin-Build-Revision'])) {
+ $version = substr($headers['Bwlp-Plugin-Build-Revision'], 0, 6);
+ if (!empty($headers['Bwlp-Plugin-Build-Timestamp'])) {
+ $ts = $headers['Bwlp-Plugin-Build-Timestamp'];
+ if (is_numeric($ts)) {
+ if ($ts > 9999999999) {
+ $ts = round($ts / 1000);
+ }
+ $ts = date('d.m.Y H:i', $ts);
+ }
+ $version .= ' (' . $ts . ')';
+ }
+}
+Property::set(RemoteAccess::PROP_PLUGIN_VERSION, $version, 2880);
+
+Header('Content-Type: application/json');
+
+$remoteLocations = RemoteAccess::getEnabledLocations();
+
+if (empty($remoteLocations)) {
+ $rows = [];
+} else {
+// TODO fail-counter for WOL, so we can ignore machines that apparently can't be woken up
+// -> Reset counter in our ~poweron hook, but only if the time roughly matches a WOL attempt (within ~5 minutes)
+ $rows = Database::queryAll("SELECT m.clientip, m.locationid, m.state, ram.password, ram.vncport, ram.woltime FROM machine m
+ LEFT JOIN remoteaccess_machine ram ON (ram.machineuuid = m.machineuuid AND (ram.password IS NOT NULL OR m.state <> 'IDLE'))
+ LEFT JOIN runmode r ON (r.machineuuid = m.machineuuid)
+ WHERE m.locationid IN (:locs)
+ AND r.machineuuid IS NULL",
+ ['locs' => $remoteLocations]);
+
+ $wolCut = time() - 90;
+ foreach ($rows as &$row) {
+ if (($row['state'] === 'OFFLINE' || $row['state'] === 'STANDBY') && $row['woltime'] > $wolCut) {
+ $row['wol_in_progress'] = true;
+ }
+ settype($row['locationid'], 'int');
+ settype($row['vncport'], 'int');
+ unset($row['woltime']);
+ }
+}
+
+$groups = Database::queryAll("SELECT g.groupid AS id, g.groupname AS name,
+ GROUP_CONCAT(l.locationid) AS locationids, g.passwd AS password
+ FROM remoteaccess_group g INNER JOIN remoteaccess_x_location l USING (groupid)
+ WHERE g.active = 1
+ GROUP BY g.groupid");
+foreach ($groups as &$group) {
+ $group['locationids'] = explode(',', $group['locationids']);
+ if (empty($group['password'])) {
+ unset($group['password']);
+ }
+ settype($group['id'], 'int');
+ foreach ($group['locationids'] as &$lid) {
+ settype($lid, 'int');
+ }
+}
+
+$fakeid = 100000;
+echo json_encode(['clients' => $rows, 'locations' => $groups]);
+
+// WTF, this makes the server return a 500 -.-
+//fastcgi_finish_request();
+
+RemoteAccess::ensureMachinesRunning();
diff --git a/modules-available/remoteaccess/baseconfig/getconfig.inc.php b/modules-available/remoteaccess/baseconfig/getconfig.inc.php
new file mode 100644
index 00000000..182daef1
--- /dev/null
+++ b/modules-available/remoteaccess/baseconfig/getconfig.inc.php
@@ -0,0 +1,50 @@
+<?php
+
+/** @var ?string $uuid */
+/** @var ?string $ip */
+
+if ($uuid !== null) {
+ // Leave clients in any runmode alone
+ $res = Database::queryFirst('SELECT machineuuid FROM runmode WHERE machineuuid = :uuid',
+ ['uuid' => $uuid], true);
+ if (is_array($res))
+ return;
+
+ // Locations from closest to furthest (order)
+ $locationId = ConfigHolder::get('SLX_LOCATIONS');
+ if ($locationId === null)
+ return;
+ $locationId = (int)$locationId;
+ $ret = Database::queryFirst("SELECT l.locationid FROM remoteaccess_x_location l
+ INNER JOIN remoteaccess_group g USING (groupid)
+ WHERE locationid = :lid AND g.active = 1",
+ ['lid' => $locationId], true); // TODO Remove true after next point release (2020-05-12)
+ if ($ret === false)
+ return;
+ // Special case – location admin can limit accessibility of this machine to never, or only when room is closed
+ $opts = Scheduler::getLocationOptions($locationId);
+ if ($opts['ra-mode'] === Scheduler::RA_NEVER)
+ return; // Completely disallowed
+ if ($opts['ra-mode'] === Scheduler::RA_SELECTIVE) {
+ // Only when room is closed
+ if (OpeningTimes::isRoomOpen($locationId, $opts['wol-offset'], $opts['sd-offset']))
+ return; // Open, do not interfere with ongoing lectures etc., do nothing
+ }
+ // TODO Properly merge
+ if (Property::get(RemoteAccess::PROP_TRY_VIRT_HANDOVER)) {
+ ConfigHolder::add("SLX_REMOTE_VNC", 'vmware virtualbox');
+ } else {
+ ConfigHolder::add("SLX_REMOTE_VNC", 'x11vnc');
+ }
+ ConfigHolder::add("SLX_REMOTE_HOST_ACCESS", Property::get(RemoteAccess::PROP_ALLOWED_VNC_NET));
+ ConfigHolder::add('SLX_REMOTE_VNC_PORT', Property::get(RemoteAccess::PROP_VNC_PORT, 5900));
+ ConfigHolder::add('SLX_RUNMODE_MODULE', 'remoteaccess');
+ // No saver
+ $saverTimeout = ConfigHolder::get('SLX_SCREEN_SAVER_TIMEOUT');
+ if (!is_numeric($saverTimeout) || $saverTimeout < 1800) {
+ ConfigHolder::add('SLX_SCREEN_SAVER_TIMEOUT', '1800', 1000);
+ }
+ ConfigHolder::add('SLX_SCREEN_SAVER_GRACE_TIME', '86400', 1000);
+ // Autologin will never work as the machine is immediately in use and will never get assigned
+ ConfigHolder::add('SLX_AUTOLOGIN', 'OFF', 10000);
+}
diff --git a/modules-available/remoteaccess/config.json b/modules-available/remoteaccess/config.json
new file mode 100644
index 00000000..1530df87
--- /dev/null
+++ b/modules-available/remoteaccess/config.json
@@ -0,0 +1,7 @@
+{
+ "category": "main.settings-client",
+ "dependencies" : [
+ "locations",
+ "rebootcontrol"
+ ]
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/hooks/client-update.inc.php b/modules-available/remoteaccess/hooks/client-update.inc.php
new file mode 100644
index 00000000..ecf5d91c
--- /dev/null
+++ b/modules-available/remoteaccess/hooks/client-update.inc.php
@@ -0,0 +1,9 @@
+<?php
+
+if ($type === '~poweron') {
+ Database::exec("UPDATE remoteaccess_machine SET password = NULL WHERE machineuuid = :uuid",
+ ['uuid' => $uuid]);
+} elseif ($type === '~poweroff') {
+ Database::exec("UPDATE remoteaccess_machine SET woltime = 0 WHERE machineuuid = :uuid",
+ ['uuid' => $uuid]);
+}
diff --git a/modules-available/remoteaccess/hooks/cron.inc.php b/modules-available/remoteaccess/hooks/cron.inc.php
new file mode 100644
index 00000000..2ee6e375
--- /dev/null
+++ b/modules-available/remoteaccess/hooks/cron.inc.php
@@ -0,0 +1,3 @@
+<?php
+
+RemoteAccess::ensureMachinesRunning();
diff --git a/modules-available/remoteaccess/inc/remoteaccess.inc.php b/modules-available/remoteaccess/inc/remoteaccess.inc.php
new file mode 100644
index 00000000..95ca3821
--- /dev/null
+++ b/modules-available/remoteaccess/inc/remoteaccess.inc.php
@@ -0,0 +1,117 @@
+<?php
+
+class RemoteAccess
+{
+
+ const PROP_ALLOWED_VNC_NET = 'remoteaccess.allowedvncaccess';
+
+ const PROP_TRY_VIRT_HANDOVER = 'remoteaccess.virthandover';
+
+ const PROP_VNC_PORT = 'remoteaccess.vncport';
+
+ const PROP_PLUGIN_VERSION = 'remoteaccess.plugin-version';
+
+ /**
+ * Get a list of locationIds where remote access is enabled. If $filterOverridden is true,
+ * the list will not contain any locations where remote access is disabled via location override.
+ * @param int $group Group to get locations for, or '0' for all locations
+ * @param bool $filterOverridden iff true, remove any locations where remote access is currently disabled
+ */
+ public static function getEnabledLocations(int $group = 0, bool $filterOverridden = true): array
+ {
+ if ($group === 0) {
+ $list = Database::queryColumnArray("SELECT DISTINCT rxl.locationid FROM remoteaccess_x_location rxl
+ INNER JOIN remoteaccess_group g ON (g.groupid = rxl.groupid AND g.active = 1)");
+ } else {
+ $list = Database::queryColumnArray("SELECT DISTINCT locationid FROM remoteaccess_x_location
+ WHERE groupid = :gid", ['gid' => $group]);
+ }
+ if (!$filterOverridden || !Module::isAvailable('rebootcontrol'))
+ return $list;
+ return array_filter($list, function (int $lid) {
+ $mode = Scheduler::getLocationOptions($lid)['ra-mode'];
+ return ($mode !== Scheduler::RA_NEVER
+ && ($mode !== Scheduler::RA_SELECTIVE || !OpeningTimes::isRoomOpen($lid, 5, 5)));
+ });
+ }
+
+ public static function ensureMachinesRunning()
+ {
+ if (!Module::isAvailable('rebootcontrol')) {
+ error_log("Not waking remote access machines: rebootcontrol missing");
+ return;
+ }
+
+ $res = Database::simpleQuery("SELECT rg.groupid, rg.groupname, rg.wolcount,
+ GROUP_CONCAT(rxl.locationid) AS locs
+ FROM remoteaccess_group rg
+ INNER JOIN remoteaccess_x_location rxl USING (groupid)
+ WHERE rg.active = 1
+ GROUP BY groupid");
+
+ // Consider machines we tried to wake in the past 90 seconds as online
+ $wolDeadline = time() - 90;
+ foreach ($res as $row) {
+ $wantNum = $row['wolcount'];
+ // This can't really be anything but a CSV list, but better be safe
+ $locs = preg_replace('/[^0-9,]/', '', $row['locs']);
+ if (!empty($locs)) {
+ // Filter out locations for which remote-access is disabled
+ $locArray = explode(',', $locs);
+ $locArray = array_filter($locArray, function (int $lid) {
+ $mode = Scheduler::getLocationOptions($lid)['ra-mode'];
+ return ($mode !== Scheduler::RA_NEVER
+ && ($mode !== Scheduler::RA_SELECTIVE || !OpeningTimes::isRoomOpen($lid, 5, 5)));
+ });
+ $locs = implode(',', $locArray);
+ }
+ if ($wantNum > 0 && !empty($locs)) {
+ $active = Database::queryFirst("SELECT Count(*) AS cnt FROM machine m
+ INNER JOIN remoteaccess_machine rm USING (machineuuid)
+ WHERE m.locationid IN ($locs) AND (m.state = 'IDLE' OR rm.woltime > $wolDeadline)");
+ $active = ($active['cnt'] ?? 0);
+ $wantNum -= $active;
+ }
+ if ($wantNum > 0) {
+ $numFailed = self::tryWakeMachines($locs, $wantNum);
+ } else {
+ $numFailed = 0;
+ }
+ Database::exec("UPDATE remoteaccess_group SET unwoken = :num WHERE groupid = :groupid",
+ ['num' => $numFailed, 'groupid' => $row['groupid']]);
+ }
+ }
+
+ private static function tryWakeMachines(string $locs, int $num): int
+ {
+ if (empty($locs))
+ return $num;
+ $res = Database::simpleQuery("SELECT m.machineuuid, m.macaddr, m.clientip, m.locationid FROM machine m
+ LEFT JOIN remoteaccess_machine rm USING (machineuuid)
+ WHERE m.locationid IN ($locs) AND m.state IN ('OFFLINE', 'STANDBY')
+ ORDER BY rm.woltime ASC");
+ $NOW = time();
+ while ($num > 0) {
+ $list = [];
+ for ($i = 0; $i < $num && ($row = $res->fetch()); ++$i) {
+ $list[] = $row;
+ Database::exec("INSERT INTO remoteaccess_machine (machineuuid, password, woltime)
+ VALUES (:uuid, NULL, :now)
+ ON DUPLICATE KEY UPDATE woltime = VALUES(woltime)",
+ ['uuid' => $row['machineuuid'], 'now' => $NOW]);
+ }
+ if (empty($list))
+ break; // No more clients in this location
+ RebootControl::wakeMachines($list, $fails);
+ $num -= (count($list) - count($fails));
+ if (!empty($fails)) {
+ $failIds = ArrayUtil::flattenByKey($fails, 'machineuuid');
+ // Reduce time so they won't be marked as wol_in_progress
+ Database::exec('UPDATE remoteaccess_machine SET woltime = :faketime WHERE machineuuid IN (:fails)',
+ ['faketime' => $NOW - 95, 'fails' => $failIds]);
+ }
+ }
+ return $num;
+ }
+
+}
diff --git a/modules-available/remoteaccess/install.inc.php b/modules-available/remoteaccess/install.inc.php
new file mode 100644
index 00000000..2a6fec36
--- /dev/null
+++ b/modules-available/remoteaccess/install.inc.php
@@ -0,0 +1,80 @@
+<?php
+
+$dbret = [];
+
+$dbret[] = tableCreate('remoteaccess_group', "
+ `groupid` int(11) NOT NULL AUTO_INCREMENT,
+ `groupname` varchar(100) NOT NULL,
+ `wolcount` smallint(11) NOT NULL,
+ `passwd` varchar(100) NOT NULL,
+ `active` tinyint(1) UNSIGNED NOT NULL DEFAULT '1',
+ `unwoken` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY (`groupid`)
+");
+
+$dbret[] = tableCreate('remoteaccess_x_location', "
+ `groupid` int(11) NOT NULL,
+ `locationid` int(11) NOT NULL,
+ PRIMARY KEY (`groupid`, `locationid`)
+");
+
+$dbret[] = tableCreate('remoteaccess_machine', "
+ `machineuuid` char(36) CHARACTER SET ascii NOT NULL,
+ `password` char(8) CHARACTER SET ascii NULL DEFAULT NULL,
+ `woltime` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `vncport` smallint(5) UNSIGNED NOT NULL DEFAULT '5900',
+ PRIMARY KEY (`machineuuid`)
+");
+
+$dbret[] = tableAddConstraint('remoteaccess_x_location', 'locationid', 'location', 'locationid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+
+$dbret[] = tableAddConstraint('remoteaccess_x_location', 'groupid', 'remoteaccess_group', 'groupid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+
+$dbret[] = tableAddConstraint('remoteaccess_machine', 'machineuuid', 'machine', 'machineuuid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+
+if (tableExists('remoteaccess_location')
+ && tableExists('remoteaccess_x_location')
+ && tableExists('remoteaccess_group')) {
+ // Migrate old version
+ $wantedIdleCount = (int)Property::get('remoteaccess.wantedclients', 0);
+ $ret = Database::exec("INSERT IGNORE INTO remoteaccess_group (groupid, groupname, wolcount, passwd)
+ SELECT l.locationid, l.locationname, $wantedIdleCount AS blu, '' AS bla FROM location l
+ INNER JOIN remoteaccess_location rl USING (locationid)");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, Database::lastError());
+ }
+ Property::set('remoteaccess.wantedclients', 0, 1);
+ $max = Database::queryFirst("SELECT groupid FROM remoteaccess_group ORDER BY groupid DESC LIMIT 1");
+ if ($max !== false) {
+ Database::exec("ALTER TABLE remoteaccess_group AUTO_INCREMENT = :next", ['next' => $max['groupid'] + 1]);
+ }
+ $ret = Database::exec("INSERT IGNORE INTO remoteaccess_x_location (groupid, locationid)
+ SELECT locationid, locationid FROM remoteaccess_location");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, Database::lastError());
+ }
+ Database::exec("DROP TABLE remoteaccess_location");
+}
+
+// 2021-03-05: Add vncport column to machine table
+if (!tableHasColumn('remoteaccess_machine', 'vncport')) {
+ $ret = Database::exec("ALTER TABLE remoteaccess_machine ADD COLUMN `vncport` smallint(5) UNSIGNED NOT NULL DEFAULT '5900'");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, Database::lastError());
+ }
+ $dbret[] = UPDATE_DONE;
+}
+
+// 2022-06-01 Unwoken machines: Keeps track of how many machines could not be WOLed
+if (!tableHasColumn('remoteaccess_group', 'unwoken')) {
+ $ret = Database::exec("ALTER TABLE remoteaccess_group ADD COLUMN `unwoken` int(10) UNSIGNED NOT NULL DEFAULT '0'");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, Database::lastError());
+ }
+ $dbret[] = UPDATE_DONE;
+}
+
+responseFromArray($dbret);
diff --git a/modules-available/remoteaccess/lang/de/messages.json b/modules-available/remoteaccess/lang/de/messages.json
new file mode 100644
index 00000000..a7b26240
--- /dev/null
+++ b/modules-available/remoteaccess/lang/de/messages.json
@@ -0,0 +1,8 @@
+{
+ "group-added": "Gruppe hinzugef\u00fcgt",
+ "group-deleted": "Gruppe {{0}} gel\u00f6scht",
+ "group-not-found": "Gruppe {{0}} existiert nicht",
+ "group-updated": "Gruppe {{0}} wurde aktualisiert",
+ "locations-not-allowed": "Gruppe {{0}} hat Orte zugewiesen, f\u00fcr die Sie keine Berechtigung haben",
+ "settings-saved": "Einstellungen gespeichert"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/lang/de/module.json b/modules-available/remoteaccess/lang/de/module.json
new file mode 100644
index 00000000..21d8ca69
--- /dev/null
+++ b/modules-available/remoteaccess/lang/de/module.json
@@ -0,0 +1,4 @@
+{
+ "module_name": "Fernzugriff",
+ "page_title": "Fernzugriff"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/lang/de/permissions.json b/modules-available/remoteaccess/lang/de/permissions.json
new file mode 100644
index 00000000..ef402eed
--- /dev/null
+++ b/modules-available/remoteaccess/lang/de/permissions.json
@@ -0,0 +1,7 @@
+{
+ "group.add": "Neue Gruppe anlegen",
+ "group.edit": "Einstellungen einer Gruppe bearbeiten, Gruppe l\u00f6schen",
+ "group.locations": "Zugewiesene R\u00e4ume einer Gruppe \u00e4ndern",
+ "set-proxy-ip": "F\u00fcr Zugriff freigegebene IP-Adresse\/Bereich \u00e4ndern",
+ "view": "Seite sehen"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/lang/de/template-tags.json b/modules-available/remoteaccess/lang/de/template-tags.json
new file mode 100644
index 00000000..1a502a6b
--- /dev/null
+++ b/modules-available/remoteaccess/lang/de/template-tags.json
@@ -0,0 +1,27 @@
+{
+ "lang_add": "Hinzuf\u00fcgen",
+ "lang_allowAccessText": "IP-Adresse oder Netz in CIDR Notation, welches auf den VNC-Port des Clients zugreifen darf. (I.d.R. nur der Guacamole-Server)",
+ "lang_allowedAccessToVncPort": "Erlaubte Quelle f\u00fcr VNC-Zugriff",
+ "lang_assignLocations": "R\u00e4ume zuweisen",
+ "lang_clientVncPort": "VNC Port (Client)",
+ "lang_general": "Allgemein",
+ "lang_group": "Gruppe",
+ "lang_groupListText": "Liste verf\u00fcgbarer Gruppen (\"virtuelle R\u00e4ume\")",
+ "lang_groups": "Gruppen",
+ "lang_keepAvailableWol": "WoL#",
+ "lang_location": "Ort",
+ "lang_locationSelectionText": "Ausgew\u00e4hlte Orte werden in den Remote-Modus geschaltet (beim n\u00e4chsten Boot des Clients) und sind damit im Pool f\u00fcr den Fernzugriff.",
+ "lang_locations": "Konfigurierte Orte",
+ "lang_locationsInUse": "Liste der Orte, die in mindestens einer Gruppe verwendet werden",
+ "lang_numLocs": "R\u00e4ume",
+ "lang_pluginVersion": "Plugin-Version",
+ "lang_pluginVersionOldOrUnknown": "Unbekannt oder zu alt",
+ "lang_reallyDelete": "Wirklich l\u00f6schen?",
+ "lang_remoteAccessSettings": "Einstellungen f\u00fcr den Fernzugriff",
+ "lang_roomRemoteAccessDisabled": "Zugriff f\u00fcr diesen Raum generell deaktiviert",
+ "lang_roomRemoteAccessWhenClosed": "Zugriff f\u00fcr diesen Raum deaktiviert, solange er laut \u00d6ffnungszeiten offen ist",
+ "lang_tryVirtualizerHandover": "Versuche, VNC-Server des Virtualisierers zu verwenden",
+ "lang_tryVirtualizerText": "Wenn aktiviert wird versucht, nach dem Start einer VM die Verbindung auf den VNC-Server des Virtualisierers umzubuchen. Zumindest f\u00fcr VMware haben wir hier allerdings eher eine Verschlechterung der Performance beobachten k\u00f6nnen; au\u00dferdem bricht die Verbindung beim Handover manchmal ab -> Nur experimentell!",
+ "lang_vncPortText": "Port, auf dem die Clients auf VNC-Verbindungen warten. Bei Verwendung eines Ports ungleich 5900 bitte sicherstellen, dass das aktuelle Guacamole-Plugin verwendet wird.",
+ "lang_wolFailCount": "Anzahl erfolgloser WoL-Versuche"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/lang/en/messages.json b/modules-available/remoteaccess/lang/en/messages.json
new file mode 100644
index 00000000..15b7e06c
--- /dev/null
+++ b/modules-available/remoteaccess/lang/en/messages.json
@@ -0,0 +1,8 @@
+{
+ "group-added": "Group added",
+ "group-deleted": "Group {{0}} deleted",
+ "group-not-found": "Group {{0}} does not exist",
+ "group-updated": "Group {{0}} updated",
+ "locations-not-allowed": "You don't have permission to view some locations in group {{0}} ",
+ "settings-saved": "Settings saved"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/lang/en/module.json b/modules-available/remoteaccess/lang/en/module.json
new file mode 100644
index 00000000..308aeb15
--- /dev/null
+++ b/modules-available/remoteaccess/lang/en/module.json
@@ -0,0 +1,4 @@
+{
+ "module_name": "Remoteaccess",
+ "page_title": "Remoteaccess"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/lang/en/permissions.json b/modules-available/remoteaccess/lang/en/permissions.json
new file mode 100644
index 00000000..e90ce398
--- /dev/null
+++ b/modules-available/remoteaccess/lang/en/permissions.json
@@ -0,0 +1,7 @@
+{
+ "group.add": "Add new group",
+ "group.edit": "Edit or delete a group",
+ "group.locations": "Assign rooms to a group",
+ "set-proxy-ip": "Set allowed proxy IP(-Range)",
+ "view": "View page"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/lang/en/template-tags.json b/modules-available/remoteaccess/lang/en/template-tags.json
new file mode 100644
index 00000000..037550e4
--- /dev/null
+++ b/modules-available/remoteaccess/lang/en/template-tags.json
@@ -0,0 +1,27 @@
+{
+ "lang_add": "Add",
+ "lang_allowAccessText": "IP address (or net in CIDR notation) which is allowed to access the VNC port of the clients (usually only the guacamole proxy-server)",
+ "lang_allowedAccessToVncPort": "Allowed source for VNC-access",
+ "lang_assignLocations": "Assing locations",
+ "lang_clientVncPort": "VNC port (client)",
+ "lang_general": "General",
+ "lang_group": "Group",
+ "lang_groupListText": "Available groups (\"virtual locations\")",
+ "lang_groups": "Groups",
+ "lang_keepAvailableWol": "WoL#",
+ "lang_location": "Location",
+ "lang_locationSelectionText": "Clients in the selected locations will start the remoteaccess-mode after the next reboot.",
+ "lang_locations": "Configured locations",
+ "lang_locationsInUse": "List of locations that are active in at least one group",
+ "lang_numLocs": "Locations",
+ "lang_pluginVersion": "Plugin version",
+ "lang_pluginVersionOldOrUnknown": "Unknown or too old",
+ "lang_reallyDelete": "Delete?",
+ "lang_remoteAccessSettings": "Settings for remoteaccess",
+ "lang_roomRemoteAccessDisabled": "Remote access for this room is disabled via override",
+ "lang_roomRemoteAccessWhenClosed": "Remote access for this room is disabled while the room is open (according to its schedule)",
+ "lang_tryVirtualizerHandover": "Try to use VNC-server of the hypervisor",
+ "lang_tryVirtualizerText": "If activated the system tries to change the remote VNC-connection to the internal VNC-server of the hypervisor after VM start.\r\nAt least in the case of VMware it seems to reduce performance and sometimes the connection during handover is lost.\r\n-> Just experimental!",
+ "lang_vncPortText": "Port on which clients will wait for VNC connections. Please make sure you're running the latest version of the Guacamole plugin when changing this to something other than 5900.",
+ "lang_wolFailCount": "Number of unsuccesful WoL attempts"
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/page.inc.php b/modules-available/remoteaccess/page.inc.php
new file mode 100644
index 00000000..ba248b4d
--- /dev/null
+++ b/modules-available/remoteaccess/page.inc.php
@@ -0,0 +1,196 @@
+<?php
+
+class Page_RemoteAccess extends Page
+{
+
+ protected function doPreprocess()
+ {
+ User::load();
+ if (!User::isLoggedIn()) {
+ Message::addError('main.no-permission');
+ Util::redirect('?do=Main');
+ }
+ User::assertPermission('view');
+ $action = Request::post('action', false, 'string');
+ // Add group adds a DB row and then falls through to regular saving
+ if ($action === 'add-group') {
+ User::assertPermission('group.add');
+ Database::exec("INSERT INTO remoteaccess_group (groupname, wolcount, passwd, active)
+ VALUES ('.new', 0, '', 0)");
+ Message::addSuccess('group-added');
+ if (User::hasPermission('group.edit')) {
+ $action = 'save-groups';
+ }
+ }
+ if ($action === 'save-groups') {
+ User::assertPermission('group.edit');
+ $groups = Request::post('group', [], 'array');
+ foreach ($groups as $id => $group) {
+ Database::exec("UPDATE remoteaccess_group SET groupname = :name, wolcount = :wol,
+ passwd = :passwd, active = :active WHERE groupid = :id", [
+ 'id' => $id,
+ 'name' => $group['groupname'] ?? $id,
+ 'wol' => $group['wolcount'] ?? 0,
+ 'passwd' => $group['passwd'] ?? 0,
+ 'active' => (int)($group['active'] ?? 0),
+ ]);
+ }
+ Message::addSuccess('settings-saved');
+ } elseif ($action === 'save-settings') {
+ User::assertPermission('set-proxy-ip');
+ Property::set(RemoteAccess::PROP_ALLOWED_VNC_NET, Request::post('allowed-source', '', 'string'));
+ Property::set(RemoteAccess::PROP_TRY_VIRT_HANDOVER, Request::post('virt-handover', false, 'int'));
+ Property::set(RemoteAccess::PROP_VNC_PORT, Request::post('vncport', 5900, 'int'));
+ Message::addSuccess('settings-saved');
+ } elseif ($action === 'delete-group') {
+ User::assertPermission('group.edit');
+ $groupid = Request::post('groupid', Request::REQUIRED, 'int');
+ $group = $this->groupNameOrFail($groupid);
+ if (!$this->checkGroupLocations($groupid)) {
+ Message::addError('locations-not-allowed', $group);
+ } else {
+ Database::exec("DELETE FROM remoteaccess_group WHERE groupid = :id", ['id' => $groupid]);
+ Message::addSuccess('group-deleted', $group);
+ }
+ } elseif ($action === 'set-locations') {
+ User::assertPermission('group.locations');
+ $groupid = Request::post('groupid', Request::REQUIRED, 'int');
+ $group = $this->groupNameOrFail($groupid);
+ $locations = array_values(Request::post('location', [], 'array'));
+ // Merge what's already set where we don't have permission
+ $locations = Permission::mergeWithDisallowed($locations, 'group.locations',
+ "SELECT locationid FROM remoteaccess_x_location WHERE groupid = :id", ['id' => $groupid]);
+ if (empty($locations)) {
+ Database::exec("DELETE FROM remoteaccess_x_location WHERE groupid = :id", ['id' => $groupid]);
+ } else {
+ Database::exec("INSERT IGNORE INTO remoteaccess_x_location (groupid, locationid)
+ VALUES :values", ['values' => array_map(function($item) use ($groupid) { return [$groupid, $item]; }, $locations)]);
+ Database::exec("DELETE FROM remoteaccess_x_location WHERE groupid = :id AND locationid NOT IN (:locations)",
+ ['id' => $groupid, 'locations' => $locations]);
+ }
+ Message::addSuccess('group-updated', $group);
+ }
+ if (Request::isPost()) {
+ Util::redirect('?do=remoteaccess');
+ }
+ }
+
+ private function groupNameOrFail($groupid)
+ {
+ $group = Database::queryFirst("SELECT groupname FROM remoteaccess_group WHERE groupid = :id",
+ ['id' => $groupid]);
+ if ($group === false) {
+ Message::addError('group-not-found', $groupid);
+ Util::redirect('?do=remoteaccess');
+ }
+ return $group['groupname'];
+ }
+
+ protected function doRender()
+ {
+ $groupid = Request::get('groupid', false, 'int');
+ if ($groupid === false) {
+ // Edit list of groups and their settings
+ $groups = Database::queryAll("SELECT g.groupid, g.groupname, g.wolcount, g.passwd,
+ Count(l.locationid) AS locs, If(g.active, 'checked', '') AS checked, unwoken
+ FROM remoteaccess_group g
+ LEFT JOIN remoteaccess_x_location l USING (groupid)
+ GROUP BY g.groupid, g.groupname
+ ORDER BY g.groupname ASC");
+ $data = [
+ 'allowed-source' => Property::get(RemoteAccess::PROP_ALLOWED_VNC_NET),
+ 'virt-handover_checked' => Property::get(RemoteAccess::PROP_TRY_VIRT_HANDOVER) ? 'checked' : '',
+ 'vncport' => Property::get(RemoteAccess::PROP_VNC_PORT, 5900),
+ 'groups' => $groups,
+ ];
+ $data['plugin_version'] = Property::get(RemoteAccess::PROP_PLUGIN_VERSION);
+ Permission::addGlobalTags($data['perms'], null, ['group.locations', 'group.add', 'group.edit', 'set-proxy-ip']);
+ // List of locations used in at least one group
+ $res = Database::simpleQuery("SELECT l.locationid, l.locationname, g.groupid, g.groupname, g.active
+ FROM location l
+ INNER JOIN remoteaccess_x_location rxl USING (locationid)
+ INNER JOIN remoteaccess_group g USING (groupid)
+ ORDER BY locationname, locationid");
+ $data['locations'] = [];
+ $last = null;
+ foreach ($res as $row) {
+ if ($last === null || $last['locationid'] !== $row['locationid']) {
+ unset($last);
+ $last = [
+ 'locationid' => $row['locationid'],
+ 'locationname' => $row['locationname'],
+ 'lclass' => 'slx-strike',
+ 'groups' => [],
+ ];
+ $data['locations'][] =& $last;
+ }
+ $last['groups'][] = [
+ 'groupid' => $row['groupid'],
+ 'groupname' => $row['groupname'],
+ 'gclass' => $row['active'] ? '' : 'slx-strike',
+ ];
+ if ($row['active']) {
+ $last['lclass'] = '';
+ }
+ }
+ unset($last);
+ $this->addSchedulerTags($data['locations']);
+ Render::addTemplate('edit-settings', $data);
+ } else {
+ // Edit locations for group
+ $group = $this->groupNameOrFail($groupid);
+ $locationList = Location::getLocationsAssoc();
+ $enabled = RemoteAccess::getEnabledLocations($groupid, false);
+ $allowed = User::getAllowedLocations('group.locations');
+ foreach ($enabled as $lid) {
+ if (isset($locationList[$lid])) {
+ $locationList[$lid]['checked'] = 'checked';
+ }
+ }
+ $this->addSchedulerTags($locationList);
+ foreach ($locationList as $lid => &$loc) {
+ if (!in_array($lid, $allowed)) {
+ $loc['disabled'] = 'disabled';
+ }
+ }
+ $data = [
+ 'groupid' => $groupid,
+ 'groupname' => $group,
+ 'locations' => array_values($locationList),
+ 'disabled' => empty($allowed) ? 'disabled' : '',
+ ];
+ Permission::addGlobalTags($data['perms'], null, ['group.locations', 'group.edit']);
+ Render::addTemplate('edit-group', $data);
+ }
+ }
+
+ /**
+ * @param int $groupid group to check
+ * @return bool if we have permission for all the locations assigned to group
+ */
+ private function checkGroupLocations(int $groupid): bool
+ {
+ $allowed = User::getAllowedLocations('group.locations');
+ if (in_array(0, $allowed))
+ return true;
+ $hasLocs = Database::queryColumnArray("SELECT locationid FROM remoteaccess_x_location WHERE groupid = :id",
+ ['id' => $groupid]);
+ $diff = array_diff($hasLocs, $allowed);
+ return empty($diff);
+ }
+
+ private function addSchedulerTags(array &$locationList)
+ {
+ if (!Module::isAvailable('rebootcontrol'))
+ return;
+ foreach ($locationList as $lid => &$loc) {
+ $options = Scheduler::getLocationOptions($loc['locationid'] ?? $lid);
+ if ($options['ra-mode'] === Scheduler::RA_SELECTIVE) {
+ $loc['ra_selective'] = true;
+ } elseif ($options['ra-mode'] === Scheduler::RA_NEVER) {
+ $loc['ra_never'] = true;
+ }
+ }
+ }
+
+}
diff --git a/modules-available/remoteaccess/permissions/permissions.json b/modules-available/remoteaccess/permissions/permissions.json
new file mode 100644
index 00000000..c91ce7ae
--- /dev/null
+++ b/modules-available/remoteaccess/permissions/permissions.json
@@ -0,0 +1,17 @@
+{
+ "view": {
+ "location-aware": false
+ },
+ "group.locations": {
+ "location-aware": true
+ },
+ "group.add": {
+ "location-aware": false
+ },
+ "group.edit": {
+ "location-aware": false
+ },
+ "set-proxy-ip": {
+ "location-aware": false
+ }
+} \ No newline at end of file
diff --git a/modules-available/remoteaccess/templates/edit-group.html b/modules-available/remoteaccess/templates/edit-group.html
new file mode 100644
index 00000000..93fa66be
--- /dev/null
+++ b/modules-available/remoteaccess/templates/edit-group.html
@@ -0,0 +1,56 @@
+<h2>{{lang_assignLocations}}</h2>
+<h3>{{groupname}}</h3>
+
+<form method="post" action="?do=remoteaccess">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="groupid" value="{{groupid}}">
+
+ <div class="buttonbar pull-right">
+ <button type="submit" class="btn btn-danger" name="action" value="delete-group" data-confirm="{{lang_reallyDelete}}"
+ {{perms.group.locations.disabled}} {{perms.group.edit.disabled}}>
+ <span class="glyphicon glyphicon-remove"></span>
+ {{lang_delete}}
+ </button>
+ <button type="submit" class="btn btn-primary" name="action" value="set-locations" {{perms.group.locations.disabled}}>
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ <div class="clearfix"></div>
+
+ <div class="form-group">
+ <p>{{lang_locationSelectionText}}</p>
+ <table class="table table-condensed table-hover">
+ {{#locations}}
+ <tr>
+ <td class="slx-smallcol">
+ <div class="checkbox checkbox-inline">
+ <input type="checkbox" name="location[]" value="{{locationid}}" id="loc-check-{{locationid}}"
+ {{checked}} {{disabled}}>
+ <label></label>
+ </div>
+ </td>
+ <td class="slx-smallcol text-nowrap">
+ <div style="display:inline-block;width:{{depth}}em"></div>
+ <label for="loc-check-{{locationid}}" class="{{disabled}}">{{locationname}}</label>
+ </td>
+ <td>
+ {{#ra_never}}
+ <span class="glyphicon glyphicon-remove text-danger" title="{{lang_roomRemoteAccessDisabled}}"></span>
+ {{/ra_never}}
+ {{#ra_selective}}
+ <span class="glyphicon glyphicon-time text-danger" title="{{lang_roomRemoteAccessWhenClosed}}"></span>
+ {{/ra_selective}}
+ </td>
+ </tr>
+ {{/locations}}
+ </table>
+ </div>
+ <div class="buttonbar pull-right">
+ <button type="submit" class="btn btn-primary" name="action" value="set-locations" {{perms.group.locations.disabled}}>
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ <div class="clearfix"></div>
+</form>
diff --git a/modules-available/remoteaccess/templates/edit-settings.html b/modules-available/remoteaccess/templates/edit-settings.html
new file mode 100644
index 00000000..4c4c011a
--- /dev/null
+++ b/modules-available/remoteaccess/templates/edit-settings.html
@@ -0,0 +1,142 @@
+<h2>{{lang_remoteAccessSettings}}</h2>
+
+<h3>{{lang_general}}</h3>
+
+<form method="post" action="?do=remoteaccess">
+ <input type="hidden" name="token" value="{{token}}">
+ <div class="row">
+ <div class="form-group col-md-6">
+ <label>
+ {{lang_allowedAccessToVncPort}}
+ <input type="text" class="form-control" name="allowed-source" value="{{allowed-source}}"
+ required {{perms.set-proxy-ip.disabled}}>
+ </label>
+ <p>{{lang_allowAccessText}}</p>
+ </div>
+ <div class="form-group col-md-6">
+ <label>
+ {{lang_clientVncPort}}
+ <input type="number" class="form-control" name="vncport" value="{{vncport}}" min="1025" max="65535"
+ required {{perms.set-proxy-ip.disabled}}>
+ </label>
+ <p>{{lang_vncPortText}}</p>
+ <div class="text-right">
+ {{lang_pluginVersion}}: {{plugin_version}}
+ {{^plugin_version}}
+ {{lang_pluginVersionOldOrUnknown}}
+ {{/plugin_version}}
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="checkbox">
+ <input type="checkbox" name="virt-handover" value="1"
+ id="virt-handover" {{virt-handover_checked}} {{perms.set-proxy-ip.disabled}}>
+ <label for="virt-handover">{{lang_tryVirtualizerHandover}}</label>
+ </div>
+ <p>{{lang_tryVirtualizerText}}</p>
+ </div>
+ <div class="buttonbar pull-right">
+ <button type="submit" class="btn btn-primary" name="action" value="save-settings" {{perms.set-proxy-ip.disabled}}>
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ <div class="clearfix"></div>
+</form>
+
+<h3>{{lang_groups}}</h3>
+
+<form method="post" action="?do=remoteaccess">
+ <input type="hidden" name="token" value="{{token}}">
+ <div class="form-group">
+ <p>{{lang_groupListText}}</p>
+ <table class="table table-condensed table-hover">
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{lang_group}}</th>
+ <th class="text-nowrap" width="10%">{{lang_numLocs}}</th>
+ <th class="text-nowrap" width="10%">{{lang_keepAvailableWol}}</th>
+ <th class="text-nowrap" width="13%">{{lang_password}}</th>
+ </tr>
+ </thead>
+ {{#groups}}
+ <tr>
+ <td class="slx-smallcol">
+ <div class="checkbox checkbox-inline">
+ <input type="checkbox" name="group[{{groupid}}][active]" value="1" id="group-check-{{groupid}}"
+ {{checked}} {{perms.group.edit.disabled}}>
+ <label for="group-check-{{groupid}}"></label>
+ </div>
+ </td>
+ <td class="text-nowrap">
+ <input type="text" class="form-control" name="group[{{groupid}}][groupname]" value="{{groupname}}"
+ {{perms.group.edit.disabled}}>
+ </td>
+ <td class="text-right text-nowrap">
+ <span class="badge">{{locs}}</span>
+ <a href="?do=remoteaccess&amp;groupid={{groupid}}" class="btn btn-xs btn-default">
+ <span class="glyphicon glyphicon-edit"></span>
+ </a>
+ </td>
+ <td class="input-group">
+ <input type="number" class="form-control" name="group[{{groupid}}][wolcount]" value="{{wolcount}}"
+ {{perms.group.edit.disabled}}>
+ {{#unwoken}}
+ <span class="input-group-addon" title="{{lang_wolFailCount}}">
+ <span class="glyphicon glyphicon-remove"></span>{{unwoken}}
+ </span>
+ {{/unwoken}}
+ </td>
+ <td>
+ <input type="text" class="form-control" name="group[{{groupid}}][passwd]" value="{{passwd}}"
+ {{perms.group.edit.disabled}}>
+ </td>
+ </tr>
+ {{/groups}}
+ </table>
+ </div>
+ <div class="buttonbar pull-right">
+ <button type="submit" class="btn btn-success" name="action" value="add-group" {{perms.group.add.disabled}}>
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_add}}
+ </button>
+ <button type="submit" class="btn btn-primary" name="action" value="save-groups" {{perms.group.edit.disabled}}>
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ <div class="clearfix"></div>
+</form>
+
+<h3>{{lang_locations}}</h3>
+
+<p>{{lang_locationsInUse}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_location}}</th>
+ <th>{{lang_groups}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#locations}}
+ <tr>
+ <td class="{{lclass}}">
+ {{locationname}}
+ {{#ra_never}}
+ <span class="glyphicon glyphicon-remove text-danger" title="{{lang_roomRemoteAccessDisabled}}"></span>
+ {{/ra_never}}
+ {{#ra_selective}}
+ <span class="glyphicon glyphicon-time text-danger" title="{{lang_roomRemoteAccessWhenClosed}}"></span>
+ {{/ra_selective}}
+ </td>
+ <td>{{#groups}}
+ [<a class="{{gclass}}" href="?do=remoteaccess&amp;groupid={{groupid}}">{{groupname}}</a>]
+ {{/groups}}</td>
+ </tr>
+ {{/locations}}
+ </tbody>
+</table> \ No newline at end of file