###########################################################################
# 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;