<?php
class ScriptBuilderGrub extends ScriptBuilderBase
{
/** @var bool */
private $confCodeEmited = false;
public function __construct(?string $platform = null, ?string $serverIp = null)
{
if (empty($platform)) {
$platform = Request::any('platform', null, 'string');
if ($platform === 'pc' || stripos($platform, 'bios') !== false) {
$platform = 'PCBIOS';
}
}
parent::__construct($platform, $serverIp, false);
}
private function getConfCode(): string
{
if ($this->confCodeEmited)
return '';
$this->confCodeEmited = true;
$str = '
if ! [ "$uuid" ] ; then
smbios --type 1 --get-uuid 8 --set uuid
fi
set serverip="' . $this->getLocalIp() . '"
';
foreach (['mac', 'ip', 'domain', 'hostname'] as $var) {
$str .= <<<EOF
if ! [ "\$$var" ] ; then
set $var="\$net_default_$var"
fi
if ! [ "\$$var" ] ; then
set $var="\$net_efinet0_dhcp_$var"
fi
EOF;
}
return $str;
}
private function getLocalIp(): string
{
if (isset($_SERVER['SCRIPT_URI']) && preg_match('#^\w+://([^/]+)#', $_SERVER['SCRIPT_URI'], $out)) {
$host = $out[1];
} elseif (isset($_SERVER['SERVER_NAME'])) {
$host = $_SERVER['SERVER_NAME'];
} elseif (isset($_SERVER['SERVER_ADDR'])) {
$host = $_SERVER['SERVER_ADDR'];
} else {
$host = $this->serverIp;
}
return $host;
}
private function getGrubBase(): string
{
return '(http,' . $this->getLocalIp() . ')';
}
private function getUrlBase(): string
{
$host = $this->getGrubBase();
if (isset($_SERVER['REQUEST_URI'])) {
$url = parse_url($_SERVER['REQUEST_URI']);
$path = $url['path'];
} else {
// Static fallback
$path = '/boot/ipxe';
}
return $host . $path;
}
private function getUrlFull(?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'], $fromQuery['redir']);
if ($key !== null) {
$fromQuery[$key] = $value;
}
$required = [
'type' => 'grub',
'uuid' => '$uuid',
'mac' => '$mac',
'platform' => '$grub_platform',
];
$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($key, $value);
return $this->getConfCode() . <<<HERE
set self="${urlfull}"
echo "Chaining to \$self..."
configfile \${self}redir=1
HERE;
}
/**
* 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 === null || $this->platform === '') {
// REQUIRED so we can hide incompatible entries
// but avoid redirect cycle
if (Request::any('redir', '', 'string') === '') {
return $this->redirect();
}
}
return false;
}
public function getBootEntry(?BootEntry $entry): string
{
if ($entry === null) {
return "echo Invalid boot entry id\nsleep --interruptible --verbose 10\n";
}
return $entry->toScript($this);
}
public function getMenu(IPxeMenu $menu, bool $bootstrap): string
{
$base = $this->getUrlFull();
return $this->getConfCode()
. "set self=\"{$base}\"\n"
. $this->menuToScript($menu);
}
public function menuToScript(IPxeMenu $menu): string
{
if ($menu->defaultEntryId === null) {
$output = <<<EOF
set timeout=0
EOF;
} else {
$secs = (int)($menu->timeoutMs / 1000);
$output = <<<EOF
set timeout={$secs}
set default="id-{$menu->defaultEntryId}"
EOF;
}
$output .= $this->getConfCode();
foreach ($menu->items as $item) {
$output .= $this->getMenuItemScript($item);
}
return $output;
}
private function getMenuItemScript(MenuEntry $entry): string
{
if (!empty($entry->md5pass) || $entry->hidden)
return ''; // TODO
$str = "menuentry '" . str_replace("'", '', $entry->title) . "' --id 'id-" . $entry->menuentryid . "' {\n";
if ($entry->gap) {
$str .= "true\n"; // AFAICT, not possible in GRUB
} elseif ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform))) {
$str .= "echo Type mismatch\n";
} elseif ($entry->hidden && $entry->hotkey === false) {
$str .= "echo Hidden entries without hotkey are illegal\n"; // Hidden entries without hotkey are illegal
} else {
if ($entry->hotkey !== false) {
// Not supported by grub...
}
if ($entry->bootEntry instanceof MenuBootEntry) {
// Link
$str .= "configfile \${self}entryid={$entry->menuentryid}\n";
} else {
// Embed directly
// TODO: Password. Use read etc.; might need hashsum.mod, in that case, don't embed entry directly but use configfile...
$str .= $this->getMenuEntry($entry, true);
}
}
return $str . "}\n";
}
public function getSpecial(string $special): string
{
if ($special === 'localboot') {
// Sync this with setup-scripts/grub_localboot occasionally...
$output = <<<'EOF'
insmod chain
if [ "$grub_platform" = "pc" ] ; then
chainloader (hd0)+1
chainloader (hd1)+1
chainloader (hd2)+1
fi
insmod fat
insmod part_gpt
echo "Scanning, first pass..."
for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \
(*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do
regexp --set=1:efi_device '^\((.*)\)/' "${efi}"
done
echo "Scanning, second pass..."
for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \
(*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do
if [ -e "${efi}" ]; then
#regexp --set=1:efi_device '^\((.*)\)/' "${efi}"
regexp --set=1:root '^(\(.*\))/' "${efi}"
regexp --set=1:efi_path '^\(.*\)(/.*)$' "${efi}"
echo " >> Found operating system! <<"
echo " Path: '${efi}' on '${root}'"
echo " Fallback '${efi_path}'"
chainloader "${efi}"
boot
echo " That failed..."
fi
done
echo "No EFI known OS found. Exiting."
exit
EOF;
} else {
$output = <<<EOF
echo "Unknown special command: $special"
sleep --interruptible --verbose 10
EOF;
}
return $output . "\n";
}
public function output(string $string): void
{
Header('Content-Type: text/plain; charset=UTF-8');
echo $string;
}
public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string
{
if ($entry === null)
return "echo Invalid menu entry id\nsleep --interruptible --verbose 10\n";
// TODO: Check for password
if ($honorPassword && !empty($entry->md5pass)) {
return "echo TODO: Implement password check...\nsleep --interruptible --verbose 10\n";
}
$meid = $entry->menuEntryId();
$output = $this->getConfCode() . "set menuentryid=$meid\n";
// Output actual entry
$output .= str_replace('%fail%', 'fail', $entry->getBootEntryScript($this));
return $output;
}
public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string
{
if ($agnostic !== null)
return $this->execDataToScriptInternal($agnostic);
if ($efi !== null && $this->platform === BootEntry::EFI)
return $this->execDataToScriptInternal($efi);
// Unknown or not EFI, should be BIOS at this point
return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData());
}
private function execDataToScriptInternal(ExecData $entry): string
{
$entry->sanitize();
$base = $this->getGrubBase();
$script = '';
// Overriding dhcpOpts probably not possible/necessary
$initrds = [];
if (!empty($entry->initRd)) {
foreach ($entry->initRd as $initrd) {
if (empty($initrd))
continue;
$initrds[] = $this->combineUrl($base, $initrd);
}
}
$file = $this->combineUrl($base, $entry->executable);
$script .= "linux $file {$entry->commandLine} slx.ipxe.id=\${menuentryid}\n";
if (!empty($initrds)) {
$script .= "initrd " . implode(' ', $initrds) . "\n";
}
return $script;
}
private function combineUrl(string $base, string $path): string
{
$url = parse_url($path);
if (isset($url['host'])) {
$scheme = $url['scheme'] ?? 'http';
$host = $url['host'];
$base = "($scheme,$host)";
$path = $url['path'] ?? '/';
if (isset($url['query'])) {
$path .= '?' . $url['query'];
}
} else {
if ($path[0] !== '/') {
$path = '/' . $path;
}
}
return $base . $path;
}
}