#!/usr/bin/perl use strict; use warnings; use Getopt::Long qw(:config posix_default bundling); use Net::SIP ':all'; ############################################################## # # Implements 3pcc according to RFC 3725,4.1 'Flow I' # ############################################################## # Usage # ------------------------------------------------------------- sub usage { print STDERR "ERROR: @_\n" if @_; print STDERR < \$debug, 'h|help' => sub { usage() }, ) || usage( 'bad options' ); Debug->level($debug || 1) if defined $debug; my ($laddr,$from,$to) = @ARGV; $to || usage( "no TO given" ); # create Dispatcher # ------------------------------------------------------------- my $loop = Dispatcher_Eventloop->new; my $leg = Leg->new( addr => $laddr ); my $disp = Dispatcher->new( [ $leg ], $loop, do_retransmits => 0 ) || die; $disp->set_receiver( \&receive ); my $me = ($disp->get_legs())[0]->{contact}; # create initial invite without SDP with # To: $to, From: $from, Contact: $me # put these info in call-id to be stateless # ------------------------------------------------------------- # assume no '|' is in $from and $to my $callid = "$from|$to|0|". sprintf( "%08x",rand(2**16)); my $invite = Request->new( "INVITE",$from, { from => $to, to => $from, contact => $me, 'call-id' => $callid, cseq => '1 INVITE', }); $disp->deliver( $invite, do_retransmits => 1 ); # and loop # ------------------------------------------------------------- my $stop_loop; $loop->loop( undef, \$stop_loop ); $loop->loop(1) if $stop_loop; # some time to forward remaining stuff ############################################################### # # callback for incoming packets: # # - there are two calls which slightly different call-id, with # a simple way one can get the other call-id from one call-id. # - responses are for me if there is only one via header, and # that's me -> handle to make requests (INVITE,ACK) from it # - all other responses get forwarded. If last via has a cseq # parameter they get forwarded after changing the cseq # - requests are for me if the URI is the contact of the local leg # -> forward to other call, but add "cseq" parameter to last # via so that the cseq of the calling uac gets saved for # responses # - all requests I get should be for me, because a contact header # is explicitly added # ############################################################### sub receive { my ($packet,$leg,$from_addr) = @_; # extract info from call-id my $callid = $packet->callid() or do { DEBUG( 1,"no callid in packet. DROP" ); return; }; my ($from,$to,$dir,$random) = split( qr{\|}, $callid ); my $new_callid = join( '|',$from,$to, $dir?0:1, $random ); my ( $request,$response ) = $packet->is_response ? ( undef,$packet ) : ( $packet, undef ); if ( $response ) { # ------------------------------------------------------------------ # Handle Responses: # - if it has only one via (and this is myself) it is a response # to a request which originated locally. In this case make # the appropriate request from it and forward it to the other side # - if it has more than one via just forward it to the other side # ------------------------------------------------------------------ # top via must be me my @via = $response->get_header( 'via' ); $leg->check_via($response) or do { DEBUG( 5, "top via isn't me: $via[0]" ); return; }; # exactly one via ? my $cseq = $response->cseq; my ($num,$method) = split( ' ',$cseq ); if ( @via == 1 ) { # cancel retransmits $disp->cancel_delivery( $response->tid ); if ( $method eq 'INVITE' && $dir == 0 ) { # --------------------------------------------------------- # response to initial INVITE ME->FROM # on success create INVITE ME->TO with SDP from response # --------------------------------------------------------- my $code = $response->code; if ( $code < 200 ) { # preliminary response, ignore and don't reply DEBUG( 10,"ignoring preliminary reply to initial invite" ); return; } elsif ( $code >= 300 ) { # non successful response (we don't care about redirects) # send ACK and ignore $disp->deliver( Request->new( 'ACK',$from, { 'call-id' => $callid, cseq => "$num ACK", to => scalar($response->get_header('from')), from => scalar($response->get_header('to')), contact => $me, })); } else { # success: extract SDP and forward in INVITE to # other party DEBUG( 10,"got success to initial INVITE" ); my $sdp = $response->sdp_body or do { DEBUG( 1,"no SDP in response to INVITE from $from" ); return; }; $disp->deliver( Request->new( 'INVITE', $to, { from => scalar($response->get_header( 'to' )), to => scalar($response->get_header( 'from' )), 'call-id' => $new_callid, contact => $me, cseq => "$num INVITE", }, $sdp, )); } } elsif ( $method eq 'INVITE' && $dir == 1 ) { # --------------------------------------------------------- # response from $to to the initial INVITE # on success create ACK # --------------------------------------------------------- my $code = $response->code; if ( $code < 200 ) { # preliminary response, ignore and don't reply DEBUG( 10,"ignoring preliminary reply from TO to initial invite" ); return; } # create ACK to TO $disp->deliver( Request->new( 'ACK', $to, { from => scalar($response->get_header( 'from' )), to => scalar($response->get_header( 'to' )), 'call-id' => $callid, contact => $me, cseq => "$num ACK", })); if ( $code >= 300 ) { # non successful response (we don't care about redirects) # cancel initial call [ME,FROM] DEBUG( 10,"got code $code on INVITE 'TO'" ); $disp->deliver( Request->new( 'CANCEL',$from, { 'call-id' => $new_callid, cseq => "$num INVITE", from => scalar($response->get_header( 'to' )), to => scalar($response->get_header( 'from' )), contact => $me, })); } else { DEBUG( 10,"got success on INVITE 'TO'" ); # success: extract SDP and forward in ACK to FROM my $sdp = $response->sdp_body or do { DEBUG( 1,"no SDP in response to INVITE from $to" ); return; }; $disp->deliver( Request->new( 'ACK', $from, { from => scalar($response->get_header( 'to' )), to => scalar($response->get_header( 'from' )), 'call-id' => $new_callid, contact => $me, cseq => "$num ACK", }, $sdp, )); } } } else { # --------------------------------------------------------- # response for forwarded request # change call-id and forward # --------------------------------------------------------- # get addr from next via my ($data) = sip_hdrval2parts( via => $via[1] ); my ($addr,$port) = $data =~m{([\w\-\.]+)(?::(\d+))?\s*$}; $port ||= 5060; # FIXME: not for sips! $response->set_header( contact => $me ); $leg->forward_incoming( $response ); $response->set_header( 'call-id' => $new_callid ); # check if the last via header had a cseq attribute. # in this case forward the response with the given cseq my ($via) = $response->get_header( 'via' ); my (undef,$param) = sip_hdrval2parts( via => $via ); if ( defined( my $num = $param->{cseq} )) { my $cseq = $response->cseq; $cseq =~s{^(\d+)}{$num}; $response->set_header( cseq => $cseq ); } # if this was response to BYE end this program $stop_loop = 1 if $method eq 'BYE'; $leg->forward_outgoing( $response,$leg ); $disp->deliver( $response, leg => $leg, dst_addr => "$addr:$port" ); } } else { # ------------------------------------------------------------------ # Handle requests from one of the parties # change call-id and cseq (because I have to use one of my cseqs) # and forward # ------------------------------------------------------------------ if ( $request->uri eq $leg->{contact} ) { # this is for me # could be CANCEL or BYE my $m = $request->method; if ( $m ne 'BYE' and $m ne 'CANCEL' ) { DEBUG( 10,"will not forward request to me with method $m" ); return; } # set URI to other party # if we were stateful we could store Contact infos from # older packets and use them here instead. $request->set_uri( $dir ? $from : $to ); } my ($num,$method) = split( ' ',$request->cseq ); # we just add 20 to the cseq we got from the uac # this is higher then every other locally generated cseq on # this side (we only used "1" until now for the first INVITE) $request->set_header( cseq => ( $num + 20 ).' '.$method ); $request->set_header( contact => $me ); $leg->forward_incoming( $request ); $request->set_header( 'call-id' => $new_callid ); # add cseq param to last via header because both calls maintain # different cseq spaces and we must know with which cseq we # need to forward the response if ( my @via = $request->get_header( 'via' ) ) { my ($data,$param) = sip_hdrval2parts( via => $via[0] ); $param->{cseq} = $num; $via[0] = sip_parts2hdrval( 'via',$data,$param ); $request->set_header( via => \@via ); } $leg->forward_outgoing( $request,$leg ); $disp->deliver( $request ) } }