Common.pm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. ########################################################################################
  2. #
  3. # Common.pm
  4. #
  5. # $Id: Common.pm 10759 2016-02-07 20:00:12Z rleins $
  6. #
  7. # Now (in this version) part of Fhem.
  8. #
  9. # Fhem is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation, either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # Fhem is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  21. #
  22. ########################################################################################
  23. package UPnP::Common;
  24. use 5.006;
  25. use strict;
  26. use warnings;
  27. use HTTP::Headers;
  28. use IO::Socket;
  29. use vars qw(@EXPORT $VERSION @ISA $AUTOLOAD);
  30. require Exporter;
  31. our @ISA = qw(Exporter);
  32. our $VERSION = '0.03';
  33. my %XP_CONSTANTS = (
  34. SSDP_IP => "239.255.255.250",
  35. SSDP_PORT => 1900,
  36. CRLF => "\015\012",
  37. IP_LEVEL => getprotobyname('ip') || 0,
  38. );
  39. #ALW - Changed from 'MSWin32' => [3,5],
  40. my @MD_CONSTANTS = qw(IP_MULTICAST_TTL IP_ADD_MEMBERSHIP);
  41. my %MD_CONSTANT_VALUES = (
  42. 'MSWin32' => [10,12],
  43. 'cygwin' => [3,5],
  44. 'darwin' => [10,12],
  45. 'linux' => [33,35],
  46. 'default' => [33,35],
  47. );
  48. @EXPORT = qw();
  49. use constant PROBE_IP => "239.255.255.251";
  50. use constant PROBE_PORT => 8950;
  51. my $ref = $MD_CONSTANT_VALUES{$^O};
  52. if (!defined($ref)) {
  53. $ref = $MD_CONSTANT_VALUES{default};
  54. }
  55. my $consts;
  56. for my $name (keys %XP_CONSTANTS) {
  57. $consts .= "use constant $name => \'" . $XP_CONSTANTS{$name} . "\';\n";
  58. }
  59. for my $index (0..$#MD_CONSTANTS) {
  60. my $name = $MD_CONSTANTS[$index];
  61. $consts .= "use constant $name => \'" . $ref->[$index] . "\';\n";
  62. }
  63. #warn $consts; # for development
  64. eval $consts;
  65. push @EXPORT, (keys %XP_CONSTANTS, @MD_CONSTANTS);
  66. #findLocalIP();
  67. my %typeMap = (
  68. 'ui1' => 'int',
  69. 'ui2' => 'int',
  70. 'ui4' => 'int',
  71. 'i1' => 'int',
  72. 'i2' => 'int',
  73. 'i4' => 'int',
  74. 'int' => 'int',
  75. 'r4' => 'float',
  76. 'r8' => 'float',
  77. 'number' => 'float',
  78. 'fixed' => 'float',
  79. 'float' => 'float',
  80. 'char' => 'string',
  81. 'string' => 'string',
  82. 'date' => 'timeInstant',
  83. 'dateTime.tz' => 'timeInstant',
  84. 'time' => 'timeInstant',
  85. 'time.tz' => 'timeInstant',
  86. 'boolean' => 'boolean',
  87. 'bin.base64' => 'base64Binary',
  88. 'bin.hex' => 'hexBinary',
  89. 'uri' => 'uriReference',
  90. 'uuid' => 'string',
  91. );
  92. BEGIN {
  93. use SOAP::Lite;
  94. $SOAP::Constants::DO_NOT_USE_XML_PARSER = 1;
  95. }
  96. sub getLocalIP {
  97. if (defined $UPnP::Common::LocalIP) {
  98. return $UPnP::Common::LocalIP;
  99. }
  100. my $probeSocket = IO::Socket::INET->new(Proto => 'udp',
  101. Reuse => 1);
  102. my $listenSocket = IO::Socket::INET->new(Proto => 'udp',
  103. Reuse => 1,
  104. LocalPort => PROBE_PORT);
  105. my $ip_mreq = inet_aton(PROBE_IP) . INADDR_ANY;
  106. setsockopt($listenSocket,
  107. getprotobyname('ip'),
  108. $ref->[1],
  109. $ip_mreq);
  110. my $destaddr = sockaddr_in(PROBE_PORT, inet_aton(PROBE_IP));
  111. send($probeSocket, "Test", 0, $destaddr);
  112. my $buf = '';
  113. my $peer = recv($listenSocket, $buf, 2048, 0);
  114. my ($port, $addr) = sockaddr_in($peer);
  115. $probeSocket->close;
  116. $listenSocket->close;
  117. setLocalIP($addr);
  118. return $UPnP::Common::LocalIP;
  119. }
  120. sub setLocalIP {
  121. my ($addr) = @_;
  122. $UPnP::Common::LocalIP = inet_ntoa($addr);
  123. }
  124. sub parseHTTPHeaders {
  125. my $buf = shift;
  126. my $headers = HTTP::Headers->new;
  127. # Header parsing code borrowed from HTTP::Daemon
  128. my($key, $val);
  129. HEADER:
  130. while ($buf =~ s/^([^\012]*)\012//) {
  131. $_ = $1;
  132. s/\015$//;
  133. if (/^([^:\s]+)\s*:\s*(.*)/) {
  134. $headers->push_header($key => $val) if $key;
  135. ($key, $val) = ($1, $2);
  136. }
  137. elsif (/^\s+(.*)/) {
  138. $val .= " $1";
  139. }
  140. else {
  141. last HEADER;
  142. }
  143. }
  144. $headers->push_header($key => $val) if $key;
  145. return $headers;
  146. }
  147. sub UPnPToSOAPType {
  148. my $upnpType = shift;
  149. return $typeMap{$upnpType};
  150. }
  151. # ----------------------------------------------------------------------
  152. package UPnP::Common::DeviceLoader;
  153. use strict;
  154. sub new {
  155. my $self = shift;
  156. my $class = ref($self) || $self;
  157. return bless {
  158. _parser => UPnP::Common::Parser->new,
  159. }, $class;
  160. }
  161. sub parser {
  162. my $self = shift;
  163. return $self->{_parser};
  164. }
  165. sub parseServiceElement {
  166. my $self = shift;
  167. my $element = shift;
  168. my($name, $attrs, $children) = @$element;
  169. my $service = $self->newService(%{$_[1]});
  170. for my $childElement (@$children) {
  171. my $childName = $childElement->[0];
  172. if (UPnP::Common::Service::isProperty($childName)) {
  173. my $value = $childElement->[2];
  174. $service->$childName($value);
  175. }
  176. }
  177. return $service;
  178. }
  179. sub parseDeviceElement {
  180. my $self = shift;
  181. my $element = shift;
  182. my $parent = shift;
  183. my($name, $attrs, $children) = @$element;
  184. my $device = $self->newDevice(%{$_[0]});
  185. $device->parent($parent);
  186. for my $childElement (@$children) {
  187. my $childName = $childElement->[0];
  188. if ($childName eq 'deviceList') {
  189. my $childDevices = $childElement->[2];
  190. next if (ref $childDevices ne "ARRAY");
  191. for my $deviceElement (@$childDevices) {
  192. my $childDevice = $self->parseDeviceElement($deviceElement,
  193. $device,
  194. @_);
  195. if ($childDevice) {
  196. $device->addChild($childDevice);
  197. }
  198. }
  199. }
  200. elsif ($childName eq 'serviceList') {
  201. my $services = $childElement->[2];
  202. next if (ref $services ne "ARRAY");
  203. for my $serviceElement (@$services) {
  204. my $service = $self->parseServiceElement($serviceElement,
  205. @_);
  206. if ($service) {
  207. $device->addService($service);
  208. }
  209. }
  210. }
  211. elsif (UPnP::Common::Device::isProperty($childName)) {
  212. my $value = $childElement->[2];
  213. $device->$childName($value);
  214. }
  215. }
  216. return $device;
  217. }
  218. sub parseDeviceDescription {
  219. my $self = shift;
  220. my $description = shift;
  221. my ($base, $device);
  222. my $parser = $self->parser;
  223. my $element = $parser->parse($description);
  224. if (defined($element) && ref $element eq 'ARRAY') {
  225. my($name, $attrs, $children) = @$element;
  226. for my $child (@$children) {
  227. my ($childName) = @$child;
  228. if ($childName eq 'URLBase') {
  229. $base = $child->[2];
  230. }
  231. elsif ($childName eq 'device') {
  232. $device = $self->parseDeviceElement($child,
  233. undef,
  234. @_);
  235. }
  236. }
  237. }
  238. return ($device, $base);
  239. }
  240. # ----------------------------------------------------------------------
  241. package UPnP::Common::Device;
  242. use strict;
  243. use Carp;
  244. use Scalar::Util qw(weaken);
  245. use vars qw($AUTOLOAD %deviceProperties);
  246. for my $prop (qw(deviceType friendlyName manufacturer
  247. manufacturerURL modelDescription modelName
  248. modelNumber modelURL serialNumber UDN
  249. presentationURL UPC location)) {
  250. $deviceProperties{$prop}++;
  251. }
  252. sub new {
  253. my $self = shift;
  254. my $class = ref($self) || $self;
  255. my %args = @_;
  256. $self = bless {}, $class;
  257. if ($args{Location}) {
  258. $self->location($args{Location});
  259. }
  260. return $self;
  261. }
  262. sub addChild {
  263. my $self = shift;
  264. my $child = shift;
  265. push @{$self->{_children}}, $child;
  266. }
  267. sub addService {
  268. my $self = shift;
  269. my $service = shift;
  270. push @{$self->{_services}}, $service;
  271. }
  272. sub parent {
  273. my $self = shift;
  274. if (@_) {
  275. $self->{_parent} = shift;
  276. weaken($self->{_parent});
  277. }
  278. return $self->{_parent};
  279. }
  280. sub children {
  281. my $self = shift;
  282. if (ref $self->{_children}) {
  283. return @{$self->{_children}};
  284. }
  285. return ();
  286. }
  287. sub services {
  288. my $self = shift;
  289. if (ref $self->{_services}) {
  290. return @{$self->{_services}};
  291. }
  292. return ();
  293. }
  294. sub getService {
  295. my $self = shift;
  296. my $id = shift;
  297. for my $service ($self->services) {
  298. if ($id &&
  299. ($id eq $service->serviceId) ||
  300. ($id eq $service->serviceType)) {
  301. return $service;
  302. }
  303. }
  304. return undef;
  305. }
  306. sub isProperty {
  307. my $prop = shift;
  308. return $deviceProperties{$prop};
  309. }
  310. sub AUTOLOAD {
  311. my $self = shift;
  312. my $attr = $AUTOLOAD;
  313. $attr =~ s/.*:://;
  314. return if $attr eq 'DESTROY';
  315. croak "invalid attribute method: ->$attr()" unless $deviceProperties{$attr};
  316. $self->{uc $attr} = shift if @_;
  317. return $self->{uc $attr};
  318. }
  319. # ----------------------------------------------------------------------
  320. package UPnP::Common::Service;
  321. use strict;
  322. use SOAP::Lite;
  323. use Carp;
  324. use vars qw($AUTOLOAD %serviceProperties);
  325. for my $prop (qw(serviceType serviceId SCPDURL controlURL
  326. eventSubURL base)) {
  327. $serviceProperties{$prop}++;
  328. }
  329. sub new {
  330. my $self = shift;
  331. my $class = ref($self) || $self;
  332. return bless {}, $class;
  333. }
  334. sub AUTOLOAD {
  335. my $self = shift;
  336. my $attr = $AUTOLOAD;
  337. $attr =~ s/.*:://;
  338. return if $attr eq 'DESTROY';
  339. croak "invalid attribute method: ->$attr()" unless $serviceProperties{$attr};
  340. $self->{uc $attr} = shift if @_;
  341. return $self->{uc $attr};
  342. }
  343. sub isProperty {
  344. my $prop = shift;
  345. return $serviceProperties{$prop};
  346. }
  347. sub addAction {
  348. my $self = shift;
  349. my $action = shift;
  350. $self->{_actions}->{$action->name} = $action;
  351. }
  352. sub addStateVariable {
  353. my $self = shift;
  354. my $var = shift;
  355. $self->{_stateVariables}->{$var->name} = $var;
  356. }
  357. sub actions {
  358. my $self = shift;
  359. $self->_loadDescription;
  360. if (defined($self->{_actions})) {
  361. return values %{$self->{_actions}};
  362. }
  363. return ();
  364. }
  365. sub getAction {
  366. my $self = shift;
  367. my $name = shift;
  368. $self->_loadDescription;
  369. if (defined($self->{_actions})) {
  370. return $self->{_actions}->{$name};
  371. }
  372. return undef;
  373. }
  374. sub stateVariables {
  375. my $self = shift;
  376. $self->_loadDescription;
  377. if (defined($self->{_stateVariables})) {
  378. return values %{$self->{_stateVariables}};
  379. }
  380. return ();
  381. }
  382. sub getStateVariable {
  383. my $self = shift;
  384. my $name = shift;
  385. $self->_loadDescription;
  386. if (defined($self->{_stateVariables})) {
  387. return $self->{_stateVariables}->{$name};
  388. }
  389. return undef;
  390. }
  391. sub getArgumentType {
  392. my $self = shift;
  393. my $arg = shift;
  394. $self->_loadDescription;
  395. my $var = $self->getStateVariable($arg->relatedStateVariable);
  396. if ($var) {
  397. return $var->SOAPType;
  398. }
  399. return undef;
  400. }
  401. sub _parseArgumentList {
  402. my $self = shift;
  403. my $list = shift;
  404. my $action = shift;
  405. return if (! ref $list);
  406. for my $argumentElement (@$list) {
  407. my($name, $attrs, $children) = @$argumentElement;
  408. if ($name eq 'argument') {
  409. my $argument = UPnP::Common::Argument->new;
  410. for my $argumentChild (@$children) {
  411. my ($childName) = @$argumentChild;
  412. if ($childName eq 'name') {
  413. $argument->name($argumentChild->[2]);
  414. }
  415. elsif ($childName eq 'direction') {
  416. my $direction = $argumentChild->[2];
  417. if ($direction eq 'in') {
  418. $action->addInArgument($argument);
  419. }
  420. elsif ($direction eq 'out') {
  421. $action->addOutArgument($argument);
  422. }
  423. }
  424. elsif ($childName eq 'relatedStateVariable') {
  425. $argument->relatedStateVariable($argumentChild->[2]);
  426. }
  427. elsif ($childName eq 'retval') {
  428. $action->retval($argument);
  429. }
  430. }
  431. }
  432. }
  433. }
  434. sub _parseActionList {
  435. my $self = shift;
  436. my $list = shift;
  437. for my $actionElement (@$list) {
  438. my($name, $attrs, $children) = @$actionElement;
  439. if ($name eq 'action') {
  440. my $action = UPnP::Common::Action->new;
  441. for my $actionChild (@$children) {
  442. my ($childName) = @$actionChild;
  443. if ($childName eq 'name') {
  444. $action->name($actionChild->[2]);
  445. }
  446. elsif ($childName eq 'argumentList') {
  447. $self->_parseArgumentList($actionChild->[2],
  448. $action);
  449. }
  450. }
  451. $self->addAction($action);
  452. }
  453. }
  454. }
  455. sub _parseStateTable {
  456. my $self = shift;
  457. my $list = shift;
  458. for my $varElement (@$list) {
  459. my($name, $attrs, $children) = @$varElement;
  460. if ($name eq 'stateVariable') {
  461. my $var = UPnP::Common::StateVariable->new(exists $attrs->{sendEvents} && ($attrs->{sendEvents} eq 'yes'));
  462. for my $varChild (@$children) {
  463. my ($childName) = @$varChild;
  464. if ($childName eq 'name') {
  465. $var->name($varChild->[2]);
  466. }
  467. elsif ($childName eq 'dataType') {
  468. $var->type($varChild->[2]);
  469. }
  470. }
  471. $self->addStateVariable($var);
  472. }
  473. }
  474. }
  475. sub parseServiceDescription {
  476. my $self = shift;
  477. my $parser = shift;
  478. my $description = shift;
  479. my $element = $parser->parse($description);
  480. if (defined($element) && ref $element eq 'ARRAY') {
  481. my($name, $attrs, $children) = @$element;
  482. for my $child (@$children) {
  483. my ($childName) = @$child;
  484. if ($childName eq 'actionList') {
  485. $self->_parseActionList($child->[2]);
  486. }
  487. elsif ($childName eq 'serviceStateTable') {
  488. $self->_parseStateTable($child->[2]);
  489. }
  490. }
  491. }
  492. else {
  493. carp("Malformed SCPD document");
  494. }
  495. }
  496. # ----------------------------------------------------------------------
  497. package UPnP::Common::Action;
  498. use strict;
  499. use Carp;
  500. use vars qw($AUTOLOAD %actionProperties);
  501. for my $prop (qw(name retval)) {
  502. $actionProperties{$prop}++;
  503. }
  504. sub new {
  505. return bless {}, shift;
  506. }
  507. sub AUTOLOAD {
  508. my $self = shift;
  509. my $attr = $AUTOLOAD;
  510. $attr =~ s/.*:://;
  511. return if $attr eq 'DESTROY';
  512. croak "invalid attribute method: ->$attr()" unless $actionProperties{$attr};
  513. $self->{uc $attr} = shift if @_;
  514. return $self->{uc $attr};
  515. }
  516. sub addInArgument {
  517. my $self = shift;
  518. my $argument = shift;
  519. push @{$self->{_inArguments}}, $argument;
  520. }
  521. sub addOutArgument {
  522. my $self = shift;
  523. my $argument = shift;
  524. push @{$self->{_outArguments}}, $argument;
  525. }
  526. sub inArguments {
  527. my $self = shift;
  528. if (defined $self->{_inArguments}) {
  529. return @{$self->{_inArguments}};
  530. }
  531. return ();
  532. }
  533. sub outArguments {
  534. my $self = shift;
  535. if (defined $self->{_outArguments}) {
  536. return @{$self->{_outArguments}};
  537. }
  538. return ();
  539. }
  540. sub arguments {
  541. my $self = shift;
  542. return ($self->inArguments, $self->outArguments);
  543. }
  544. # ----------------------------------------------------------------------
  545. package UPnP::Common::Argument;
  546. use strict;
  547. use Carp;
  548. use vars qw($AUTOLOAD %argumentProperties);
  549. for my $prop (qw(name relatedStateVariable)) {
  550. $argumentProperties{$prop}++;
  551. }
  552. sub new {
  553. return bless {}, shift;
  554. }
  555. sub AUTOLOAD {
  556. my $self = shift;
  557. my $attr = $AUTOLOAD;
  558. $attr =~ s/.*:://;
  559. return if $attr eq 'DESTROY';
  560. croak "invalid attribute method: ->$attr()" unless $argumentProperties{$attr};
  561. $self->{uc $attr} = shift if @_;
  562. return $self->{uc $attr};
  563. }
  564. # ----------------------------------------------------------------------
  565. package UPnP::Common::StateVariable;
  566. use strict;
  567. use Carp;
  568. use vars qw($AUTOLOAD %varProperties);
  569. for my $prop (qw(name type evented)) {
  570. $varProperties{$prop}++;
  571. }
  572. sub new {
  573. my $self = bless {}, shift;
  574. $self->evented(shift);
  575. return $self;
  576. }
  577. sub SOAPType {
  578. my $self = shift;
  579. return UPnP::Common::UPnPToSOAPType($self->type);
  580. }
  581. sub AUTOLOAD {
  582. my $self = shift;
  583. my $attr = $AUTOLOAD;
  584. $attr =~ s/.*:://;
  585. return if $attr eq 'DESTROY';
  586. croak "invalid attribute method: ->$attr()" unless $varProperties{$attr};
  587. $self->{uc $attr} = shift if @_;
  588. return $self->{uc $attr};
  589. }
  590. # ----------------------------------------------------------------------
  591. package UPnP::Common::Parser;
  592. use XML::Parser::Lite;
  593. # Parser code borrowed from SOAP::Lite. This package uses the
  594. # event-driven XML::Parser::Lite parser to construct a nested data
  595. # structure - a poor man's DOM. Each XML element in the data structure
  596. # is represented by an array ref, with the values (listed by subscript
  597. # below) corresponding with:
  598. # 0 - The element name.
  599. # 1 - A hash ref representing the element attributes.
  600. # 2 - An array ref holding either child elements or concatenated
  601. # character data.
  602. sub new {
  603. my $class = shift;
  604. return bless { _parser => XML::Parser::Lite->new }, $class;
  605. }
  606. sub parse {
  607. my $self = shift;
  608. my $parser = $self->{_parser};
  609. $parser->setHandlers(Final => sub { shift; $self->final(@_) },
  610. Start => sub { shift; $self->start(@_) },
  611. End => sub { shift; $self->end(@_) },
  612. Char => sub { shift; $self->char(@_) },);
  613. $parser->parse(shift);
  614. }
  615. sub final {
  616. my $self = shift;
  617. my $parser = $self->{_parser};
  618. # clean handlers, otherwise ControlPoint::Parser won't be deleted:
  619. # it refers to XML::Parser which refers to subs from ControlPoint::Parser
  620. undef $self->{_values};
  621. $parser->setHandlers(Final => undef,
  622. Start => undef,
  623. End => undef,
  624. Char => undef,);
  625. $self->{_done};
  626. }
  627. sub start { push @{shift->{_values}}, [shift, {@_}] }
  628. sub char { push @{shift->{_values}->[-1]->[3]}, shift }
  629. sub end {
  630. my $self = shift;
  631. my $done = pop @{$self->{_values}};
  632. $done->[2] = defined $done->[3] ? join('',@{$done->[3]}) : '' unless ref $done->[2];
  633. undef $done->[3];
  634. @{$self->{_values}} ? (push @{$self->{_values}->[-1]->[2]}, $done)
  635. : ($self->{_done} = $done);
  636. }
  637. 1;
  638. __END__
  639. =head1 NAME
  640. UPnP::Common - Internal modules and methods for the UPnP
  641. implementation. The C<UPnP::ControlPoint> and C<UPnP::DeviceManager>
  642. modules should be used.
  643. =head1 DESCRIPTION
  644. Part of the Perl UPnP implementation suite.
  645. =head1 SEE ALSO
  646. UPnP documentation and resources can be found at L<http://www.upnp.org>.
  647. The C<SOAP::Lite> module can be found at L<http://www.soaplite.com>.
  648. UPnP implementations in other languages include the UPnP SDK for Linux
  649. (L<http://upnp.sourceforge.net/>), Cyberlink for Java
  650. (L<http://www.cybergarage.org/net/upnp/java/index.html>) and C++
  651. (L<http://sourceforge.net/projects/clinkcc/>), and the Microsoft UPnP
  652. SDK
  653. (L<http://msdn.microsoft.com/library/default.asp?url=/library/en-us/upnp/upnp/universal_plug_and_play_start_page.asp>).
  654. =head1 AUTHOR
  655. Vidur Apparao (vidurapparao@users.sourceforge.net)
  656. =head1 COPYRIGHT AND LICENSE
  657. Copyright (C) 2004 by Vidur Apparao
  658. This library is free software; you can redistribute it and/or modify
  659. it under the same terms as Perl itself, either Perl version 5.8 or,
  660. at your option, any later version of Perl 5 you may have available.
  661. =cut
  662. # Local Variables:
  663. # tab-width:4
  664. # indent-tabs-mode:t
  665. # End: