friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Dns\Resolver;
4
5use React\Cache\ArrayCache;
6use React\Cache\CacheInterface;
7use React\Dns\Config\Config;
8use React\Dns\Config\HostsFile;
9use React\Dns\Query\CachingExecutor;
10use React\Dns\Query\CoopExecutor;
11use React\Dns\Query\ExecutorInterface;
12use React\Dns\Query\FallbackExecutor;
13use React\Dns\Query\HostsFileExecutor;
14use React\Dns\Query\RetryExecutor;
15use React\Dns\Query\SelectiveTransportExecutor;
16use React\Dns\Query\TcpTransportExecutor;
17use React\Dns\Query\TimeoutExecutor;
18use React\Dns\Query\UdpTransportExecutor;
19use React\EventLoop\Loop;
20use React\EventLoop\LoopInterface;
21
22final class Factory
23{
24 /**
25 * Creates a DNS resolver instance for the given DNS config
26 *
27 * As of v1.7.0 it's recommended to pass a `Config` object instead of a
28 * single nameserver address. If the given config contains more than one DNS
29 * nameserver, all DNS nameservers will be used in order. The primary DNS
30 * server will always be used first before falling back to the secondary or
31 * tertiary DNS server.
32 *
33 * @param Config|string $config DNS Config object (recommended) or single nameserver address
34 * @param ?LoopInterface $loop
35 * @return \React\Dns\Resolver\ResolverInterface
36 * @throws \InvalidArgumentException for invalid DNS server address
37 * @throws \UnderflowException when given DNS Config object has an empty list of nameservers
38 */
39 public function create($config, $loop = null)
40 {
41 if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
42 throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
43 }
44
45 $executor = $this->decorateHostsFileExecutor($this->createExecutor($config, $loop ?: Loop::get()));
46
47 return new Resolver($executor);
48 }
49
50 /**
51 * Creates a cached DNS resolver instance for the given DNS config and cache
52 *
53 * As of v1.7.0 it's recommended to pass a `Config` object instead of a
54 * single nameserver address. If the given config contains more than one DNS
55 * nameserver, all DNS nameservers will be used in order. The primary DNS
56 * server will always be used first before falling back to the secondary or
57 * tertiary DNS server.
58 *
59 * @param Config|string $config DNS Config object (recommended) or single nameserver address
60 * @param ?LoopInterface $loop
61 * @param ?CacheInterface $cache
62 * @return \React\Dns\Resolver\ResolverInterface
63 * @throws \InvalidArgumentException for invalid DNS server address
64 * @throws \UnderflowException when given DNS Config object has an empty list of nameservers
65 */
66 public function createCached($config, $loop = null, $cache = null)
67 {
68 if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
69 throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
70 }
71
72 if ($cache !== null && !$cache instanceof CacheInterface) { // manual type check to support legacy PHP < 7.1
73 throw new \InvalidArgumentException('Argument #3 ($cache) expected null|React\Cache\CacheInterface');
74 }
75
76 // default to keeping maximum of 256 responses in cache unless explicitly given
77 if (!($cache instanceof CacheInterface)) {
78 $cache = new ArrayCache(256);
79 }
80
81 $executor = $this->createExecutor($config, $loop ?: Loop::get());
82 $executor = new CachingExecutor($executor, $cache);
83 $executor = $this->decorateHostsFileExecutor($executor);
84
85 return new Resolver($executor);
86 }
87
88 /**
89 * Tries to load the hosts file and decorates the given executor on success
90 *
91 * @param ExecutorInterface $executor
92 * @return ExecutorInterface
93 * @codeCoverageIgnore
94 */
95 private function decorateHostsFileExecutor(ExecutorInterface $executor)
96 {
97 try {
98 $executor = new HostsFileExecutor(
99 HostsFile::loadFromPathBlocking(),
100 $executor
101 );
102 } catch (\RuntimeException $e) {
103 // ignore this file if it can not be loaded
104 }
105
106 // Windows does not store localhost in hosts file by default but handles this internally
107 // To compensate for this, we explicitly use hard-coded defaults for localhost
108 if (DIRECTORY_SEPARATOR === '\\') {
109 $executor = new HostsFileExecutor(
110 new HostsFile("127.0.0.1 localhost\n::1 localhost"),
111 $executor
112 );
113 }
114
115 return $executor;
116 }
117
118 /**
119 * @param Config|string $nameserver
120 * @param LoopInterface $loop
121 * @return CoopExecutor
122 * @throws \InvalidArgumentException for invalid DNS server address
123 * @throws \UnderflowException when given DNS Config object has an empty list of nameservers
124 */
125 private function createExecutor($nameserver, LoopInterface $loop)
126 {
127 if ($nameserver instanceof Config) {
128 if (!$nameserver->nameservers) {
129 throw new \UnderflowException('Empty config with no DNS servers');
130 }
131
132 // Hard-coded to check up to 3 DNS servers to match default limits in place in most systems (see MAXNS config).
133 // Note to future self: Recursion isn't too hard, but how deep do we really want to go?
134 $primary = reset($nameserver->nameservers);
135 $secondary = next($nameserver->nameservers);
136 $tertiary = next($nameserver->nameservers);
137
138 if ($tertiary !== false) {
139 // 3 DNS servers given => nest first with fallback for second and third
140 return new CoopExecutor(
141 new RetryExecutor(
142 new FallbackExecutor(
143 $this->createSingleExecutor($primary, $loop),
144 new FallbackExecutor(
145 $this->createSingleExecutor($secondary, $loop),
146 $this->createSingleExecutor($tertiary, $loop)
147 )
148 )
149 )
150 );
151 } elseif ($secondary !== false) {
152 // 2 DNS servers given => fallback from first to second
153 return new CoopExecutor(
154 new RetryExecutor(
155 new FallbackExecutor(
156 $this->createSingleExecutor($primary, $loop),
157 $this->createSingleExecutor($secondary, $loop)
158 )
159 )
160 );
161 } else {
162 // 1 DNS server given => use single executor
163 $nameserver = $primary;
164 }
165 }
166
167 return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop)));
168 }
169
170 /**
171 * @param string $nameserver
172 * @param LoopInterface $loop
173 * @return ExecutorInterface
174 * @throws \InvalidArgumentException for invalid DNS server address
175 */
176 private function createSingleExecutor($nameserver, LoopInterface $loop)
177 {
178 $parts = \parse_url($nameserver);
179
180 if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') {
181 $executor = $this->createTcpExecutor($nameserver, $loop);
182 } elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') {
183 $executor = $this->createUdpExecutor($nameserver, $loop);
184 } else {
185 $executor = new SelectiveTransportExecutor(
186 $this->createUdpExecutor($nameserver, $loop),
187 $this->createTcpExecutor($nameserver, $loop)
188 );
189 }
190
191 return $executor;
192 }
193
194 /**
195 * @param string $nameserver
196 * @param LoopInterface $loop
197 * @return TimeoutExecutor
198 * @throws \InvalidArgumentException for invalid DNS server address
199 */
200 private function createTcpExecutor($nameserver, LoopInterface $loop)
201 {
202 return new TimeoutExecutor(
203 new TcpTransportExecutor($nameserver, $loop),
204 5.0,
205 $loop
206 );
207 }
208
209 /**
210 * @param string $nameserver
211 * @param LoopInterface $loop
212 * @return TimeoutExecutor
213 * @throws \InvalidArgumentException for invalid DNS server address
214 */
215 private function createUdpExecutor($nameserver, LoopInterface $loop)
216 {
217 return new TimeoutExecutor(
218 new UdpTransportExecutor(
219 $nameserver,
220 $loop
221 ),
222 5.0,
223 $loop
224 );
225 }
226}