Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2eb022b

Browse files
committedAug 5, 2020
Refactored StdClient: move message transport operations in their own classes TcpTransport and UdpTransport, fixed Tcp transport behavior, added some logging
1 parent e24db22 commit 2eb022b

12 files changed

+243
-71
lines changed
 

‎composer.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@
2626
"psr/log": "^1.1",
2727
"psr/http-server-middleware": "^1.0",
2828
"php-http/guzzle6-adapter": "^2.0",
29-
"react/dns": "^1.3.0",
30-
"react/datagram": "^1.5"
29+
"react/dns": "^1.3.0"
3130
},
3231
"require-dev": {
32+
"react/datagram": "^1.5",
3333
"phpunit/phpunit": "^8.1",
3434
"mockery/mockery": "^1.2",
3535
"monolog/monolog": "^1.24",
36-
"squizlabs/php_codesniffer": "*",
37-
"phpstan/phpstan": "^0.11.8"
36+
"squizlabs/php_codesniffer": "3.*",
37+
"phpstan/phpstan": "^0.11.8",
38+
"symfony/process": "^5.1"
3839
},
3940
"autoload": {
4041
"psr-4": {

‎src/Client/DnsClientInterface.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
interface DnsClientInterface
99
{
1010
/**
11-
* Resolve a DNS message using the provided upstream with the current client
11+
* Resolve a DNS message using the provided upstream
1212
* @param DnsUpstream $dnsUpstream
1313
* @param MessageInterface $dnsRequestMessage
1414
*

‎src/Client/StdClient.php

+62-57
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace NoGlitchYo\Dealdoh\Client;
44

55
use Exception;
6-
use InvalidArgumentException;
76
use LogicException;
7+
use NoGlitchYo\Dealdoh\Client\Transport\DnsTransportInterface;
88
use NoGlitchYo\Dealdoh\Entity\Dns\Message\Header;
99
use NoGlitchYo\Dealdoh\Entity\Dns\MessageInterface;
1010
use NoGlitchYo\Dealdoh\Entity\DnsUpstream;
@@ -22,112 +22,117 @@ class StdClient implements DnsClientInterface
2222
* @var MessageFactoryInterface
2323
*/
2424
private $dnsMessageFactory;
25+
/**
26+
* @var DnsTransportInterface
27+
*/
28+
private $tcpTransport;
29+
/**
30+
* @var DnsTransportInterface
31+
*/
32+
private $udpTransport;
2533

26-
public function __construct(MessageFactoryInterface $dnsMessageFactory)
27-
{
34+
public function __construct(
35+
MessageFactoryInterface $dnsMessageFactory,
36+
DnsTransportInterface $tcpTransport,
37+
DnsTransportInterface $udpTransport
38+
) {
2839
$this->dnsMessageFactory = $dnsMessageFactory;
40+
$this->tcpTransport = $tcpTransport;
41+
$this->udpTransport = $udpTransport;
2942
}
3043

3144
/**
3245
* Resolve message using regular UDP/TCP queries towards DNS upstream
3346
*
34-
* @param DnsUpstream $dnsUpstream
47+
* @param DnsUpstream $dnsUpstream
3548
* @param MessageInterface $dnsRequestMessage
3649
*
3750
* @return MessageInterface
3851
* @throws Exception
3952
*/
4053
public function resolve(DnsUpstream $dnsUpstream, MessageInterface $dnsRequestMessage): MessageInterface
4154
{
42-
$scheme = $dnsUpstream->getScheme();
43-
4455
$dnsRequestMessage = $this->enableRecursionForDnsMessage($dnsRequestMessage);
56+
$address = $this->getSanitizedUpstreamAddress($dnsUpstream);
4557

46-
// Clean up the protocol from URI supported by the client but which can not be used with sockets (e.g. dns://).
47-
$address = str_replace($scheme . '://', '', $dnsUpstream->getUri());
48-
49-
if (in_array($scheme, ['udp', 'dns']) || $dnsUpstream->getScheme() === null) {
50-
$dnsWireResponseMessage = $this->sendWithSocket('udp', $address, $dnsRequestMessage);
51-
} elseif ($dnsUpstream->getScheme() === 'tcp') {
52-
$dnsWireResponseMessage = $this->sendWithSocket('tcp', $address, $dnsRequestMessage);
58+
if ($this->isUdp($dnsUpstream)) {
59+
$dnsWireResponseMessage = $this->sendWith('udp', $address, $dnsRequestMessage);
60+
} elseif ($this->isTcp($dnsUpstream)) {
61+
$dnsWireResponseMessage = $this->sendWith('tcp', $address, $dnsRequestMessage);
5362
} else {
54-
throw new LogicException(sprintf('Scheme `%s` is not supported', $scheme));
63+
throw new LogicException(sprintf('Scheme `%s` is not supported', $dnsUpstream->getScheme()));
5564
}
5665

5766
return $dnsWireResponseMessage;
5867
}
5968

60-
6169
public function supports(DnsUpstream $dnsUpstream): bool
6270
{
63-
return in_array($dnsUpstream->getScheme(), ['udp', 'tcp', 'dns']) || $dnsUpstream->getScheme() === null;
71+
return $this->isUdp($dnsUpstream) || $this->isTcp($dnsUpstream);
72+
}
73+
74+
private function isUdp($dnsUpstream): bool
75+
{
76+
return in_array($dnsUpstream->getScheme(), ['udp', 'dns']) || $dnsUpstream->getScheme() === null;
77+
}
78+
79+
private function isTcp($dnsUpstream): bool
80+
{
81+
return $dnsUpstream->getScheme() === 'tcp';
6482
}
6583

6684
/**
67-
* Send DNS message using socket with the given protocol: UDP or TCP
68-
* @param string $protocol
85+
* Send DNS message using socket with the chosen protocol: `udp` or `tcp`
86+
* Allow a sender to force usage of a specific protocol (e.g. protocol blocked by network/firewall)
87+
*
88+
* @param string $protocol Protocol to use to send the message
6989
* @param string $address
7090
* @param MessageInterface $dnsRequestMessage
7191
*
7292
* @return MessageInterface
7393
* @throws Exception
7494
*/
75-
private function sendWithSocket(
95+
private function sendWith(
7696
string $protocol,
7797
string $address,
7898
MessageInterface $dnsRequestMessage
7999
): MessageInterface {
80-
$url = parse_url($address);
100+
$dnsWireMessage = $this->dnsMessageFactory->createDnsWireMessageFromMessage($dnsRequestMessage);
81101

82-
$socket = stream_socket_client($protocol . '://' . $url['host'] . ':' . $url['port'], $errno, $errstr, 4);
102+
if ($protocol === 'udp') {
103+
if (strlen($dnsWireMessage) <= static::EDNS_SIZE) { // Must use TCP if message is bigger
104+
$dnsWireResponseMessage = $this->udpTransport->send($address, $dnsWireMessage);
83105

84-
if ($socket === false) {
85-
throw new Exception('Unable to connect:' . $errno . ' - ' . $errstr);
86-
} else {
87-
$dnsMessage = $this->dnsMessageFactory->createDnsWireMessageFromMessage($dnsRequestMessage);
88-
89-
switch ($protocol) {
90-
case 'udp':
91-
if (isset($dnsMessage[static::EDNS_SIZE])) { // Must use TCP if message is bigger
92-
return $this->sendWithSocket('tcp', $address, $dnsRequestMessage);
93-
}
94-
95-
\fputs($socket, $dnsMessage);
96-
97-
$dnsWireResponseMessage = \fread($socket, static::EDNS_SIZE);
98-
if ($dnsWireResponseMessage === false) {
99-
throw new Exception('something happened');
100-
}
101-
102-
break;
103-
case 'tcp':
104-
\fputs($socket, $dnsMessage);
105-
$dnsWireResponseMessage = '';
106-
while (!feof($socket)) {
107-
$dnsWireResponseMessage .= fgets($socket, 512);
108-
}
109-
break;
110-
default:
111-
throw new InvalidArgumentException(
112-
"Only `tcp`, `udp` are supported protocol to be used with socket."
113-
);
106+
$message = $this->dnsMessageFactory->createMessageFromDnsWireMessage($dnsWireResponseMessage);
107+
// Only if message is not truncated response is returned, otherwise retry with TCP
108+
if (!$message->getHeader()->isTc()) {
109+
return $message;
110+
}
114111
}
115112
}
116113

117-
\fclose($socket);
114+
$dnsWireResponseMessage = $this->tcpTransport->send($address, $dnsWireMessage);
118115

119116
$message = $this->dnsMessageFactory->createMessageFromDnsWireMessage($dnsWireResponseMessage);
120117

121-
// Message was truncated, retry with TCP
122-
if ($message->getHeader()->isTc()) {
123-
return $this->sendWithSocket('tcp', $address, $dnsRequestMessage);
124-
}
125-
126118
return $message;
127119
}
128120

121+
/**
122+
* Clean up the protocol from URI supported by the client but which can not be used with transport (e.g. dns://).
123+
*
124+
* @param DnsUpstream $dnsUpstream
125+
*
126+
* @return string
127+
*/
128+
private function getSanitizedUpstreamAddress(DnsUpstream $dnsUpstream): string
129+
{
130+
return str_replace($dnsUpstream->getScheme() . '://', '', $dnsUpstream->getUri());
131+
}
132+
129133
/**
130134
* Enable recursion for the given DNS message
135+
*
131136
* @param MessageInterface $dnsRequestMessage
132137
*
133138
* @return MessageInterface
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NoGlitchYo\Dealdoh\Client\Transport;
6+
7+
use Exception;
8+
use NoGlitchYo\Dealdoh\Client\StdClient;
9+
10+
/**
11+
* Implement DNS Transport over TCP
12+
* @see https://tools.ietf.org/html/rfc7766
13+
*/
14+
class DnsOverTcpTransport implements DnsTransportInterface
15+
{
16+
public function send(string $address, string $dnsWireMessage): string
17+
{
18+
$url = parse_url($address);
19+
20+
$socket = @stream_socket_client('tcp://' . $url['host'] . ':' . $url['port'], $errno, $errstr, 4);
21+
22+
if ($socket === false) {
23+
throw new Exception('Unable to connect to DNS server: <' . $errno . '> ' . $errstr);
24+
}
25+
26+
$dnsWireMessage = pack('n', strlen($dnsWireMessage)) . $dnsWireMessage;
27+
stream_set_blocking($socket, false);
28+
if (!@fputs($socket, $dnsWireMessage)) {
29+
throw new Exception('Unable to write to DNS server: <' . $errno . '> ' . $errstr);
30+
}
31+
$dnsWireResponseMessage = '';
32+
while (!feof($socket)) {
33+
$chunk = fread($socket, StdClient::EDNS_SIZE);
34+
if ($chunk === false) {
35+
throw new Exception('DNS message transfer from DNS server failed');
36+
}
37+
38+
$dnsWireResponseMessage .= $chunk;
39+
}
40+
41+
if (!$this->hasHeader($dnsWireResponseMessage)) {
42+
throw new Exception("DNS message corrupted: no header was found.");
43+
}
44+
45+
if (!$this->hasData($dnsWireMessage)) {
46+
throw new Exception('DNS message corrupted: no data were found.');
47+
}
48+
49+
fclose($socket);
50+
51+
return substr($dnsWireResponseMessage, 2, $this->getLength($dnsWireResponseMessage));
52+
}
53+
54+
/**
55+
* Check if message has data.
56+
* @param string $dnsWireMessage
57+
*
58+
* @return bool
59+
*/
60+
private function hasData(string $dnsWireMessage)
61+
{
62+
return strlen($dnsWireMessage) > $this->getLength($dnsWireMessage);
63+
}
64+
65+
/**
66+
* Check if message has header
67+
* Response header is 12 bytes min.
68+
* @param string $dnsWireMessage
69+
*
70+
* @return bool
71+
*/
72+
private function hasHeader(string $dnsWireMessage)
73+
{
74+
return strlen($dnsWireMessage) >= 12;
75+
}
76+
77+
/**
78+
* Retrieve length of the message from the first 2 bytes
79+
* @see https://tools.ietf.org/html/rfc7766#section-8
80+
*
81+
* @param string $dnsWireMessage
82+
*
83+
* @return mixed
84+
*/
85+
private function getLength(string $dnsWireMessage)
86+
{
87+
return unpack('n', $dnsWireMessage)[1];
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NoGlitchYo\Dealdoh\Client\Transport;
6+
7+
use Exception;
8+
use NoGlitchYo\Dealdoh\Client\StdClient;
9+
10+
class DnsOverUdpTransport implements DnsTransportInterface
11+
{
12+
public function send(string $address, string $dnsWireMessage): string
13+
{
14+
$url = parse_url($address);
15+
$length = strlen($dnsWireMessage);
16+
$socket = @stream_socket_client('udp://' . $url['host'] . ':' . $url['port'], $errno, $errstr, 4);
17+
18+
if ($socket === false) {
19+
throw new Exception('Unable to connect to DNS server: <' . $errno . '> ' . $errstr);
20+
}
21+
22+
// Must use DNS over TCP if message is bigger
23+
if ($length > StdClient::EDNS_SIZE) {
24+
throw new Exception(
25+
sprintf(
26+
'DNS message is `%s` bytes, maximum `%s` bytes allowed. Use TCP transport instead',
27+
$length,
28+
StdClient::EDNS_SIZE
29+
)
30+
);
31+
}
32+
33+
if (!@fputs($socket, $dnsWireMessage)) {
34+
throw new Exception('Unable to write to DNS server: <' . $errno . '> ' . $errstr);
35+
}
36+
$dnsWireResponseMessage = fread($socket, StdClient::EDNS_SIZE);
37+
if ($dnsWireResponseMessage === false) {
38+
throw new Exception('Unable to read from DNS server: Error <' . $errno . '> ' . $errstr);
39+
}
40+
fclose($socket);
41+
42+
return $dnsWireResponseMessage;
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NoGlitchYo\Dealdoh\Client\Transport;
6+
7+
interface DnsTransportInterface
8+
{
9+
public function send(string $address, string $dnsWireMessage): string;
10+
}

‎src/Exception/DnsPoolResolveFailedException.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public static function unableToResolveFromClients(array $dnsClients): self
5050

5151
public static function unableToResolveFromUpstreams(array $dnsUpstreams): self
5252
{
53-
return new static('Unable to resolve DNS message', [], $dnsUpstreams, static::EC_UPSTREAMS_FAILED);
53+
return new static(
54+
'Unable to resolve DNS message from upstreams', [], $dnsUpstreams, static::EC_UPSTREAMS_FAILED
55+
);
5456
}
5557
}

‎src/Exception/InvalidDnsWireMessageException.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
namespace NoGlitchYo\Dealdoh\Exception;
44

55
use Exception;
6+
use Throwable;
67

78
class InvalidDnsWireMessageException extends Exception
89
{
9-
10+
public function __construct(string $dnsWireMessage, $message = "", $code = 0, Throwable $previous = null)
11+
{
12+
parent::__construct(sprintf("Invalid DNS wire message: `%s`", $dnsWireMessage), $code, $previous);
13+
}
1014
}

‎src/Factory/Dns/MessageFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function createMessageFromDnsWireMessage(string $dnsWireMessage): Message
7272
try {
7373
$dnsWireMessage = $this->parser->parseMessage($dnsWireMessage);
7474
} catch (InvalidArgumentException $exception) {
75-
throw new InvalidDnsWireMessageException();
75+
throw new InvalidDnsWireMessageException($dnsWireMessage);
7676
}
7777

7878
return self::createFromReactDnsMessage($dnsWireMessage);
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Please sign in to comment.