summaryrefslogtreecommitdiffstats
path: root/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php')
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php478
1 files changed, 478 insertions, 0 deletions
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php
new file mode 100644
index 00000000..9421684f
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php
@@ -0,0 +1,478 @@
+<?php
+
+class ScriptBuilderIpxe extends ScriptBuilderBase
+{
+
+ private function getUrlBase(): string
+ {
+ if (isset($_SERVER['REQUEST_URI'])) {
+ $url = parse_url($_SERVER['REQUEST_URI']);
+ if (isset($_SERVER['SCRIPT_URI']) && preg_match('#^(\w+://[^/]+)#', $_SERVER['SCRIPT_URI'], $out)) {
+ $urlbase = $out[1];
+ } elseif (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['SERVER_NAME'])) {
+ $urlbase = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'];
+ } elseif (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['SERVER_ADDR'])) {
+ $urlbase = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_ADDR'];
+ } else {
+ $urlbase = 'http://' . $this->serverIp;
+ }
+ return $urlbase . $url['path'];
+ }
+ // Static fallback
+ return 'http://' . $this->serverIp . '/boot/ipxe';
+
+ }
+
+ private function getUrlFull(?bool &$hasExt = null, ?string $key = null, ?string $value = null): string
+ {
+ $url = parse_url($_SERVER['REQUEST_URI']);
+ $urlbase = $this->getUrlBase();
+ if (empty($url['query'])) {
+ $fromQuery = [];
+ } else {
+ parse_str($url['query'], $fromQuery);
+ foreach ($fromQuery as &$v) {
+ $v = urlencode($v);
+ }
+ unset($v);
+ }
+ unset($fromQuery['entryid'], $fromQuery['special']);
+ if ($key !== null) {
+ $fromQuery[$key] = $value;
+ }
+ $hasExt = isset($fromQuery['slx-extensions']);
+ $required = [
+ 'uuid' => '${uuid}',
+ 'mac' => '${mac}',
+ 'manuf' => '${manufacturer:uristring}',
+ 'product' => '${product:uristring}',
+ 'platform' => '${platform:uristring}',
+ ];
+ $fullQuery = '?';
+ foreach ($required + $fromQuery as $k => $v) { // Loop instead of http_build_query since we don't want escaping for the varnames!
+ $fullQuery .= $k . '=' . $v . '&';
+ }
+ return $urlbase . $fullQuery;
+ }
+
+ /**
+ * Redirect to same URL, but add our extended params
+ */
+ private function redirect(string $key = null, string $value = null): string
+ {
+ // Redirect to self with added parameters
+ $urlfull = $this->getUrlFull($hasExt, $key, $value);
+ if ($hasExt) {
+ $output = "#!ipxe\nset self {$urlfull} ||\n";
+ } else {
+ $output = <<<HERE
+#!ipxe
+set slxtest:string something ||
+iseq \${slxtest:md5} \${} && set slxext 0 || set slxext 1 ||
+clear slxtest ||
+set self {$urlfull}slx-extensions=\${slxext} ||
+
+HERE;
+ }
+ $output .= <<<HERE
+:retry
+echo Chaining to \${self}
+chain -ar \${self} ||
+echo Chaining to self failed with \${errno}, retrying in a bit...
+sleep 5
+goto retry
+
+HERE;
+ return $output;
+ }
+
+ /**
+ * Called when we handle a real client request, and don't just generate static data
+ * for whatever use-case that might have. In the latter case, it wouldn't make much sense
+ * to generate a redirect code snippet.
+ * @return string
+ */
+ public function bootstrapLive()
+ {
+ // Check if required arguments are given; if not, spit out according script and chain to self
+ if ($this->uuid === false || $this->platform === '') {
+ // REQUIRED so we can hide incompatible entries
+ return $this->redirect();
+ }
+ return false;
+ }
+
+ public function getBootEntry(?BootEntry $entry): string
+ {
+ if ($entry === null) {
+ return "#!ipxe\nprompt --timeout 5000 Invalid boot entry id\n";
+ }
+ return $entry->toScript($this);
+ }
+
+ public function getMenu(IPxeMenu $menu, bool $bootstrap): string
+ {
+ if ($bootstrap) {
+ return "#!ipxe\nimgfree ||\n" . $this->menuToScript($menu);
+ }
+ $base = $this->getUrlFull();
+ return "#!ipxe\nset self {$base} ||\n" . $this->menuToScript($menu);
+ }
+
+ public function menuToScript(IPxeMenu $menu): string
+ {
+ if ($this->hasExtension) {
+ $slxConsoleUpdate = '--update';
+ } else {
+ $slxConsoleUpdate = '';
+ }
+
+ $output = <<<HERE
+:start
+
+imgstat bg-menu || imgfetch --name bg-menu /tftp/pxe-menu.png ||
+console --left 55 --top 88 --right 63 --bottom 64 --keep --picture bg-menu ||
+
+colour --rgb 0xffffff 7 ||
+colour --rgb 0xcccccc 5 ||
+colour --rgb 0x000000 0 ||
+colour --rgb 0xdddddd 6 ||
+cpair --foreground 0 --background 4 1 ||
+cpair --foreground 0 --background 5 2 ||
+cpair --foreground 7 --background 9 0 ||
+
+:slx_menu
+
+console --left 55 --top 88 --right 63 --bottom 64 $slxConsoleUpdate --keep --picture bg-menu ||
+
+menu -- {$menu->title} || prompt --timeout 5000 Error creating menu ||
+
+HERE;
+ foreach ($menu->items as $item) {
+ $output .= $this->getMenuItemScript($menu->defaultEntryId, $item);
+ }
+ if ($menu->defaultEntryId === null) {
+ $default = "poweroff || exit 1 ||";
+ } else {
+ $default = "chain -a \${self}&entryid={$menu->defaultEntryId} ||";
+ }
+ $output .= "choose";
+ if ($menu->timeoutMs > 0) {
+ $output .= " --timeout {$menu->timeoutMs}";
+ }
+ $output .= " selection || goto default || goto fail\n";
+ $output .= <<<HERE
+console --left 60 --top 130 --right 67 --bottom 86 $slxConsoleUpdate ||
+set slx_exit \${} ||
+chain -a \${self}&entryid=\${selection} ||
+iseq \${slx_exit} \${} || console ||
+iseq \${slx_exit} \${} || echo Exiting with code \${slx_exit} ||
+iseq \${slx_exit} \${} || exit \${slx_exit}
+goto fail || goto start
+goto \${target} ||
+echo Could not find menu entry in script.
+prompt Press any key to continue.
+goto start
+:default
+$default
+:fail
+prompt Boot failed. Press any key to start.
+goto start
+
+HERE;
+ return $output;
+ }
+
+ private function getMenuItemScript(int $requestedDefaultId, MenuEntry $entry): string
+ {
+ $str = 'item ';
+ if ($entry->gap) {
+ $str .= '--gap -- ';
+ } else {
+ if ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform)))
+ return '';
+ if ($entry->hidden && $this->hasExtension) {
+ if ($entry->hotkey === false)
+ return ''; // Hidden entries without hotkey are illegal
+ $str .= '--hidden ';
+ }
+ if ($entry->hotkey !== false) {
+ $str .= '--key ' . $entry->hotkey . ' ';
+ }
+ if ($entry->menuentryid == $requestedDefaultId) {
+ $str .= '--default ';
+ }
+ $str .= "-- {$entry->menuentryid} ";
+ }
+ if (empty($entry->title)) {
+ $str .= '${}';
+ } else {
+ $str .= $entry->title;
+ }
+ return $str . " || prompt Could not create menu item for {$entry->menuentryid}\n";
+ }
+
+ public function getSpecial(string $special): string
+ {
+ if ($special === 'localboot') {
+ // Get preferred localboot method, depending on system model
+ // Check if required arguments are given; if not, spit out according script and chain to self
+ // Get platform - EFI or PCBIOS
+ $manuf = Request::any('manuf', false, 'string');
+ $product = Request::any('product', false, 'string');
+ if ($this->uuid === false && $manuf === false && $product === false) {
+ return $this->redirect('special', 'localboot');
+ }
+ $BOOT_METHODS = Localboot::BOOT_METHODS[$this->platform];
+ $localboot = false;
+ $model = false;
+ if ($this->uuid !== false && Module::get('statistics') !== false) {
+ // If we have the machine table, we rather try to look up the system model from there, using the UUID
+ $row = Database::queryFirst('SELECT systemmodel FROM machine WHERE machineuuid = :uuid', ['uuid' => $this->uuid]);
+ if ($row !== false && !empty($row['systemmodel'])) {
+ $model = $row['systemmodel'];
+ }
+ }
+ if ($model === false) {
+ // Otherwise use what iPXE sent us
+ $manuf = $this->modfilt($manuf);
+ $product = $this->modfilt($product);
+ if (!empty($product)) {
+ $model = $product;
+ if (!empty($manuf)) {
+ $model .= " ($manuf)";
+ }
+ $model = Util::ansiToUtf8($model);
+ }
+ }
+ // Query
+ if ($model !== false) {
+ $e = strtolower($this->platform); // We made sure $this->platform is either PCBIOS or EFI, so no injection possible
+ $row = Database::queryFirst("SELECT $e AS bootmethod FROM serversetup_localboot WHERE systemmodel = :model LIMIT 1",
+ ['model' => $model]);
+ if ($row !== false) {
+ $localboot = $row['bootmethod'];
+ }
+ }
+ if ($localboot === false || !isset($BOOT_METHODS[$localboot])) {
+ $localboot = Localboot::getDefault()[$this->platform];
+ if (!isset($BOOT_METHODS[$localboot])) {
+ $localboot = array_keys($BOOT_METHODS)[0];
+ }
+ }
+ // Convert to actual ipxe code
+ $localboot = $BOOT_METHODS[$localboot] ?? 'prompt Localboot not possible';
+ $output = <<<BLA
+imgfree ||
+console ||
+$localboot || goto fail
+
+BLA;
+ //
+ } else {
+ $output = "prompt --timeout 5000 Unknown special command '$special' ||\nchain -ar \${self}\n";
+ }
+ return $output;
+ }
+
+ public function output(string $string): void
+ {
+ // iPXE introduced UTF-8 support at some point in 2022, and now expects all text/script files to be
+ // encoded as such. Since we still offer to use older versions, we need to detect that here and handle
+ // all non-ASCII chars differently.
+ // Use 'ipxe.compile-time' instead of const from IpxeBuilder to avoid pulling in another include
+ if (!preg_match('/Version: (\d{4})-\d{2}-\d{2}\b/', Property::get('ipxe.compile-time'), $out)
+ || (int)$out[1] >= 2022) {
+ Header('Content-Type: text/plain; charset=UTF-8');
+ echo $string;
+ } else {
+ if ($this->platform === 'EFI') {
+ $cs = 'ASCII';
+ } else {
+ $cs = 'IBM437';
+ }
+ Header('Content-Type: text/plain; charset=' . $cs);
+
+ setlocale(LC_ALL, 'de_DE.UTF-8', 'de_DE.utf-8', 'de_DE.utf8', 'de_DE', 'de', 'German', 'ge', 'en_US.UTF-8', 'en_US.utf-8');
+ echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $string);
+ }
+ }
+
+ public function modfilt($str)
+ {
+ if (empty($str) || preg_match('/product\s+name|be\s+filled|unknown|default\s+string|system\s+model|manufacturer/i', $str))
+ return false;
+ return trim(preg_replace('/\s+/', ' ', $str));
+ }
+
+ const PROP_PW_SALT = 'ipxe.salt.';
+
+ private function passwordDialog(MenuEntry $entry): string
+ {
+ if ($this->hasExtension) {
+ $salt = dechex(mt_rand(0x100000, 0xFFFFFF));
+ Property::addToList(self::PROP_PW_SALT . $this->clientIp, $salt, 5);
+ return <<<HERE
+set password \${} ||
+login --nouser ||
+set password \${password:md5}-{$entry->menuentryid}
+set password \${password:md5}$salt
+params
+param pwhash \${password:md5}
+chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail ||
+
+HERE;
+ }
+ return <<<HERE
+set username PASSWORD ONLY ||
+login ||
+params
+param pwplain \${password}
+chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail ||
+
+HERE;
+ }
+
+ public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string
+ {
+ if ($entry === null)
+ return "#!ipxe\nprompt --timeout 10000 Invalid menu entry id\n";
+ $base = $this->getUrlBase();
+ $meid = $entry->menuEntryId();
+ // Make sure legacy variables are set; they might get used
+ $output = <<<HERE
+#!ipxe
+set ipappend1 ip=\${ip}:{$this->serverIp}:\${gateway}:\${netmask}
+set ipappend2 BOOTIF=01-\${mac:hexhyp}
+set serverip {$this->serverIp} ||
+iseq \${idx} \${} && set idx:string X ||
+iseq \${self} \${} && set self {$base}? ||
+set menuentryid $meid ||
+
+HERE;
+ // Check for password
+ if ($honorPassword && !empty($entry->md5pass)) {
+ $pwh = Request::post('pwhash', false, 'string');
+ $pwp = Request::post('pwplain', false, 'string');
+ if ($pwh === false && $pwp === false) {
+ return $output . $this->passwordDialog($entry);
+ }
+ $ok = false;
+ if ($pwh !== false) {
+ $list = Property::getList(self::PROP_PW_SALT . $this->clientIp);
+ foreach ($list as $salt) {
+ if ($pwh === md5($entry->md5pass . $salt)) {
+ $ok = true;
+ break;
+ }
+ }
+ }
+ if (!$ok && $pwp !== false && !empty($entry->plainpass)) {
+ $ok = ($pwp === $entry->plainpass);
+ }
+ if (!$ok) {
+ return $output . "prompt --timeout 10000 Wrong password ||\n";
+ }
+ }
+ // Output actual entry
+ $output .= str_replace('%fail%', 'fail', $entry->getBootEntryScript($this));
+ $output .= <<<HERE
+
+goto end
+:fail
+prompt --timeout 5000 Error launching selected boot entry ||
+:end
+
+HERE;
+ return $output;
+ }
+
+ public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi) : string
+ {
+ if ($agnostic !== null)
+ return $this->execDataToScriptInternal($agnostic) . "\ngoto fail\n";
+
+ if (empty($this->platform)) {
+ // output dynamic code that decides client-side
+ $biosLabel = $this->getLabel();
+ $output = 'iseq ${platform} efi || goto ' . $biosLabel . "\n";
+ // EFI
+ if ($efi !== null) {
+ $output .= $this->execDataToScriptInternal($efi) . "\n";
+ } else {
+ $output .= "echo EFI not supported\n";
+ }
+ $output .= "goto fail\n"
+ . ':' . $biosLabel . "\n";
+ if ($bios !== null) {
+ $output .= $this->execDataToScriptInternal($bios) . "\n";
+ } else {
+ $output .= "echo BIOS not supported\n";
+ }
+ return $output . "goto fail\n";
+ }
+ // static, we know in advance
+ if ($efi !== null && $this->platform === BootEntry::EFI)
+ return $this->execDataToScriptInternal($efi) . "\ngoto fail\n";
+ // Should be BIOS at this point
+ return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData()) . "\ngoto fail\n";
+ }
+
+ private function execDataToScriptInternal(ExecData $entry) : string
+ {
+ $entry->sanitize();
+ $script = '';
+ if ($entry->resetConsole) {
+ $script .= "console ||\n";
+ }
+ if ($entry->imageFree) {
+ $script .= "imgfree ||\n";
+ }
+ foreach ($entry->dhcpOptions as $opt) {
+ if (empty($opt['value'])) {
+ $val = '${}';
+ } else {
+ if (empty($opt['hex'])) {
+ $val = bin2hex($opt['value']);
+ } else {
+ $val = $opt['value'];
+ }
+ preg_match_all('/[0-9a-f]{2}/', $val, $out);
+ $val = implode(':', $out[0]);
+ }
+ $script .= 'set net${idx}/' . $opt['opt'] . ':hex ' . $val
+ . ' || prompt Cannot override DHCP server option ' . $opt['opt'] . ". Press any key to continue anyways.\n";
+ }
+ $initrds = [];
+ if (!empty($entry->initRd)) {
+ foreach (array_values($entry->initRd) as $i => $initrd) {
+ if (empty($initrd))
+ continue;
+ $script .= "initrd --name initrd$i $initrd || goto fail\n";
+ $initrds[] = "initrd$i";
+ }
+ }
+ $script .= "boot ";
+ if ($entry->autoUnload) {
+ $script .= "-a ";
+ }
+ if ($entry->replace) {
+ $script .= "-r ";
+ }
+ $script .= $entry->executable;
+ if (!empty($initrds)) {
+ foreach ($initrds as $initrd) {
+ $script .= " initrd=$initrd";
+ }
+ }
+ if (!empty($entry->commandLine)) {
+ $script .= ' ' . $entry->commandLine . ' slx.ipxe.id=${menuentryid}';
+ }
+ $script .= " || goto fail\n";
+ if ($entry->resetConsole) {
+ $script .= "goto start ||\n";
+ }
+ return $script;
+ }
+
+}