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 = <<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 = <<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 .= <<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 = <<= 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 <<menuentryid} set password \${password:md5}$salt params param pwhash \${password:md5} chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail || HERE; } return <<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 = <<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 .= <<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; } }