diff options
author | Simon Rettberg | 2020-06-10 17:12:02 +0200 |
---|---|---|
committer | Simon Rettberg | 2020-06-10 17:12:02 +0200 |
commit | 3fb99a5e8c229885602198bea4dce26bcf0fcc4f (patch) | |
tree | 775501f868c5afe913ef6485dabb89feefea7fb0 /modules-available/serversetup-bwlp-ipxe | |
parent | [statistics] Fix querying location 0 (no location) (diff) | |
download | slx-admin-3fb99a5e8c229885602198bea4dce26bcf0fcc4f.tar.gz slx-admin-3fb99a5e8c229885602198bea4dce26bcf0fcc4f.tar.xz slx-admin-3fb99a5e8c229885602198bea4dce26bcf0fcc4f.zip |
[serversetup-bwlp-ipxe] Start refactoring ipxe script generator
This is WIP. Mostly restored all the old functionality.
Boot entries are fetched when selected, not embedded in the main script,
so password protection is a bit stronger.
Hopefully allows for other script generators in the future.
Diffstat (limited to 'modules-available/serversetup-bwlp-ipxe')
8 files changed, 749 insertions, 461 deletions
diff --git a/modules-available/serversetup-bwlp-ipxe/api.inc.php b/modules-available/serversetup-bwlp-ipxe/api.inc.php index c3804a03..303f1560 100644 --- a/modules-available/serversetup-bwlp-ipxe/api.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/api.inc.php @@ -1,273 +1,12 @@ <?php -// Menu mode - -$serverIp = Property::getServerIp(); - -// Check if required arguments are given; if not, spit out according script and chain to self -$uuid = Request::any('uuid', false, 'string'); -// Get platform - EFI or PCBIOS -$platform = Request::any('platform', false, 'string'); -$manuf = Request::any('manuf', false, 'string'); -$product = Request::any('product', false, 'string'); -$slxExtensions = Request::any('slx-extensions', false, 'int'); - -if ($platform === false || ($uuid === false && $product === false) || $slxExtensions === false) { - // Redirect to self with added parameters - $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']; +(function() { + $builder = new ScriptBuilderIpxe(); + $entryId = Request::get('entryid', false, 'int'); + if ($entryId !== false) { + $data = $builder->getMenuEntry($entryId); } else { - $urlbase = 'http://' . $serverIp; - } - $urlbase .= $url['path']; - if (empty($url['query'])) { - $arr = []; - } else { - parse_str($url['query'], $arr); - foreach ($arr as &$v) { - $v = urlencode($v); - } - unset($v); - } - $arr['uuid'] = '${uuid}'; - $arr['mac'] = '${mac}'; - $arr['manuf'] = '${manufacturer:uristring}'; - $arr['product'] = '${product:uristring}'; - $arr['platform'] = '${platform:uristring}'; - $query = '?'; - foreach ($arr as $k => $v) { - $query .= $k . '=' . $v . '&'; - } - //$query = substr($query, 0, -1); - echo <<<HERE -#!ipxe -set slxtest:string something || -iseq \${slxtest:md5} \${} && set slxext 0 || set slxext 1 || -clear slxtest || -set self {$urlbase}{$query}slx-extensions=\${slxext} -:retry -echo Chaining to \${self} -chain -ar \${self} || -echo Chaining to self failed with \${errno}, retrying in a bit... -sleep 5 -goto retry -HERE; - exit; -} -// ipxe has it lowercase, but we use uppercase -$platform = strtoupper($platform); -if ($platform !== 'PCBIOS' && $platform !== 'EFI') { - $platform = 'PCBIOS'; // Just hope for the best? -} - -$BOOT_METHODS = Localboot::BOOT_METHODS[$platform]; - -$ip = $_SERVER['REMOTE_ADDR']; -if (substr($ip, 0, 7) === '::ffff:') { - $ip = substr($ip, 7); -} -$menu = Request::get('menuid', false, 'int'); -if ($menu !== false) { - $menu = new IPxeMenu($menu); - $initLabel = 'slx_menu'; -} else { - $menu = IPxeMenu::forClient($ip, $uuid); - $initLabel = 'init'; -} -// If this is a menu with a single item, treat a timeout of 0 as "boot immediately" instead of "infinite" -if ($menu->itemCount() === 1 && $menu->timeoutMs() === 0 && ($tmp = $menu->getDefaultScriptLabel()) !== false) { - $directBoot = "goto $tmp ||"; - $initLabel = 'init'; -} else { - $directBoot = ''; -} - -// Get preferred localboot method, depending on system model -$localboot = false; -$model = false; -if ($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' => $uuid]); - if ($row !== false && !empty($row['systemmodel'])) { - $model = $row['systemmodel']; - } -} -if ($model === false) { - // Otherwise use what iPXE sent us - 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)); - } - $manuf = modfilt($manuf); - $product = modfilt($product); - if (!empty($product)) { - $model = $product; - if (!empty($manuf)) { - $model .= " ($manuf)"; - } + $data = $builder->fallback(); } -} -// Query -if ($model !== false) { - $e = strtolower($platform); // We made sure $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()[$platform]; - if (!isset($BOOT_METHODS[$localboot])) { - $localboot = array_keys($BOOT_METHODS)[0]; - } -} -// Convert to actual ipxe code -if (isset($BOOT_METHODS[$localboot])) { - $localboot = $BOOT_METHODS[$localboot]; -} else { - $localboot = 'prompt Localboot not possible'; -} - -if ($slxExtensions) { - $slxConsoleUpdate = '--update'; - $slxPasswordOnly = '--nouser'; -} else { - $slxConsoleUpdate = ''; - $slxPasswordOnly = ''; -} - -$output = <<<HERE -#!ipxe - -goto $initLabel || goto fail || - -# functions - -# password check with gotos -# set slx_hash to the expected hash -# slx_salt to the salt to use -# slx_pw_ok to the label to jump on success -# slx_pw_fail to label for wrong pw -:slx_pass_check -login $slxPasswordOnly || -set slxtmp_pw \${password:md5}-\${slx_salt} || goto fail -set slxtmp_pw \${slxtmp_pw:md5} || goto fail -clear password || -iseq \${slxtmp_pw} \${slx_hash} || prompt Wrong password. Press a key. || -iseq \${slxtmp_pw} \${slx_hash} || goto \${slx_pw_fail} || -iseq \${slxtmp_pw} \${slx_hash} && goto \${slx_pw_ok} || -goto fail - -:slx_localboot -imgfree || -console || -$localboot || goto fail - -# start -:init - -set ipappend1 ip=\${ip}:{$serverIp}:\${gateway}:\${netmask} -set ipappend2 BOOTIF=01-\${mac:hexhyp} -set serverip $serverIp || -iseq \${idx} \${} && set idx:string X || - -# Clean up in case we've been chained to -imgfree || - -$directBoot - -imgfetch --name bg-menu /tftp/pxe-menu.png || - -:start - -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 - -iseq \${serverip} \${} || goto ip_check_ok -goto init -:ip_check_ok - -console --left 55 --top 88 --right 63 --bottom 64 $slxConsoleUpdate --keep --picture bg-menu || - -HERE; - -$output .= $menu->getMenuDefinition('target', $platform, $slxExtensions); - -$output .= <<<HERE - -console --left 60 --top 130 --right 67 --bottom 86 $slxConsoleUpdate || -goto \${target} || -echo Could not find menu entry in script. -prompt Press any key to continue. -goto start - -HERE; - -$output .= $menu->getItemsCode($platform); - -/* - -:i5 -chain -a /tftp/memtest.0 passes=1 onepass || goto membad -prompt Memory OK. Press a key. -goto init - -:i8 -set x:int32 0 -:again -console --left 60 --top 130 --right 67 --bottom 96 --picture bg-load --keep || -console --left 55 --top 88 --right 63 --bottom 64 --picture bg-menu --keep || -inc x -iseq \${x} 20 || goto again -prompt DONE. Press dein Knie. -goto slx_menu - -:membad -iseq \${errno} 0x1 || goto memaborted -params -param scrot \${vram} -imgfetch -a http://132.230.8.113/screen.php##params || -prompt Memory is bad. Press a key. -goto init - -:memaborted -params -param scrot \${vram} -imgfetch -a http://132.230.8.113/screen.php##params || -prompt Memory test aborted. Press a key. -goto init - -*/ - -$output .= <<<HERE -:fail -prompt Boot failed. Press any key to start. -goto init -HERE; - -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'); -if ($platform === 'EFI') { - $cs = 'ASCII'; -} else { - $cs = 'IBM437'; -} -Header('Content-Type: text/plain; charset=' . $cs); - -echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $output); + $builder->output($data); +})(); diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php index e89380ce..7b8fb4b5 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php @@ -14,7 +14,11 @@ abstract class BootEntry public abstract function supportsMode($mode); - public abstract function toScript($failLabel, $mode); + /** + * @param ScriptBuilderBase $builder + * @return string + */ + public abstract function toScript($builder); public abstract function toArray(); @@ -54,6 +58,9 @@ abstract class BootEntry if ($module === '.exec') { return new StandardBootEntry($data); } + if ($module === '.special') { + return new SpecialBootEntry($data); + } return null; } @@ -84,7 +91,7 @@ abstract class BootEntry public static function newCustomBootEntry($initData) { - if (empty($initData['script'])) + if (!is_array($initData) || empty($initData)) return null; return new CustomBootEntry($initData); } @@ -135,8 +142,10 @@ class StandardBootEntry extends BootEntry * @var ExecData same for EFI */ protected $efi; - - protected $arch; // Constants below + /** + * @var string BootEntry Constants above + */ + protected $arch; const KEYS = ['executable', 'initRd', 'commandLine', 'replace', 'imageFree', 'autoUnload', 'resetConsole', 'dhcpOptions']; @@ -257,74 +266,13 @@ class StandardBootEntry extends BootEntry return false; } - public function toScript($failLabel, $mode) + public function toScript($builder) { - if (!$this->supportsMode($mode)) { - return "prompt Entry doesn't have an executable for mode $mode\n"; - } - if ($this->arch === BootEntry::AGNOSTIC || $mode == BootEntry::BIOS) { - $entry = $this->pcbios; - } else { - $entry = $this->efi; - } - $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 $failLabel\n"; - $initrds[] = "initrd$i"; - } - } - $script .= "boot "; - if ($entry->autoUnload) { - $script .= "-a "; - } - if ($entry->replace) { - $script .= "-r "; - } - $script .= $entry->executable; - if (!empty($initrds)) { - if ($mode === BootEntry::BIOS) { - $script .= " initrd=" . implode(',', $initrds); - } else { - foreach ($initrds as $initrd) { - $script .= " initrd=$initrd"; - } - } - } - if (!empty($entry->commandLine)) { - $script .= ' ' . $entry->commandLine; - } - $script .= " || goto $failLabel\n"; - if ($entry->resetConsole) { - $script .= "goto start ||\n"; - } - return $script; + if ($this->arch === BootEntry::AGNOSTIC) // Same as below, could construct fall-through but this is more clear + return $builder->execDataToScript($this->pcbios, null, null); + return $builder->execDataToScript(null, + $this->supportsMode(BootEntry::BIOS) ? $this->pcbios : null, + $this->supportsMode(BootEntry::EFI) ? $this->efi : null); } public function addFormFields(&$array) @@ -347,12 +295,22 @@ class StandardBootEntry extends BootEntry class CustomBootEntry extends BootEntry { - protected $script; + /** + * @var string iPXE + */ + protected $ipxe; + + protected $bash; + + protected $grub; public function __construct($data) { - if (is_array($data) && isset($data['script'])) { - $this->script = $data['script']; + if (is_array($data)) { + $this->ipxe = $data['script'] ?? ''; // LEGACY + foreach (['bash', 'grub'] as $key) { + $this->{$key} = $data[$key] ?? ''; + } } } @@ -361,22 +319,24 @@ class CustomBootEntry extends BootEntry return true; } - public function toScript($failLabel, $mode) + public function toScript($builder) { - return str_replace('%fail%', $failLabel, $this->script) . "\n"; + if ($builder instanceof ScriptBuilderIpxe) + return $this->ipxe; + return ''; } public function addFormFields(&$array) { $array['entry'] = [ - 'script' => $this->script, + 'script' => $this->ipxe, ]; $array['script_checked'] = 'checked'; } public function toArray() { - return ['script' => $this->script]; + return ['script' => $this->ipxe]; } } @@ -394,9 +354,10 @@ class MenuBootEntry extends BootEntry return true; } - public function toScript($failLabel, $mode) + public function toScript($builder) { - return 'chain -ar ${self}&menuid=' . $this->menuId . ' || goto ' . $failLabel . "\n"; + $menu = IPxeMenu::get($this->menuId); + return $builder->menuToScript($menu); } public function toArray() @@ -409,3 +370,31 @@ class MenuBootEntry extends BootEntry } } +class SpecialBootEntry extends BootEntry +{ + + private $type; + + public function __construct($type) + { + $this->type = $type['type'] ?? $type; + } + + public function supportsMode($mode) + { + return true; + } + + public function toScript($builder) + { + return $builder->getSpecial($this->type); + } + + public function toArray() + { + return []; + } + + public function addFormFields(&$array) { } + +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php index 3ee33cae..29885588 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php @@ -377,10 +377,8 @@ class IPxe 'entryid' => 'localboot', 'hotkey' => 'L', 'title' => 'Lokales System starten', - 'module' => '.script', - 'data' => json_encode([ - 'script' => 'goto slx_localboot || goto %fail% ||', - ]), + 'module' => '.special', + 'data' => json_encode(['type' => 'localboot']), ]); Database::exec($query, [ @@ -389,7 +387,7 @@ class IPxe 'title' => 'Power off', 'module' => '.script', 'data' => json_encode([ - 'script' => 'poweroff || goto %fail% ||', + 'script' => 'poweroff || goto fail ||', ]), ]); Database::exec($query, @@ -399,7 +397,7 @@ class IPxe 'title' => 'Reboot', 'module' => '.script', 'data' => json_encode([ - 'script' => 'reboot || goto %fail% ||', + 'script' => 'reboot || goto fail ||', ]), ]); } diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php index f87d15c2..15766227 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php @@ -4,23 +4,35 @@ class IPxeMenu { protected $menuid; - protected $timeoutMs; - protected $title; - protected $defaultEntryId; + public $timeoutMs; + public $title; + public $defaultEntryId; /** * @var MenuEntry[] */ - protected $items = []; + public $items = []; + /** + * @param int $menuId + */ + public static function get($menuId, $emptyFallback = false) + { + $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid FROM serversetup_menu + WHERE menuid = :menuid LIMIT 1", ['menuid' => $menuId]); + if ($menu !== false) + return new IPxeMenu($menu); + if (!$emptyFallback) + return null; + return new EmptyIPxeMenu(); + } + + /** + * IPxeMenu constructor. + * + * @param array $menu array for according menu row + */ public function __construct($menu) { - if (!is_array($menu)) { - $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid FROM serversetup_menu - WHERE menuid = :menuid LIMIT 1", ['menuid' => $menu]); - if (!is_array($menu)) { - $menu = ['menuid' => 'foo', 'title' => 'Invalid Menu ID: ' . (int)$menu]; - } - } $this->menuid = (int)$menu['menuid']; $this->timeoutMs = (int)$menu['timeoutms']; $this->title = $menu['title']; @@ -40,41 +52,6 @@ class IPxeMenu } } - public function getMenuDefinition($targetVar, $mode, $slxExtensions) - { - $str = "menu -- {$this->title}\n"; - foreach ($this->items as $item) { - $str .= $item->getMenuItemScript("m_{$this->menuid}", $this->defaultEntryId, $mode, $slxExtensions); - } - if ($this->defaultEntryId === null) { - $defaultLabel = "mx_{$this->menuid}_poweroff"; - } else { - $defaultLabel = "m_{$this->menuid}_{$this->defaultEntryId}"; - } - $str .= "choose"; - if ($this->timeoutMs > 0) { - $str .= " --timeout {$this->timeoutMs}"; - } - $str .= " $targetVar || goto $defaultLabel || goto fail\n"; - if ($this->defaultEntryId === null) { - $str .= "goto skip_{$defaultLabel}\n" - . ":{$defaultLabel}\n" - . "poweroff || goto fail\n" - . ":skip_{$defaultLabel}\n"; - } - return $str; - } - - public function getItemsCode($mode) - { - $str = ''; - foreach ($this->items as $item) { - $str .= $item->getBootEntryScript("m_{$this->menuid}", 'fail', $mode); - $str .= "goto slx_menu\n"; - } - return $str; - } - public function title() { return $this->title; @@ -94,13 +71,11 @@ class IPxeMenu } /** - * @return string|false Return script label of default entry, false if not set + * @return string|null Return script label of default entry, null if not set */ - public function getDefaultScriptLabel() + public function getDefaultEntryId() { - if ($this->defaultEntryId !== null) - return "m_{$this->menuid}_{$this->defaultEntryId}"; - return false; + return $this->defaultEntryId; } /** @@ -126,10 +101,11 @@ class IPxeMenu $chain = Location::getLocationRootChain($locationId); } if (!empty($chain)) { - $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title, IFNULL(ml.defaultentryid, m.defaultentryid) AS defaultentryid, ml.locationid - FROM serversetup_menu m - INNER JOIN serversetup_menu_location ml USING (menuid) - WHERE ml.locationid IN (:chain)", ['chain' => $chain]); + $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title, + IFNULL(ml.defaultentryid, m.defaultentryid) AS defaultentryid, ml.locationid + FROM serversetup_menu m + INNER JOIN serversetup_menu_location ml USING (menuid) + WHERE ml.locationid IN (:chain)", ['chain' => $chain]); if ($res->rowCount() > 0) { // Make the location id key, preserving order (closest location is first) $chain = array_flip($chain); diff --git a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php index a65e9f98..1e567448 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php @@ -5,33 +5,51 @@ class MenuEntry /** * @var int id of entry, used for pw */ - private $menuentryid; + public $menuentryid; /** * @var false|string key code as expected by iPXE */ - private $hotkey; + public $hotkey; /** * @var string */ - private $title; + public $title; /** * @var bool */ - private $hidden; + public $hidden; /** * @var bool */ - private $gap; + public $gap; /** * @var int */ - private $sortval; + public $sortval; /** * @var BootEntry */ - private $bootEntry = null; + public $bootEntry = null; - private $md5pass = null; + public $plainpass = null; + + public $md5pass = null; + + /** + * @param int $menuEntryId + * @return MenuEntry|null + */ + public static function get($menuEntryId) + { + $row = Database::queryFirst("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title, e.hidden, e.sortval, e.plainpass, e.md5pass, + b.module, b.data AS bootentry + FROM serversetup_menuentry e + LEFT JOIN serversetup_bootentry b USING (entryid) + WHERE e.menuentryid = :id", ['id' => $menuEntryId]); + if ($row === false) + return null; + return new MenuEntry($row); + } /** * MenuEntry constructor. @@ -46,10 +64,10 @@ class MenuEntry $this->{$key} = $value; } } - $this->hotkey = self::getKeyCode($row['hotkey']); + $this->hotkey = self::getKeyCode($row['hotkey'] ?? ''); if (!empty($row['bootentry'])) { $this->bootEntry = BootEntry::fromJson($row['module'], $row['bootentry']); - } elseif ($row['refmenuid'] !== null) { + } elseif (isset($row['refmenuid'])) { $this->bootEntry = BootEntry::forMenu($row['refmenuid']); } $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null && $row['refmenuid'] === null); @@ -60,49 +78,11 @@ class MenuEntry settype($this->menuentryid, 'int'); } - public function getMenuItemScript($lblPrefix, $requestedDefaultId, $mode, $slxExtensions) - { - if ($this->bootEntry !== null && !$this->bootEntry->supportsMode($mode)) - return ''; - $str = 'item '; - if ($this->gap) { - $str .= '--gap -- '; - } else { - if ($this->hidden && $slxExtensions) { - if ($this->hotkey === false) - return ''; // Hidden entries without hotkey are illegal - $str .= '--hidden '; - } - if ($this->hotkey !== false) { - $str .= '--key ' . $this->hotkey . ' '; - } - if ($this->menuentryid == $requestedDefaultId) { - $str .= '--default '; - } - $str .= "-- {$lblPrefix}_{$this->menuentryid} "; - } - if (empty($this->title)) { - $str .= '${}'; - } else { - $str .= $this->title; - } - return $str . " || prompt Could not create menu item for {$lblPrefix}_{$this->menuentryid}\n"; - } - - public function getBootEntryScript($lblPrefix, $failLabel, $mode) + public function getBootEntryScript($builder) { - if ($this->bootEntry === null || !$this->bootEntry->supportsMode($mode)) + if ($this->bootEntry === null) return ''; - $str = ":{$lblPrefix}_{$this->menuentryid}\n"; - if (!empty($this->md5pass)) { - $str .= "set slx_hash {$this->md5pass} || goto $failLabel\n" - . "set slx_salt {$this->menuentryid} || goto $failLabel\n" - . "set slx_pw_ok {$lblPrefix}_ok || goto $failLabel\n" - . "set slx_pw_fail slx_menu || goto $failLabel\n" - . "goto slx_pass_check || goto $failLabel\n" - . ":{$lblPrefix}_ok\n"; - } - return $str . $this->bootEntry->toScript($failLabel, $mode); + return $this->bootEntry->toScript($builder); } public function menuEntryId() diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php new file mode 100644 index 00000000..c6adc953 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php @@ -0,0 +1,83 @@ +<?php + +abstract class ScriptBuilderBase +{ + + private $lblId = 0; + + protected $serverIp; + + protected $platform = ''; + + protected $clientIp; + + /** + * @var bool Running iPXE has slx-extensions + */ + protected $hasExtension = false; + + public function hasExtensions() + { + return $this->hasExtension; + } + + public function platform() + { + return $this->platform; + } + + public function getLabel() + { + return 'b' . mt_rand(100, 999) . 'x' . (++$this->lblId); + } + + public function __construct($platform = null, $serverIp = null, $slxExtensions = null) + { + $this->clientIp = $_SERVER['REMOTE_ADDR']; + if (substr($this->clientIp, 0, 7) === '::ffff:') { + $this->clientIp = substr($this->clientIp, 7); + } + $this->serverIp = $serverIp ?? $_SERVER['SERVER_ADDR'] ?? Property::getServerIp(); + $this->platform = $platform ?? Request::any('platform', false, 'string'); + if ($this->platform !== false) { + $this->platform = strtoupper($this->platform); + } + Header('X-Popo: ' . $this->platform); + if ($this->platform !== 'EFI' && $this->platform !== 'PCBIOS') { + $this->platform = ''; + } + $this->hasExtension = $slxExtensions ?? (bool)Request::any('slx-extensions', false, 'int'); + } + + /** + * Output given string (script) to client, in a suitable encoding, headers, etc. + * @param string $string + */ + public abstract function output($string); + + public abstract function getMenu($menuId); + + public abstract function getMenuEntry($menuEntryId); + + public abstract function getSpecial($special); + + public abstract function fallback(); + + /** + * @param IPxeMenu|null $menu + * @return string + */ + public abstract function menuToScript($menu); + + /** + * Pass EITHER only $agnostic, OR $bios and/or $efi + * If $agnostic is given, it should be used unconditionally, + * and $bios/$efi should be ignored. + * @param ExecData $agnostic + * @param ExecData $bios + * @param ExecData $efi + * @return string + */ + public abstract function execDataToScript($agnostic, $bios, $efi); + +}
\ No newline at end of file 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..23c4bd70 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php @@ -0,0 +1,522 @@ +<?php + +class ScriptBuilderIpxe extends ScriptBuilderBase +{ + + private function getUrlBase() + { + 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(&$hasExt, $key = null, $value = null) + { + $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['menuid'], $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($key = null, $value = null) + { + // 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; + } + + public function fallback() + { + // Check if required arguments are given; if not, spit out according script and chain to self + $uuid = Request::any('uuid', false, 'string'); + error_log("Got UUID='$uuid' PLATF='" . $this->platform . "'"); + if ($uuid === false || $this->platform === '') { + // REQUIRED so we can hide incompatible entries + return $this->redirect(); + } + + $menu = IPxeMenu::forClient($this->clientIp, $uuid); + $out = $this->menuCheckAutostart($menu); + if (!empty($out)) + return "#!ipxe\nimgfree ||\n" . $out; + + return "#!ipxe\nimgfree ||\n" . $this->menuToScript($menu); + } + + public function getMenu($menuId) + { + $menu = IPxeMenu::get($menuId, true); + $base = $this->getUrlFull($he); + return "#!ipxe\nset self {$base} ||\n" . $this->menuToScript($menu); + } + + + /** + * @param IPxeMenu $menu + */ + public function menuToScript($menu) + { + if ($this->hasExtension) { + $slxConsoleUpdate = '--update'; + $slxPasswordOnly = '--nouser'; + } else { + $slxConsoleUpdate = ''; + $slxPasswordOnly = ''; + } + + $serverIp = $this->serverIp; + $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 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 || +chain -a \${self}&entryid=\${selection} || +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; + + /* + + :i5 + chain -a /tftp/memtest.0 passes=1 onepass || goto membad + prompt Memory OK. Press a key. + goto init + + :i8 + set x:int32 0 + :again + console --left 60 --top 130 --right 67 --bottom 96 --picture bg-load --keep || + console --left 55 --top 88 --right 63 --bottom 64 --picture bg-menu --keep || + inc x + iseq \${x} 20 || goto again + prompt DONE. Press dein Knie. + goto slx_menu + + :membad + iseq \${errno} 0x1 || goto memaborted + params + param scrot \${vram} + imgfetch -a http://132.230.8.113/screen.php##params || + prompt Memory is bad. Press a key. + goto init + + :memaborted + params + param scrot \${vram} + imgfetch -a http://132.230.8.113/screen.php##params || + prompt Memory test aborted. Press a key. + goto init + + */ + return $output; + } + + /** + * @param $requestedDefaultId + * @param MenuEntry $entry + * @return string + */ + private function getMenuItemScript($requestedDefaultId, $entry) + { + if ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform))) + return ''; + $str = 'item '; + if ($entry->gap) { + $str .= '--gap -- '; + } else { + 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($special) + { + 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 + $uuid = Request::any('uuid', false, 'string'); + // Get platform - EFI or PCBIOS + $manuf = Request::any('manuf', false, 'string'); + $product = Request::any('product', false, 'string'); + if ($uuid === false && $manuf === false && $product === false) { + return $this->redirect('special', 'localboot'); + } + $BOOT_METHODS = Localboot::BOOT_METHODS[$this->platform]; + $localboot = false; + $model = false; + if ($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' => $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)"; + } + } + } + // 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 + if (isset($BOOT_METHODS[$localboot])) { + $localboot = $BOOT_METHODS[$localboot]; + } else { + $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) + { + 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'); + if ($this->platform === 'EFI') { + $cs = 'ASCII'; + } else { + $cs = 'IBM437'; + } + Header('Content-Type: text/plain; charset=' . $cs); + + 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)); + } + + /** + * @param IPxeMenu $menu + */ + private function menuCheckAutostart($menu) + { + // If this is a menu with a single item, treat a timeout of 0 as "boot immediately" instead of "infinite" + if ($menu->itemCount() === 1 && $menu->timeoutMs() === 0 && ($tmp = $menu->getDefaultEntryId()) !== null) { + if (empty($menu->items[0]->md5pass)) { + return $menu->items[0]->getBootEntryScript($this); + } else { + return $this->passwordDialog($menu->items[0]); + } + } + return ''; + } + + const PROP_PW_SALT = 'ipxe.salt.'; + + /** + * @param MenuEntry $menuEntryId + */ + private function passwordDialog($entry) + { + 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($menuEntryId) + { + $entry = MenuEntry::get($menuEntryId); + if ($entry === null) + return "#!ipxe\nprompt --timeout 10000 Invalid menu entry id: $menuEntryId\n"; + $base = $this->getUrlBase(); + // 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}? || + +HERE; + // Check for password + if (!empty($entry->md5pass)) { // TODO: This should be split out so we can unconditionally get entry code + $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($agnostic, $bios, $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 + 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; + } + $script .= " || goto fail\n"; + if ($entry->resetConsole) { + $script .= "goto start ||\n"; + } + return $script; + } + +} diff --git a/modules-available/serversetup-bwlp-ipxe/page.inc.php b/modules-available/serversetup-bwlp-ipxe/page.inc.php index 4845b2f8..4e3501fb 100644 --- a/modules-available/serversetup-bwlp-ipxe/page.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/page.inc.php @@ -303,6 +303,7 @@ class Page_ServerSetup extends Page FROM serversetup_bootentry be LEFT JOIN serversetup_menuentry sme USING (entryid) LEFT JOIN serversetup_menu sm USING (menuid) + WHERE be.module <> '.special' GROUP BY be.entryid, be.title ORDER BY be.title ASC"); |