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