###########################################################################
# package Net::SIP::Request
# subclass from Net::SIP::Packet for managing the request packets
# has methods for creating ACK, CANCEL based on the request (and response)
# and for adding Digest authorization (md5+qop=auth only) to the
# request based on the requirements in the response
###########################################################################

use strict;
use warnings;

package Net::SIP::Request;
use base 'Net::SIP::Packet';
use Net::SIP::Debug;
use Net::SIP::Util 'invoke_callback';

use Digest::MD5 'md5_hex';

my %ResponseCode = (
    # Informational
    100 => 'Trying',
    180 => 'Ringing',
    181 => 'Call Is Being Forwarded',
    182 => 'Queued',
    183 => 'Session Progress',

    # Success
    200 => 'OK',

    # Redirection
    300 => 'Multiple Choices',
    301 => 'Moved Permanently',
    302 => 'Moved Temporarily',
    305 => 'Use Proxy',
    380 => 'Alternative Service',

    # Client-Error
    400 => 'Bad Request',
    401 => 'Unauthorized',
    402 => 'Payment Required',
    403 => 'Forbidden',
    404 => 'Not Found',
    405 => 'Method Not Allowed',
    406 => 'Not Acceptable',
    407 => 'Proxy Authentication Required',
    408 => 'Request Timeout',
    410 => 'Gone',
    413 => 'Request Entity Too Large',
    414 => 'Request-URI Too Large',
    415 => 'Unsupported Media Type',
    416 => 'Unsupported URI Scheme',
    420 => 'Bad Extension',
    421 => 'Extension Required',
    423 => 'Interval Too Brief',
    480 => 'Temporarily not available',
    481 => 'Call Leg/Transaction Does Not Exist',
    482 => 'Loop Detected',
    483 => 'Too Many Hops',
    484 => 'Address Incomplete',
    485 => 'Ambiguous',
    486 => 'Busy Here',
    487 => 'Request Terminated',
    488 => 'Not Acceptable Here',
    491 => 'Request Pending',
    493 => 'Undecipherable',

    # Server-Error
    500 => 'Internal Server Error',
    501 => 'Not Implemented',
    502 => 'Bad Gateway',
    503 => 'Service Unavailable',
    504 => 'Server Time-out',
    505 => 'SIP Version not supported',
    513 => 'Message Too Large',

    # Global-Failure
    600 => 'Busy Everywhere',
    603 => 'Decline',
    604 => 'Does not exist anywhere',
    606 => 'Not Acceptable',
);

###########################################################################
# Redefine methods from Net::SIP::Packet, no need to find out dynamically
###########################################################################
sub is_request  {1}
sub is_response {0}

###########################################################################
# Accessors for method and URI
###########################################################################
sub method      { return (shift->as_parts())[0] }
sub uri         { return (shift->as_parts())[1] }

sub set_uri {
    my Net::SIP::Request $self = shift;
    $self->_update_string;
    $self->{text} = shift;
}

###########################################################################
# set cseq
# Args: ($self,$number)
#   $number: new cseq number
# Returns: $self
###########################################################################
sub set_cseq {
    my Net::SIP::Request $self = shift;
    my $cseq = shift;
    $self->set_header( cseq => "$cseq ".$self->method );
    return $self;
}

###########################################################################
# create ack to response based on original request
# see RFC3261 "17.1.1.3 Construction of the ACK Request"
# Args: ($self,$response)
#  $response: Net::SIP::Response object for request $self
# Returns: $cancel
#  $ack: Net::SIP::Request object for ACK method
###########################################################################
sub create_ack {
    my Net::SIP::Request $self = shift;
    my $response = shift;
    # ACK uses cseq from request
    $self->cseq =~m{(\d+)};
    my $cseq = "$1 ACK";
    my %auth;
    for (qw(authorization proxy-authorization)) {
	my $v = scalar($self->get_header($_)) or next;
	$auth{$_} = $v;
    }
    my $header = {
	'call-id' => scalar($self->get_header('call-id')),
	from      => scalar($self->get_header('from')),
	# unlike CANCEL the 'to' header is from the response
	to        => [ $response->get_header('to') ],
	via       => [ ($self->get_header( 'via' ))[0] ],
	route     => [ $self->get_header( 'route' ) ],
	cseq      => $cseq,
	'max-forwards' => 70,
	%auth,
    };
    return Net::SIP::Request->new( 'ACK',$self->uri,$header );
}

###########################################################################
# Create cancel for request
# Args: $self
# Returns: $cancel
#   $cancel: Net::SIP::Request containing CANCEL for $self
###########################################################################
sub create_cancel {
    my Net::SIP::Request $self = shift;
    # CANCEL uses cseq from request
    $self->cseq =~m{(\d+)};
    my $cseq = "$1 CANCEL";
    my %auth;
    for (qw(authorization proxy-authorization)) {
	my $v = scalar($self->get_header($_)) or next;
	$auth{$_} = $v;
    }
    my $header = {
	'call-id' => scalar($self->get_header('call-id')),
	from      => scalar($self->get_header('from')),
	# unlike ACK the 'to' header is from the original request
	to        => [ $self->get_header('to') ],
	via       => [ ($self->get_header( 'via' ))[0] ],
	route     => [ $self->get_header( 'route' ) ],
	cseq      => $cseq,
	'max-forwards' => 70,
	%auth
    };
    return Net::SIP::Request->new( 'CANCEL',$self->uri,$header );
}

###########################################################################
# Create response to request
# Args: ($self,$code,[$msg],[$args,$body])
#   $code: numerical response code
#   $msg: msg for code, if arg not given it will be used from %ResponseCode
#   $args: additional args for SIP header
#   $body: body as string
# Returns: $response
#   $response: Net::SIP::Response
###########################################################################
sub create_response {
    my Net::SIP::Request $self = shift;
    my $code = shift;
    my ($msg,$args,$body) = ( defined $_[0] && ref($_[0]) ) ? (undef,@_):@_;
    $msg = $ResponseCode{$code} if ! defined $msg;
    my %header = (
	cseq      => scalar($self->get_header('cseq')),
	'call-id' => scalar($self->get_header('call-id')),
	from      => scalar($self->get_header('from')),
	to        => [ $self->get_header('to') ],
	'record-route'  => [ $self->get_header( 'record-route' ) ],
	via       => [ $self->get_header( 'via' ) ],
	$args ? %$args : ()
    );
    return Net::SIP::Response->new($code,$msg,\%header,$body);
}


###########################################################################
# Authorize Request based on credentials in response using
# Digest Authorization specified in RFC2617
# Args: ($self,$response,@args)
#   $response: Net::SIP::Response for $self which has code 401 or 407
#   @args: either [ $user,$pass ] if there is one user+pass for all realms
#       or { realm1 => [ $user,$pass ], realm2 => [...].. }
#       for different user,pass in different realms
#       or callback(realm)->[ user,pass ]
# Returns:  0|1
#    1: if (proxy-)=authorization headers were added to $self
#    0: if $self was not modified, e.g. no usable authenticate
#       headers were found
###########################################################################
sub authorize {
    my Net::SIP::Request $self = shift;
    my ($response,$user2pass) = @_;

    # find out format of user2pass
    my ($default_upw,$realm2upw,$cb_upw);
    if ( ref($user2pass) eq 'ARRAY' && ! ref( $user2pass->[0] )) {
	$default_upw = $user2pass;
    } elsif ( ref($user2pass) eq 'HASH' ) {
	$realm2upw = $user2pass;
    } else {
	$cb_upw = $user2pass;
    }


    my $auth = 0;
    my %auth_map = (
	'proxy-authenticate' => 'proxy-authorization',
	'www-authenticate' => 'authorization',
    );

    while ( my ($req,$resp) = each %auth_map ) {
	my $existing_auth;
	if ( my @auth = $response->get_header_hashval( $req ) ) {
	    foreach my $a (@auth) {
		my $h = $a->{parameter};

		# check if we already have an authorize header for this realm
		if ( ! $existing_auth ) {
		    $existing_auth = {};
		    foreach my $hdr ( $self->get_header_hashval( $resp )) {
			my @auth = grep { defined } map { $hdr->{parameter}{$_} }qw( realm );
			$existing_auth->{ join( "\0",@auth ) } = 1;
		    }
		}

		my @auth = grep { defined } map { $h->{$_} }qw( realm );
		if ( $existing_auth->{ join( "\0",@auth ) } ) {
		    # we have this auth header already, don't repeat
		    next;
		}

		# RFC2617
		# we support only md5 (not md5-sess or other)
		# and only empty qop or qop=auth (not auth-int or other)

		if ( lc($a->{data}) ne 'digest'
		    || $h->{algorithm} && lc($h->{algorithm}) ne 'md5'
		    || $h->{qop} && $h->{qop} !~ m{(?:^|,\s*)auth(?:$|,)}i ) {
		    no warnings;
		    DEBUG(10,"unsupported authorization method $a->{data} method=$h->{method} qop=$h->{qop}");
		    next;
		}
		my $realm = $h->{realm};
		my $upw =
		    $cb_upw      ? invoke_callback( $cb_upw, $realm ) :
		    $realm2upw   ? $realm2upw->{$realm} :
		    $default_upw ? $default_upw :
		    next;

		# for meaning of a1,a2... and for the full algorithm see RFC2617, 3.2.2
		my $a1 = join(':',$upw->[0],$realm,$upw->[1] ); # 3.2.2.2
		my $a2 = join(':',$self->method,$self->uri );   # 3.2.2.3, qop == auth|undef

		my %digest = (
		    username => $upw->[0],
		    realm => $realm,
		    nonce => $h->{nonce},
		    uri => $self->uri,
		);
		$digest{opaque} = $h->{opaque} if defined $h->{opaque};

		# 3.2.2.1
		if ( $h->{qop} ) {
		    $h->{qop} = 'auth'; # in case it was 'auth,auth-int'
		    my $nc = $digest{nc} = '00000001';
		    my $cnonce = $digest{cnonce} = sprintf("%08x",rand(2**32));
		    $digest{qop} = $h->{qop};
		    $digest{response} = md5_hex( join(':',
			md5_hex($a1),
			$h->{nonce},
			$nc,
			$cnonce,
			$h->{qop},
			md5_hex($a2)
		    ));
		} else {
		    # 3.2.2.1 compability with RFC2069
		    $digest{response} = md5_hex( join(':',
			md5_hex($a1),
			$h->{nonce},
			md5_hex($a2),
		    ));
		}

		# RFC2617 has it's specific ideas what should be quoted and what not
		# so we assemble it manually
		my $header = qq[Digest username="$digest{username}",realm="$digest{realm}",].
		    qq[nonce="$digest{nonce}",uri="$digest{uri}",response="$digest{response}"];
		$header.= qq[,opaque="$digest{opaque}"] if defined $digest{opaque};
		$header.= qq[,cnonce="$digest{cnonce}"] if defined $digest{cnonce};
		$header.= qq[,qop=$digest{qop}] if defined $digest{qop};
		$header.= qq[,nc=$digest{nc}] if defined $digest{nc};
		# Echo back the algorithm if specifically set in response
		$header.= qq[,algorithm=$h->{algorithm}] if defined $h->{algorithm};
		$self->add_header( $resp, $header );
		$auth++;
	    }
	}
    }

    return if !$auth; # no usable authenticate headers found

    my ($rseq) = $response->cseq =~m{^(\d+)};
    $self->cseq =~m{^(\d+)(.*)};
    if ( defined $1 and $1 <= $rseq ) {
	# increase cseq, because this will be a new request, not a retransmit
	$self->set_header( cseq => ($rseq+1).$2 );
    }

    return 1;
}

1;
