File: Context.pm

package info (click to toggle)
libnet-sip-perl 0.46-1
  • links: PTS, VCS
  • area: main
  • in suites: lenny
  • size: 720 kB
  • ctags: 328
  • sloc: perl: 7,312; makefile: 2
file content (526 lines) | stat: -rw-r--r-- 17,419 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526

############################################################################
# Net::SIP::Endpoint::Context
# the calling context for a call managed by the endpoint
############################################################################

use strict;
use warnings;

package Net::SIP::Endpoint::Context;

use fields (

	# ===== can be set with new()
	'from',    # from where
	'to',      # to where
	'auth',    # [ user,pass ] or { realm1 => [ user1,pass1 ], realm2 => [ user2,pass2 ],... }
			   # or callback(realm,user)->pass
			   # if given, handle_response might automatically try to authorize requests
	'contact', # optional contact
	'callid',  # call-id value
	'cseq',    # number in cseq header
	'route',   # for 'route' header, comes usually from 'record-route' info in response
	'via',     # for 'via' header in created responses, comes from incoming request
	'incoming', # flag if call is incoming, e.g. 'to' is myself
	'local_tag', # local tag which gets assigned to either from or to depending on incoming

	# ===== Internals
	# \@array of hashrefs for infos about pending transactions
	'_transactions',
	# arrayref specifying a user defined callback for request success or failure
	'_callback',
	# cseq counter for incoming requests
	'_cseq_incoming',
	# last request in current incoming transaction
	'_last_transreq',

);


use Digest::MD5 'md5_hex';
use Net::SIP::Request;
use Net::SIP::Response;
use Net::SIP::Debug;
use Errno qw( EINVAL EPERM EFAULT );
use Hash::Util 'lock_keys';
use List::Util 'first';
use Net::SIP::Util ':all';

############################################################################
# Creates new context
# Args: ($class,@args)
#   @args: either single \%args (hash-ref) or %args (hash) with at least
#     values for from and to
#     callid,cseq will be generated if not given
#     routes will default to [] and usually set from record-route header
#     in response packets
# Returns: $self
############################################################################
sub new {
	my $class = shift;
	my %args = @_ == 1 ? %{ shift(@_) } : @_;
	my $self = fields::new( $class );
	%$self = %args;
	$self->{callid} ||= md5_hex( time(), rand(2**32) );
	$self->{cseq} ||= 0;
	$self->{route} ||= [];
	$self->{_transactions} = [];
	$self->{_cseq_incoming} = 0;

	# create tag on my side (to|from)
	my $side = $self->{incoming} ? 'to':'from';
	my ($data,$param) = sip_hdrval2parts( $side => $self->{$side} );
	if ( my $tag = $param->{tag} ) {
		# FIXME: what to do if local_tag was already set to different value?
		$self->{local_tag} = $tag;
	} else {
		$self->{$side}.=";tag=".(
			$self->{local_tag} = md5_hex( time(), rand(2**32), $self->{$side} )
		);
	}

	DEBUG( 100,"CREATE context $self callid=$self->{callid}" );
	return $self
}

# destroying of fields in perl5.8 cleanup can cause strange errors, where
# it complains, that it cannot coerce array into hash. So use this function
# on your own risks and rename it to DETSTROY if you want to have debugging
# info
sub _DESTROY {
	DEBUG( 100,"DESTROY context $_[0] callid=$_[0]->{callid}" );
}

############################################################################
# returns callid for context
# Args: $self
# Returns: $id
############################################################################
sub callid {
	my Net::SIP::Endpoint::Context $self = shift;
	return $self->{callid};
}

############################################################################
# get peer
# Args: $self
# Returns: $peer
#  $peer: for incoming calls this is 'from', for outgoing 'to'
############################################################################
sub peer {
	my Net::SIP::Endpoint::Context $self = shift;
	my $peer = $self->{incoming} ? $self->{from} : $self->{to};
	my ($data) = sip_hdrval2parts( from => $peer ); # strip parameters like tag etc
	return $data;
}

############################################################################
# return list of outstanding requests matching filter, if no filter is given
# returns all requests
# Args: ($self,%filter)
#  %filter
#     method => name: filter for requests with given method
#     request => packet: filter for packet, e.g. finds if packet is outstanding
# Returns: @requests
#   returns all matching requests (Net::SIP::Request objects), newest
#   requests first
############################################################################
sub find_outstanding_requests {
	my Net::SIP::Endpoint::Context $self = shift;
	my %filter = @_;
	my @trans = @{$self->{_transactions}} or return;
	if ( my $pkt = $filter{request} ) {
		@trans = grep { $pkt == $_->{request} } @trans or return;
	}
	if ( my $method = $filter{method} ) {
		@trans = grep { $method eq $_->{request}->method } @trans or return;
	}
	return map { $_->{request} } @trans;
}

############################################################################
# creates a new SIP request packet within this context
# Args: ($self,$method;$body,%args)
#   $method: method for request, eg 'INVITE','BYE'...
#      or already a Net::SIP::Request object
#   $body: (optional) body for SIP packet
#   %args: (optional) additional args given to Net::SIP::Request->new
# Returns: $request
#   $request: Net::SIP::Request object
############################################################################
sub new_request {
	my Net::SIP::Endpoint::Context $self = shift;
	my ($method,$body,%args) = @_;

	my $request;
	if ( ref($method)) {
		# already a request object
		$request = $method;
		$method = $request->method;
	} else {

		# increase cseq unless its explicitly specified
		# the latter case is useful for ACK and CANCEL
		# which need the same sequence number as the INVITE
		# they belong to
		my $cseq = delete $args{cseq} || ++$self->{cseq};

		$method = uc($method);
		my $uri = delete $args{uri};
		my ($to,$from,$contact,$remote_contact) = $self->{incoming}
			? ( $self->{from}, $self->{to},undef,$self->{contact} )
			: ( $self->{to}, $self->{from},$self->{contact},undef )
			;
		if ( !$uri ) {
			($uri) = sip_hdrval2parts( to => $remote_contact||$to);
			# XXX handle quotes right, e.g "<bla>" <sip:bla@fasel.com>
			$uri = $1 if $uri =~m{<(\S+)>$};
		}

		# contact is mandatory for INVITE
		# will be added within Leg

		$request = Net::SIP::Request->new(
			$method,     # Method
			$uri,        # URI
			{
				from => $from,
				to => $to,
				$contact ? ( contact => $contact ):(),
				route => $self->{route},
				cseq => "$cseq $method",
				'call-id' => $self->{callid},
				'max-forwards' => 70,
				%args
			},
			$body
		)
	}

	# create new transaction
	my %trans = (
		tid      => $request->tid,
		request  => $request,
		callback => $self->{_callback},
	);
	lock_keys(%trans);
	unshift @{ $self->{_transactions} }, \%trans; # put as first

	return $request;
}

############################################################################
# set callback for context
# Args: ($self,$cb)
#  $cb: [ \&sub,@arg ]
# Returns: NONE
############################################################################
sub set_callback {
	my Net::SIP::Endpoint::Context $self = shift;
	$self->{_callback} = shift;
}

############################################################################
# notify context that current delivery is permanently done (e.g successful
# or failed). On failure call current callback to notify upper layer about
# permanent failure of request
# This is used for errors from the transport layer, errors from the SIP
# layer (e.g response with 400 Bad request) are handled by handle_response()
# Args: ($self,$tid;$error)
#  $tid: Transaction ID
#  $error: errno if error occured
# Returns: NONE
############################################################################
sub request_delivery_done {
	my Net::SIP::Endpoint::Context $self = shift;
	my ($endpoint,$tid,$error) = @_;
	my $trans = $self->{_transactions};
	my @ntrans;
	foreach my $tr (@$trans) {
		if ( $tr->{tid} eq $tid ) {
			$self->{_transactions} = \@ntrans;
			if ( $error && ( my $cb = $tr->{callback} )) {
				# permanently failed
				invoke_callback( $cb, $self,$endpoint,$error );
			}
		} else {
			push @ntrans,$tr
		}
	}
}

############################################################################
# handle response packet for this context
# cseq of response must match the cseq of the current delivery!
# if there is no current delivery or the cseq does not match the response
# gets dropped
# Args: ($self,$response,$leg,$from,$endpoint)
#    $response: incoming Net::SIP::Response packet
#    $leg: Net::SIP::Leg through which the response came in
#    $from: ip:port where response came in
#    $endpoint: endpoint responsable for this context, used for redeliveries...
# Returns: NONE
############################################################################
sub handle_response {
	my Net::SIP::Endpoint::Context $self = shift;
	my ($response,$leg,$from,$endpoint) = @_;

	# find and remove transaction because I got response for it
	# if response does not terminates transaction one need to add
	# it again
	my $tid = $response->tid;
	my $method = $response->method;
	my $trans = $self->{_transactions};
	my (@ntrans,$tr);
	foreach my $t (@$trans) {
		if ( !$tr and $t->{tid} eq $tid and $method eq $t->{request}->method) {
			$tr = $t;
		} else {
			push @ntrans,$t
		}
	}
	$tr || do {
		# no delivery pending
		DEBUG( 10,"got response for unkown transaction. DROP" );
		return;
	};
	$self->{_transactions} = \@ntrans;

	DEBUG( 10,"got response for transaction ".$tr->{request}->dump );

	# match response to client transaction, RFC3261 17.1.3
	# check if the response came in through the same leg, where the
	# request was send, e.g that the branch tag is the same
	$leg->check_via( $response ) || do {
		DEBUG( 10,"response came in through the wrong leg" );
		return;
	};

	my $cb = $tr->{callback};
	my @arg = ($endpoint,$self);
	my $code = $response->code;

	# Don't care about the response for a CANCEL  or a BYE
	# because this connection close is issued by this side
	# and no matter what the peer wants the call be will closed
	# But invoke callback to notify upper layer that the BYE was received
	if ( $method eq 'CANCEL' or $method eq 'BYE' ) {
		if ( $code >=100 and $code<=199 ) {
			push @ntrans,$tr
		} else {
			invoke_callback($cb,@arg,0,$code,$response,$leg,$from);
			$endpoint->close_context( $self );
		}
		return;
	}

	# for 300-699 an ACK must be created (RFC3261, 17.1.1.2)
	# notification of upper layer will be done down in the method
	# XXXXXXXXXXXXXX do we need to wait that the ACK was accepted
	# XXXXXXXXXXXXXX before sending new request??
	# XXXXXXXXXXXXXX (e.g for 401,407,302..)
	if ( $method eq 'INVITE' && $code>=300 ) {
		# must create ACK
		DEBUG( 50,"code=$code, must generate ACK" );
		my $ack = $tr->{request}->create_ack( $response );
		$endpoint->new_request( $ack,$self,undef,undef,leg => $leg, dst_addr => $from );
	}


	if ( $code =~m{^1\d\d} ) {
		# transaction is not done
		push @ntrans,$tr if $code >=100 and $code<=199;

		# forward preliminary responses to INVITE to app
		# ignore all other preliminary responses
		if ( $method eq 'INVITE' ) {
			invoke_callback($cb,@arg,0,$code,$response,$leg,$from);
		}

	} elsif ( $code =~m{^2\d\d} ) {
		# 2xx OK

		# extract route information
		if ( my @route = $response->get_header( 'record-route' )) {
			$self->{route} = [ reverse @route ];
		}

		if ( $method eq 'INVITE' ) {
			# is response to INVITE, create ACK
			# and propagate to upper layer
			my $req = $tr->{request};
			if ( my $contact = $response->get_header( 'contact' )) {
				# 12.1.2 - set URI for dialog to contact given in response which
				# establishes the dialog
				$contact = $1 if $contact =~m{<(\w+:[^>\s]+)>};
				$req->set_uri( $contact );
			}
			my $ack = $req->create_ack( $response );
			invoke_callback($cb,@arg,0,$code,$response,$leg,$from,$ack);
			$endpoint->new_request( $ack,$self,undef,undef,leg => $leg, dst_addr => $from );

			# use to-tag from this request to update 'to'
			# FIXME: this should probably be better done by the upper layer
			# which decides, which call to accept (in case of call-forking with
			# multiple 2xx responses)
			$self->{to} = $response->get_header( 'to' );

		} else {
			# response to ACK, REGISTER...
			# simply propagate to upper layer, only INVITE needs
			# special handling
			invoke_callback($cb,@arg,0,$code,$response,$leg,$from);
		}

	} elsif ( $code == 401 || $code == 407 ) {
		# Authorization required
		my $r = $tr->{request};
		my $auth = $self->{auth};
		if ( $auth && $r->authorize( $response, $auth )) {
			# found something to authorize
			# redo request
			# update local cseq from cseq in request
			($self->{cseq}) = $r->cseq =~m{(\d+)};
			$endpoint->new_request( $r,$self );
		} else {
			# need user feedback
			invoke_callback($cb,@arg,EPERM,$code,$response,$leg,$from);
		}

	} elsif ( $code == 300 || $code == 301 ) {
		# need user feedback in these cases
		# 21.3.1 300 multiple choices
		# 21.3.2 301 moved permanently
		invoke_callback($cb,@arg,EFAULT,$code,$response,$leg,$from);

	} elsif ( $code == 302 ) {
		# 21.3.3 302 moved temporarily
		# redo request and insert request again
		my $contact = $self->{to} = $response->get_header( 'contact' );
		$contact = $1 if $contact =~m{<(\w+:[^>\s]+)>};
		( my $r = $tr->{request} )->set_uri( $contact );
		$r->set_cseq( ++$self->{cseq} );
		$endpoint->new_request( $r,$self );

	} elsif ( $code == 305 ) {
		# 21.3.4 305 use proxy
		# set proxy as the first route and insert request again
		my $route = $self->{route};
		unshift @$route,$response->get_header( 'contact' );
		( my $r = $tr->{request} )->set_header( route => $route );
		$r->set_cseq( ++$self->{cseq} );
		$endpoint->new_request( $r,$self );

	} else {
		# some kind of unrecoverable error
		invoke_callback($cb,@arg,EINVAL,$code,$response,$leg,$from);
	}
}

############################################################################
# handle incoming request
# Args: ($self,$request,$leg,$endpoint)
#   $request: incoming Net::SIP::Request packet
#   $leg: Net::SIP::Leg through which the request came in
#   $from: ip:port where request came in
#   $endpoint: endpoint responsable for this context, used for responses...
# Returns: NONE
# Comment: only new requests will be delivered to this method, because the dispatcher
#   cares about retransmits, eg requests for which I issued already a response
#   within the last 64*T1
############################################################################
sub handle_request {
	my Net::SIP::Endpoint::Context $self = shift;
	my ($request,$leg,$from,$endpoint) = @_;

	my $cseq = $request->cseq;
	my ($cseq_num) = $cseq=~m{^(\d+)};

	DEBUG( 100,"method=%s cseq=%s/%s inc=%d", $request->method, $cseq_num,$cseq, $self->{_cseq_incoming} );
	if ( $cseq_num < $self->{_cseq_incoming} ) {
		# must be an retransmit of an really old request, drop
		DEBUG( 10,"retransmit of really old request? Dropping" );
		return;
	}

	# check with last request in transaction
	if ( my $trans = $self->{_last_transreq} ) {
		my $last_cseq = $trans->cseq;
		if ( $last_cseq eq $cseq ) {
			DEBUG( 10,"retransmit of last request. DROP" );
			return;
		}
	}
	$self->{_last_transreq} = $request;

	my $method = $request->method;

	if ( $method eq 'ACK' || $method eq 'CANCEL' ) {
		# must be have same cseq_num as last request, otherwise drop
		if ( $cseq_num != $self->{_cseq_incoming} ) {
			DEBUG( 10,"received $method for unreceived INVITE: $cseq_num|$self->{_cseq_incoming}" );
			return;
		}
	} else {
		# cannot have the same cseq_num as last request
		if ( $cseq_num == $self->{_cseq_incoming} ) {
			DEBUG( 10,"reused cseq for $method. DROP" );
			return;
		}
	}
	$self->{_cseq_incoming} = $cseq_num;

	my $cb = $self->{_callback} || do {
		DEBUG( 50,"no callback at context!" );
		return;
	};
	my @arg = ($endpoint,$self);

	# extract route information for future requests to the UAC (re-invites)
	if ( my @route = $request->get_header( 'record-route' )) {
		$self->{route} = \@route;
	}

	{
		# check if to has already a (my) tag, if not add it to request,
		# so that it gets added to responses
		my $to = $request->get_header( 'to' );
		my ($data,$param) = sip_hdrval2parts( to => $to );
		if ( ! $param->{tag} ) {
			DEBUG( 50,"added my tag to to header in request" );
			$param->{tag} = $self->{local_tag};
			$to = sip_parts2hdrval( 'to',$data,$param );
			$request->set_header( to => $to );
		}
	}

	if ( $method eq 'BYE' || $method eq 'CANCEL' ) {
		# if the peer wants to hangup we must confirm
		my $response = $request->create_response( '200','Closing' );
		$endpoint->new_response( $self,$response,$leg,$from );

		# invoke callback before closing context, so that we have more
		# information about the current call
		invoke_callback($cb,@arg,0,0,$request,$leg,$from);
		$endpoint->close_context($self);
		return;
	}

	# If new INVITE, send 100 Trying
	if ( $method eq 'INVITE' ) {
		my $response = $request->create_response( '100','Trying' );
		$endpoint->new_response( $self,$response,$leg,$from );
	}


	# propagate to upper layer, which needs
	# - for INVITE send 180 Ringing periodically and after some time a final response
	# - for ACK to establish the call
	# - BYE|CANCEL is already handled above
	# - for everything else to handle the Option fully, eg issue final response..

	invoke_callback($cb,@arg,0,0,$request,$leg,$from);
}

1;