fhem-mirror/FHEM/lib/UPnP/Common.pm
rleins f59bc2230b UPnP: Added SVN-Id for versionlisting
git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@10759 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2016-02-07 20:00:12 +00:00

853 lines
19 KiB
Perl
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

########################################################################################
#
# 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 <http://www.gnu.org/licenses/>.
#
########################################################################################
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<UPnP::ControlPoint> and C<UPnP::DeviceManager>
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<http://www.upnp.org>.
The C<SOAP::Lite> module can be found at L<http://www.soaplite.com>.
UPnP implementations in other languages include the UPnP SDK for Linux
(L<http://upnp.sourceforge.net/>), Cyberlink for Java
(L<http://www.cybergarage.org/net/upnp/java/index.html>) and C++
(L<http://sourceforge.net/projects/clinkcc/>), and the Microsoft UPnP
SDK
(L<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/upnp/upnp/universal_plug_and_play_start_page.asp>).
=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: