package NGCP::Rtpclient::DTLS;

use strict;
use warnings;
use NGCP::Rtpclient::SRTP;
use File::Temp;
use Crypt::OpenSSL::RSA;
use IO::Socket::INET;
use IPC::Open3;
use IO::Multiplex;
use Time::HiRes qw(sleep time);

sub new {
	my ($class, $mux, $local_sockets, $output_func, $tag, $cert) = @_;

	my $self = {};
	bless $self, $class;

	$self->{_output_func} = $output_func;
	$self->{_mux} = $mux;
	$self->{_tag} = $tag;
	$self->{_local_sockets} = $local_sockets;

	if ($cert) {
		$self->set_cert($cert);
	}
	else {
		$self->new_cert();
	}

	return $self;
}

sub new_cert {
	my ($self) = @_;

	my $rsa_key = Crypt::OpenSSL::RSA->generate_key(4096);
	my $priv_key = $rsa_key->get_private_key_string();
	my $key_file = File::Temp->new();
	print $key_file $priv_key;
	close($key_file);

	my $cert_file = File::Temp->new();
	system(qw(openssl req -key), $key_file->filename(), '-out', $cert_file->filename(),
		qw(-new -x509 -days 30 -subj /CN=tester -batch));
	my $cert;
	read($cert_file, $cert, 10000);
	close($cert_file);

	my $cert_key_file = File::Temp->new();
	print $cert_key_file $cert;
	print $cert_key_file $priv_key;
	close($cert_key_file);

	$self->set_cert($cert_key_file);
	return $cert_key_file;
}

sub get_cert {
	my ($self) = @_;
	return $self->{_cert_key_file};
}

sub set_cert {
	my ($self, $file) = @_;
	$self->{_cert_key_file} = $file;
}

# XXX unify these two
sub connect { ## no critic: Subroutines::ProhibitBuiltinHomonyms
	my ($self) = @_;

	$self->{_connected} and return;

	$self->_kill_openssl_child();

	my $near = $self->{_near};

	$near or ($near = $self->{_near} = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost',
		Proto => 'udp'));

	my $near_port = $near->sockport();

	my ($openssl_in, $openssl_out);
	$self->{_openssl_pid} = open3($openssl_in, $openssl_out, undef,
		qw(openssl s_client -4 -connect),
		"localhost:$near_port",
		'-cert', $self->{_cert_key_file}->filename(), qw(-dtls1_2 -use_srtp
		SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32 -keymatexport EXTRACTOR-dtls_srtp
		-keymatexportlen 60));
	$self->{_openssl_out} = $openssl_out;
	$self->{_openssl_in} = $openssl_in;
	$self->{_openssl_buf} = '';

	$self->{_mux}->add($near);
	$self->{_mux}->add($openssl_out);
}
sub accept { ## no critic: Subroutines::ProhibitBuiltinHomonyms
	my ($self) = @_;

	$self->{_connected} and return;

	$self->_kill_openssl_child();

	my ($near_port, $near_peer);
	my $near = $self->{_near};
	if ($near) {
		$near_port = $near->peerport();
		$near_peer = $near->peeraddr();
	}
	else {
		my $tmp = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost', Proto => 'udp');
		$near_port = $tmp->sockport();
		undef($tmp);

		$near = $self->{_near} = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost',
			Proto => 'udp');
		$near_peer = pack_sockaddr_in($near_port, inet_aton("localhost"));
		# $near gets connected below
	}

	my ($openssl_in, $openssl_out);
	$self->{_openssl_pid} = open3($openssl_in, $openssl_out, undef,
		qw(openssl s_server -4 -accept),
		$near_port,
		'-cert', $self->{_cert_key_file}->filename(), qw(-dtls1_2 -use_srtp
		SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32 -keymatexport EXTRACTOR-dtls_srtp
		-keymatexportlen 60));

	sleep(0.2); # given openssl a short while to start up

	$self->_near_peer($near_peer);

	$self->{_openssl_out} = $openssl_out;
	$self->{_openssl_in} = $openssl_in;
	$self->{_openssl_buf} = '';

	$self->{_mux}->add($near);
	$self->{_mux}->add($openssl_out);
}

sub _kill_openssl_child {
	my ($self) = @_;

	delete($self->{_openssl_in});
	delete($self->{_openssl_out});

	if ($self->{_openssl_pid}) {
		waitpid($self->{_openssl_pid}, 0);
	}

	delete($self->{_openssl_pid});
}

sub DESTROY {
	my ($self) = @_;

	$self->_kill_openssl_child();
}

sub _openssl_input {
	my ($self, $fh, $s_r, $peer) = @_;

	if ($self->{_openssl_done}) {
		$$s_r = '';
		return;
	}

	$self->{_openssl_buf} .= $$s_r;
	$$s_r = '';

	if ($self->{_openssl_buf} =~ /SRTP Extension negotiated, profile=(\S+).*Keying material: ([0-9a-fA-F]{120})/s) {
		$self->{_profile} = $1;
		$self->{_keys} = pack('H*', $2);
		$self->{_connected} = 1;
		$self->{_openssl_done} = 1;
	}
	if ($self->{_openssl_buf} =~ /\nDONE\n/s) {
		$self->{_openssl_done} = 1;
	}
}

sub _near_peer {
	my ($self, $peer) = @_;

	$self->{_near_peer} and return;

	$self->{_near_peer} = $peer;
	CORE::connect($self->{_near}, $self->{_near_peer});
}

sub _near_input {
	my ($self, $fh, $s_r, $peer) = @_;

	my $func = $self->{_output_func};
	if (ref($func) eq 'CODE') {
		$func->($self->{_tag}, $$s_r);
	}
	else {
		# object
		$func->dtls_send($self->{_tag}, $$s_r);
	}

	$self->_near_peer($peer);

	$$s_r = '';
}

sub input {
	my ($self, $fh, $s_r, $peer) = @_;

	$$s_r eq '' and return;

	if ($fh == $self->{_openssl_out}) { # openssl's stdout
		return $self->_openssl_input($fh, $s_r, $peer);
	}
	elsif ($fh == $self->{_near}) { # UDP input from openssl - forward to peer
		return $self->_near_input($fh, $s_r, $peer);
	}

	# UDP input from peer - demux and forward to openssl
	is_dtls($$s_r) or return;
	$self->{_near_peer} or return; # nowhere to forward it to
	grep {$fh == $_} @{$self->{_local_sockets}} or return; # not one of ours

	send($self->{_near}, $$s_r, 0);
	$$s_r = '';
}

sub cert_fingerprint {
	my ($cert_file) = @_;
	my $fd;
	open($fd, '-|', qw(openssl x509 -in), $cert_file->filename(), qw(-fingerprint -noout -sha256));
	my $fp = <$fd>;
	close($fd);
	$fp =~ /SHA256 Fingerprint=([0-9a-f:]+)/i or return;
	return $1;
}

sub fingerprint {
	my ($self) = @_;
	return cert_fingerprint($self->{_cert_key_file});
}

sub get_keys {
	my ($self) = @_;

	$self->{_keys} =~ /^(.{16})(.{16})(.{14})(.{14})$/s or return;
	return ($self->{_profile}, $1, $2, $3, $4);
}

sub is_dtls {
	my ($s) = @_;
	length($s) < 1 and return 0;
	my $c = ord(substr($s, 0, 1));
	$c < 20 and return 0;
	$c > 63 and return 0;
	return 1;
}

sub encode {
	my ($self) = @_;
	my @ret;
	push(@ret, 'a=setup:actpass');
	push(@ret, 'a=fingerprint:sha-1 ' . $self->fingerprint());
	return @ret;
}

package NGCP::Rtpclient::DTLS::Group;

sub new {
	my ($class, $mux, $output_func, $socket_components, $cert) = @_;

	my $self = [];
	bless $self, $class;

	my $max_component = $#{$socket_components};

	for my $idx (0 .. $max_component) {
		my $local_sockets = $socket_components->[$idx];
		my $cl = NGCP::Rtpclient::DTLS->new($mux, $local_sockets, $output_func, $idx, $cert);
		push(@$self, $cl);
		$cert = $cl->get_cert();
	}

	return $self;
}

sub encode {
	my ($self, @rest) = @_;
	return $self->[0]->encode(@rest);
}
sub connect { ## no critic: Subroutines::ProhibitBuiltinHomonyms
	my ($self, @rest) = @_;
	for my $cl (@$self) {
		$cl->connect(@rest);
	}
}
sub accept { ## no critic: Subroutines::ProhibitBuiltinHomonyms
	my ($self, @rest) = @_;
	for my $cl (@$self) {
		$cl->accept(@rest);
	}
}
sub input {
	my ($self, @rest) = @_;
	for my $cl (@$self) {
		$cl->input(@rest);
	}
}

1;
