Hey Kazuldur, our people sent us this, saying your people might be interested:
http://www.satoshigalaxy.xyz/tmp/faucetbox.patch.txt
Here's a detailed explanation of what this patch does, in case it doesn't get accepted by FaucetBOX team, but you (faucet owner) still want to apply it.
NOTE: You should never trust third-party code unless you have complete understanding of what it does. I'll try my best to break down this patch and explain what it does, but if you're new to PHP it might be a good idea to avoid it.
The problem:Sometimes, FaucetBOX takes too long to respond. Meanwhile, all PHP scripts executed by SAPI have a "max_execution_time" setting, mercilessly cutting execution off at some point.
So, for example, if you have this pseudo-code:
$fb = new FaucetBox(blah);
$fb->send(blah)
echo "OK, Sent some satoshi";
-- it's quite possible to arrive at situation when your "echo" IS NEVER executed, because "$fb->send()" took too much time.
Before we tackle this problem, we shall also allow faucetbox.php to report errors a little bit better and also handle some more regular errors.
Connection error patch:Let's imagine a situation when faucetbox.com doesn't connect at all.
(To test such situation, change
-protected $api_base = "https://faucetbox.com/api/v1/";
+protected $api_base = "http://localhost:91234/";
where "91234" is an unused port on your machine.)
In __execPHP (the "fopen" method):
$ctx = stream_context_create($opts);
- $fp = fopen($this->api_base . $method, 'rb', null, $ctx);
+ $fp = @fopen($this->api_base . $method, 'rb', null, $ctx);
+ if (!$fp) {
+ return array(
+ 'status' => 503,
+ 'message' => 'Connection error',
+ );
+ }
$response = stream_get_contents($fp);
if($response && !$this->disable_curl) {
$this->curl_warning = true;
fopen may echo warnings, so we silence it with an "@".
At the same time we add an error check to make sure it doesn't return NULL, for broken connections.
In __execCURL (the "curl" method):
$response = curl_exec($ch);
- if(!$response) {
- $response = $this->__execPHP($method, $params);
- }
curl_close($ch);
+ if(!$response) {
+ return array(
+ 'status' => 504,
+ 'message' => 'Connection error',
+ );
+ }
return $response;
Same deal with curl.
Note, that this changes the behavior of those 2 methods: originally they would either return JSON, either NULL/false, now they can also return an array. The error codes 503 and 504 were picked from the HTTP (they are for "503 (Service unavailable)" and "504 (Gateway timeout)"), but most importantly, they are currently unused by FaucetBOX.
Also note! Originally, faucetbox.php tries to use curl (unless $disable_curl is true) and if that fails, tries to use "fopen". The above change removes the "fopen" fallback from the curl method: if it failed, it failed, end of story.
To handle this new array, we also make this change in __exec:
if($this->disable_curl) {
$response = $this->__execPHP($method, $params);
} else {
$response = $this->__execCURL($method, $params);
}
- $response = json_decode($response, true);
+ if(is_array($response)) { //connection error
+ return $response;
+ }
+ $response = @json_decode($response, true);
This way, we now ALWAYS get an array, and probably will NEVER hit the following code:
return array(
'success' => false,
'message' => 'Unknown error.',
'html' => '<div class="alert alert-danger">Unknown error.</div>',
'response' => json_encode($r)
);
in "send".
Invalid response patch:Let's imagine our next problem. Say faucetbox.com connects, but returns some unexpected data, instead of JSON. Why would that happen? Well, let's say they use CloudFlare protection; if they're being (D)DoS'ed, CloudFlare might return a response, but full of it's HTML instead of JSON. Some other proxying service or a gateway on their chain might do something similar.
(To test such situation, change
-protected $api_base = "https://faucetbox.com/api/v1/";
+protected $api_base = "http://localhost:80/";
where "localhost:80" is an actual webserver you control, and that would return some garbage/html/404 error page instead of expected JSON.)
Here's how we deal with it. In __exec:
+ $response = @json_decode($response, true);
if($response) {
$this->last_status = $response['status'];
} else {
$this->last_status = null;
+ $response = array(
+ 'status' => 502,
+ 'message' => 'Invalid response',
+ );
}
return $response;
Again, we silence possible json_decode warnings with an "@".
Additionally, if we can't parse it correctly, we return an array anyway.
502 error code (HTTP's "Bad gateway/Invalid response from upstream server") was picked for similar reasons: it's currently unused by FaucetBOX.
Timeout patch:Now, we're ready to handle the more serious problem of timeouts. To test a timeout problem you might need a custom socket handler that never returns. If you have PHP 5.4, you can use the nifty 'php -S' feature, you might also be able to achive something like that with netcat, or a python script like this:
#!/usr/bin/env python
import socket
import time
host = ''
port = 50000
waste_seconds = 600
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host,port))
s.listen(5)
while 1:
client, address = s.accept()
data = client.recv(1024)
print ("Connection from", address)
if data:
for i in range(0, waste_seconds):
#client.send()
time.sleep(1)
print ("Letting go")
client.close()
This script will run a TCP server on port 50000 and hang for "waste_seconds" (600) seconds instead of providing a response.
(Again, to test, we will change the api_base like that
-protected $api_base = "https://faucetbox.com/api/v1/";
+protected $api_base = "http://localhost:50000/";
where "50000" is the port our dummy server runs on.)
On to patching. Here, implemenations may differ quite a bit, but I think using half of PHP's max_execution_time is fair for both faucetbox and our script.
{
protected $api_key;
protected $currency;
+ public $timeout = null;
public $last_status = null;
protected $api_base = "https://faucetbox.com/api/v1/";
- declared new variable to handle timeouts
public function __exec($method, $params = array()) {
$this->last_status = null;
+ if($this->timeout === null) {
+ $socket_timeout = ini_get('default_socket_timeout');
+ $script_timeout = ini_get('max_execution_time');
+ $this->timeout = min($script_timeout / 2, $socket_timeout);
+ }
if($this->disable_curl) {
- set it to something, if it's NULL (half of max_execution_time)
+ public function setTimeout($timeout) {
+ $this->timeout = $timeout;
+ }
+
- allow to change it. This breaks faucetbox.php's coding standards a bit.
"http" => array(
"method" => "POST",
"header" => "Content-type: application/x-www-form-urlencoded\r\n",
- "content" => http_build_query($params)
+ "content" => http_build_query($params),
+ "timeout" => $this->timeout,
),
"ssl" => array(
"verify_peer" => $this->verify_peer
)
);
- using it in "fopen" method (__execPHP)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, (int)$this->timeout);
$response = curl_exec($ch);
- using it in "curl" method (__execCURL)
Notes: for streams, floats are allowed as "timeout" parameter, but for curl we should pass ints. In either case, if timeout is 0, there's no timeout (old behavior).
With all those patches applied, we can now do this:
$fb = new FaucetBOX();
$fb->setTimeout(NULL); //auto-generate timeout, default
$fb->setTimeout(0); //no timeout
$fb->setTimeout(10); //timeout is 10 seconds
$fb->send(....);
Bonus patch: public function send($to, $amount, $referral = "false") {
+ if ($referral === false) $referral = "false";
+ if ($referral === true) $referral = "true";
FaucetBOX library's send method expects $referral as string. This might be counter-intuitive to PHP devs, who would assume it's a bool.
This small patch allows for both bool and string to be used, i.e:
$fb->send('addr', 10000, true);
$fb->send('addr', 10000, 'true');
//same result
Phew! That took longer to explain, than to implement :)