Initial commit
This commit is contained in:
102
src/HTTPU/Endpoint.php
Normal file
102
src/HTTPU/Endpoint.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
|
8
src/HTTPU/EndpointException.php
Normal file
8
src/HTTPU/EndpointException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\UPnP\HTTPU;
|
||||
|
||||
class EndpointException extends HTTPUException
|
||||
{
|
||||
}
|
||||
|
10
src/HTTPU/HTTPUException.php
Normal file
10
src/HTTPU/HTTPUException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\UPnP\HTTPU;
|
||||
|
||||
use NoccyLabs\UPnP\UPnPException;
|
||||
|
||||
class HTTPUException extends UPnPException
|
||||
{
|
||||
}
|
||||
|
28
src/HTTPU/MSearchRequest.php
Normal file
28
src/HTTPU/MSearchRequest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
13
src/HTTPU/MSearchResponse.php
Normal file
13
src/HTTPU/MSearchResponse.php
Normal 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
53
src/HTTPU/Request.php
Normal 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
45
src/HTTPU/Response.php
Normal 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
123
src/SSDP/Device.php
Normal 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
65
src/SSDP/Discovery.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
8
src/SSDP/DiscoveryException.php
Normal file
8
src/SSDP/DiscoveryException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\UPnP\SSDP;
|
||||
|
||||
class DiscoveryException extends SSDPException
|
||||
{
|
||||
}
|
||||
|
11
src/SSDP/SSDPException.php
Normal file
11
src/SSDP/SSDPException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\UPnP\SSDP;
|
||||
|
||||
use Exception;
|
||||
|
||||
class SSDPException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
|
35
src/SSDP/SearchTarget.php
Normal file
35
src/SSDP/SearchTarget.php
Normal 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
50
src/SSDP/Service.php
Normal 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
10
src/UPnPException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\UPnP\HTTPU;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UPnPException extends Exception
|
||||
{
|
||||
}
|
||||
|
Reference in New Issue
Block a user