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