Initial commit

This commit is contained in:
2017-02-16 16:29:55 +01:00
commit c3531ce5fa
21 changed files with 1383 additions and 0 deletions

102
src/HTTPU/Endpoint.php Normal file
View File

@ -0,0 +1,102 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
class Endpoint
{
protected $socket;
/**
*
* @param string $bind The IP to bind to (0.0.0.0 for all)
* @throws EndpointException
*/
public function __construct($bind='0.0.0.0', $port=0, $reuse_port=false)
{
//Create the socket.
$socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
// Handle errors on failure
if (!$socket) {
$errno = socket_last_error();
$errmsg = socket_error($errno);
throw new EndpointException("Could not create socket: {$errmsg} ({$errno})");
}
//Set socket options.
socket_set_nonblock($socket);
socket_set_option($socket, SOL_SOCKET, SO_BROADCAST, 1);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
if ($reuse_port) {
socket_set_option($socket, SOL_SOCKET, SO_REUSEPORT, 1);
}
if(!socket_bind($socket, $bind, $port)) {
$errno = socket_last_error();
$errmsg = socket_error($errno);
throw new EndpointException("Could not bind socket to {$bind}:{$port}: {$errmsg} ({$errno})");
}
$this->socket = $socket;
}
public function __destruct()
{
if ($this->socket) {
@socket_close($this->socket);
}
}
/**
*
*
* @param Request $request The request to send
* @return Response[] The received responses if any
*/
public function send(Request $request, $wait_time=5)
{
// Get the details from the request
$buffer = $request->getBuffer();
$addr = $request->getAddress();
$port = $request->getPort();
if (false === socket_sendto($this->socket, $buffer, strlen($buffer), 0, $addr, $port)) {
$errno = socket_last_error();
$errmsg = socket_error($errno);
throw new EndpointException("Send failed: {$errmsg} ({$errno})");
}
// Wait $wait_time seconds for responses to arrive
$read_expires = microtime(true) + $wait_time;
$responses = [];
while (microtime(true) < $read_expires) {
if (socket_recvfrom($this->socket, $data, 5120, MSG_DONTWAIT, $raddr, $rport)) {
//while(is_string($data = socket_read($this->socket, 5120))) {
$response = Response::createFromString($data, $raddr);
if ($response) {
$responses[] = $response;
}
}
usleep(10000);
}
return $responses;
/*
while(socket_select($read, $write, $except, 3)) {
//Read received packets with a maximum size of 5120 bytes.
while(is_string($data = socket_read($this->socket, 5120))) {
echo "READ: {$data}\n";
return $data;
}
}
*/
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
class EndpointException extends HTTPUException
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
use NoccyLabs\UPnP\UPnPException;
class HTTPUException extends UPnPException
{
}

View File

@ -0,0 +1,28 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
class MSearchRequest extends Request
{
protected $max_wait = 0;
public function __construct($host='239.255.255.250', $port='1900')
{
parent::__construct('M-SEARCH * HTTP/1.1', $host, $port);
$this->headers['Man'] = '"ssdp:discover"';
$this->headers['MX'] = max(0,$this->max_wait);
}
public function setMaxWait($seconds)
{
$this->max_wait = $seconds;
}
public function setSearchType($search_type)
{
$this->headers['ST'] = $search_type;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
class MSearchResponse extends Response
{
public function getId()
{
return "foo";
}
}

53
src/HTTPU/Request.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
class Request
{
protected $request;
protected $host;
protected $port;
public function __construct($request_line, $host, $port)
{
$this->request = $request_line;
$this->host = $host;
$this->port = $port;
}
public function getAddress()
{
return $this->host;
}
public function getPort()
{
return $this->port;
}
public function getBuffer()
{
$this->headers['Host'] = sprintf("%s:%d", $this->host, $this->port);
$buffer = $this->request . "\r\n";
foreach ($this->headers as $key=>$value) {
$buffer.= sprintf("%s: %s\r\n", $key, $value);
}
$buffer.= "\r\n";
return $buffer;
}
public function setUserAgent($os, $os_version, $product, $product_version)
{
$this->headers['User-Agent'] = sprintf("%s/%s UPnP/1.1 %s/%s",
$os, $os_version,
$product, $product_version
);
}
}

45
src/HTTPU/Response.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
class Response
{
protected $headers = [];
protected $ip;
public function __construct(array $headers, $ip)
{
$this->headers = $headers;
$this->ip = $ip;
}
public static function createFromString($string, $ip)
{
$data = explode("\r\n", trim($string));
$status = array_shift($data);
$headers = [];
foreach ($data as $line) {
list ($header, $value) = array_map("trim", explode(":",$line,2));
$headers[strtolower($header)] = $value;
}
// Test for search responses using the ST header
if (array_key_exists('st', $headers)) {
return new MSearchResponse($headers, $ip);
}
}
public function getIp()
{
return $this->ip;
}
public function getLocation()
{
return $this->headers['location'];
}
}

123
src/SSDP/Device.php Normal file
View File

@ -0,0 +1,123 @@
<?php
namespace NoccyLabs\UPnP\SSDP;
/*
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice</friendlyName>
<manufacturer>MiniUPnP</manufacturer>
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
<modelDescription>WAN Device</modelDescription>
<modelName>WAN Device</modelName>
<modelNumber>20161205</modelNumber>
<modelURL>http://miniupnp.free.fr/</modelURL>
<serialNumber>00000000</serialNumber>
<UDN>uuid:ea80ec5e-2ff9-4834-866a-257fccd9e572</UDN>
<UPC>000000000000</UPC>
*/
use SimpleXMLElement;
class Device
{
protected $deviceType;
protected $friendlyName;
protected $manufacturer;
protected $manufacturerUrl;
protected $modelName;
protected $modelDescription;
protected $modelNumber;
protected $modelUrl;
protected $serialNumber;
protected $specUrl;
protected $services = [];
protected $devices = [];
public static function createFromSchema($url, $ip)
{
$xml = simplexml_load_file($url);
$spec = $xml->children('urn:schemas-upnp-org:device-1-0');
if (count($spec)>0) {
$device = $spec->device;
return new Device($device, $url, $ip);
}
}
public function __construct(SimpleXMLElement $spec, $spec_url, $ip)
{
$this->ip = $ip;
$this->specUrl = (string)$spec_url;
$this->deviceType = (string)$spec->deviceType;
$this->friendlyName = (string)$spec->friendlyName;
$this->manufacturer = (string)$spec->manufacturer;
$this->manufacturerUrl = (string)$spec->manufacturerURL;
$this->modelName = (string)$spec->modelName;
$this->modelDescription = (string)$spec->modelDescription;
$this->modelNumber = (string)$spec->modelNumber;
$this->modelUrl = (string)$spec->modelURL;
$this->serialNumber = (string)$spec->serialNumber;
$devices = $spec->deviceList;
if (count($devices)>0) {
foreach ($devices->children() as $device) {
$this->devices[] = new Device($device, $spec_url, $ip);
}
}
$services = $spec->serviceList;
if (count($services)>0) {
foreach ($services->children() as $service) {
$this->services[] = new Service($service);
}
}
}
public function getIp()
{
return $this->ip;
}
public function __toString()
{
$info = sprintf("Device: %s [%s] at %s\nManufacturer: %s\nModel: %s (%s)\nURL: %s\n",
$this->friendlyName,
$this->deviceType,
$this->ip,
$this->manufacturer,
$this->modelName,
$this->modelDescription,
$this->specUrl
);
if (count($this->services)>0) {
$services = " = ".join("\n = ",array_map("strval", $this->services));
$services = join("\n ", explode("\n", rtrim($services)));
$info.= $services."\n";
}
if (count($this->devices)>0) {
$devices = " + ".join("\n + ",array_map("strval", $this->devices));
$devices = join("\n ", explode("\n", rtrim($devices)));
$info.= $devices."\n";
}
return $info;
}
}

65
src/SSDP/Discovery.php Normal file
View File

@ -0,0 +1,65 @@
<?php
namespace NoccyLabs\UPnP\SSDP;
use IteratorAggregate;
use ArrayIterator;
use NoccyLabs\UPnP\HTTPU\Endpoint;
use NoccyLabs\UPnP\HTTPU\EndpointException;
use NoccyLabs\UPnP\HTTPU\MSearchRequest;
use NoccyLabs\UPnP\HTTPU\MSearchResponse;
/**
* Basic implementation of the SSDP protocol.
*
*
*/
class Discovery implements IteratorAggregate
{
protected $devices = [];
/**
*
*
* @param string $search_type The device type or schema to search for
* @return int The number of devices found
*/
public function discover($search_type, $timeout=1)
{
// Set up the endpoint
try {
$endpoint = new Endpoint();
} catch (EndpointException $e) {
throw new DiscoveryException("Discovery failed", 0, $e);
}
// Clean up previous state
$this->devices = [];
// Create the request
$request = new MSearchRequest();
$request->setSearchType($search_type);
$request->setMaxWait($timeout-1);
// Send the request and wait for responses
$responses = $endpoint->send($request, $timeout);
foreach ($responses as $response) {
// Add the relevant responses to the list
if ($response instanceof MSearchResponse) {
$device = Device::createFromSchema($response->getLocation(), $response->getIp());
$this->devices[] = $device;
}
}
return count($this->devices);
}
public function getIterator()
{
return new ArrayIterator($this->devices);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\UPnP\SSDP;
class DiscoveryException extends SSDPException
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace NoccyLabs\UPnP\SSDP;
use Exception;
class SSDPException extends Exception
{
}

35
src/SSDP/SearchTarget.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace NoccyLabs\UPnP\SSDP;
class SearchTarget
{
// Common
const ALL = "ssdp:all"; // Search for all devices and services
const ROOT_DEVICE = "upnp:rootdevice"; // Search for root devices only
// Device schemas
const URN_SCHEMA_DEVICE_IGD_1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2";
const URN_SCHEMA_DEVICE_IGD_2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2";
// Helpers to generate targets
public static function UUID($device_uuid)
{ return sprintf("uuid:%s", $device_uuid); }
public static function URN_SCHEMA_DEVICE($type, $version)
{ return sprintf("urn:schemas-upnp-org:device:%s:%d", $type, $version); }
public static function URN_SCHEMA_SERVICE($type, $version)
{ return sprintf("urn:schemas-upnp-org:service:%s:%d", $type, $version); }
public static function URN_DEVICE($domain, $type, $version)
{ return sprintf("urn:%s:device:%s:%d", $domain, $type, $version); }
public static function URN_SERVICE($domain, $type, $version)
{ return sprintf("urn:%s:service:%s:%d", $domain, $type, $version); }
}

50
src/SSDP/Service.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace NoccyLabs\UPnP\SSDP;
/*
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId>
<SCPDURL>/L3F.xml</SCPDURL>
<controlURL>/ctl/L3F</controlURL>
<eventSubURL>/evt/L3F</eventSubURL>
*/
use SimpleXMLElement;
class Service
{
protected $serviceType;
protected $serviceId;
protected $scpdUrl;
protected $controlUrl;
protected $eventSubUrl;
public function __construct(SimpleXMLElement $spec)
{
$this->serviceType = (string)$spec->serviceType;
$this->serviceId = (string)$spec->serviceId;
$this->scpdUrl = (string)$spec->SCPDURL;
$this->controlUrl = (string)$spec->controlURL;
$this->eventSubUrl = (string)$spec->eventSubURL;
}
public function __toString()
{
return sprintf("Service: %s [%s]\nControl URL: %s\nEventSub URL: %s\nSCPD URL: %s\n",
$this->serviceId,
$this->serviceType,
$this->controlUrl,
$this->eventSubUrl,
$this->scpdUrl
);
}
}

10
src/UPnPException.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace NoccyLabs\UPnP\HTTPU;
use Exception;
class UPnPException extends Exception
{
}