├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Factory.php └── MulticastExecutor.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.0 (2016-03-08) 4 | 5 | * Feature: Support Promise cancellation 6 | (#7 by @clue) 7 | 8 | * Improve documentation, update dependencies and 9 | first class support for PHP 7 and HHVM. 10 | 11 | ## 0.1.0 (2015-03-26) 12 | 13 | * First tagged release 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Christian Lück 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/reactphp-mdns 2 | 3 | [![CI status](https://github.com/clue/reactphp-mdns/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-mdns/actions) 4 | [![installs on Packagist](https://img.shields.io/packagist/dt/clue/mdns-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/mdns-react) 5 | 6 | Simple, async multicast DNS (mDNS) resolver for zeroconf networking, built on top of [ReactPHP](https://reactphp.org/). 7 | 8 | [Multicast DNS](http://www.multicastdns.org/) name resolution is commonly used 9 | as part of [zeroconf networking](https://en.wikipedia.org/wiki/Zero-configuration_networking). 10 | It is used by Mac OS X (Bonjour), many Linux distributions (Avahi) and quite a few other networking devices such as printers, camers etc. to resolve hostnames of your local LAN clients to IP addresses. 11 | 12 | This library implements the mDNS protocol as defined in [RFC 6762](https://tools.ietf.org/html/rfc6762). 13 | Note that this protocol is related to, but independent of, DNS-Based Service Discovery (DNS-SD) 14 | as defined in [RFC 6763](https://tools.ietf.org/html/rfc6763). 15 | 16 | **Table of Contents** 17 | 18 | * [Quickstart example](#quickstart-example) 19 | * [Usage](#usage) 20 | * [Factory](#factory) 21 | * [createResolver()](#createresolver) 22 | * [Resolver](#resolver) 23 | * [Promises](#promises) 24 | * [Blocking](#blocking) 25 | * [Install](#install) 26 | * [Tests](#tests) 27 | * [License](#license) 28 | * [More](#more) 29 | 30 | > Note: This project is in beta stage! Feel free to report any issues you encounter. 31 | 32 | ## Quickstart example 33 | 34 | Once [installed](#install), you can use the following code to look up the address of a local domain name: 35 | 36 | ```php 37 | createResolver(); 43 | 44 | $resolver->resolve('hostname.local')->then(function ($ip) { 45 | echo 'Found: ' . $ip . PHP_EOL; 46 | }, function (Exception $e) { 47 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 48 | }); 49 | ``` 50 | 51 | See also the [examples](examples/). 52 | 53 | ## Usage 54 | 55 | ### Factory 56 | 57 | The `Factory` is responsible for creating your [`Resolver`](#resolver) instance. 58 | 59 | ```php 60 | $factory = new Factory(); 61 | ``` 62 | 63 | This class takes an optional `LoopInterface|null $loop` parameter that can be used to 64 | pass the event loop instance to use for this object. You can use a `null` value 65 | here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). 66 | This value SHOULD NOT be given unless you're sure you want to explicitly use a 67 | given event loop instance. 68 | 69 | #### createResolver() 70 | 71 | The `createResolver()` method can be used to create a mDNS resolver instance that sends multicast DNS queries and waits for incoming unicast DNS responses. It returns a [`Resolver`](#resolver) instance. 72 | 73 | ```php 74 | $resolver = $factory->createResolver(); 75 | ``` 76 | 77 | ### Resolver 78 | 79 | The [`Factory`](#factory) creates instances of the `React\Dns\Resolver\Resolver` class from the [react/dns](https://github.com/reactphp/dns) package. 80 | 81 | While ReactPHP's *normal* DNS resolver uses unicast UDP messages (and TCP streams) to query a given nameserver, 82 | this resolver instance uses multicast UDP messages to query all reachable hosts in your network. 83 | 84 | #### Promises 85 | 86 | Sending queries is async (non-blocking), so you can actually send multiple DNS queries in parallel. 87 | The mDNS hosts will respond to each DNS query message with a DNS response message. The order is not guaranteed. 88 | Sending queries uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a query is *fulfilled* 89 | (i.e. either successfully resolved or rejected with an error): 90 | 91 | ```php 92 | $resolver->resolve($hostname)->then( 93 | function ($ip) { 94 | // IP successfully resolved 95 | }, 96 | function (Exception $e) { 97 | // an error occurred while looking up the given hostname 98 | } 99 | }); 100 | ``` 101 | 102 | Please refer to the [DNS documentation](https://github.com/reactphp/dns#readme) for more details. 103 | 104 | #### Blocking 105 | 106 | As stated above, this library provides you a powerful, async API by default. 107 | 108 | If, however, you want to integrate this into your traditional, blocking environment, 109 | you should look into also using [clue/reactphp-block](https://github.com/clue/reactphp-block). 110 | 111 | The resulting blocking code could look something like this: 112 | 113 | ```php 114 | createResolver(); 122 | 123 | $promise = $resolver->resolve('me.local'); 124 | 125 | try { 126 | $ip = Block\await($promise, $loop); 127 | // IP successfully resolved 128 | } catch (Exception $e) { 129 | // an error occurred while performing the request 130 | } 131 | ``` 132 | 133 | Similarly, you can also process multiple lookups concurrently and await an array of results: 134 | 135 | ```php 136 | $promises = array( 137 | $resolver->resolve('first.local'), 138 | $resolver->resolve('second.local'), 139 | ); 140 | 141 | $ips = Block\awaitAll($promises, $loop); 142 | ``` 143 | 144 | Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. 145 | 146 | ## Install 147 | 148 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 149 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 150 | 151 | ```bash 152 | composer require clue/mdns-react:~0.2.0 153 | ``` 154 | 155 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 156 | 157 | This project aims to run on any platform and thus does not require any PHP 158 | extensions and supports running on legacy PHP 5.3 through current PHP 8+ and 159 | HHVM. 160 | It's *highly recommended to use the latest supported PHP version* for this project. 161 | 162 | ## Tests 163 | 164 | To run the test suite, you first need to clone this repo and then install all 165 | dependencies [through Composer](https://getcomposer.org/): 166 | 167 | ```bash 168 | composer install 169 | ``` 170 | 171 | To run the test suite, go to the project root and run: 172 | 173 | ```bash 174 | vendor/bin/phpunit 175 | ``` 176 | 177 | ## License 178 | 179 | This project is released under the permissive [MIT license](LICENSE). 180 | 181 | > Did you know that I offer custom development services and issuing invoices for 182 | sponsorships of releases and for contributions? Contact me (@clue) for details. 183 | 184 | ## More 185 | 186 | * Multicast DNS is defined in [RFC 6762](https://tools.ietf.org/html/rfc6762), in particular 187 | this specification also highlights the 188 | [differences to normal DNS operation](https://tools.ietf.org/html/rfc6762#section-19). 189 | * Please refer to the [react/dns component](https://github.com/reactphp/dns#readme) for more details 190 | about normal DNS operation. 191 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/mdns-react", 3 | "description": "Simple, async multicast DNS (mDNS) resolver for zeroconf networking, built on top of ReactPHP.", 4 | "keywords": ["Multicast DNS", "mDNS", "RFC 6762", "zeroconf", "Bonjour", "Avahi", "ReactPHP", "async"], 5 | "homepage": "https://github.com/clue/reactphp-mdns", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@lueck.tv" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Clue\\React\\Mdns\\": "src/" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "Clue\\Tests\\React\\Mdns\\": "tests/" 21 | } 22 | }, 23 | "require": { 24 | "php": ">=5.3", 25 | "clue/multicast-react": "^1.0 || ^0.2", 26 | "react/dns": "~0.4.0|~0.3.0", 27 | "react/event-loop": "^1.2", 28 | "react/promise": "^2.1 || ^1.2.1" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | loop = $loop ?: Loop::get(); 27 | $this->executor = $executor ?: new MulticastExecutor($loop); 28 | } 29 | 30 | public function createResolver() 31 | { 32 | return new Resolver(self::DNS, $this->executor); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MulticastExecutor.php: -------------------------------------------------------------------------------- 1 | loop = $loop ?: Loop::get(); 50 | $this->parser = $parser ?: new Parser(); 51 | $this->dumper = $dumper ?: new BinaryDumper(); 52 | $this->timeout = $timeout; 53 | $this->factory = $factory ?: new DatagramFactory($this->loop); 54 | } 55 | 56 | public function query($nameserver, Query $query) 57 | { 58 | $request = $this->prepareRequest($query); 59 | 60 | $queryData = $this->dumper->toBinary($request); 61 | 62 | return $this->doQuery($nameserver, $queryData, $query->name); 63 | } 64 | 65 | public function prepareRequest(Query $query) 66 | { 67 | $request = new Message(); 68 | $request->header->set('id', $this->generateId()); 69 | $request->header->set('rd', 1); 70 | $request->questions[] = (array) $query; 71 | $request->prepare(); 72 | 73 | return $request; 74 | } 75 | 76 | public function doQuery($nameserver, $queryData, $name) 77 | { 78 | $that = $this; 79 | $parser = $this->parser; 80 | $loop = $this->loop; 81 | 82 | $deferred = new Deferred(function ($_, $reject) use (&$conn, &$timer, $loop, $name) { 83 | $conn->close(); 84 | $loop->cancelTimer($timer); 85 | 86 | $reject(new \RuntimeException(sprintf("DNS query for %s cancelled", $name))); 87 | }); 88 | 89 | $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) { 90 | $conn->close(); 91 | $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name))); 92 | }); 93 | 94 | $conn = $this->factory->createSender(); 95 | 96 | $conn->on('message', function ($data) use ($conn, $parser, $deferred, $timer, $loop) { 97 | $response = new Message(); 98 | $responseReady = $parser->parseChunk($data, $response); 99 | 100 | $conn->close(); 101 | $loop->cancelTimer($timer); 102 | 103 | if (!$responseReady) { 104 | $deferred->reject(new BadServerException('Invalid response received')); 105 | 106 | return; 107 | } 108 | 109 | if ($response->header->isTruncated()) { 110 | $deferred->reject(new BadServerException('The server set the truncated bit although we issued a TCP request')); 111 | 112 | return; 113 | } 114 | 115 | $deferred->resolve($response); 116 | }); 117 | 118 | $conn->send($queryData, $nameserver); 119 | 120 | return $deferred->promise(); 121 | } 122 | 123 | protected function generateId() 124 | { 125 | return mt_rand(0, 0xffff); 126 | } 127 | } 128 | --------------------------------------------------------------------------------