########################################################################### # 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, %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, %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/opaque if ( ! $existing_auth ) { $existing_auth = {}; foreach my $hdr ( $self->get_header_hashval( $resp )) { my @auth = grep { defined } map { $hdr->{parameter}{$_} }qw( realm opaque ); $existing_auth->{ join( "\0",@auth ) } = 1; } } my @auth = grep { defined } map { $h->{$_} }qw( realm opaque ); 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}; $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;