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 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23/**
24 * @since 1.29
25 */
26class DnsSrvDiscoverer {
27    /**
28     * @var string
29     */
30    private $service;
31
32    /**
33     * @var string
34     */
35    private $protocol;
36
37    /**
38     * @var string|null
39     */
40    private $domain;
41
42    /**
43     * @var callable
44     */
45    private $resolver;
46
47    /**
48     * Construct a new discoverer for the given domain, service, and protocol.
49     *
50     * @param string $service Name of the service to discover.
51     * @param string $protocol Service protocol. Defaults to 'tcp'
52     * @param ?string $domain The hostname/domain on which to perform discovery
53     *  of the given service and protocol. Defaults to null which effectively
54     *  performs a query relative to the host's configured search domain.
55     * @param ?callable $resolver Resolver function. Defaults to using
56     *  dns_get_record. Primarily useful in testing.
57     */
58    public function __construct(
59        string $service,
60        string $protocol = 'tcp',
61        ?string $domain = null,
62        ?callable $resolver = null
63    ) {
64        $this->service = $service;
65        $this->protocol = $protocol;
66        $this->domain = $domain;
67
68        $this->resolver = $resolver ?? static function ( $srv ) {
69            return dns_get_record( $srv, DNS_SRV );
70        };
71    }
72
73    /**
74     * Queries the resolver for an SRV resource record matching the service,
75     * protocol, and domain and returns all target/port/priority/weight
76     * records.
77     *
78     * @return array
79     */
80    public function getRecords() {
81        $result = [];
82
83        $records = ( $this->resolver )( $this->getSrvName() );
84
85        // Respect RFC 2782 with regard to a single '.' entry denoting a valid
86        // empty response
87        if (
88            !$records
89            || ( count( $records ) === 1 && $records[0]['target'] === '.' )
90        ) {
91            return $result;
92        }
93
94        foreach ( $records as $record ) {
95            $result[] = [
96                'target' => $record['target'],
97                'port' => (int)$record['port'],
98                'pri' => (int)$record['pri'],
99                'weight' => (int)$record['weight'],
100            ];
101        }
102
103        return $result;
104    }
105
106    /**
107     * Performs discovery for the domain, service, and protocol, and returns a
108     * list of resolved server name/ip and port number pairs sorted by each
109     * record's priority, with servers of the same priority randomly shuffled.
110     *
111     * @return array
112     */
113    public function getServers() {
114        $records = $this->getRecords();
115
116        usort( $records, static function ( $a, $b ) {
117            if ( $a['pri'] === $b['pri'] ) {
118                return mt_rand( 0, 1 ) ? 1 : -1;
119            }
120
121            return $a['pri'] - $b['pri'];
122        } );
123
124        $serversAndPorts = [];
125
126        foreach ( $records as $record ) {
127            $serversAndPorts[] = [ $record['target'], $record['port'] ];
128        }
129
130        return $serversAndPorts;
131    }
132
133    /**
134     * Returns the SRV resource record name.
135     *
136     * @return string
137     */
138    public function getSrvName(): string {
139        $srv = "_{$this->service}._{$this->protocol}";
140
141        if ( $this->domain === null || $this->domain === '' ) {
142            return $srv;
143        }
144
145        return "$srv.{$this->domain}";
146    }
147}