1#!/usr/bin/perl 2# 3# Set PXELINUX hard-coded options 4# 5 6use Socket; # For gethostbyname 7use Fcntl; 8use bytes; 9 10%option_names = ( 11 6 => 'domain-name-servers', 12 15 => 'domain-name', 13 54 => 'next-server', 14 209 => 'config-file', 15 210 => 'path-prefix', 16 211 => 'reboottime' 17 ); 18 19@fmt_oneip = ("ip-address", \&parse_oneip, \&show_ip); 20@fmt_multiip = ("ip-address-list", \&parse_multiip, \&show_ip); 21@fmt_string = ("string", \&parse_string, \&show_string); 22@fmt_uint32 = ("uint32", \&parse_uint32, \&show_uint32); 23 24%option_format = ( 25 6 => \@fmt_multiip, 26 15 => \@fmt_string, 27 54 => \@fmt_oneip, 28 67 => \@fmt_string, 29 209 => \@fmt_string, 30 210 => \@fmt_string, 31 211 => \@fmt_uint32 32 ); 33 34sub parse_oneip($) 35{ 36 my($s) = @_; 37 my($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($s); 38 39 return ($addrtype == AF_INET) ? $addrs[0] : undef; 40} 41 42sub parse_multiip($) 43{ 44 my($l) = @_; 45 my $s; 46 my @a = (); 47 my $addr; 48 my $d = ''; 49 50 foreach $s (split(/,/, $l)) { 51 my($name,$aliases,$addrtype,$length,@addrs) 52 = gethostbyname($s); 53 if ($addrtype == AF_INET) { 54 foreach $addr (@addrs) { 55 $d .= $addr; 56 } 57 } 58 } 59 60 return $d ne '' ? $d : undef; 61} 62 63sub show_ip($) 64{ 65 my($l) = @_; 66 67 if (length($l) & 3) { 68 return undef; 69 } else { 70 my @h = (); 71 my $i; 72 73 for ($i = 0; $i < length($l); $i += 4) { 74 push(@h, inet_ntoa(substr($l, $i, 4))); 75 } 76 77 return join(',', @h); 78 } 79} 80 81sub parse_string($) 82{ 83 return $_[0]; 84} 85 86sub show_string($) 87{ 88 my($s) = @_; 89 my $o, $i, $c; 90 91 $o = "\'"; 92 for ($i = 0; $i < length($s); $i++) { 93 $c = substr($s, $i, 1); 94 if ($c eq "\'" || $c eq '!') { 95 $o .= "\'\\$c\'"; 96 } else { 97 $o .= $c; 98 } 99 } 100 $o .= "\'"; 101 102 return $o; 103} 104 105sub parse_uint32($) 106{ 107 my($s) = @_; 108 109 if ($s =~ /^[0-9]+$/) { 110 return pack("N", $s); 111 } else { 112 return undef; 113 } 114} 115 116sub show_uint32($) 117{ 118 my($l) = @_; 119 120 if (length($l) == 4) { 121 return unpack("N", $l); 122 } else { 123 return undef; 124 } 125} 126 127sub parse_generic($) 128{ 129 my($s) = @_; 130 131 if ($s =~ /^[0-9a-f]{1,2}(:[0-9a-f]{1,2})*$/) { 132 my $h; 133 my @b = (); 134 135 foreach $h (split(/\:/, $s)) { 136 push(@b, hex $h); 137 } 138 139 return pack("C", @b); 140 } else { 141 return undef; 142 } 143} 144 145sub show_generic($) 146{ 147 my($l) = @_; 148 my $i; 149 my @h; 150 151 for ($i = 0; $i < length($l); $i++) { 152 push(@h, sprintf("%02x", unpack("C", substr($l, $i, $1)))); 153 } 154 155 return join(':', @h); 156} 157 158sub parse_option($$) 159{ 160 my($opt, $arg) = @_; 161 my $v; 162 163 if (defined($option_format{$opt})) { 164 $v = $option_format{$opt}[1]($arg); 165 return $v if (defined($v)); 166 } 167 168 return parse_generic($arg); 169} 170 171sub show_option($$) 172{ 173 my($opt, $arg) = @_; 174 my $v; 175 176 if (defined($option_format{$opt})) { 177 $v = $option_format{$opt}[2]($arg); 178 return $v if (defined($v)); 179 } 180 181 return show_generic($arg); 182} 183 184sub option_number($) 185{ 186 my($n) = @_; 187 188 if (defined($option_rnames{$n})) { 189 return $option_rnames{$n}; 190 } elsif ($n =~ /^[0-9]+$/ && $n >= 1 && $n <= 254) { 191 return $n+0; 192 } else { 193 return undef; 194 } 195} 196 197sub read_optsets($) 198{ 199 my($file) = @_; 200 my $data, $bdata, $adata; 201 my $patch_start = (stat($file))[7]; 202 my $hdroffset = 0; # 0 means non-deep-embedded 203 my $bufsize = 0; 204 my $junk; 205 my %hdr; 206 207 return undef unless (seek($file, 0, SEEK_SET)); 208 return undef unless (read($file, $data, 48) == 48); 209 210 my($mzmagic, $junk, $magic, $len, $flags, $boff, $blen, $aoff, $alen) 211 = unpack("va[6]VVVVVVV", $data); 212 213 if ($mzmagic == 0x5a4d) { 214 # It is an EFI file... search for the magic number 215 $hdroffset = 48; 216 my $magic = pack("VVVV", 0x2a171ead, 0x0600e65e, 217 0x4025a4e4, 0x42388fc8); 218 219 while (1) { 220 return undef unless (read($file, $data, 16) == 16); 221 last if ($data eq $magic); 222 223 $hdroffset += 16; 224 } 225 226 return undef unless (read($file, $data, 16) == 16); 227 ($blen, $alen, $bufsize, $junk) = unpack("VVVV", $data); 228 229 $patch_start = $boff = $hdroffset + 32; 230 $aoff = $boff + $blen; 231 232 $hdr{'deep'} = 1; 233 $hdr{'bufsize'} = $bufsize; 234 $hdr{'hdroffset'} = $hdroffset; 235 } else { 236 # It is a BIOS PXE file 237 238 return undef if ($magic != 0x2983c8ac); 239 return undef if ($len < 7*4); 240 241 $hdr{'deep'} = 0; 242 } 243 244 if ($blen == 0) { 245 $bdata = ''; 246 } else { 247 return undef unless (seek($file, $boff, SEEK_SET)); 248 return undef unless (read($file, $bdata, $blen) == $blen); 249 $patch_start = $boff if ($boff < patch_start); 250 } 251 252 if ($alen == 0) { 253 $adata = ''; 254 } else { 255 return undef unless (seek($file, $aoff, SEEK_SET)); 256 return undef unless (read($file, $adata, $alen) == $alen); 257 $patch_start = $aoff if ($aoff < $patch_start); 258 } 259 260 $hdr{'patch_start'} = $patch_start; 261 262 return (\%hdr, $bdata, $adata); 263} 264 265sub write_optsets($$@) 266{ 267 my($file, $hdr, $bdata, $adata) = @_; 268 my $boff = 0; 269 my $aoff = 0; 270 my $bufsize = 0; 271 my $patch_start = $hdr->{'patch_start'}; 272 my $len; 273 274 $bdata .= "\xff" unless ($bdata eq ''); 275 $adata .= "\xff" unless ($adata eq ''); 276 277 $len = length($bdata) + length($adata); 278 279 if (defined($hdr->{'bufsize'})) { 280 return undef unless ($len <= $hdr->{'bufsize'}); 281 } 282 283 return undef unless (seek($file, $patch_start, SEEK_SET)); 284 285 if (length($bdata)) { 286 $boff = $patch_start; 287 return undef unless (print $file $bdata); 288 $patch_start += length($bdata); 289 } 290 291 if (length($adata)) { 292 $aoff = $patch_start; 293 return undef unless (print $file $adata); 294 $patch_start += length($adata); 295 } 296 297 if ($hdr->{'deep'}) { 298 return undef unless (print $file "\0" x ($hdr->{'bufsize'} - $len)); 299 return undef unless (seek($file, $hdr->{'hdroffset'} + 16, SEEK_SET)); 300 my $hdr = pack("VV", length($bdata), length($adata)); 301 return undef unless (print $file $hdr); 302 } else { 303 my $hdr = pack("VVVV", $boff, length($bdata), $aoff, length($adata)); 304 305 return undef unless (seek($file, 8+3*4, SEEK_SET)); 306 return undef unless (print $file $hdr); 307 308 truncate($file, $patch_start); 309 } 310 311 return 1; 312} 313 314sub delete_option($$) 315{ 316 my ($num, $block) = @_; 317 my $o, $l, $c, $x; 318 319 $x = 0; 320 while ($x < length($block)) { 321 ($o, $l) = unpack("CC", substr($block, $x, 2)); 322 if ($o == $num) { 323 # Delete this option 324 substr($block, $x, $l+2) = ''; 325 } elsif ($o == 0) { 326 # Delete a null option 327 substr($block, $x, 1) = ''; 328 } elsif ($o == 255) { 329 # End marker - truncate block 330 $block = substr($block, 0, $x); 331 last; 332 } else { 333 # Skip to the next option 334 $x += $l+2; 335 } 336 } 337 338 return $block; 339} 340 341sub add_option($$$) 342{ 343 my ($num, $data, $block) = @_; 344 345 $block = delete_option($num, $block); 346 347 if (length($data) == 0) { 348 return $block; 349 } elsif (length($data) > 255) { 350 die "$0: option $num has too much data (max 255 bytes)\n"; 351 } else { 352 return $block . pack("CC", $num, length($data)) . $data; 353 } 354} 355 356sub list_options($$) 357{ 358 my($pfx, $data) = @_; 359 my $x, $o, $l; 360 361 while ($x < length($data)) { 362 ($o, $l) = unpack("CC", substr($data, $x, 2)); 363 364 if ($o == 0) { 365 $x++; 366 } elsif ($o == 255) { 367 last; 368 } else { 369 my $odata = substr($data, $x+2, $l); 370 last if (length($odata) != $l); # Incomplete option 371 372 printf "%s%-20s %s\n", $pfx, 373 $option_names{$o} || sprintf("%d", $o), 374 show_option($o, $odata); 375 376 $x += $l+2; 377 } 378 } 379} 380 381sub usage() 382{ 383 my $i; 384 385 print STDERR "Usage: $0 options pxelinux.0\n"; 386 print STDERR "Options:\n"; 387 print STDERR "--before option value -b Add an option before DHCP data\n"; 388 print STDERR "--after option value -a Add an option after DHCP data\n"; 389 print STDERR "--delete option -d Delete an option\n"; 390 print STDERR "--list -l List set options\n"; 391 print STDERR "--dry-run -n Don't modify the target file\n"; 392 print STDERR "--help -h Display this help text\n"; 393 print STDERR "\n"; 394 print STDERR "The following DHCP options are currently recognized:\n"; 395 printf STDERR "%-23s %-3s %s\n", 'Name', 'Num', 'Value Format'; 396 397 foreach $i (sort { $a <=> $b } keys(%option_names)) { 398 printf STDERR "%-23s %3d %s\n", 399 $option_names{$i}, $i, $option_format{$i}[0]; 400 } 401} 402 403%option_rnames = (); 404foreach $opt (keys(%option_names)) { 405 $option_rnames{$option_names{$opt}} = $opt; 406} 407 408%before = (); 409%after = (); 410@clear = (); 411$usage = 0; 412$err = 0; 413$list = 0; 414$no_write = 0; 415undef $file; 416 417while (defined($opt = shift(@ARGV))) { 418 if ($opt !~ /^-/) { 419 if (defined($file)) { 420 $err = $usage = 1; 421 last; 422 } 423 $file = $opt; 424 } elsif ($opt eq '-b' || $opt eq '--before') { 425 $oname = shift(@ARGV); 426 $odata = shift(@ARGV); 427 428 if (!defined($odata)) { 429 $err = $usage = 1; 430 last; 431 } 432 433 $onum = option_number($oname); 434 if (!defined($onum)) { 435 print STDERR "$0: unknown option name: $oname\n"; 436 $err = 1; 437 next; 438 } 439 440 $odata = parse_option($onum, $odata); 441 if (!defined($odata)) { 442 print STDERR "$0: unable to parse data for option $oname\n"; 443 $err = 1; 444 next; 445 } 446 447 delete $after{$onum}; 448 $before{$onum} = $odata; 449 push(@clear, $onum); 450 } elsif ($opt eq '-a' || $opt eq '--after') { 451 $oname = shift(@ARGV); 452 $odata = shift(@ARGV); 453 454 if (!defined($odata)) { 455 $err = $usage = 1; 456 last; 457 } 458 459 $onum = option_number($oname); 460 if (!defined($onum)) { 461 print STDERR "$0: unknown option name: $oname\n"; 462 $err = 1; 463 next; 464 } 465 466 $odata = parse_option($onum, $odata); 467 if (!defined($odata)) { 468 print STDERR "$0: unable to parse data for option $oname\n"; 469 $err = 1; 470 next; 471 } 472 473 delete $before{$onum}; 474 $after{$onum} = $odata; 475 push(@clear, $onum); 476 } elsif ($opt eq '-d' || $opt eq '--delete') { 477 $oname = shift(@ARGV); 478 479 if (!defined($oname)) { 480 $err = $usage = 1; 481 last; 482 } 483 484 $onum = option_number($oname); 485 if (!defined($onum)) { 486 print STDERR "$0: unknown option name: $oname\n"; 487 $err = 1; 488 next; 489 } 490 491 push(@clear, $onum); 492 delete $before{$onum}; 493 delete $after{$onum}; 494 } elsif ($opt eq '-n' || $opt eq '--no-write' || $opt eq '--dry-run') { 495 $no_write = 1; 496 } elsif ($opt eq '-l' || $opt eq '--list') { 497 $list = 1; 498 } elsif ($opt eq '-h' || $opt eq '--help') { 499 $usage = 1; 500 } else { 501 print STDERR "Invalid option: $opt\n"; 502 $err = $usage = 1; 503 } 504} 505 506if (!defined($file) && !$usage) { 507 $err = $usage = 1; 508} 509if ($usage) { 510 usage(); 511} 512if ($err || $usage) { 513 exit($err); 514} 515 516if (!scalar(@clear)) { 517 $no_write = 1; # No modifications requested 518} 519 520$mode = $no_write ? '<' : '+<'; 521 522open(FILE, $mode, $file) 523 or die "$0: cannot open: $file: $!\n"; 524($hdrinfo, @data) = read_optsets(\*FILE); 525if (!defined($hdrinfo)) { 526 die "$0: $file: patch block not found or file corrupt\n"; 527} 528 529foreach $o (@clear) { 530 $data[0] = delete_option($o, $data[0]); 531 $data[1] = delete_option($o, $data[1]); 532} 533foreach $o (keys(%before)) { 534 $data[0] = add_option($o, $before{$o}, $data[0]); 535} 536foreach $o (keys(%after)) { 537 $data[1] = add_option($o, $after{$o}, $data[1]); 538} 539 540if ($list) { 541 list_options('-b ', $data[0]); 542 list_options('-a ', $data[1]); 543} 544 545if (!$no_write) { 546 if (!write_optsets(\*FILE, $hdrinfo, @data)) { 547 die "$0: $file: failed to write options: $!\n"; 548 } 549} 550 551close(FILE); 552exit 0; 553