Skip to content

Commit 08a8bf6

Browse files
committed
initial
0 parents  commit 08a8bf6

9 files changed

Lines changed: 428 additions & 0 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor/

composer.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "solcloud/proxy",
3+
"require": {
4+
"php": ">= 7.1",
5+
"solcloud/http": "^1.0",
6+
"solcloud/curl": "^1.0"
7+
},
8+
"autoload": {
9+
"psr-4": {
10+
"Solcloud\\Proxy\\": "src/"
11+
}
12+
}
13+
}

src/AbstractProxy.php

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Solcloud\Proxy;
6+
7+
use InvalidArgumentException;
8+
use Solcloud\Curl\CurlRequest;
9+
use Solcloud\Http\Contract\IRequestDownloader;
10+
use Solcloud\Http\Exception\HttpException;
11+
use Solcloud\Http\Request;
12+
use Solcloud\Http\Response;
13+
use Solcloud\Proxy\Exception\InternalProxyException;
14+
use Solcloud\Proxy\Exception\ProxyException;
15+
use Throwable;
16+
17+
abstract class AbstractProxy
18+
{
19+
20+
/**
21+
* @var Request
22+
*/
23+
private $request;
24+
25+
/**
26+
* @var Response
27+
*/
28+
private $response;
29+
30+
/**
31+
* @var IRequestDownloader
32+
*/
33+
private $curlRequestFactory;
34+
35+
/**
36+
* @var string
37+
*/
38+
private $requestPostKey = 'request';
39+
40+
/**
41+
* @var int
42+
*/
43+
private $subsequentProxyProcessingOverheadMilliseconds = 1000;
44+
45+
/**
46+
* @var int
47+
*/
48+
private $numberOfHops = 1;
49+
50+
/**
51+
* @var string
52+
*/
53+
private $internalCommunicationUrl = '';
54+
55+
/**
56+
* @param int $numberOfInternalProxyAfterThisProxy Specify how many internal proxy is after this proxy
57+
* Use for timeout multiplication between internal proxies to carry timeout from target, for Client set to > 0
58+
* with request connect timeout 10 sec and request timeout 20 sec, whole request will have *maximum* timeout of
59+
* connectTimeoutMs + (($numberOfInternalProxyAfterThisProxy + 1) * (connectTimeoutMs + requestTimeoutMs + $subsequentProxyProcessingOverheadMilliseconds)) ms,
60+
* so eg. ProxyClient connecting to one Dispatcher that connect to one Endpoint that making final request to target will have *maximum*
61+
* of 103 sec [10000 + ((2 + 1) * (10000 + 20000 + 1000))] / 1000, so it is good idea to have internal proxies on network
62+
* with low latency and many php threats so only Endpoint is waiting maximum timeout of (connectTimeoutMs + requestTimeoutMs) for target website
63+
*
64+
* If on Client it is set to 0 although there are some Proxy after Client, than Client timeout on internal channel (unless you have really fast internal intercom and/or high connectTimeoutMs)
65+
* after maximum of (connectTimeoutMs + requestTimeoutMs) no matter what other proxies in chain are doing
66+
* --> this is kinda BC compatible (v1.0) solution for consistent timeouts from Request but unless you have really fast internal intercom and/or high connectTimeoutMs
67+
* you loose information about target url being timeout (you get dispatcher url timeout) and also exception is instanceof InternalProxyException
68+
*/
69+
public function __construct(int $numberOfInternalProxyAfterThisProxy, ?IRequestDownloader $requestDownloader = null)
70+
{
71+
if ($numberOfInternalProxyAfterThisProxy < 0) {
72+
throw new InvalidArgumentException('Value of $numberOfInternalProxyAfterThisProxy should be bigger or equal 0');
73+
}
74+
75+
$this->setResponse(new Response);
76+
$this->curlRequestFactory = $requestDownloader ?? new CurlRequest;
77+
$this->numberOfHops = $numberOfInternalProxyAfterThisProxy + 1;
78+
if ($numberOfInternalProxyAfterThisProxy === 0) {
79+
$this->subsequentProxyProcessingOverheadMilliseconds = 0;
80+
}
81+
}
82+
83+
/**
84+
* Process Request and print Response
85+
* @param Request $request if NULL Request will be parsed from POST data using requestPostKey
86+
*/
87+
public function process(Request $request = NULL): void
88+
{
89+
try {
90+
$this->setRequest($request === NULL ? $this->parseRequest() : $request);
91+
92+
$this->run();
93+
} catch (Throwable $ex) {
94+
if ($ex instanceof HttpException) {
95+
$this->response->setException($this->repackHttpException($ex));
96+
} else {
97+
$this->response->setException(new ProxyException($ex->getMessage(), $ex->getCode(), $ex));
98+
}
99+
}
100+
101+
$this->printResponse();
102+
}
103+
104+
/**
105+
* Repack some interesting exception
106+
* @return HttpException
107+
*/
108+
protected function repackHttpException(HttpException $ex): HttpException
109+
{
110+
if (!($ex instanceof InternalProxyException) && $ex->getLastUrl() !== '' && $ex->getLastUrl() === $this->internalCommunicationUrl) { // if last exception was on internal urls
111+
$ex = new InternalProxyException($ex->getMessage(), $ex->getCode(), $ex, $ex->getLastUrl(), $ex->getLastIP());
112+
}
113+
114+
return $ex;
115+
}
116+
117+
/**
118+
* Hook function called from process(), mostly for setting Response object either directly or by forwarding
119+
*/
120+
protected function run(): void
121+
{
122+
// empty hook
123+
}
124+
125+
/**
126+
* Try parse Request from POST data
127+
* @throws ProxyException
128+
*/
129+
protected function parseRequest(): Request
130+
{
131+
if (empty($_POST[$this->requestPostKey])) {
132+
throw new ProxyException('Request not found in POST');
133+
}
134+
135+
$request = @unserialize($_POST[$this->requestPostKey]);
136+
if ($request === FALSE) {
137+
throw new ProxyException('Unserialization of Request failed');
138+
}
139+
140+
return $request;
141+
}
142+
143+
/**
144+
* Send POST request with Request object to given url, parsing Response object from (hopefully serialized) response
145+
* @param string $url intercom url
146+
* @param array $aditionalData additional POST data for intercom
147+
*/
148+
protected function forward(string $url, array $aditionalData = [])
149+
{
150+
$this->internalCommunicationUrl = $url;
151+
152+
$postFields = array_merge(
153+
$aditionalData
154+
, [
155+
$this->requestPostKey => serialize($this->request),
156+
]
157+
);
158+
159+
$response = $this->curlRequestFactory->fetchResponse($this->createCommunicationRequest($url, $postFields));
160+
161+
$this->setResponseFromSerializedString($response->getBody());
162+
}
163+
164+
/**
165+
* Create POST Request object used for intercom
166+
*/
167+
private function createCommunicationRequest(string $url, array $postFields): Request
168+
{
169+
$request = new Request;
170+
$request
171+
->setMethod('POST')
172+
->setUrl($url)
173+
->setPostFields($postFields)
174+
->setFollowLocation(TRUE)
175+
->setVerifyHost($this->getRequest()->getVerifyHost())
176+
->setVerifyPeer($this->getRequest()->getVerifyPeer())
177+
->setConnectionTimeoutSec($this->getRequest()->getConnectionTimeoutSec())
178+
->setRequestTimeoutMs(
179+
(int) floor(
180+
$this->numberOfHops * (
181+
$this->getRequest()->getConnectionTimeoutMs()
182+
+ $this->getRequest()->getRequestTimeoutMs()
183+
+ $this->getSubsequentProxyProcessingOverheadMilliseconds()
184+
)
185+
)
186+
)
187+
;
188+
189+
return $request;
190+
}
191+
192+
/**
193+
* Print serialized response object
194+
*/
195+
protected function printResponse()
196+
{
197+
echo serialize($this->getResponse());
198+
}
199+
200+
/**
201+
* Try to set Response by unserializing $serializedResponseString
202+
* @throws ProxyException or subclass - if unserializition failed or Response has exception
203+
*/
204+
protected function setResponseFromSerializedString(string $serializedResponseString): void
205+
{
206+
$response = @unserialize($serializedResponseString);
207+
if ($response === FALSE) {
208+
throw new ProxyException('Unserialization of Response failed');
209+
}
210+
211+
$this->setResponse($response);
212+
}
213+
214+
protected function setRequest(Request $request): void
215+
{
216+
$this->request = $request;
217+
}
218+
219+
protected function getRequest(): Request
220+
{
221+
return $this->request;
222+
}
223+
224+
/**
225+
* @throws HttpException if $response getException() has one
226+
*/
227+
protected function setResponse(Response $response): void
228+
{
229+
$responseException = $response->getException();
230+
if ($responseException !== NULL && $responseException instanceof HttpException) {
231+
throw $responseException;
232+
}
233+
234+
$this->response = $response;
235+
}
236+
237+
/**
238+
* Set value based on connection latency and php time overhead from script start to performing internal request,
239+
* if script is performing time intensive task before proxy forward (db,api,..) increase this value on proxy before this proxy and before that and before ...
240+
* also if script is doing something after response arrived include this time too
241+
* Default value (1s) should be good, so unless you know what you are doing stay away from setting manually
242+
* If you decide to set it manually make sure you are setting good value for all proxies in whole proxy chain
243+
* @param int $subsequentProxyProcessingOverheadMilliseconds
244+
*/
245+
public function setSubsequentProxyProcessingOverheadMilliseconds(int $subsequentProxyProcessingOverheadMilliseconds): void
246+
{
247+
if ($subsequentProxyProcessingOverheadMilliseconds <= 0) {
248+
throw new InvalidArgumentException('Error $subsequentProxyProcessingOverheadMilliseconds should be bigger than 0');
249+
}
250+
251+
$this->subsequentProxyProcessingOverheadMilliseconds = $subsequentProxyProcessingOverheadMilliseconds;
252+
}
253+
254+
public function getResponse(): Response
255+
{
256+
return $this->response;
257+
}
258+
259+
protected function getCurlRequestFactory(): IRequestDownloader
260+
{
261+
return $this->curlRequestFactory;
262+
}
263+
264+
protected function getRequestPostKey(): string
265+
{
266+
return $this->requestPostKey;
267+
}
268+
269+
public function getSubsequentProxyProcessingOverheadMilliseconds(): int
270+
{
271+
return $this->subsequentProxyProcessingOverheadMilliseconds;
272+
}
273+
274+
}

src/DispatcherProxyExample.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Solcloud\Proxy;
6+
7+
use Solcloud\Proxy\Exception\ProxyException;
8+
9+
/**
10+
* Just example how dispatcher could be implemented
11+
* for real implementation design your own
12+
*/
13+
class DispatcherProxyExample extends AbstractProxy
14+
{
15+
16+
protected $proxies = [];
17+
18+
protected function run(): void
19+
{
20+
$this->anyProxyForward();
21+
}
22+
23+
protected function anyProxyForward(): void
24+
{
25+
if (empty($this->proxies)) {
26+
throw new ProxyException('No proxies defined');
27+
}
28+
29+
$proxy = $this->proxies[mt_rand(0, count($this->proxies) - 1)];
30+
if (!empty($proxy['ips'])) {
31+
$outIp = $proxy['ips'][mt_rand(0, count($proxy['ips']) - 1)];
32+
$this->getRequest()->setOutgoingIp($outIp);
33+
}
34+
35+
$this->forward($proxy['url']);
36+
}
37+
38+
public function addProxy(string $url, array $ipAddresses = []): void
39+
{
40+
$this->proxies[] = [
41+
'url' => $url,
42+
'ips' => $ipAddresses,
43+
];
44+
}
45+
46+
public function getProxies(): array
47+
{
48+
return $this->proxies;
49+
}
50+
51+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Solcloud\Proxy\Exception;
4+
5+
class DomainLimitException extends ProxyException
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Solcloud\Proxy\Exception;
4+
5+
class InternalProxyException extends ProxyException
6+
{
7+
8+
}

src/Exception/ProxyException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Solcloud\Proxy\Exception;
4+
5+
use Solcloud\Http\Exception\HttpException;
6+
7+
class ProxyException extends HttpException
8+
{
9+
10+
}

0 commit comments

Comments
 (0)