summaryrefslogblamecommitdiffstats
path: root/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php
blob: 5e0531abca68c6fd2deb59401e510bed25c09fd9 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11

     
          

 
           




                                                                                    


                                                                                                                 
                                               
           
                                                                            
         
                                                                                                                       
                            
                                        





                                                                        

                                                                                       
                                         

                                                            
                                         



                                                                                                              
                                                                                                     


                                                                                                         
                                                                                            
                                                





                                                                                                                                 
                                 


                                                    
                                                                        



                                                                                     







                                                                                                                                       
                                                                                   
                                                   

                                                                       


                                                                                                                    

                                         
                                                                                                                                                          




                                                                                                                                         
                                                                                            
                                                                                                           



                                                                            

                                                                                                                                       

                                                                                                    
                                                 
                                                           

                                               
                                                      







                                                                                                                                 

                         
                                    

         
                                                                          
         

                                                                                                                       


                                                       
                                                                         



                                                      

                                                                                                                          
















                                                                                  
                 
                           



                                                   
                  

                                                                                                                                         

         
           
                                                                                                  
                                                                                        


                                                                                             
           

                                                                                                                                     
         

                                        


                                                             
                         



                                                                                                            
                         


                                                                           
                 
                                      


                                                
                                    
                            
                                                                                                     

                                                                          
                        
                                                     
                 







                                                                                                                              


                                                                                                 
                                         
                                                                










                                                                             


                                                
                                                           
                 
                                            
                                           
                              
                                                              
                                                                      






                                                                                                          
                                                                                           




                                                                   
                                                                                                                                                                   



                                            
                                              

                                                                                                 



                                                                                 



                                                                                      
                                                                                                             







                                                                                                                      

                                                                               


                                               


                                                                                                                          



                                                       

         

                                                                   

                                                                                       
           
                                                                                            
         


                                                                
                                                      
                                                      
                                                                     

                                         

                                                          

                                         
                                                                    

                                                               
                                                          
                                                                                                               




                                                                                                                              
                                                                                                                        













                                                                                                                           

                                                                                                                                        
                                                          
                                                                                                                



                                                                     
                         



                                                        
                                                     
         
                                                                                                                  

                                                                                                           
                                      
                         


                                                                         
                                                        
                                                       


                                                          
                                   



                                                                
                                                
                                                                                                  
                                                        
                                                       


                                                          



                                      
                                                               
                                                
                                                                                                         
                                                        
                                                       


                                                                 



                                      


                                                                    

                                                                               





                                                        
                                                      
                                                       
                                                                               






                                                      
                                                      
                                                       
                                                                             


                                   
 
           

                                                                                   
                                                                               
                                      
          
                                                                        
           
                                                                       












                                                                                                                  
                                                                       



                                                                                       
                             
         
 
           
                                                                                   
           
                                                                              
         
                                                                                      







                                                                                   
                                                                                                      

                                                                                         
                                                                                                      

                                                                                     
                                                                                                     







                                                                                                                                                           
                                                                                                                          

                                                 
                                                            







                                                                      
                                                                                                          

                                                          
                                                                                                       

                                                                                
                                                                                            










                                                                                     
           

                                                      
           
                                                               





                                                                    
 
                                                                         



                                                                                                              
                                                                                   




                                                          
 







                                                                                                            
          
                                                        
                                                                                   

                                                   
                                                                                           


                                                                                 
                                               








                                                                                                             
 
<?php

class IPxe
{

	/**
	 * @var BootEntry[]|false Contains all known boot entries (for dup checking)
	 */
	private static $allEntries = false;

	/**
	 * Import all IP-Range based pxe menus from the given directory.
	 *
	 * @param string $configPath The pxelinux.cfg path where to look for menu files in hexadecimal IP format.
	 * @return int Number of menus imported
	 */
	public static function importSubnetPxeMenus(string $configPath): int
	{
		$res = Database::simpleQuery('SELECT menuid, entryid FROM serversetup_menuentry ORDER BY sortval ASC');
		$menus = [];
		foreach ($res as $row) {
			if (!isset($menus[$row['menuid']])) {
				$menus[(int)$row['menuid']] = [];
			}
			$menus[(int)$row['menuid']][] = $row['entryid'];
		}
		$importCount = 0;
		foreach (glob($configPath . '/*', GLOB_NOSORT) as $file) {
			if (!is_file($file) || !preg_match('~/[A-F0-9]{1,8}$~', $file))
				continue;
			$content = file_get_contents($file);
			if ($content === false)
				continue;
			$file = basename($file);
			$start = hexdec(str_pad($file,8, '0'));
			$end =  hexdec(str_pad($file,8, 'f')); // TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^PREFIX
			error_log('From ' . long2ip($start) . ' to ' . long2ip($end));
			// Get all subnets that lie within the range defined by the pxelinux filename
			$res = Database::simpleQuery("SELECT locationid, startaddr, endaddr FROM subnet
				WHERE startaddr >= :start AND endaddr <= :end", compact('start', 'end'));
			$locations = [];
			// Iterate over result, eliminate those that are dominated by others
			foreach ($res as $row) {
				foreach ($locations as &$loc) {
					if ($row['startaddr'] <= $loc['startaddr'] && $row['endaddr'] >= $loc['endaddr']) {
						$loc = false;
					} elseif ($row['startaddr'] >= $loc['startaddr'] && $row['endaddr'] <= $loc['endaddr']) {
						continue 2;
					}
				}
				unset($loc);
				$locations[] = $row;
			}
			$menu = PxeLinux::parsePxeLinux($content, true);
			if ($menu === null) {
				error_log("Skipping empty pxelinux menu file $file");
				continue;
			}
			// Insert all entries first, so we can get the list of entry IDs
			$entries = [];
			self::importPxeMenuEntries($menu, $entries);
			$entries = array_keys($entries);
			$defId = null;
			// Look up entry IDs, if match, ref for this location
			if (($menuId = array_search($entries, $menus)) !== false) {
				error_log('Imported menu ' . $menu->title . ' exists, using for ' . count($locations) . ' locations.');
				// Figure out the default label, get its label name
				$defSection = null;
				foreach ($menu->sections as $section) {
					if ($section->isDefault) {
						$defSection = $section;
					} elseif ($defSection === null && $section->label === $menu->timeoutLabel) {
						$defSection = $section;
					}
				}
				if ($defSection !== null && ($defIdEntry = array_search(self::pxe2BootEntry($defSection), self::$allEntries)) !== false) {
					// Confirm it actually exists (it should since the menu seems identical) and get menuEntryId
					$me = Database::queryFirst('SELECT m.defaultentryid, me.menuentryid FROM serversetup_bootentry be
							INNER JOIN serversetup_menuentry me ON (be.entryid = me.entryid)
							INNER JOIN serversetup_menu m ON (m.menuid = me.menuid)
							WHERE be.entryid = :id AND me.menuid = :menuid',
						['id' => $defIdEntry, 'menuid' => $menuId]);
					if ($me !== false && $me['defaultentryid'] != $me['menuentryid']) {
						$defId = $me['menuentryid'];
					}
				}
			} else {
				error_log('Imported menu ' . $menu->title . ' is NEW, using for ' . count($locations) . ' locations.');
				// Insert new menu
				$menuId = self::insertMenu($menu, 'Auto Imported', null, 0, [], []);
				if ($menuId === null)
					continue;
				$menus[$menuId] = $entries;
				$importCount++;
			}
			foreach ($locations as $loc) {
				if ($loc === false)
					continue;
				Database::exec('INSERT IGNORE INTO serversetup_menu_location (menuid, locationid, defaultentryid)
						VALUES (:menuid, :locationid, :def)', [
					'menuid' => $menuId,
					'locationid' => $loc['locationid'],
					'def' => $defId,
				]);
			}
		}
		return $importCount;
	}

	public static function importLegacyMenu(bool $force = false): bool
	{
		// See if anything is there
		if (!$force && false !== Database::queryFirst("SELECT menuentryid FROM serversetup_menuentry LIMIT 1"))
			return false; // Already exists
		// Now create the default entry
		self::createDefaultEntries();
		$prepend = ['bwlp-default' => null, 'localboot' => null];
		$defaultLabel = 'bwlp-default';
		$menuTitle = 'bwLehrpool Bootauswahl';
		$pxeConfig = '';
		$timeoutSecs = 60;
		// Try to import any customization of the legacy PXELinux menu (despite the property name hinting at iPXE)
		$oldMenu = json_decode(Property::get('ipxe-menu'), true);
		if (is_array($oldMenu)) {
			//
			if (isset($oldMenu['timeout'])) {
				$timeoutSecs = (int)$oldMenu['timeout'];
			}
			if (isset($oldMenu['defaultentry'])) {
				if ($oldMenu['defaultentry'] === 'net') {
					$defaultLabel = 'bwlp-default';
				} elseif ($oldMenu['defaultentry'] === 'hdd') {
					$defaultLabel = 'localboot';
				} elseif ($oldMenu['defaultentry'] === 'custom') {
					$defaultLabel = 'custom';
				}
			}
			if (!empty($oldMenu['custom'])) {
				$pxeConfig = $oldMenu['custom'];
			}
		}
		$append = [
			new PxeSection(null),
			'bwlp-default-dbg' => null,
			new PxeSection(null),
			'poweroff' => null,
		];
		self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig, false), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append);
		return !empty($pxeConfig);
	}

	/**
	 * @param ?string $defaultLabel Fallback for the default label, if PxeMenu doesn't set one
	 * @param int $defaultTimeoutSeconds Default timeout, if PxeMenu doesn't set one
	 * @param (?PxeSection)[] $prepend
	 * @param (?PxeSection)[] $append
	 * @return ?int ID of newly created menu, or null on error, e.g. if the menu is empty
	 */
	public static function insertMenu(?PxeMenu $pxeMenu, string $menuTitle, ?string $defaultLabel, int $defaultTimeoutSeconds,
												 array $prepend, array $append): ?int
	{
		$timeoutMs = [];
		$menuEntries = $prepend;
		if ($pxeMenu !== null) {
			if (!empty($pxeMenu->title)) {
				$menuTitle = $pxeMenu->title;
			}
			if ($pxeMenu->timeoutLabel !== null && $pxeMenu->hasLabel($pxeMenu->timeoutLabel)) {
				$defaultLabel = $pxeMenu->timeoutLabel;
			} elseif ($pxeMenu->hasLabel($pxeMenu->default)) {
				$defaultLabel = $pxeMenu->default;
			}
			$timeoutMs[] = $pxeMenu->timeoutMs;
			$timeoutMs[] = $pxeMenu->totalTimeoutMs;
			self::importPxeMenuEntries($pxeMenu, $menuEntries);
		}
		if (!empty($append)) {
			$menuEntries += $append;
		}
		if (empty($menuEntries))
			return null;
		// Make menu
		$timeoutMs = array_filter($timeoutMs, function($x) { return is_int($x) && $x > 0; });
		if (empty($timeoutMs)) {
			$timeoutMs = (int)($defaultTimeoutSeconds * 1000);
		} else {
			$timeoutMs = min($timeoutMs);
		}
		$isDefault = (int)(Database::queryFirst('SELECT menuid FROM serversetup_menu WHERE isdefault = 1') === false);
		Database::exec("INSERT INTO serversetup_menu (timeoutms, title, defaultentryid, isdefault)
			VALUES (:timeoutms, :title, NULL, :isdefault)", [
			'title' => self::sanitizeIpxeString($menuTitle),
			'timeoutms' => $timeoutMs,
			'isdefault' => $isDefault,
		]);
		$menuId = Database::lastInsertId();
		// Figure out entryid for default label
		// Fiddly diddly way of getting the mangled entryid for the wanted pxe menu label
		$defaultEntryId = false;
		$fallbackDefault = false;
		foreach ($menuEntries as $entryId => $section) {
			if ($section === null)
				continue;
			if ($section->isDefault) {
				$defaultEntryId = $entryId;
				break;
			}
			if ($section->label === $defaultLabel) {
				$defaultEntryId = $entryId;
			}
			if ($fallbackDefault === false && !empty($entryId)) {
				$fallbackDefault = $entryId;
			}
		}
		if ($defaultEntryId === false) {
			$defaultEntryId = $fallbackDefault;
		}
		// Link boot entries to menu
		$defaultMenuEntryId = null;
		$order = 1000;
		foreach ($menuEntries as $entryId => $entry) {
			if ($entry !== null && $entry->isTextOnly()) {
				// Gap entry
				Database::exec("INSERT INTO serversetup_menuentry
				(menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass)
				VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, '', '')", [
					'menuid' => $menuId,
					'entryid' => null,
					'hotkey' => '',
					'title' => self::sanitizeIpxeString($entry->title),
					'hidden' => 0,
					'sortval' => $order += 100,
				]);
				continue;
			}
			$data = Database::queryFirst("SELECT entryid, hotkey, title FROM serversetup_bootentry WHERE entryid = :entryid", ['entryid' => $entryId]);
			if ($data === false)
				continue;
			$data['pass'] = '';
			$data['hidden'] = 0;
			if ($entry !== null) {
				$data['hidden'] = (int)$entry->isHidden;
				// Prefer explicit data from this imported menu over the defaults
				$title = self::sanitizeIpxeString($entry->title);
				if (!empty($title)) {
					$data['title'] = $title;
				}
				if (MenuEntry::getKeyCode($entry->hotkey) !== false) {
					$data['hotkey'] = $entry->hotkey;
				}
				if (!empty($entry->passwd)) {
					// Most likely it's a hash, so we cannot recover; ask people to reset
					$data['pass'] ='please_reset';
				}
			}
			$data['menuid'] = $menuId;
			$data['sortval'] = $order += 100;
			$res = Database::exec("INSERT INTO serversetup_menuentry
				(menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass)
				VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, :pass, :pass)", $data);
			if ($res !== false && $entryId === $defaultEntryId) {
				$defaultMenuEntryId = Database::lastInsertId();
			}
		}
		// Now we can set default entry
		if (!empty($defaultMenuEntryId)) {
			Database::exec("UPDATE serversetup_menu SET defaultentryid = :menuentryid WHERE menuid = :menuid",
				['menuid' => $menuId, 'menuentryid' => $defaultMenuEntryId]);
		}
		// TODO: masterpw? rather pointless....
		//$oldMenu['masterpasswordclear'];
		return $menuId;
	}

	/**
	 * Import only the bootentries from the given PXELinux menu
	 *
	 * @param PxeSection[] $menuEntries Where to append the generated menu items to
	 */
	public static function importPxeMenuEntries(PxeMenu $pxe, array &$menuEntries): void
	{
		if (self::$allEntries === false) {
			self::$allEntries = BootEntry::getAll();
		}
		foreach ($pxe->sections as $section) {
			if ($section->isLocalboot()) {
				$menuEntries['localboot'] = $section;
				continue;
			}
			if ($section->isTextOnly()) {
				$menuEntries[] = $section;
				continue;
			}
			$label = self::cleanLabelFixLocal($section);
			$entry = self::pxe2BootEntry($section);
			if ($entry === null)
				continue; // Error? Ignore
			if ($label !== false || ($label = array_search($entry, self::$allEntries)) !== false) {
				// Exact Duplicate, Do Nothing
				error_log('Ignoring duplicate boot entry ' . $section->label . ' (' . $section->kernel . ')');
			} else {
				// Seems new one; make sure label doesn't collide
				error_log('Adding new boot entry ' . $section->label . ' (' . $section->kernel . ')');
				$label = substr(preg_replace('/[^a-z0-9_\-]/', '', strtolower($section->label)), 0, 16);
				while (empty($label) || array_key_exists($label, self::$allEntries)) {
					$label = 'i-' . substr(md5(microtime(true) . $section->kernel . mt_rand()), 0, 14);
				}
				self::$allEntries[$label] = $entry;
				$hotkey = MenuEntry::filterKeyName($section->hotkey);
				// Create boot entry
				$data = $entry->toArray();
				$title = self::sanitizeIpxeString($section->title);
				if (empty($title)) {
					$title = self::sanitizeIpxeString($section->label);
				}
				if (empty($title)) {
					$title = $label;
				}
				Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, module, hotkey, title, builtin, data)
					VALUES (:label, :module, :hotkey, :title, 0, :data)', [
					'label' => $label,
					'module' => ($entry instanceof StandardBootEntry) ? '.exec' : '.script',
					'hotkey' => $hotkey,
					'title' => $title,
					'data' => json_encode($data),
				]);
			}
			$menuEntries[$label] = $section;
		}
	}

	public static function createDefaultEntries()
	{
		$query = 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, module, data)
			VALUES (:entryid, :hotkey, :title, 1, :module, :data)
			ON DUPLICATE KEY UPDATE builtin = 1, module = VALUES(module), data = VALUES(data)';
		Database::exec($query,
			[
				'entryid' => 'bwlp-default',
				'hotkey' => 'B',
				'title' => 'bwLehrpool-Umgebung starten',
				'module' => 'minilinux',
				'data' => json_encode([
					'id' => 'default',
					'kcl-extra' => '',
					'debug' => false,
				]),
			]);
		Database::exec($query,
			[
				'entryid' => 'bwlp-default-dbg',
				'hotkey' => 'D',
				'title' => 'bwLehrpool-Umgebung starten (nosplash, debug output)',
				'module' => 'minilinux',
				'data' => json_encode([
					'id' => 'default',
					'kcl-extra' => '',
					'debug' => true,
				]),
			]);
		Database::exec($query,
			[
				'entryid' => 'bwlp-default-sh',
				'hotkey' => 'S',
				'title' => 'bwLehrpool-Umgebung starten (nosplash, !!! debug shell !!!)',
				'module' => 'minilinux',
				'data' => json_encode([
					'id' => 'default',
					'kcl-extra' => 'debug=1',
					'debug' => true,
				]),
			]);
		Database::exec($query,
			[
				'entryid' => 'localboot',
				'hotkey' => 'L',
				'title' => 'Lokales System starten',
				'module' => '.special',
				'data' => json_encode(['type' => 'localboot']),
			]);
		Database::exec($query,
			[
				'entryid' => 'poweroff',
				'hotkey' => 'P',
				'title' => 'Power off',
				'module' => '.script',
				'data' => json_encode([
					'script' => 'poweroff || goto fail ||',
				]),
			]);
		Database::exec($query,
			[
				'entryid' => 'reboot',
				'hotkey' => 'R',
				'title' => 'Reboot',
				'module' => '.script',
				'data' => json_encode([
					'script' => 'reboot || goto fail ||',
				]),
			]);
	}

	/**
	 * Try to figure out whether this is one of our default entries and returns
	 * that according label.
	 * Also it patches the entry if it's referencing the local bwlp install
	 * but with different options.
	 *
	 * @return string|false existing label if match, false otherwise
	 */
	private static function cleanLabelFixLocal(PxeSection $section)
	{
		$myip = Property::getServerIp();
		// Detect our "old" entry types
		if (count($section->initrd) === 1 && preg_match(",$myip/boot/default/kernel\$,", $section->kernel)
				&& preg_match(",$myip/boot/default/initramfs-stage31\$,", $section->initrd[0])) {
			// Kernel and initrd match, examine KCL
			if ($section->append === 'slxbase=boot/default vga=current quiet splash') {
				// Normal
				return 'bwlp-default';
			} elseif ($section->append === 'slxbase=boot/default') {
				// Debug output
				return 'bwlp-default-dbg';
			} else {
				// Transform to relative URL, leave KCL
				$section->kernel = '/boot/default/kernel';
				$section->initrd = ['/boot/default/initramfs-stage31'];
			}
		}
		return false;
	}

	/**
	 * @return BootEntry|null The according boot entry, null if it's unparsable
	 */
	private static function pxe2BootEntry(PxeSection $section): ?BootEntry
	{
		if (preg_match('/(pxechain\.com|pxechn\.c32)$/i', $section->kernel)) {
			// Chaining -- create script
			$args = preg_split('/\s+/', $section->append);
			$script = '';
			$file = false;
			for ($i = 0; $i < count($args); ++$i) {
				$arg = $args[$i];
				if ($arg === '-c') { // PXELINUX config file option
					++$i;
					$script .= "set netX/209:string {$args[$i]} || goto %fail%\n";
				} elseif ($arg === '-p') { // PXELINUX prefix path option
					++$i;
					$script .= "set netX/210:string {$args[$i]} || goto %fail%\n";
				} elseif ($arg === '-t') { // PXELINUX timeout option
					++$i;
					$script .= "set netX/211:int32 {$args[$i]} || goto %fail%\n";
				} elseif ($arg === '-o') { // Overriding various DHCP options
					++$i;
					if (preg_match('/^((?:0x)?[a-f0-9]{1,4})\.([bwlsh])=(.*)$/i', $args[$i], $out)) {
						// TODO: 'q' (8byte) unsupported for now
						$opt = intval($out[1], 0);
						if ($opt > 0 && $opt < 255) {
							static $optType = ['b' => 'uint8', 'w' => 'uint16', 'l' => 'int32', 's' => 'string', 'h' => 'hex'];
							$type = $optType[$out[2]];
							$script .= "set netX/{$opt}:{$type} {$args[$i]} || goto %fail%\n";
						}
					}
				} elseif ($arg[0] === '-') {
					continue;
				} elseif ($file === false) {
					$file = self::parseFile($arg);
				}
			}
			if ($file !== false) {
				$url = parse_url($file);
				if (isset($url['host'])) {
					$script .= "set netX/next-server {$url['host']} || goto %fail%\n";
				}
				if (isset($url['path'])) {
					$script .= "set netX/filename {$url['path']} || goto %fail%\n";
				}
				$script .= "chain -ar {$file} || goto %fail%\n";
				return BootEntry::newCustomBootEntry(['script' => $script]);
			}
			return null;
		}
		// "Normal" entry that should be convertible into a StandardBootEntry
		$section->kernel = self::parseFile($section->kernel);
		foreach ($section->initrd as &$initrd) {
			$initrd = self::parseFile($initrd);
		}
		return BootEntry::newStandardBootEntry($section);
	}

	/**
	 * Parse PXELINUX file notion. Basically, turn
	 * server::file into tftp://server/file.
	 */
	private static function parseFile(string $file): string
	{
		if (preg_match(',^([^:/]+)::(.*)$,', $file, $out)) {
			return 'tftp://' . $out[1] . '/' . $out[2];
		}
		return $file;
	}

	public static function sanitizeIpxeString(string $string): string
	{
		return str_replace(['&', '|', ';', '$', "\r", "\n"], ['+', '/', ':', 'S', ' ', ' '], $string);
	}

	public static function makeMd5Pass(string $plainpass, string $salt): string
	{
		if (empty($plainpass))
			return '';
		return md5(md5($plainpass) . '-' . $salt);
	}

	/**
	 * Modify a kernel command line. Add or remove items to a given command line. The $modifier
	 * string is a list of space separated arguments. If the argument starts with a '-', all
	 * according occurrences of the given option will be removed from the command line. It is assumed
	 * that options are either of format "option" or "option=value", so a modifier of "-option" will
	 * remove any occurrence of either "option" or "option=something". If the argument starts with a
	 * '+', it will be added to the command line after removing the '+'. If the argument starts with any
	 * other character, it will also be added to the command line.
	 *
	 * @param string $cmdLine command line to modify
	 * @param string $modifier modification string of space separated arguments
	 * @return string the modified command line
	 */
	public static function modifyCommandLine(string $cmdLine, string $modifier): string
	{
		$items = preg_split('/\s+/', $modifier, -1, PREG_SPLIT_NO_EMPTY);
		foreach ($items as $item) {
			if ($item[0] === '-') {
				$item = preg_quote(substr($item, 1), '/');
				$cmdLine = preg_replace('/(^|\s)' . $item . '(=\S*)?($|\s)/', ' ', $cmdLine);
			} else {
				$cmdLine .= ' ' . $item;
			}
		}
		return $cmdLine;
	}

}