Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
32 / 33
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DnsSrvDiscoverer
96.97% covered (success)
96.97%
32 / 33
75.00% covered (warning)
75.00%
3 / 4
13
0.00% covered (danger)
0.00%
0 / 1
 __construct
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
1.00
 getRecords
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getServers
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getSrvName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Service discovery using DNS SRV records
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9/**
10 * @since 1.29
11 */
12class DnsSrvDiscoverer {
13    /**
14     * @var string
15     */
16    private $service;
17
18    /**
19     * @var string
20     */
21    private $protocol;
22
23    /**
24     * @var string|null
25     */
26    private $domain;
27
28    /**
29     * @var callable
30     */
31    private $resolver;
32
33    /**
34     * Construct a new discoverer for the given domain, service, and protocol.
35     *
36     * @param string $service Name of the service to discover.
37     * @param string $protocol Service protocol. Defaults to 'tcp'
38     * @param ?string $domain The hostname/domain on which to perform discovery
39     *  of the given service and protocol. Defaults to null which effectively
40     *  performs a query relative to the host's configured search domain.
41     * @param ?callable $resolver Resolver function. Defaults to using
42     *  dns_get_record. Primarily useful in testing.
43     */
44    public function __construct(
45        string $service,
46        string $protocol = 'tcp',
47        ?string $domain = null,
48        ?callable $resolver = null
49    ) {
50        $this->service = $service;
51        $this->protocol = $protocol;
52        $this->domain = $domain;
53
54        $this->resolver = $resolver ?? static function ( $srv ) {
55            return dns_get_record( $srv, DNS_SRV );
56        };
57    }
58
59    /**
60     * Queries the resolver for an SRV resource record matching the service,
61     * protocol, and domain and returns all target/port/priority/weight
62     * records.
63     *
64     * @return array
65     */
66    public function getRecords() {
67        $result = [];
68
69        $records = ( $this->resolver )( $this->getSrvName() );
70
71        // Respect RFC 2782 with regard to a single '.' entry denoting a valid
72        // empty response
73        if (
74            !$records
75            || ( count( $records ) === 1 && $records[0]['target'] === '.' )
76        ) {
77            return $result;
78        }
79
80        foreach ( $records as $record ) {
81            $result[] = [
82                'target' => $record['target'],
83                'port' => (int)$record['port'],
84                'pri' => (int)$record['pri'],
85                'weight' => (int)$record['weight'],
86            ];
87        }
88
89        return $result;
90    }
91
92    /**
93     * Performs discovery for the domain, service, and protocol, and returns a
94     * list of resolved server name/ip and port number pairs sorted by each
95     * record's priority, with servers of the same priority randomly shuffled.
96     *
97     * @return array
98     */
99    public function getServers() {
100        $records = $this->getRecords();
101
102        usort( $records, static function ( $a, $b ) {
103            if ( $a['pri'] === $b['pri'] ) {
104                return mt_rand( 0, 1 ) ? 1 : -1;
105            }
106
107            return $a['pri'] - $b['pri'];
108        } );
109
110        $serversAndPorts = [];
111
112        foreach ( $records as $record ) {
113            $serversAndPorts[] = [ $record['target'], $record['port'] ];
114        }
115
116        return $serversAndPorts;
117    }
118
119    /**
120     * Returns the SRV resource record name.
121     */
122    public function getSrvName(): string {
123        $srv = "_{$this->service}._{$this->protocol}";
124
125        if ( $this->domain === null || $this->domain === '' ) {
126            return $srv;
127        }
128
129        return "$srv.{$this->domain}";
130    }
131}