######################################################################################## # # Common.pm # # $Id$ # # Now (in this version) part of Fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ######################################################################################## package UPnP::Common; use 5.006; use strict; use warnings; use HTTP::Headers; use IO::Socket; use vars qw(@EXPORT $VERSION @ISA $AUTOLOAD); require Exporter; our @ISA = qw(Exporter); our $VERSION = '0.03'; my %XP_CONSTANTS = ( SSDP_IP => "239.255.255.250", SSDP_PORT => 1900, CRLF => "\015\012", IP_LEVEL => getprotobyname('ip') || 0, ); #ALW - Changed from 'MSWin32' => [3,5], my @MD_CONSTANTS = qw(IP_MULTICAST_TTL IP_ADD_MEMBERSHIP); my %MD_CONSTANT_VALUES = ( 'MSWin32' => [10,12], 'cygwin' => [3,5], 'darwin' => [10,12], 'linux' => [33,35], 'default' => [33,35], ); @EXPORT = qw(); use constant PROBE_IP => "239.255.255.251"; use constant PROBE_PORT => 8950; my $ref = $MD_CONSTANT_VALUES{$^O}; if (!defined($ref)) { $ref = $MD_CONSTANT_VALUES{default}; } my $consts; for my $name (keys %XP_CONSTANTS) { $consts .= "use constant $name => \'" . $XP_CONSTANTS{$name} . "\';\n"; } for my $index (0..$#MD_CONSTANTS) { my $name = $MD_CONSTANTS[$index]; $consts .= "use constant $name => \'" . $ref->[$index] . "\';\n"; } #warn $consts; # for development eval $consts; push @EXPORT, (keys %XP_CONSTANTS, @MD_CONSTANTS); #findLocalIP(); my %typeMap = ( 'ui1' => 'int', 'ui2' => 'int', 'ui4' => 'int', 'i1' => 'int', 'i2' => 'int', 'i4' => 'int', 'int' => 'int', 'r4' => 'float', 'r8' => 'float', 'number' => 'float', 'fixed' => 'float', 'float' => 'float', 'char' => 'string', 'string' => 'string', 'date' => 'timeInstant', 'dateTime.tz' => 'timeInstant', 'time' => 'timeInstant', 'time.tz' => 'timeInstant', 'boolean' => 'boolean', 'bin.base64' => 'base64Binary', 'bin.hex' => 'hexBinary', 'uri' => 'uriReference', 'uuid' => 'string', ); BEGIN { use SOAP::Lite; $SOAP::Constants::DO_NOT_USE_XML_PARSER = 1; } sub getLocalIP { if (defined $UPnP::Common::LocalIP) { return $UPnP::Common::LocalIP; } my $probeSocket = IO::Socket::INET->new(Proto => 'udp', Reuse => 1); my $listenSocket = IO::Socket::INET->new(Proto => 'udp', Reuse => 1, LocalPort => PROBE_PORT); my $ip_mreq = inet_aton(PROBE_IP) . INADDR_ANY; setsockopt($listenSocket, getprotobyname('ip'), $ref->[1], $ip_mreq); my $destaddr = sockaddr_in(PROBE_PORT, inet_aton(PROBE_IP)); send($probeSocket, "Test", 0, $destaddr); my $buf = ''; my $peer = recv($listenSocket, $buf, 2048, 0); my ($port, $addr) = sockaddr_in($peer); $probeSocket->close; $listenSocket->close; setLocalIP($addr); return $UPnP::Common::LocalIP; } sub setLocalIP { my ($addr) = @_; $UPnP::Common::LocalIP = inet_ntoa($addr); } sub parseHTTPHeaders { my $buf = shift; my $headers = HTTP::Headers->new; # Header parsing code borrowed from HTTP::Daemon my($key, $val); HEADER: while ($buf =~ s/^([^\012]*)\012//) { $_ = $1; s/\015$//; if (/^([^:\s]+)\s*:\s*(.*)/) { $headers->push_header($key => $val) if $key; ($key, $val) = ($1, $2); } elsif (/^\s+(.*)/) { $val .= " $1"; } else { last HEADER; } } $headers->push_header($key => $val) if $key; return $headers; } sub UPnPToSOAPType { my $upnpType = shift; return $typeMap{$upnpType}; } # ---------------------------------------------------------------------- package UPnP::Common::DeviceLoader; use strict; sub new { my $self = shift; my $class = ref($self) || $self; return bless { _parser => UPnP::Common::Parser->new, }, $class; } sub parser { my $self = shift; return $self->{_parser}; } sub parseServiceElement { my $self = shift; my $element = shift; my($name, $attrs, $children) = @$element; my $service = $self->newService(%{$_[1]}); for my $childElement (@$children) { my $childName = $childElement->[0]; if (UPnP::Common::Service::isProperty($childName)) { my $value = $childElement->[2]; $service->$childName($value); } } return $service; } sub parseDeviceElement { my $self = shift; my $element = shift; my $parent = shift; my($name, $attrs, $children) = @$element; my $device = $self->newDevice(%{$_[0]}); $device->parent($parent); for my $childElement (@$children) { my $childName = $childElement->[0]; if ($childName eq 'deviceList') { my $childDevices = $childElement->[2]; next if (ref $childDevices ne "ARRAY"); for my $deviceElement (@$childDevices) { my $childDevice = $self->parseDeviceElement($deviceElement, $device, @_); if ($childDevice) { $device->addChild($childDevice); } } } elsif ($childName eq 'serviceList') { my $services = $childElement->[2]; next if (ref $services ne "ARRAY"); for my $serviceElement (@$services) { my $service = $self->parseServiceElement($serviceElement, @_); if ($service) { $device->addService($service); } } } elsif (UPnP::Common::Device::isProperty($childName)) { my $value = $childElement->[2]; $device->$childName($value); } } return $device; } sub parseDeviceDescription { my $self = shift; my $description = shift; my ($base, $device); my $parser = $self->parser; my $element = $parser->parse($description); if (defined($element) && ref $element eq 'ARRAY') { my($name, $attrs, $children) = @$element; for my $child (@$children) { my ($childName) = @$child; if ($childName eq 'URLBase') { $base = $child->[2]; } elsif ($childName eq 'device') { $device = $self->parseDeviceElement($child, undef, @_); } } } return ($device, $base); } # ---------------------------------------------------------------------- package UPnP::Common::Device; use strict; use Carp; use Scalar::Util qw(weaken); use vars qw($AUTOLOAD %deviceProperties); for my $prop (qw(deviceType friendlyName manufacturer manufacturerURL modelDescription modelName modelNumber modelURL serialNumber UDN presentationURL UPC location)) { $deviceProperties{$prop}++; } sub new { my $self = shift; my $class = ref($self) || $self; my %args = @_; $self = bless {}, $class; if ($args{Location}) { $self->location($args{Location}); } return $self; } sub addChild { my $self = shift; my $child = shift; push @{$self->{_children}}, $child; } sub addService { my $self = shift; my $service = shift; push @{$self->{_services}}, $service; } sub parent { my $self = shift; if (@_) { $self->{_parent} = shift; weaken($self->{_parent}); } return $self->{_parent}; } sub children { my $self = shift; if (ref $self->{_children}) { return @{$self->{_children}}; } return (); } sub services { my $self = shift; if (ref $self->{_services}) { return @{$self->{_services}}; } return (); } sub getService { my $self = shift; my $id = shift; for my $service ($self->services) { if ($id && ($id eq $service->serviceId) || ($id eq $service->serviceType)) { return $service; } } return undef; } sub isProperty { my $prop = shift; return $deviceProperties{$prop}; } sub AUTOLOAD { my $self = shift; my $attr = $AUTOLOAD; $attr =~ s/.*:://; return if $attr eq 'DESTROY'; croak "invalid attribute method: ->$attr()" unless $deviceProperties{$attr}; $self->{uc $attr} = shift if @_; return $self->{uc $attr}; } # ---------------------------------------------------------------------- package UPnP::Common::Service; use strict; use SOAP::Lite; use Carp; use vars qw($AUTOLOAD %serviceProperties); for my $prop (qw(serviceType serviceId SCPDURL controlURL eventSubURL base)) { $serviceProperties{$prop}++; } sub new { my $self = shift; my $class = ref($self) || $self; return bless {}, $class; } sub AUTOLOAD { my $self = shift; my $attr = $AUTOLOAD; $attr =~ s/.*:://; return if $attr eq 'DESTROY'; croak "invalid attribute method: ->$attr()" unless $serviceProperties{$attr}; $self->{uc $attr} = shift if @_; return $self->{uc $attr}; } sub isProperty { my $prop = shift; return $serviceProperties{$prop}; } sub addAction { my $self = shift; my $action = shift; $self->{_actions}->{$action->name} = $action; } sub addStateVariable { my $self = shift; my $var = shift; $self->{_stateVariables}->{$var->name} = $var; } sub actions { my $self = shift; $self->_loadDescription; if (defined($self->{_actions})) { return values %{$self->{_actions}}; } return (); } sub getAction { my $self = shift; my $name = shift; $self->_loadDescription; if (defined($self->{_actions})) { return $self->{_actions}->{$name}; } return undef; } sub stateVariables { my $self = shift; $self->_loadDescription; if (defined($self->{_stateVariables})) { return values %{$self->{_stateVariables}}; } return (); } sub getStateVariable { my $self = shift; my $name = shift; $self->_loadDescription; if (defined($self->{_stateVariables})) { return $self->{_stateVariables}->{$name}; } return undef; } sub getArgumentType { my $self = shift; my $arg = shift; $self->_loadDescription; my $var = $self->getStateVariable($arg->relatedStateVariable); if ($var) { return $var->SOAPType; } return undef; } sub _parseArgumentList { my $self = shift; my $list = shift; my $action = shift; return if (! ref $list); for my $argumentElement (@$list) { my($name, $attrs, $children) = @$argumentElement; if ($name eq 'argument') { my $argument = UPnP::Common::Argument->new; for my $argumentChild (@$children) { my ($childName) = @$argumentChild; if ($childName eq 'name') { $argument->name($argumentChild->[2]); } elsif ($childName eq 'direction') { my $direction = $argumentChild->[2]; if ($direction eq 'in') { $action->addInArgument($argument); } elsif ($direction eq 'out') { $action->addOutArgument($argument); } } elsif ($childName eq 'relatedStateVariable') { $argument->relatedStateVariable($argumentChild->[2]); } elsif ($childName eq 'retval') { $action->retval($argument); } } } } } sub _parseActionList { my $self = shift; my $list = shift; for my $actionElement (@$list) { my($name, $attrs, $children) = @$actionElement; if ($name eq 'action') { my $action = UPnP::Common::Action->new; for my $actionChild (@$children) { my ($childName) = @$actionChild; if ($childName eq 'name') { $action->name($actionChild->[2]); } elsif ($childName eq 'argumentList') { $self->_parseArgumentList($actionChild->[2], $action); } } $self->addAction($action); } } } sub _parseStateTable { my $self = shift; my $list = shift; for my $varElement (@$list) { my($name, $attrs, $children) = @$varElement; if ($name eq 'stateVariable') { my $var = UPnP::Common::StateVariable->new(exists $attrs->{sendEvents} && ($attrs->{sendEvents} eq 'yes')); for my $varChild (@$children) { my ($childName) = @$varChild; if ($childName eq 'name') { $var->name($varChild->[2]); } elsif ($childName eq 'dataType') { $var->type($varChild->[2]); } } $self->addStateVariable($var); } } } sub parseServiceDescription { my $self = shift; my $parser = shift; my $description = shift; my $element = $parser->parse($description); if (defined($element) && ref $element eq 'ARRAY') { my($name, $attrs, $children) = @$element; for my $child (@$children) { my ($childName) = @$child; if ($childName eq 'actionList') { $self->_parseActionList($child->[2]); } elsif ($childName eq 'serviceStateTable') { $self->_parseStateTable($child->[2]); } } } else { carp("Malformed SCPD document"); } } # ---------------------------------------------------------------------- package UPnP::Common::Action; use strict; use Carp; use vars qw($AUTOLOAD %actionProperties); for my $prop (qw(name retval)) { $actionProperties{$prop}++; } sub new { return bless {}, shift; } sub AUTOLOAD { my $self = shift; my $attr = $AUTOLOAD; $attr =~ s/.*:://; return if $attr eq 'DESTROY'; croak "invalid attribute method: ->$attr()" unless $actionProperties{$attr}; $self->{uc $attr} = shift if @_; return $self->{uc $attr}; } sub addInArgument { my $self = shift; my $argument = shift; push @{$self->{_inArguments}}, $argument; } sub addOutArgument { my $self = shift; my $argument = shift; push @{$self->{_outArguments}}, $argument; } sub inArguments { my $self = shift; if (defined $self->{_inArguments}) { return @{$self->{_inArguments}}; } return (); } sub outArguments { my $self = shift; if (defined $self->{_outArguments}) { return @{$self->{_outArguments}}; } return (); } sub arguments { my $self = shift; return ($self->inArguments, $self->outArguments); } # ---------------------------------------------------------------------- package UPnP::Common::Argument; use strict; use Carp; use vars qw($AUTOLOAD %argumentProperties); for my $prop (qw(name relatedStateVariable)) { $argumentProperties{$prop}++; } sub new { return bless {}, shift; } sub AUTOLOAD { my $self = shift; my $attr = $AUTOLOAD; $attr =~ s/.*:://; return if $attr eq 'DESTROY'; croak "invalid attribute method: ->$attr()" unless $argumentProperties{$attr}; $self->{uc $attr} = shift if @_; return $self->{uc $attr}; } # ---------------------------------------------------------------------- package UPnP::Common::StateVariable; use strict; use Carp; use vars qw($AUTOLOAD %varProperties); for my $prop (qw(name type evented)) { $varProperties{$prop}++; } sub new { my $self = bless {}, shift; $self->evented(shift); return $self; } sub SOAPType { my $self = shift; return UPnP::Common::UPnPToSOAPType($self->type); } sub AUTOLOAD { my $self = shift; my $attr = $AUTOLOAD; $attr =~ s/.*:://; return if $attr eq 'DESTROY'; croak "invalid attribute method: ->$attr()" unless $varProperties{$attr}; $self->{uc $attr} = shift if @_; return $self->{uc $attr}; } # ---------------------------------------------------------------------- package UPnP::Common::Parser; use XML::Parser::Lite; # Parser code borrowed from SOAP::Lite. This package uses the # event-driven XML::Parser::Lite parser to construct a nested data # structure - a poor man's DOM. Each XML element in the data structure # is represented by an array ref, with the values (listed by subscript # below) corresponding with: # 0 - The element name. # 1 - A hash ref representing the element attributes. # 2 - An array ref holding either child elements or concatenated # character data. sub new { my $class = shift; return bless { _parser => XML::Parser::Lite->new }, $class; } sub parse { my $self = shift; my $parser = $self->{_parser}; $parser->setHandlers(Final => sub { shift; $self->final(@_) }, Start => sub { shift; $self->start(@_) }, End => sub { shift; $self->end(@_) }, Char => sub { shift; $self->char(@_) },); $parser->parse(shift); } sub final { my $self = shift; my $parser = $self->{_parser}; # clean handlers, otherwise ControlPoint::Parser won't be deleted: # it refers to XML::Parser which refers to subs from ControlPoint::Parser undef $self->{_values}; $parser->setHandlers(Final => undef, Start => undef, End => undef, Char => undef,); $self->{_done}; } sub start { push @{shift->{_values}}, [shift, {@_}] } sub char { push @{shift->{_values}->[-1]->[3]}, shift } sub end { my $self = shift; my $done = pop @{$self->{_values}}; $done->[2] = defined $done->[3] ? join('',@{$done->[3]}) : '' unless ref $done->[2]; undef $done->[3]; @{$self->{_values}} ? (push @{$self->{_values}->[-1]->[2]}, $done) : ($self->{_done} = $done); } 1; __END__ =head1 NAME UPnP::Common - Internal modules and methods for the UPnP implementation. The C and C modules should be used. =head1 DESCRIPTION Part of the Perl UPnP implementation suite. =head1 SEE ALSO UPnP documentation and resources can be found at L. The C module can be found at L. UPnP implementations in other languages include the UPnP SDK for Linux (L), Cyberlink for Java (L) and C++ (L), and the Microsoft UPnP SDK (L). =head1 AUTHOR Vidur Apparao (vidurapparao@users.sourceforge.net) =head1 COPYRIGHT AND LICENSE Copyright (C) 2004 by Vidur Apparao This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8 or, at your option, any later version of Perl 5 you may have available. =cut # Local Variables: # tab-width:4 # indent-tabs-mode:t # End: