package dhcpd;

=head1 dhcpd

start with:

 perl -Ilib/PXElator -Ilib -Mdhcpd -e start

based on L<http://www.perlmonks.org/index.pl?node_id=325248>

=cut

use strict;
use warnings;

use autodie;

use IO::Socket::INET;
use File::Slurp;
use Data::Dump qw/dump/;

use lib '..';
use Net::DHCP::Packet;
use Net::DHCP::Constants 0.67;

use CouchDB;
use format;

use server;
my $debug = server::debug;

if ( ! $server::ip ) {
	my $server_ip = `/sbin/ifconfig`;
	$server_ip =~ s/^.+?addr:([\d\.]+).*$/$1/gs;
	$server::ip = $server_ip;
}

warn "server ip $server::ip range: $server::ip_from - $server::ip_to\n";

use client;

sub client_mac_ip {
	my ( $mac, $request_ip ) = @_;

	if ( ! $mac ) {
		warn "W: no mac in requiest\n";
		return;
	}

	my $conf = $server::conf;
	mkdir $conf unless -e $conf;

	my $ip;

	if ( $ip = client::ip_from_mac( $mac ) ) {
		print "RENEW $mac $ip\n";
		client::save_ip_mac( $ip, $mac );
		return $ip;
	} elsif ( ip::in_dhcp_range( $request_ip ) || $request_ip eq '0.0.0.0' ) {
		$ip = client::next_ip( $mac );
		print "NEW $mac $ip\n";
	} else {
		$ip = $request_ip;
		client::save_ip_mac( $ip, $mac );
		warn "W: $ip out of server range $server::ip/$server::netmask\n";
	}

	return $ip;
}

use log;
use config;
use pxelinux;
use client;

our $file;
our $transaction = 0; # FIXME predictible transaction numbers

sub process_packet {
	my $sock = shift;

	my $buf;
	$sock->recv($buf, 1024);
	my $size = 'empty';
	$size = length($buf) . ' bytes' if defined $buf;

	print "packet from ",$sock->peerhost,":",$sock->peerport," $size\n" if $debug;
	return unless $buf;

	my $dhcp = Net::DHCP::Packet->new($buf);

	warn "recv: ", $dhcp->toString if $debug;

	$dhcp->comment( $transaction++ );

	my $mac = format::mac( substr($dhcp->chaddr(),0,$dhcp->hlen()*2) );
	my $ip = client_mac_ip($mac, $dhcp->ciaddr);

	my $hostname = $dhcp->getOptionValue(DHO_HOST_NAME);
	print "$ip ", client::conf( $ip => 'hostname', default => $hostname ), " >> /etc/hosts\n" if $hostname;

	my $audit = { mac => $mac, ip => $ip, hostname => $hostname,
		options => {
			map {
				( $_ => $dhcp->getOptionValue( $_ ) )
			} @{ $dhcp->{options_order} }
		},
	};

=for later

	my $user_class = $dhcp->getOptionValue(DHO_USER_CLASS());

	if ( $user_class eq 'gPXE' ) {
		$file = $gpxe_file;
	} elsif ( ! $file ) {
		$file = 'undionly.kpxe';
	}

=cut

	config::for_ip( $ip );

	my $server = server::as_hash_for $ip;

	my $packet = {
		Op		=> BOOTREPLY(),
		Hops	=> $dhcp->hops(),
		Xid		=> $dhcp->xid(),
		Flags	=> $dhcp->flags(),
		Ciaddr	=> $dhcp->ciaddr(),
		Yiaddr	=> $ip,
		Siaddr	=> $server->{ip},
		Giaddr	=> $dhcp->giaddr(),
		Chaddr	=> $dhcp->chaddr(),
		File	=> $file,
		DHO_DHCP_SERVER_IDENTIFIER()	=> $server->{ip},	# busybox/udhcpc needs it but doesn't request
	};

	my $options = {
		DHO_SUBNET_MASK()	=> $server->{netmask},
		DHO_ROUTERS()		=> ( $server->{gw} || $server->{ip} ),
		DHO_DOMAIN_NAME()	=> $server->{domain},
		DHO_NAME_SERVERS()	=> $server->{ip},
		DHO_DOMAIN_NAME_SERVERS() => ( $server->{dns} || $server->{ip} ),
		DHO_HOST_NAME()		=> client::conf( $ip, 'hostname' ),
		DHO_BROADCAST_ADDRESS() => $server->{bcast},
#		DHO_NTP_SERVERS() => '',
	};

	my @requested = split(/\s/, $dhcp->getOptionValue(DHO_DHCP_PARAMETER_REQUEST_LIST));
	warn "options ",dump( $options ), ' requested: ',dump( @requested ) if $debug;

	my @missing;
	foreach ( @requested ) {
		if ( defined $options->{$_} ) {
			$packet->{$_} = $options->{$_};
		} else {
			push @missing, $_;
		}
	}

	warn "W: options requested but missing: ",dump( @missing ),$/;
	$audit->{requested} = [ @requested ];
	$audit->{missing}   = [ @missing   ];

	foreach my $opt ( 'magic', 'config_file', 'path_prefix', 'reboot_time' ) {
		my $DH0 = eval 'DHO_PXELINUX_' . uc $opt;
		warn "DH0: $@" if $@;
		my $v = eval "\$pxelinux::$opt";
		warn "v: $@" if $@;
		next unless defined $v;
		warn "pxelinux dhcp option $opt = $DH0 = $v\n" if $debug;
		$packet->{ $DH0 } = $v;
	}

	my $messagetype = $dhcp->getOptionValue(DHO_DHCP_MESSAGE_TYPE());

	my @type;

	if ($messagetype eq DHCPDISCOVER()) {
		$packet->{Comment} = $dhcp->comment();
		$packet->{DHO_DHCP_MESSAGE_TYPE()} = DHCPOFFER();
		@type = qw( discover offer );
	} elsif ($messagetype eq DHCPREQUEST()) {
		@type = qw( request );
		my $requested_ip = $dhcp->getOptionValue(DHO_DHCP_REQUESTED_ADDRESS()) || $dhcp->ciaddr();
		if ( $ip eq $requested_ip ) {
			$packet->{DHO_DHCP_MESSAGE_TYPE()}      = DHCPACK();
			$packet->{DHO_DHCP_LEASE_TIME()}        = 5 * 60; # 5 min
#			$packet->{DHO_ROOT_PATH()}              = '/exports/foobar';
			$type[1] = 'ack';
		} else {
			$packet->{DHO_DHCP_MESSAGE_TYPE()} = DHCPNAK();
			$packet->{DHO_DHCP_MESSAGE()} = "Bad request, expected $ip got $requested_ip";
			$type[1] = 'nak';
		}
	} elsif ($messagetype eq DHCPINFORM()) {
		@type = qw( inform ignored );
	} else {
		@type = ( $messagetype, 'ignored' );
	}

	warn "# type ",dump @type;
	$audit->{type} = [ @type ];

	$audit->{response} = $packet;

	$packet = new Net::DHCP::Packet( %$packet );
	warn "send ",$packet->toString() if $debug;

	if ( ip::in_dhcp_range( $ip ) ) {
		my $buff = $packet->serialize();

		my $reply = IO::Socket::INET->new(
			LocalAddr => $server->{ip},
			LocalPort => 67,
			Proto => "udp",
			Broadcast => 1,
#			PeerAddr => '255.255.255.255',
			PeerAddr => $server->{bcast},
			PeerPort => 68,
			Reuse => 1,
		) or die "socket: $@";

		$reply->send( $buff, 0 ) or die "Error sending: $!\n";
		warn ">> $mac == $ip server: $server->{ip}", $file ? " file: $file\n" : "\n";
	} else {
		$audit->{error} = "$ip not in server range $server::ip $server::netmask - no packet sent";
		warn $audit->{error};
	}

	CouchDB::audit( @type, $audit );

#	system("arp -s $ip $mac"),

}

sub start {

	my $sock = IO::Socket::INET->new(
		LocalPort	=> 67,
#		LocalAddr	=> 'localhost',
#		LocalAddr	=> '10.0.0.100',
		LocalAddr	=> '0.0.0.0',
		Proto		=> 'udp',
		ReuseAddr	=> 1,
#		PeerPort	=> getservbyname('bootpc', 'udp'),
		Broadcast	=> 1,
		Type		=> SOCK_DGRAM,
	) or die "Failed to bind to socket: $@";

	print "DHCP listen on ",$sock->sockhost,":",$sock->sockport,"\n";

	CouchDB::audit( 'start', { addr => $sock->sockhost, port => $sock->sockport } );

	while (1) {
		server->refresh;
		process_packet $sock;
	}
}

warn "loaded";

1;
