Skip to content

Commit 1360508

Browse files
committed
ConnectionManagerSelective now accepts connector list in constructor
1 parent dd4a2e6 commit 1360508

File tree

3 files changed

+254
-161
lines changed

3 files changed

+254
-161
lines changed

README.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,56 @@ ALL of the given `ConnectionManager`s at once, until the first one succeeds.
9393

9494
### Selective
9595

96-
The `ConnectionManagerSelective()` manages several `Connector`s and forwards connection through either of
97-
those besed on lists similar to to firewall or networking access control lists (ACLs).
96+
The `ConnectionManagerSelective($connectors)` manages a list of `Connector`s and
97+
forwards each connection through the first matching one.
98+
This can be used to implement networking access control lists (ACLs) or firewill
99+
rules like a blacklist or whitelist.
98100

99-
This allows fine-grained control on how to handle outgoing connections, like rejecting advertisements,
100-
delaying HTTP requests, or forwarding HTTPS connection through a foreign country.
101+
This allows fine-grained control on how to handle outgoing connections, like
102+
rejecting advertisements, delaying unencrypted HTTP requests or forwarding HTTPS
103+
connection through a foreign country.
104+
105+
If none of the entries in the list matches, the connection will be rejected.
106+
This can be used to implement a very simple whitelist like this:
107+
108+
```php
109+
$selective = new ConnectionManagerSelective(array(
110+
'github.com' => $connector,
111+
'*:443' => $connector
112+
));
113+
```
114+
115+
If you want to implement a blacklist (i.e. reject only certain targets), make
116+
sure to add a default target to the end of the list like this:
117+
118+
```php
119+
$reject = new ConnectionManagerReject();
120+
$selective = new ConnectionManagerSelective(array(
121+
'ads.example.com' => $reject,
122+
'*:80-81' => $reject,
123+
'*' => $connector
124+
));
125+
```
126+
127+
Similarly, you can also combine any other the other connectors to implement more
128+
advanced connection setups, such as delaying unencrypted connections only and
129+
retrying unreliable hosts:
101130

102131
```php
103-
$connectorSelective->addConnectionManagerFor($connector, $targetHost, $targetPort, $priority);
132+
// delay connection by 2 seconds
133+
$delayed = new ConnectionManagerDelay($connector, $loop, 2.0);
134+
135+
// maximum of 3 tries, each taking no longer than 3 seconds
136+
$retry = new ConnectionManagerRepeat(
137+
new ConnectionManagerTimeout($connector, $loop, 3.0),
138+
2
139+
);
140+
141+
$selective = new ConnectionManagerSelective(array(
142+
'*:80' => $delayed,
143+
'unreliable.example.com' => $retry,
144+
'*' => $connector
145+
));
104146
```
105147

106148
## Install

src/Multiple/ConnectionManagerSelective.php

Lines changed: 70 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -9,148 +9,94 @@
99

1010
class ConnectionManagerSelective implements ConnectorInterface
1111
{
12-
const MATCH_ALL = '*';
12+
private $managers;
1313

14-
private $targets = array();
15-
16-
public function create($host, $port)
14+
/**
15+
*
16+
* @param ConnectorInterface[] $managers
17+
*/
18+
public function __construct(array $managers)
1719
{
18-
try {
19-
$cm = $this->getConnectionManagerFor($host, $port);
20-
}
21-
catch (UnderflowException $e) {
22-
return Promise\reject($e);
23-
}
24-
return $cm->create($host, $port);
25-
}
20+
foreach ($managers as $filter => $manager) {
21+
$host = $filter;
22+
$portMin = 0;
23+
$portMax = 65535;
2624

27-
public function addConnectionManagerFor($connectionManager, $targetHost=self::MATCH_ALL, $targetPort=self::MATCH_ALL, $priority=0)
28-
{
29-
$this->targets []= array(
30-
'connectionManager' => $connectionManager,
31-
'matchHost' => $this->createMatcherHost($targetHost),
32-
'matchPort' => $this->createMatcherPort($targetPort),
33-
'host' => $targetHost,
34-
'port' => $targetPort,
35-
'priority' => $priority
36-
);
37-
38-
// return the key as new entry ID
39-
end($this->targets);
40-
$id = key($this->targets);
41-
42-
// sort array by priority
43-
$targets =& $this->targets;
44-
uksort($this->targets, function ($a, $b) use ($targets) {
45-
$pa = $targets[$a]['priority'];
46-
$pb = $targets[$b]['priority'];
47-
return ($pa < $pb ? -1 : ($pa > $pb ? 1 : ($a - $b)));
48-
});
49-
50-
return $id;
51-
}
25+
// search colon (either single one OR preceded by "]" due to IPv6)
26+
$colon = strrpos($host, ':');
27+
if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) {
28+
if (!isset($host[$colon + 1])) {
29+
throw new InvalidArgumentException('Entry "' . $filter . '" has no port after colon');
30+
}
5231

53-
public function getConnectionManagerEntries()
54-
{
55-
return $this->targets;
56-
}
32+
$minus = strpos($host, '-', $colon);
33+
if ($minus === false) {
34+
$portMin = $portMax = (int)substr($host, $colon + 1);
5735

58-
public function removeConnectionManagerEntry($id)
59-
{
60-
unset($this->targets[$id]);
61-
}
36+
if (substr($host, $colon + 1) !== (string)$portMin) {
37+
throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port after colon');
38+
}
39+
} else {
40+
$portMin = (int)substr($host, $colon + 1, ($minus - $colon));
41+
$portMax = (int)substr($host, $minus + 1);
6242

63-
public function getConnectionManagerFor($targetHost, $targetPort)
64-
{
65-
foreach ($this->targets as $target) {
66-
if ($target['matchPort']($targetPort) && $target['matchHost']($targetHost)) {
67-
return $target['connectionManager'];
68-
}
69-
}
70-
throw new UnderflowException('No connection manager for given target found');
71-
}
43+
if (substr($host, $colon + 1) !== ($portMin . '-' . $portMax)) {
44+
throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port range after colon');
45+
}
7246

73-
// *
74-
// singlePort
75-
// startPort - targetPort
76-
// port1, port2, port3
77-
// startPort - targetPort, portAdditional
78-
public function createMatcherPort($pattern)
79-
{
80-
if ($pattern === self::MATCH_ALL) {
81-
return function() {
82-
return true;
83-
};
84-
} else if (strpos($pattern, ',') !== false) {
85-
$checks = array();
86-
foreach (explode(',', $pattern) as $part) {
87-
$checks []= $this->createMatcherPort(trim($part));
88-
}
89-
return function ($port) use ($checks) {
90-
foreach ($checks as $check) {
91-
if ($check($port)) {
92-
return true;
47+
if ($portMin > $portMax) {
48+
throw new InvalidArgumentException('Entry "' . $filter . '" has port range mixed up');
9349
}
9450
}
95-
return false;
96-
};
97-
} else if (preg_match('/^(\d+)$/', $pattern, $match)) {
98-
$single = $this->coercePort($match[1]);
99-
return function ($port) use ($single) {
100-
return ($port == $single);
101-
};
102-
} else if (preg_match('/^(\d+)\s*\-\s*(\d+)$/', $pattern, $match)) {
103-
$start = $this->coercePort($match[1]);
104-
$end = $this->coercePort($match[2]);
105-
if ($start >= $end) {
106-
throw new InvalidArgumentException('Invalid port range given');
51+
$host = substr($host, 0, $colon);
52+
}
53+
54+
if ($host === '') {
55+
throw new InvalidArgumentException('Entry "' . $filter . '" has an empty host');
56+
}
57+
58+
if (!$manager instanceof ConnectorInterface) {
59+
throw new InvalidArgumentException('Entry "' . $filter . '" is not a valid connector');
10760
}
108-
return function($port) use ($start, $end) {
109-
return ($port >= $start && $port <= $end);
110-
};
111-
} else {
112-
throw new InvalidArgumentException('Invalid port matcher given');
11361
}
62+
63+
$this->managers = $managers;
11464
}
11565

116-
private function coercePort($port)
66+
public function create($host, $port)
11767
{
118-
// TODO: check 0-65535
119-
return (int)$port;
68+
try {
69+
$connector = $this->getConnectorForTarget($host, $port);
70+
} catch (UnderflowException $e) {
71+
return Promise\reject($e);
72+
}
73+
return $connector->create($host, $port);
12074
}
12175

122-
// *
123-
// targetHostname
124-
// targetIp
125-
// targetHostname, otherTargetHostname, anotherTargetHostname
126-
// TODO: targetIp/netmaskNum
127-
// TODO: targetIp/netmaskIp
128-
public function createMatcherHost($pattern)
76+
private function getConnectorForTarget($targetHost, $targetPort)
12977
{
130-
if ($pattern === self::MATCH_ALL) {
131-
return function() {
132-
return true;
133-
};
134-
} else if (strpos($pattern, ',') !== false) {
135-
$checks = array();
136-
foreach (explode(',', $pattern) as $part) {
137-
$checks []= $this->createMatcherHost(trim($part));
138-
}
139-
return function ($host) use ($checks) {
140-
foreach ($checks as $check) {
141-
if ($check($host)) {
142-
return true;
143-
}
78+
foreach ($this->managers as $host => $connector) {
79+
$portMin = 0;
80+
$portMax = 65535;
81+
82+
// search colon (either single one OR preceded by "]" due to IPv6)
83+
$colon = strrpos($host, ':');
84+
if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) {
85+
$minus = strpos($host, '-', $colon);
86+
if ($minus === false) {
87+
$portMin = $portMax = (int)substr($host, $colon + 1);
88+
} else {
89+
$portMin = (int)substr($host, $colon + 1, ($minus - $colon));
90+
$portMax = (int)substr($host, $minus + 1);
14491
}
145-
return false;
146-
};
147-
} else if (is_string($pattern)) {
148-
$pattern = strtolower($pattern);
149-
return function($target) use ($pattern) {
150-
return fnmatch($pattern, strtolower($target));
151-
};
152-
} else {
153-
throw new InvalidArgumentException('Invalid host matcher given');
92+
$host = trim(substr($host, 0, $colon), '[]');
93+
}
94+
95+
if ($targetPort >= $portMin && $targetPort <= $portMax && fnmatch($host, $targetHost)) {
96+
return $connector;
97+
}
15498
}
99+
100+
throw new UnderflowException('No connector for given target found');
155101
}
156102
}

0 commit comments

Comments
 (0)