###########################################################################
# package Net::SIP::Authorize
# use in ReceiveChain in front of StatelessProxy, Endpoint.. to authorize request
# by enforcing authorization and only handling request only if it was
# fully authorized
###########################################################################

use strict;
use warnings;

package Net::SIP::Authorize;
use Carp 'croak';
use Net::SIP::Debug;
use Net::SIP::Util ':all';
use Digest::MD5 'md5_hex';
use fields qw( realm opaque user2pass user2a1 i_am_proxy dispatcher filter );

###########################################################################
# creates new Authorize object
# Args: ($class,%args)
#   %args
#     realm: which realm to announce
#     user2pass: hash of (username => password) or callback which returns
#        password if given username
#     dispatcher: Dispatcher object
#     i_am_proxy: true if should send Proxy-Authenticate, not WWW-Authenticate
#     filter: hashref with extra verification chain, see packages below.
#      Usage:
#      filter => {
#       # filter chain for registration
#       REGISTER => [
#        # all of this three must succeed (user can regist himself)
#        [ 'ToIsFrom','FromIsRealm','FromIsAuthUser' ],
#        # or this must succeed
#        \&call_back, # callback. If arrayref you MUST set [ \&call_back ]
#       ]
#       # filter chain for invites
#       INVITE => 'FromIsRealm',
#      }
# Returns: $self
###########################################################################
sub new {
    my ($class,%args) = @_;
    my $self = fields::new( $class );
    $self->{realm} = $args{realm} || 'p5-net-sip';
    $self->{opaque} = $args{opaque};

    $args{user2pass} || $args{user2a1} || croak 'no user2pass or user2a1 known';

    $self->{user2pass} = $args{user2pass};
    $self->{user2a1} = $args{user2a1};
    $self->{i_am_proxy} = $args{i_am_proxy};
    $self->{dispatcher} = $args{dispatcher} || croak 'no dispatcher';

    if ( my $f = $args{filter}) {
	croak 'filter must be hashref' if ref($f) ne 'HASH';
	my %filter;
	while (my($method,$chain) = each %$f) {
	    $chain = [ $chain ] if ref($chain) ne 'ARRAY';
	    map { $_ = [$_] if ref($_) ne 'ARRAY' } @$chain;
	    # now we have:
	    # method => [[ cb00,cb01,cb02,..],[ cb10,cb11,cb12,..],...]
	    # where either the cb0* chain or the cb1* chain or the cbX* has to succeed
	    for my $or (@$chain) {
		for (@$or) {
		    if (ref($_)) {
			# assume callback
		    } else {
			# must have authorize class with verify method
			my $pkg = __PACKAGE__."::$_";
			my $sub = UNIVERSAL::can($pkg,'verify') || do {
			    # load package
			    eval "require $pkg";
			    UNIVERSAL::can($pkg,'verify')
			} or die "cannot find sub ${pkg}::verify";
			$_ = $sub;
		    }
		}
	    }
	    $filter{uc($method)} = $chain;
	}
	$self->{filter} = \%filter;
    }
    return $self;
}

###########################################################################
# handle packet, called from Net::SIP::Dispatcher on incoming requests
# Args: ($self,$packet,$leg,$addr)
#  $packet: Net::SIP::Request
#  $leg: Net::SIP::Leg where request came in (and response gets send out)
#  $addr: ip:port where request came from and response will be send
# Returns: TRUE if it handled the packet
###########################################################################
sub receive {
    my Net::SIP::Authorize $self = shift;
    my ($packet,$leg,$addr) = @_;

    # don't handle responses
    if ( $packet->is_response ) {
	DEBUG( 100,"pass thru response" );
	return;
    }
    my $method = $packet->method;

    # check authorization on request
    my ($rq_key,$rs_key,$acode) = $self->{i_am_proxy}
	? ( 'proxy-authorization', 'proxy-authenticate',407 )
	: ( 'authorization','www-authenticate',401 )
	;
    my @auth = $packet->get_header( $rq_key );
    my $user2pass = $self->{user2pass};
    my $user2a1 = $self->{user2a1};
    my $realm = $self->{realm};
    my $opaque = $self->{opaque};

    # there might be multiple auth, pick the right realm
    my (@keep_auth,$authorized);

    foreach my $auth ( @auth ) {
	# RFC 2617
	my ($data,$param) = sip_hdrval2parts( $rq_key => $auth );
	if ( $param->{realm} ne $realm ) {
	    # not for me
	    push @keep_auth,$auth;
	    next;
	}
	if ( defined $opaque ) {
	    if ( ! defined $param->{opaque} ) {
		DEBUG( 10,"expected opaque value, but got nothing" );
		next;
	    } elsif ( $param->{opaque} ne $opaque ) {
		DEBUG( 10,"got wrong opaque value '$param->{opaque}', expected '$opaque'" );
		next;
	    }
	}

	my ($user,$nonce,$uri,$resp,$qop,$cnonce,$algo ) =
	    @{$param}{ qw/ username nonce uri response qop cnonce algorithm / };
	if ( lc($data) ne 'digest'
	    || ( $algo && lc($algo) ne 'md5' )
	    || ( $qop && $qop ne 'auth' ) ) {
	    DEBUG( 10,"unsupported response: $auth" );
	    next;
	};

	# we support with and w/o qop
	# get a1_hex from either user2a1 or user2pass
	my $a1_hex;
	if ( ref($user2a1)) {
	    if ( ref($user2a1) eq 'HASH' ) {
		$a1_hex = $user2a1->{$user}
	    } else {
		$a1_hex = invoke_callback( $user2a1,$user,$realm );
	    }
	}
	if ( ! defined($a1_hex) && ref($user2pass)) {
	    my $pass;
	    if ( ref($user2pass) eq 'HASH' ) {
		$pass = $user2pass->{$user}
	    } else {
		$pass = invoke_callback( $user2pass,$user );
	    }
	    # if wrong credentials ask again for authorization
	    last if ! defined $pass;
	    $a1_hex = md5_hex(join( ':',$user,$realm,$pass ));
	}

	last if ! defined $a1_hex; # not in user2a1 || user2pass

	# ACK just reuse the authorization from INVITE, so they should
	# be checked against method INVITE
	# for CANCEL the RFC doesn't say anything, so we assume it uses
	# CANCEL but try INVITE if this fails
	my @a2 =
	    $method eq 'ACK' ? ("INVITE:$uri") :
	    $method eq 'CANCEL' ? ("CANCEL:$uri","INVITE:$uri") :
	    ("$method:$uri");

	while (my $a2 = shift(@a2)) {
	    my $want_response;
	    if ( $qop ) {
		# 3.2.2.1
		$want_response = md5_hex( join( ':',
		    $a1_hex,
		    $nonce,
		    1,
		    $cnonce,
		    $qop,
		    md5_hex($a2)
		));
	    } else {
		 # 3.2.2.1 compability with RFC2069
		 $want_response = md5_hex( join( ':',
		    $a1_hex,
		    $nonce,
		    md5_hex($a2)
		));
	    }

	    if ( $resp eq $want_response ) {
		if ($self->{filter} and my $or = $self->{filter}{$method}) {
		    for my $and (@$or) {
			$authorized = 1;
			for my $cb (@$and) {
			    if ( ! invoke_callback(
				$cb,$packet,$leg,$addr,$user,$realm)) {
				$authorized = 0;
				last;
			    }
			}
			last if $authorized;
		    }
		} else {
		    $authorized = 1;
		}
		last;
	    }
	}
    }

    # if authorized remove authorization data from this realm
    # and pass packet thru
    if ( $authorized ) {
	DEBUG( 10, "Request authorized ". $packet->dump );
	# set header again
	$packet->set_header( $rq_key => \@keep_auth );
	return;
    }

    # CANCEL or ACK cannot be prompted for authorization, so
    # they should provide the right data already
    # unauthorized CANCEL or ACK are only valid as response to
    # 401/407 from this Authorize, so they should not be propagated
    if ($method eq 'ACK') {
	# cancel delivery of response to INVITE
	$self->{dispatcher}->cancel_delivery( $packet->tid );
	return $acode;
    } elsif ($method eq 'CANCEL') {
	return $acode;
    }

    # not authorized yet, ask to authenticate
    # keep it simple RFC2069 style
    my $digest = qq[Digest algorithm=MD5, realm="$realm",].
	( defined($opaque) ? qq[ opaque="$opaque",] : '' ).
	' nonce="'. md5_hex( $realm.rand(2**32)).'"';

    my $resp = $packet->create_response(
	$acode,
	'Authorization required',
	{ $rs_key => $digest }
    );

    $self->{dispatcher}->deliver( $resp, leg => $leg, dst_addr => $addr );

    # return $acode (TRUE) to show that packet should
    # not passed thru
    return $acode;
}

###########################################################################
# additional verifications
#  Net::SIP::Authorize::FromIsRealm - checks if the domain in 'From' is
#   the same as the realm in 'Authorization'
#  Net::SIP::Authorize::FromIsAuthUser - checks if the user in 'From' is
#   the same as the username in 'Authorization'
#  Net::SIP::Authorize::ToIsFrom - checks if 'To' and 'From' are equal
#
# Args each: ($packet,$leg,$addr,$auth_user,$auth_realm)
#  $packet: Net::SIP::Request
#  $leg: Net::SIP::Leg where request came in (and response gets send out)
#  $addr: ip:port where request came from and response will be send
#  $auth_user: username from 'Authorization'
#  $auth_realm: realm from 'Authorization'
# Returns: TRUE (1) | FALSE (0)
###########################################################################

package Net::SIP::Authorize::FromIsRealm;
use Net::SIP::Util qw( sip_hdrval2parts sip_uri2parts );
use Net::SIP::Debug;
sub verify {
    my ($packet,$leg,$addr,$auth_user,$auth_realm) = @_;
    my $from = $packet->get_header('from');
    ($from) = sip_hdrval2parts( from => $from );
    my ($domain) = sip_uri2parts($from);
    $domain =~s{:\w+$}{};
    return 1 if lc($domain) eq lc($auth_realm); # exact domain
    return 1 if $domain =~m{\.\Q$auth_realm\E$}i; # subdomain
    DEBUG( 10, "No Auth-success: From-domain is '$domain' and realm is '$auth_realm'" );
    return 0;
}

package Net::SIP::Authorize::FromIsAuthUser;
use Net::SIP::Util qw( sip_hdrval2parts sip_uri2parts );
use Net::SIP::Debug;
sub verify {
    my ($packet,$leg,$addr,$auth_user,$auth_realm) = @_;
    my $from = $packet->get_header('from');
    ($from) = sip_hdrval2parts( from => $from );
    my (undef,$user) = sip_uri2parts($from);
    return 1 if lc($user) eq lc($auth_user);
    DEBUG( 10, "No Auth-success: From-user is '$user' and auth_user is '$auth_user'" );
    return 0;
}

package Net::SIP::Authorize::ToIsFrom;
use Net::SIP::Util qw( sip_hdrval2parts );
use Net::SIP::Debug;
sub verify {
    my ($packet,$leg,$addr,$auth_user,$auth_realm) = @_;
    my $from = $packet->get_header('from');
    ($from) = sip_hdrval2parts( from => $from );
    my $to = $packet->get_header('to');
    ($to) = sip_hdrval2parts( to => $to );
    return 1 if lc($from) eq lc($to);
    DEBUG( 10, "No Auth-success: To is '$to' and From is '$from'" );
    return 0;
}

1;
