Home | History | Annotate | Download | only in utils
      1 #!/usr/bin/perl
      2 #
      3 # Set PXELINUX hard-coded options
      4 #
      5 
      6 use Socket;			# For gethostbyname
      7 use Fcntl;
      8 use 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 
     34 sub 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 
     42 sub 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 
     63 sub 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 
     81 sub parse_string($)
     82 {
     83     return $_[0];
     84 }
     85 
     86 sub 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 
    105 sub 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 
    116 sub 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 
    127 sub 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 
    145 sub 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 
    158 sub 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 
    171 sub 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 
    184 sub 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 
    197 sub 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 
    265 sub 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 
    314 sub 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 
    341 sub 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 
    356 sub 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 
    381 sub 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 = ();
    404 foreach $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;
    415 undef $file;
    416 
    417 while (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 
    506 if (!defined($file) && !$usage) {
    507     $err = $usage = 1;
    508 }
    509 if ($usage) {
    510     usage();
    511 }
    512 if ($err || $usage) {
    513     exit($err);
    514 }
    515 
    516 if (!scalar(@clear)) {
    517     $no_write = 1;		# No modifications requested
    518 }
    519 
    520 $mode = $no_write ? '<' : '+<';
    521 
    522 open(FILE, $mode, $file)
    523     or die "$0: cannot open: $file: $!\n";
    524 ($hdrinfo, @data) = read_optsets(\*FILE);
    525 if (!defined($hdrinfo)) {
    526     die "$0: $file: patch block not found or file corrupt\n";
    527 }
    528 
    529 foreach $o (@clear) {
    530     $data[0] = delete_option($o, $data[0]);
    531     $data[1] = delete_option($o, $data[1]);
    532 }
    533 foreach $o (keys(%before)) {
    534     $data[0] = add_option($o, $before{$o}, $data[0]);
    535 }
    536 foreach $o (keys(%after)) {
    537     $data[1] = add_option($o, $after{$o}, $data[1]);
    538 }
    539 
    540 if ($list) {
    541     list_options('-b ', $data[0]);
    542     list_options('-a ', $data[1]);
    543 }
    544 
    545 if (!$no_write) {
    546     if (!write_optsets(\*FILE, $hdrinfo, @data)) {
    547 	die "$0: $file: failed to write options: $!\n";
    548     }
    549 }
    550 
    551 close(FILE);
    552 exit 0;
    553