File Coverage

blib/lib/RTSP/Proxy.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package RTSP::Proxy;
2              
3 1     1   23789 use Moose;
  0            
  0            
4             extends 'Net::Server::PreFork';
5              
6             use RTSP::Proxy::Session;
7             use Carp qw/croak/;
8              
9             our $VERSION = '0.07';
10              
11             =head1 NAME
12              
13             RTSP::Proxy - Simple RTSP proxy server
14              
15             =head1 SYNOPSIS
16              
17             use RTSP::Proxy;
18             my $proxy = RTSP::Proxy->new({
19             rtsp_client => {
20             address => '10.0.1.105',
21             media_path => '/mpeg4/media.amp',
22             client_port_range => '6970-6971',
23             transport_protocol => 'RTP/AVP;unicast',
24             },
25             port => 554,
26             listen => 5,
27             });
28            
29             $proxy->run;
30              
31             =head1 DESCRIPTION
32              
33             This module is a simple RTSP proxy based on L<Net::Server> and L<RTSP::Client>.
34              
35             When a client connects and sends commands to the server, it will pass them through the RTSP client and return the results back.
36              
37             This module will also automatically proxy the media transport protocol as well. Currently it only proxies RTP over UDP, but support for other transports may be added if requested.
38              
39             This has only been tested with VLC and Axis IP cameras, it may not work with your setup. Patches and feedback welcome.
40              
41             Note: you will need to be root to bind to port 554, you may drop privs if you wish. See the configuration options in L<Net::Server> for more details.
42              
43             =head2 EXPORT
44              
45             None by default.
46              
47             =head2 METHODS
48              
49             =over 4
50              
51             =cut
52              
53             has session => (
54             is => 'rw',
55             isa => 'RTSP::Proxy::Session',
56             );
57              
58             sub transport_listen_port_start { 6970 }
59             sub transport_listen_port_end { 6971 }
60              
61             sub process_request {
62             my $self = shift;
63            
64             my $method;
65             my $uri;
66             my $proto;
67             my $headers = {};
68             my $sock = $self->{server}->{client} or die "Could not find client socket";
69            
70             READ: while (my $line = <$sock>) {
71             $self->log(5, "got line: $line");
72            
73             unless ($method) {
74             # first line should be method
75             ($method, $uri, $proto) = $line =~ m!(\w+)\s+(\S+)(?:\s+(\S+))?\r\n!ism;
76            
77             $self->log(4, "received: method: $method, uri: $uri, protocol: $proto");
78            
79             unless ($method && $uri && $proto =~ m!RTSP/1.\d!i) {
80             $self->log(1, "Invalid request: $line");
81             return $self->return_status(403, 'Bad request');
82             }
83             next READ;
84             } else {
85             goto DONE if $line eq "\r\n";
86            
87             # header
88             my ($header_name, $header_value) = $line =~ /^([-A-Za-z0-9]+)\s*:\s*(.*)$/;
89             unless ($header_name) {
90             $self->log(1, "Invalid header: $line");
91             next;
92             }
93            
94             $headers->{$header_name} ||= [];
95             push @{$headers->{$header_name}}, $header_value;
96             next READ;
97             }
98            
99             DONE:
100             last unless $method && $proto;
101            
102             $method = uc $method;
103            
104            
105             # get/create session
106             my $session;
107             if ($self->{server}{session}) {
108             $session = $self->{server}{session};
109             } else {
110             my $client_settings = $self->{server}{rtsp_client} or die "Could not find client configuration";
111             my $transport_handler_settings = $self->{server}{transport_handler};
112            
113             my $transport_handler_class = $self->{server}{transport_handler_class};
114             croak "build_transport_handler() called without transport_handler_class being defined"
115             unless $transport_handler_class;
116              
117             # get client address
118             my $sock = $self->{server}{client};
119             my $client_address = $sock->peerhost;
120              
121             # create RTSP session object
122             $self->log(3, "creating session");
123             $session = RTSP::Proxy::Session->new(
124             client_address => $client_address,
125             media_uri => $uri,
126             rtsp_client_opts => $client_settings,
127             transport_handler_opts => $transport_handler_settings,
128             transport_handler_class => $transport_handler_class,
129             );
130            
131             # save session
132             $self->{server}{session} = $session;
133             }
134            
135             if ($method eq 'PLAY') {
136             $session->rtsp_client->reset;
137             }
138            
139             # parse out setup info
140             my ($client_port_start, $client_port_end);
141             if ($method eq 'SETUP') {
142             # parse out the client requested ports
143             my $transport;
144             $transport = @{$headers->{Transport}}[0] if $headers->{Transport};
145             $transport = @{$headers->{transport}}[0] if $headers->{transport};
146             $self->log(3, "transport: '$transport'") if $transport;
147            
148             # rewrite the client port range
149             my ($client_port_start, $client_port_end); # FIX THIS
150             ($client_port_start, $client_port_end) =
151             $self->rewrite_transport(
152             \$transport,
153             $self->transport_listen_port_start,
154             $self->transport_listen_port_end) if $transport;
155            
156             if ($client_port_start) {
157             # replace transport header with our transport specification
158             delete $headers->{Transport};
159             delete $headers->{transport};
160             $headers->{Transport} = [$transport];
161            
162             $self->set_client_port_range($client_port_start, $client_port_end);
163             }
164             }
165            
166             $self->proxy_request($method, $uri, $session, $headers);
167            
168             # so we can reuse the client for more requests
169             if ($method eq 'SETUP' || $method eq 'DESCRIBE' || $method eq 'TEARDOWN') {
170             $self->log(4, "resetting rtsp client");
171             $session->rtsp_client->reset;
172             }
173            
174             if ($method eq 'TEARDOWN') {
175             delete $self->{server}{session};
176             }
177            
178             $method = '';
179             $uri = '';
180             $proto = '';
181             $headers = {};
182             }
183             }
184              
185             sub set_client_port_range {
186             my ($self, $client_port_start, $client_port_end) = @_;
187            
188             my $session = $self->{server}{session};
189             unless ($session) {
190             $self->log(1, "error: didn't find session in set_client_port_range");
191             return;
192             }
193            
194             return if $session->client_port_start && $session->client_port_start == $client_port_start;
195            
196             $client_port_end ||= $client_port_start;
197            
198             $self->log(3, "setting session client port $client_port_start");
199             $session->client_port_start($client_port_start);
200             $session->client_port_end($client_port_end);
201            
202             # now ready to run server to proxy media transport
203             $self->log(1, "starting transport handler server");
204             $session->run_transport_handler_server;
205             }
206              
207             sub rewrite_transport {
208             my ($self, $transportref, $client_port_start, $client_port_end) = @_;
209             return "" unless $transportref && $$transportref;
210            
211             my $old_transport = $$transportref;
212            
213             my ($orig_port_start, $orig_port_end) = $$transportref =~ /client_port=(\d+)(?:-(\d+))?/i;
214             if ($orig_port_start) {
215             $orig_port_end ||= $orig_port_start;
216            
217             # kinda sketchy
218             my $port_range = "${client_port_start}-$client_port_end";
219             $$transportref =~ s/client_port=((?:\d+)(?:-(?:\d+)))?/client_port=$port_range/ims;
220             $self->log(3, "rewriting transport request:\n old: $old_transport\n new: $$transportref");
221            
222             return ($orig_port_start, $orig_port_end);
223             }
224            
225             return;
226             }
227              
228             sub proxy_request {
229             my ($self, $method, $uri, $session, $headers) = @_;
230            
231             $self->log(2, "\n-------------------------\nproxying $method / $uri to " . $session->rtsp_client->address);
232            
233             my $client = $session->rtsp_client;
234            
235             unless ($client->connected) {
236             # open a connection
237             unless ($client->open) {
238             $self->log(0, "Failed to connect to camera: $!");
239             return $self->return_status(404, "Resource not found");
240             }
241             }
242            
243             # pass through some headers
244             foreach my $header_name (qw/
245             Accept Bandwidth Accept-Language ClientChallenge PlayerStarttime RegionData
246             GUID ClientID Transport x-retransmit x-dynamic-rate x-transport-options Session
247             Range/) {
248            
249             my $header_value = $headers->{$header_name};
250             next unless defined $header_value && @$header_value;
251            
252             # can be multiple versions of a header
253             foreach my $h (@$header_value) {
254             $self->chomp_line(\$h);
255            
256             # if (lc $header_name eq 'transport' && $h) {
257             # my ($client_port_start, $client_port_end) = $self->rewrite_transport(
258             # \$h,
259             # $self->transport_listen_port_start,
260             # $self->transport_listen_port_end
261             # );
262             # $self->set_client_port_range($client_port_start, $client_port_end);
263             # }
264             #
265             $client->add_req_header($header_name, $h);
266             $self->log(3, "passing through header $header_name\t=$h");
267             }
268             }
269            
270             # do request
271             my $ok;
272             my $body;
273             if ($method eq 'SETUP') {
274             $ok = $client->setup;
275             } elsif ($method eq 'DESCRIBE') {
276             # proxy body response
277             $body = $client->describe;
278             } elsif ($method eq 'OPTIONS') {
279             $ok = $client->options;
280             } elsif ($method eq 'TEARDOWN') {
281             $ok = $client->teardown;
282             } else {
283             $ok = $client->request($method);
284             }
285            
286             my $status_message = $client->status_message;
287             my $status_code = $client->status;
288            
289             $self->log(4, "$status_code $status_message - got headers: " . $client->_rtsp->headers_string . "\n");
290            
291             unless ($status_code) {
292             $status_code = 405;
293             $status_message = "Bad request";
294             }
295            
296             my $res = '';
297              
298             # return status
299             $res .= "RTSP/1.0 $status_code $status_message\r\n";
300            
301             # pass some headers back
302             foreach my $header_name (qw/
303             Content-Type Content-Base Public Allow Transport Session
304             Rtp-Info Range transport Date Www-Authenticate/) {
305             my $header_values = $client->get_header($header_name);
306             next unless defined $header_values;
307             foreach my $val (@$header_values) {
308             $self->log(4, "header: $header_name, value: '$val'");
309             $self->chomp_line(\$val);
310            
311             $self->rewrite_transport_response($session, \$val)
312             if lc $header_name eq 'transport';
313            
314             $res .= "$header_name: $val\r\n";
315             }
316             }
317            
318             # respond with correct CSeq
319             my $cseq;
320             $cseq = @{$headers->{CSeq}} if $headers->{CSeq};
321             $cseq = @{$headers->{Cseq}} if $headers->{Cseq};
322             $cseq = @{$headers->{cseq}} if $headers->{cseq};
323             if ($cseq) {
324             $self->chomp_line(\$cseq);
325             $res .= "cseq: $cseq\r\n";
326             }
327            
328             $self->write($res, $body);
329             }
330              
331             sub write {
332             my ($self, $headers_string, $body) = @_;
333              
334             my $res = $headers_string;
335              
336             if ($body) {
337             $self->chomp_line(\$body);
338             $res .= "Content-Length: " . length($body) . "\r\n\r\n$body";
339             } else {
340             $res .= "\r\n";
341             }
342              
343             my $sock = $self->{server}->{client} or die "Could not find client socket";
344             $sock->write("$res");
345              
346             $self->log(4, ">>$res\n");
347             }
348              
349             sub rewrite_transport_response {
350             my ($self, $session, $respref) = @_;
351             return unless $respref && $$respref;
352            
353             my $old_resp = $$respref;
354             $self->log(3, "transport response: $old_resp");
355            
356             my $client_port_start = $session->client_port_start;
357             my $client_port_end = $session->client_port_end;
358            
359             return unless $client_port_start && $client_port_end;
360            
361             my $port_range = "${client_port_start}-$client_port_end";
362             $$respref =~ s/client_port=((?:\d+)(?:-(?:\d+)))?/client_port=$port_range/ims;
363             $self->log(3, "rewriting transport response:\n old: $old_resp\n new: $$respref");
364             }
365              
366             # clean up stuff!
367             sub post_client_connection_hook {
368             my $self = shift;
369            
370             $self->log(3, "client connection closed");
371            
372             my $session = $self->{server}{session};
373             if ($session) {
374             delete $self->{server}{session};
375             }
376             }
377              
378             #####
379              
380             sub return_status {
381             my ($self, $code, $msg) = @_;
382             print STDOUT "$code $msg\r\n";
383             $self->log(3, "Returning status $code $msg");
384             }
385              
386             sub chomp_line {
387             my ($self, $lineref) = @_;
388             $$lineref =~ s/([\r\n]+)$//sm;
389             }
390              
391             sub default_values {
392             return {
393             proto => 'tcp',
394             listen => 3,
395             port => 554,
396             no_client_stdout => 1,
397             }
398             }
399              
400             sub options {
401             my $self = shift;
402             my $prop = $self->{'server'};
403             my $template = shift;
404              
405             ### setup options in the parent classes
406             $self->SUPER::options($template);
407            
408            
409             ### rtsp client args
410             my $client = $prop->{rtsp_client}
411             or croak "No rtsp_client definition specified";
412            
413             $template->{rtsp_client} = \ $prop->{rtsp_client};
414            
415            
416             ### transport class
417             my $tc = $prop->{'transport_handler_class'} || 'RTP';
418             $tc = "RTSP::Proxy::Transport::$tc";
419             eval "use $tc; 1;" or die $@;
420            
421             $prop->{'transport_handler_class'} = $tc;
422             $template->{'transport_handler_class'} = \ $prop->{'transport_handler_class'};
423            
424             my $transport_handler = $prop->{transport_handler} || {};
425             $prop->{transport_handler} = $transport_handler;
426             $template->{transport_handler} = \ $prop->{transport_handler};
427             }
428              
429             __PACKAGE__->meta->make_immutable(inline_constructor => 0);
430              
431             __END__
432              
433              
434              
435              
436             =head1 SEE ALSO
437              
438             L<RTSP::Client>
439              
440             =head1 AUTHOR
441              
442             Mischa Spiegelmock, E<lt>revmischa@cpan.orgE<gt>
443              
444             =head1 COPYRIGHT AND LICENSE
445              
446             Copyright (C) 2010 by Mischa Spiegelmock
447              
448             This library is free software; you can redistribute it and/or modify
449             it under the same terms as Perl itself, either Perl version 5.10.0 or,
450             at your option, any later version of Perl 5 you may have available.
451              
452             =head1 GUINEAS
453              
454             SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS 8DDDDDDDDDDDDDDDDDDDDDDDD horseBERD
455              
456             =cut