friendship ended with social-app. php is my new best friend
at main 12 kB view raw
1<?php 2 3namespace React\Socket; 4 5use React\Dns\Model\Message; 6use React\Dns\Resolver\ResolverInterface; 7use React\EventLoop\LoopInterface; 8use React\EventLoop\TimerInterface; 9use React\Promise; 10use React\Promise\PromiseInterface; 11 12/** 13 * @internal 14 */ 15final class HappyEyeBallsConnectionBuilder 16{ 17 /** 18 * As long as we haven't connected yet keep popping an IP address of the connect queue until one of them 19 * succeeds or they all fail. We will wait 100ms between connection attempts as per RFC. 20 * 21 * @link https://tools.ietf.org/html/rfc8305#section-5 22 */ 23 const CONNECTION_ATTEMPT_DELAY = 0.1; 24 25 /** 26 * Delay `A` lookup by 50ms sending out connection to IPv4 addresses when IPv6 records haven't 27 * resolved yet as per RFC. 28 * 29 * @link https://tools.ietf.org/html/rfc8305#section-3 30 */ 31 const RESOLUTION_DELAY = 0.05; 32 33 public $loop; 34 public $connector; 35 public $resolver; 36 public $uri; 37 public $host; 38 public $resolved = array( 39 Message::TYPE_A => false, 40 Message::TYPE_AAAA => false, 41 ); 42 public $resolverPromises = array(); 43 public $connectionPromises = array(); 44 public $connectQueue = array(); 45 public $nextAttemptTimer; 46 public $parts; 47 public $ipsCount = 0; 48 public $failureCount = 0; 49 public $resolve; 50 public $reject; 51 52 public $lastErrorFamily; 53 public $lastError6; 54 public $lastError4; 55 56 public function __construct(LoopInterface $loop, ConnectorInterface $connector, ResolverInterface $resolver, $uri, $host, $parts) 57 { 58 $this->loop = $loop; 59 $this->connector = $connector; 60 $this->resolver = $resolver; 61 $this->uri = $uri; 62 $this->host = $host; 63 $this->parts = $parts; 64 } 65 66 public function connect() 67 { 68 $that = $this; 69 return new Promise\Promise(function ($resolve, $reject) use ($that) { 70 $lookupResolve = function ($type) use ($that, $resolve, $reject) { 71 return function (array $ips) use ($that, $type, $resolve, $reject) { 72 unset($that->resolverPromises[$type]); 73 $that->resolved[$type] = true; 74 75 $that->mixIpsIntoConnectQueue($ips); 76 77 // start next connection attempt if not already awaiting next 78 if ($that->nextAttemptTimer === null && $that->connectQueue) { 79 $that->check($resolve, $reject); 80 } 81 }; 82 }; 83 84 $that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA)); 85 $that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function (array $ips) use ($that) { 86 // happy path: IPv6 has resolved already (or could not resolve), continue with IPv4 addresses 87 if ($that->resolved[Message::TYPE_AAAA] === true || !$ips) { 88 return $ips; 89 } 90 91 // Otherwise delay processing IPv4 lookup until short timer passes or IPv6 resolves in the meantime 92 $deferred = new Promise\Deferred(function () use (&$ips) { 93 // discard all IPv4 addresses if cancelled 94 $ips = array(); 95 }); 96 $timer = $that->loop->addTimer($that::RESOLUTION_DELAY, function () use ($deferred, $ips) { 97 $deferred->resolve($ips); 98 }); 99 100 $that->resolverPromises[Message::TYPE_AAAA]->then(function () use ($that, $timer, $deferred, &$ips) { 101 $that->loop->cancelTimer($timer); 102 $deferred->resolve($ips); 103 }); 104 105 return $deferred->promise(); 106 })->then($lookupResolve(Message::TYPE_A)); 107 }, function ($_, $reject) use ($that) { 108 $reject(new \RuntimeException( 109 'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : '') . ' (ECONNABORTED)', 110 \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 111 )); 112 $_ = $reject = null; 113 114 $that->cleanUp(); 115 }); 116 } 117 118 /** 119 * @internal 120 * @param int $type DNS query type 121 * @param callable $reject 122 * @return \React\Promise\PromiseInterface<string[]> Returns a promise that 123 * always resolves with a list of IP addresses on success or an empty 124 * list on error. 125 */ 126 public function resolve($type, $reject) 127 { 128 $that = $this; 129 return $that->resolver->resolveAll($that->host, $type)->then(null, function (\Exception $e) use ($type, $reject, $that) { 130 unset($that->resolverPromises[$type]); 131 $that->resolved[$type] = true; 132 133 if ($type === Message::TYPE_A) { 134 $that->lastError4 = $e->getMessage(); 135 $that->lastErrorFamily = 4; 136 } else { 137 $that->lastError6 = $e->getMessage(); 138 $that->lastErrorFamily = 6; 139 } 140 141 // cancel next attempt timer when there are no more IPs to connect to anymore 142 if ($that->nextAttemptTimer !== null && !$that->connectQueue) { 143 $that->loop->cancelTimer($that->nextAttemptTimer); 144 $that->nextAttemptTimer = null; 145 } 146 147 if ($that->hasBeenResolved() && $that->ipsCount === 0) { 148 $reject(new \RuntimeException( 149 $that->error(), 150 0, 151 $e 152 )); 153 } 154 155 // Exception already handled above, so don't throw an unhandled rejection here 156 return array(); 157 }); 158 } 159 160 /** 161 * @internal 162 */ 163 public function check($resolve, $reject) 164 { 165 $ip = \array_shift($this->connectQueue); 166 167 // start connection attempt and remember array position to later unset again 168 $this->connectionPromises[] = $this->attemptConnection($ip); 169 \end($this->connectionPromises); 170 $index = \key($this->connectionPromises); 171 172 $that = $this; 173 $that->connectionPromises[$index]->then(function ($connection) use ($that, $index, $resolve) { 174 unset($that->connectionPromises[$index]); 175 176 $that->cleanUp(); 177 178 $resolve($connection); 179 }, function (\Exception $e) use ($that, $index, $ip, $resolve, $reject) { 180 unset($that->connectionPromises[$index]); 181 182 $that->failureCount++; 183 184 $message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage()); 185 if (\strpos($ip, ':') === false) { 186 $that->lastError4 = $message; 187 $that->lastErrorFamily = 4; 188 } else { 189 $that->lastError6 = $message; 190 $that->lastErrorFamily = 6; 191 } 192 193 // start next connection attempt immediately on error 194 if ($that->connectQueue) { 195 if ($that->nextAttemptTimer !== null) { 196 $that->loop->cancelTimer($that->nextAttemptTimer); 197 $that->nextAttemptTimer = null; 198 } 199 200 $that->check($resolve, $reject); 201 } 202 203 if ($that->hasBeenResolved() === false) { 204 return; 205 } 206 207 if ($that->ipsCount === $that->failureCount) { 208 $that->cleanUp(); 209 210 $reject(new \RuntimeException( 211 $that->error(), 212 $e->getCode(), 213 $e 214 )); 215 } 216 }); 217 218 // Allow next connection attempt in 100ms: https://tools.ietf.org/html/rfc8305#section-5 219 // Only start timer when more IPs are queued or when DNS query is still pending (might add more IPs) 220 if ($this->nextAttemptTimer === null && (\count($this->connectQueue) > 0 || $this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false)) { 221 $this->nextAttemptTimer = $this->loop->addTimer(self::CONNECTION_ATTEMPT_DELAY, function () use ($that, $resolve, $reject) { 222 $that->nextAttemptTimer = null; 223 224 if ($that->connectQueue) { 225 $that->check($resolve, $reject); 226 } 227 }); 228 } 229 } 230 231 /** 232 * @internal 233 */ 234 public function attemptConnection($ip) 235 { 236 $uri = Connector::uri($this->parts, $this->host, $ip); 237 238 return $this->connector->connect($uri); 239 } 240 241 /** 242 * @internal 243 */ 244 public function cleanUp() 245 { 246 // clear list of outstanding IPs to avoid creating new connections 247 $this->connectQueue = array(); 248 249 // cancel pending connection attempts 250 foreach ($this->connectionPromises as $connectionPromise) { 251 if ($connectionPromise instanceof PromiseInterface && \method_exists($connectionPromise, 'cancel')) { 252 $connectionPromise->cancel(); 253 } 254 } 255 256 // cancel pending DNS resolution (cancel IPv4 first in case it is awaiting IPv6 resolution delay) 257 foreach (\array_reverse($this->resolverPromises) as $resolverPromise) { 258 if ($resolverPromise instanceof PromiseInterface && \method_exists($resolverPromise, 'cancel')) { 259 $resolverPromise->cancel(); 260 } 261 } 262 263 if ($this->nextAttemptTimer instanceof TimerInterface) { 264 $this->loop->cancelTimer($this->nextAttemptTimer); 265 $this->nextAttemptTimer = null; 266 } 267 } 268 269 /** 270 * @internal 271 */ 272 public function hasBeenResolved() 273 { 274 foreach ($this->resolved as $typeHasBeenResolved) { 275 if ($typeHasBeenResolved === false) { 276 return false; 277 } 278 } 279 280 return true; 281 } 282 283 /** 284 * Mixes an array of IP addresses into the connect queue in such a way they alternate when attempting to connect. 285 * The goal behind it is first attempt to connect to IPv6, then to IPv4, then to IPv6 again until one of those 286 * attempts succeeds. 287 * 288 * @link https://tools.ietf.org/html/rfc8305#section-4 289 * 290 * @internal 291 */ 292 public function mixIpsIntoConnectQueue(array $ips) 293 { 294 \shuffle($ips); 295 $this->ipsCount += \count($ips); 296 $connectQueueStash = $this->connectQueue; 297 $this->connectQueue = array(); 298 while (\count($connectQueueStash) > 0 || \count($ips) > 0) { 299 if (\count($ips) > 0) { 300 $this->connectQueue[] = \array_shift($ips); 301 } 302 if (\count($connectQueueStash) > 0) { 303 $this->connectQueue[] = \array_shift($connectQueueStash); 304 } 305 } 306 } 307 308 /** 309 * @internal 310 * @return string 311 */ 312 public function error() 313 { 314 if ($this->lastError4 === $this->lastError6) { 315 $message = $this->lastError6; 316 } elseif ($this->lastErrorFamily === 6) { 317 $message = 'Last error for IPv6: ' . $this->lastError6 . '. Previous error for IPv4: ' . $this->lastError4; 318 } else { 319 $message = 'Last error for IPv4: ' . $this->lastError4 . '. Previous error for IPv6: ' . $this->lastError6; 320 } 321 322 if ($this->hasBeenResolved() && $this->ipsCount === 0) { 323 if ($this->lastError6 === $this->lastError4) { 324 $message = ' during DNS lookup: ' . $this->lastError6; 325 } else { 326 $message = ' during DNS lookup. ' . $message; 327 } 328 } else { 329 $message = ': ' . $message; 330 } 331 332 return 'Connection to ' . $this->uri . ' failed' . $message; 333 } 334}