Skip to content

Commit 1712012

Browse files
committed
Add HostsFile configuration loader and parser
1 parent 457fbc4 commit 1712012

2 files changed

Lines changed: 188 additions & 0 deletions

File tree

src/Config/HostsFile.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace React\Dns\Config;
4+
5+
use RuntimeException;
6+
7+
/**
8+
* Represents a static hosts file which maps hostnames to IPs
9+
*
10+
* Hosts files are used on most systems to avoid actually hitting the DNS for
11+
* certain common hostnames.
12+
*
13+
* Most notably, this file usually contains an entry to map "localhost" to the
14+
* local IP. Windows is a notable exception here, as Windows does not actually
15+
* include "localhost" in this file by default.
16+
*
17+
* This class mostly exists to abstract the parsing/extraction process so this
18+
* can be replaced with a faster alternative in the future.
19+
*/
20+
class HostsFile
21+
{
22+
/**
23+
* Returns the default path for the hosts file on this system
24+
*
25+
* @return string
26+
* @codeCoverageIgnore
27+
*/
28+
public static function getDefaultPath()
29+
{
30+
// use static path for all Unix-based systems
31+
if (DIRECTORY_SEPARATOR !== '\\') {
32+
return '/etc/hosts';
33+
}
34+
35+
// Windows actually stores the path in the registry under
36+
// \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath
37+
$path = '%SystemRoot%\\system32\drivers\etc\hosts';
38+
39+
$base = getenv('SystemRoot');
40+
if ($base === false) {
41+
$base = 'C:\\Windows';
42+
}
43+
44+
return str_replace('%SystemRoot%', $base, $path);
45+
}
46+
47+
/**
48+
* Loads a hosts file (from the given path or default location)
49+
*
50+
* Note that this method blocks while loading the given path and should
51+
* thus be used with care! While this should be relatively fast for normal
52+
* hosts file, this may be an issue if this file is located on a slow device
53+
* or contains an excessive number of entries. In particular, this method
54+
* should only be executed before the loop starts, not while it is running.
55+
*
56+
* @param ?string $path (optional) path to hosts file or null=load default location
57+
* @return self
58+
* @throws RuntimeException if the path can not be loaded (does not exist)
59+
*/
60+
public static function loadFromPathBlocking($path = null)
61+
{
62+
if ($path === null) {
63+
$path = self::getDefaultPath();
64+
}
65+
66+
$contents = @file_get_contents($path);
67+
if ($contents === false) {
68+
throw new RuntimeException('Unable to load hosts file "' . $path . '"');
69+
}
70+
71+
return new self($contents);
72+
}
73+
74+
/**
75+
* Instantiate new hosts file with the given hosts file contents
76+
*
77+
* @param string $contents
78+
*/
79+
public function __construct($contents)
80+
{
81+
// remove all comments from the contents
82+
$contents = preg_replace('/ *#.*/', '', strtolower($contents));
83+
84+
$this->contents = $contents;
85+
}
86+
87+
/**
88+
* Returns all IPs for the given hostname
89+
*
90+
* @param string $name
91+
* @return string[]
92+
*/
93+
public function getIpsForHost($name)
94+
{
95+
$name = strtolower($name);
96+
97+
$ips = array();
98+
foreach (preg_split('/\r?\n/', $this->contents) as $line) {
99+
$parts = preg_split('/\s+/', $line);
100+
$ip = array_shift($parts);
101+
if ($parts && array_search($name, $parts) !== false) {
102+
$ips[] = $ip;
103+
}
104+
}
105+
106+
return $ips;
107+
}
108+
}

tests/Config/HostsFileTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace React\Tests\Dns\Config;
4+
5+
use React\Tests\Dns\TestCase;
6+
use React\Dns\Config\HostsFile;
7+
8+
class HostsFileTest extends TestCase
9+
{
10+
public function testLoadsFromDefaultPath()
11+
{
12+
$hosts = HostsFile::loadFromPathBlocking();
13+
14+
$this->assertInstanceOf('React\Dns\Config\HostsFile', $hosts);
15+
}
16+
17+
public function testDefaultShouldHaveLocalhostMapped()
18+
{
19+
if (DIRECTORY_SEPARATOR === '\\') {
20+
$this->markTestSkipped('Not supported on Windows');
21+
}
22+
23+
$hosts = HostsFile::loadFromPathBlocking();
24+
25+
$this->assertContains('127.0.0.1', $hosts->getIpsForHost('localhost'));
26+
}
27+
28+
/**
29+
* @expectedException RuntimeException
30+
*/
31+
public function testLoadThrowsForInvalidPath()
32+
{
33+
HostsFile::loadFromPathBlocking('does/not/exist');
34+
}
35+
36+
public function testContainsSingleLocalhostEntry()
37+
{
38+
$hosts = new HostsFile('127.0.0.1 localhost');
39+
40+
$this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost'));
41+
$this->assertEquals(array(), $hosts->getIpsForHost('example.com'));
42+
}
43+
44+
public function testSkipsComments()
45+
{
46+
$hosts = new HostsFile('# start' . PHP_EOL .'#127.0.0.1 localhost' . PHP_EOL . '127.0.0.2 localhost # example.com');
47+
48+
$this->assertEquals(array('127.0.0.2'), $hosts->getIpsForHost('localhost'));
49+
$this->assertEquals(array(), $hosts->getIpsForHost('example.com'));
50+
}
51+
52+
public function testContainsSingleLocalhostEntryWithCaseIgnored()
53+
{
54+
$hosts = new HostsFile('127.0.0.1 LocalHost');
55+
56+
$this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('LOCALHOST'));
57+
}
58+
59+
public function testEmptyFileContainsNothing()
60+
{
61+
$hosts = new HostsFile('');
62+
63+
$this->assertEquals(array(), $hosts->getIpsForHost('example.com'));
64+
}
65+
66+
public function testSingleEntryWithMultipleNames()
67+
{
68+
$hosts = new HostsFile('127.0.0.1 localhost example.com');
69+
70+
$this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('example.com'));
71+
$this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost'));
72+
}
73+
74+
public function testMergesEntriesOverMultipleLines()
75+
{
76+
$hosts = new HostsFile("127.0.0.1 localhost\n127.0.0.2 localhost\n127.0.0.3 a localhost b\n127.0.0.4 a localhost");
77+
78+
$this->assertEquals(array('127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'), $hosts->getIpsForHost('localhost'));
79+
}
80+
}

0 commit comments

Comments
 (0)