summaryrefslogblamecommitdiffstats
path: root/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php
blob: a02c5180488a6bec74b98194cb91235fe484306b (plain) (tree)
























































































































































































                                                                                                                                                 

                                                              



































































                                                                                                                                                        
                                      











































































                                                                                                              
<?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;
	}

}