/
VhostContainer.php
241 lines (211 loc) · 9.14 KB
/
VhostContainer.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
<?php
namespace Aerys;
class VhostContainer implements \Countable, Monitor {
private $vhosts = [];
private $cachedVhostCount = 0;
private $httpDrivers = [];
private $defaultHttpDriver;
private $setupHttpDrivers = [];
private $setupArgs;
public function __construct(HttpDriver $driver) {
$this->defaultHttpDriver = $driver;
}
/**
* Add a virtual host to the collection.
*
* @param \Aerys\Vhost $vhost
* @return void
*/
public function use(Vhost $vhost) {
$vhost = clone $vhost; // do not allow change of state after use()
$this->preventCryptoSocketConflict($vhost);
foreach ($vhost->getIds() as $id) {
if (isset($this->vhosts[$id])) {
list($host, $port, $interfaceAddr, $interfacePort) = explode(":", $id);
throw new \LogicException(
$host === "*"
? "Cannot have two default hosts " . ($interfacePort == $port ? "" : "on port $port ") . "on the same interface ($interfaceAddr:$interfacePort)"
: "Cannot have two hosts with the same name ($host" . ($interfacePort == $port ? "" : ":$port") . ") on the same interface ($interfaceAddr:$interfacePort)"
);
}
$this->vhosts[$id] = $vhost;
}
$this->addHttpDriver($vhost);
$this->cachedVhostCount++;
}
// TLS is inherently bound to a specific interface. Unencrypted wildcard hosts will not work on encrypted interfaces and vice versa.
private function preventCryptoSocketConflict(Vhost $new) {
foreach ($this->vhosts as $old) {
// If both hosts are encrypted or both unencrypted there is no conflict
if ($new->isEncrypted() == $old->isEncrypted()) {
continue;
}
foreach ($old->getInterfaces() as list($address, $port)) {
if (in_array($port, $new->getPorts($address))) {
throw new \Error(
sprintf(
"Cannot register encrypted host `%s`; unencrypted " .
"host `%s` registered on conflicting interface `%s`",
$new->IsEncrypted() ? $new->getName() : $old->getName(),
$new->IsEncrypted() ? $old->getName() : $new->getName(),
"$address:$port"
)
);
}
}
}
}
private function addHttpDriver(Vhost $vhost) {
$driver = $vhost->getHttpDriver() ?? $this->defaultHttpDriver;
foreach ($vhost->getInterfaces() as list($address, $port)) {
$defaultDriver = $this->httpDrivers[$port][$address[0] == "/" ? "" : \strlen(inet_pton($address)) === 4 ? "0.0.0.0" : "::"] ?? $driver;
if (($this->httpDrivers[$port][$address] ?? $defaultDriver) !== $driver) {
throw new \Error(
"Cannot use two different HttpDriver instances on an equivalent address-port pair"
);
}
if ($address == "0.0.0.0" || $address == "::") {
foreach ($this->httpDrivers[$port] ?? [] as $oldAddr => $oldDriver) {
if ($oldDriver !== $driver && (\strlen(inet_pton($address)) === 4) == ($address == "0.0.0.0")) {
throw new \Error(
"Cannot use two different HttpDriver instances on an equivalent address-port pair"
);
}
}
}
$this->httpDrivers[$port][$address] = $driver;
}
$hash = spl_object_hash($driver);
if ($this->setupArgs && $this->setupHttpDrivers[$hash] ?? false) {
$driver->setup(...$this->setupArgs);
$this->setupHttpDrivers[$hash] = true;
}
}
public function setupHttpDrivers(...$args) {
if ($this->setupHttpDrivers) {
throw new \Error("Can setup http drivers only once");
}
$this->setupArgs = $args;
foreach ($this->httpDrivers as $drivers) {
foreach ($drivers as $driver) {
$hash = spl_object_hash($driver);
if ($this->setupHttpDrivers[$hash] ?? false) {
continue;
}
$this->setupHttpDrivers[$hash] = true;
$driver->setup(...$args);
}
}
}
/**
* Select the suited HttpDriver instance, filtered by address and port pair.
*/
public function selectHttpDriver($address, $port) {
return $this->httpDrivers[$port][$address] ??
$this->httpDrivers[$port][\strpos($address, ":") === false ? "0.0.0.0" : "::"];
}
/**
* Select a virtual host match for the specified request according to RFC 7230 criteria.
*
* Note: For HTTP/1.0 requests (aka omitting a Host header), a proper Vhost will only ever be returned
* if there is a matching wildcard host.
*
* @param \Aerys\InternalRequest $ireq
* @return Vhost|null Returns a Vhost object and boolean TRUE if a valid host selected, FALSE otherwise
* @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.2
* @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.6.1.1
*/
public function selectHost(InternalRequest $ireq) {
$client = $ireq->client;
$serverId = ":{$client->serverAddr}:{$client->serverPort}";
$explicitHostId = "{$ireq->uriHost}:{$ireq->uriPort}{$serverId}";
if (isset($this->vhosts[$explicitHostId])) {
return $this->vhosts[$explicitHostId];
}
$addressWildcardHost = "*:{$ireq->uriPort}{$serverId}";
if (isset($this->vhosts[$addressWildcardHost])) {
return $this->vhosts[$addressWildcardHost];
}
$portWildcardHostId = "{$ireq->uriHost}:0{$serverId}";
if (isset($this->vhosts[$portWildcardHostId])) {
return $this->vhosts[$portWildcardHostId];
}
$addressPortWildcardHost = "*:0{$serverId}";
if (isset($this->vhosts[$addressPortWildcardHost])) {
return $this->vhosts[$addressPortWildcardHost];
}
if ($client->serverAddr[0] == "/") { // unix domain socket
// there is no such thing like interface wildcards for unix domain sockets
return null; // nothing found
}
$wildcardIP = \strpos($client->serverAddr, ":") === false ? "0.0.0.0" : "[::]";
$serverId = ":$wildcardIP:{$client->serverPort}";
$explicitHostId = "{$ireq->uriHost}:{$ireq->uriPort}{$serverId}";
if (isset($this->vhosts[$explicitHostId])) {
return $this->vhosts[$explicitHostId];
}
$addressWildcardHost = "*:{$ireq->uriPort}{$serverId}";
if (isset($this->vhosts[$addressWildcardHost])) {
return $this->vhosts[$addressWildcardHost];
}
$portWildcardHostId = "{$ireq->uriHost}:0{$serverId}";
if (isset($this->vhosts[$portWildcardHostId])) {
return $this->vhosts[$portWildcardHostId];
}
$addressPortWildcardHost = "*:0{$serverId}";
if (isset($this->vhosts[$addressPortWildcardHost])) {
return $this->vhosts[$addressPortWildcardHost];
}
return null; // nothing found
}
/**
* Retrieve an array of unique socket addresses on which hosts should listen.
*
* @return array Returns an array of unique host addresses in the form: tcp://ip:port
*/
public function getBindableAddresses(): array {
return array_unique(array_merge(...array_values(array_map(function ($vhost) {
return $vhost->getBindableAddresses();
}, $this->vhosts))));
}
/**
* Retrieve stream encryption settings by bind address.
*
* @return array
*/
public function getTlsBindingsByAddress(): array {
$bindMap = [];
$sniNameMap = [];
foreach ($this->vhosts as $vhost) {
if (!$vhost->isEncrypted()) {
continue;
}
foreach ($vhost->getBindableAddresses() as $bindAddress) {
$contextArr = $vhost->getTlsContextArr();
$bindMap[$bindAddress] = $contextArr;
if ($vhost->hasName()) {
$sniNameMap[$bindAddress][$vhost->getName()] = $contextArr["local_cert"];
}
}
}
// If we have multiple different TLS certs on the same bind address we need to assign
// the "SNI_server_name" key to enable the SNI extension.
foreach (array_keys($bindMap) as $bindAddress) {
if (isset($sniNameMap[$bindAddress]) && count($sniNameMap[$bindAddress]) > 1) {
$bindMap[$bindAddress]["SNI_server_name"] = $sniNameMap[$bindAddress];
}
}
return $bindMap;
}
public function count() {
return $this->cachedVhostCount;
}
public function __debugInfo() {
return [
"vhosts" => $this->vhosts,
];
}
public function monitor(): array {
return array_map(function ($vhost) { return $vhost->monitor(); }, $this->vhosts);
}
}