My application has a production or development setting that I can toggle. When setting up the server, I set up this flag in Applications > Mamp > Conf > Apache > httpd.conf. Its purpose is to give my local API directory a server alias. It also defines the document root etc.
Listen 44447
<VirtualHost *:44447>
DocumentRoot "/Users/user/Desktop/PushChatServer/api"
ServerName 192.168.1.5:44447
ServerAlias pushchat.local
CustomLog "/Users/user/Desktop/PushChatServer/log/apache_access.log" combined
ErrorLog "/Users/user/Desktop/PushChatServer/log/apache_error.log"
SetEnv APPLICATION_ENV development
php_flag magic_quotes_gpc off
<Directory "/Users/user/Desktop/PushChatServer/api">
Options Indexes MultiViews FollowSymLinks
AllowOverride All
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
My goal is to set up a global variable in my api and pass it to another file. The php file my app uses is called api.php and I'd like to use a global variable named $Var1. I set up $Var1 like this in api.php
//In api.php
global $Var1;
$Var1 = '1';
When I try to call it, no out put is received. I am calling it like this:
<?php
//Checks for warnings
error_reporting(E_ALL);
ini_set("display_errors", 1);
error_reporting(E_ALL|E_STRICT); ini_set('display_errors', 'on');
include 'api.php'
echo "Var1";
?>
The thing is no output at all is echoed in the browser. I now realize the code block in api.php that is interfering with the global variable. I still am not sure why though. When I delete this code block from api.php the global variable is successfully displayed in Test.php. How can I keep the code block and successfully display the global in Test.php?
try
{
// Are we running in development or production mode? You can easily switch
// between these two in the Apache VirtualHost configuration.
if (!defined('APPLICATION_ENV'))
define('APPLICATION_ENV', getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production');
// In development mode, we show all errors because we obviously want to
// know about them. We don't do this in production mode because that might
// expose critical details of our app or our database. Critical PHP errors
// will still be logged in the PHP and Apache error logs, so it's always
// a good idea to keep an eye on them.
if (APPLICATION_ENV == 'development')
{
error_reporting(E_ALL|E_STRICT);
ini_set('display_errors', 'on');
}
else
{
error_reporting(0);
ini_set('display_errors', 'off');
}
// Load the config file. I prefer to keep all configuration settings in a
// separate file so you don't have to mess around in the main code if you
// just want to change some settings.
require_once 'api_config.php';
$config = $config[APPLICATION_ENV];
// In development mode, we fake a delay that makes testing more realistic.
// You're probably running this on a fast local server but in production
// mode people will be using it on a mobile device over a slow connection.
if (APPLICATION_ENV == 'development')
sleep(0);
// To keep the code clean, I put the API into its own class. Create an
// instance of that class and let it handle the request.
$api = new API($config);
$api->handleCommand();
echo "OK" . PHP_EOL;
}
catch (Exception $e)
{
// The code throws an exception when something goes horribly wrong; e.g.
// no connection to the database could be made. In development mode, we
// show these exception messages. In production mode, we simply return a
// "500 Server Error" message.
if (APPLICATION_ENV == 'development')
var_dump($e);
else
exitWithHttpError(500);
}
////////////////////////////////////////////////////////////////////////////////
function exitWithHttpError($error_code, $message = '')
{
switch ($error_code)
{
case 400: header("HTTP/1.0 400 Bad Request"); break;
case 403: header("HTTP/1.0 403 Forbidden"); break;
case 404: header("HTTP/1.0 404 Not Found"); break;
case 500: header("HTTP/1.0 500 Server Error"); break;
}
header('Content-Type: text/plain');
if ($message != '')
header('X-Error-Description: ' . $message);
exit;
}
function isValidUtf8String($string, $maxLength, $allowNewlines = false)
{
if (empty($string) || strlen($string) > $maxLength)
return false;
if (mb_check_encoding($string, 'UTF-8') === false)
return false;
// Don't allow control characters, except possibly newlines
for ($t = 0; $t < strlen($string); $t++)
{
$ord = ord($string{$t});
if ($allowNewlines && ($ord == 10 || $ord == 13))
continue;
if ($ord < 32)
return false;
}
return true;
}
function truncateUtf8($string, $maxLength)
{
$origString = $string;
$origLength = $maxLength;
while (strlen($string) > $origLength)
{
$string = mb_substr($origString, 0, $maxLength, 'utf-8');
$maxLength--;
}
return $string;
}
UPDATE
Full source code of api.php. My goal is to declare a global variable and pass it a long using a require statement to a new file. I am only able to do so if I delete the entire try block at the beginning of this file and I need to know why.
<?php
global $Var1;
$Var1 = '1';
// This is the server API for the PushChat iPhone app. To use the API, the app
// sends an HTTP POST request to our URL. The POST data contains a field "cmd"
// that indicates what API command should be executed.
try
{
// Are we running in development or production mode? You can easily switch
// between these two in the Apache VirtualHost configuration.
if (!defined('APPLICATION_ENV'))
define('APPLICATION_ENV', getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production');
// In development mode, we show all errors because we obviously want to
// know about them. We don't do this in production mode because that might
// expose critical details of our app or our database. Critical PHP errors
// will still be logged in the PHP and Apache error logs, so it's always
// a good idea to keep an eye on them.
if (APPLICATION_ENV == 'development')
{
error_reporting(E_ALL|E_STRICT);
ini_set('display_errors', 'on');
}
else
{
error_reporting(0);
ini_set('display_errors', 'off');
}
// Load the config file. I prefer to keep all configuration settings in a
// separate file so you don't have to mess around in the main code if you
// just want to change some settings.
require_once 'api_config.php';
$config = $config[APPLICATION_ENV];
// In development mode, we fake a delay that makes testing more realistic.
// You're probably running this on a fast local server but in production
// mode people will be using it on a mobile device over a slow connection.
if (APPLICATION_ENV == 'development')
sleep(0);
// To keep the code clean, I put the API into its own class. Create an
// instance of that class and let it handle the request.
$api = new API($config);
$api->handleCommand();
echo "OK" . PHP_EOL;
}
catch (Exception $e)
{
// The code throws an exception when something goes horribly wrong; e.g.
// no connection to the database could be made. In development mode, we
// show these exception messages. In production mode, we simply return a
// "500 Server Error" message.
if (APPLICATION_ENV == 'development')
var_dump($e);
else
exitWithHttpError(500);
}
////////////////////////////////////////////////////////////////////////////////
function exitWithHttpError($error_code, $message = '')
{
switch ($error_code)
{
case 400: header("HTTP/1.0 400 Bad Request"); break;
case 403: header("HTTP/1.0 403 Forbidden"); break;
case 404: header("HTTP/1.0 404 Not Found"); break;
case 500: header("HTTP/1.0 500 Server Error"); break;
}
header('Content-Type: text/plain');
if ($message != '')
header('X-Error-Description: ' . $message);
exit;
}
function isValidUtf8String($string, $maxLength, $allowNewlines = false)
{
if (empty($string) || strlen($string) > $maxLength)
return false;
if (mb_check_encoding($string, 'UTF-8') === false)
return false;
// Don't allow control characters, except possibly newlines
for ($t = 0; $t < strlen($string); $t++)
{
$ord = ord($string{$t});
if ($allowNewlines && ($ord == 10 || $ord == 13))
continue;
if ($ord < 32)
return false;
}
return true;
}
function truncateUtf8($string, $maxLength)
{
$origString = $string;
$origLength = $maxLength;
while (strlen($string) > $origLength)
{
$string = mb_substr($origString, 0, $maxLength, 'utf-8');
$maxLength--;
}
return $string;
}
////////////////////////////////////////////////////////////////////////////////
class API
{
// Because the payload only allows for 256 bytes and there is some overhead
// we limit the message text to 190 characters.
const MAX_MESSAGE_LENGTH = 190;
private $pdo;
function __construct($config)
{
// Create a connection to the database.
$this->pdo = new PDO(
'mysql:host=' . $config['db']['host'] . ';dbname=' . $config['db']['dbname'],
$config['db']['username'],
$config['db']['password'],
array());
// If there is an error executing database queries, we want PDO to
// throw an exception. Our exception handler will then exit the script
// with a "500 Server Error" message.
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// We want the database to handle all strings as UTF-8.
$this->pdo->query('SET NAMES utf8');
}
function handleCommand()
{
// Figure out which command the client sent and let the corresponding
// method handle it. If the command is unknown, then exit with an error
// message.
if (isset($_POST['cmd']))
{
switch (trim($_POST['cmd']))
{
case 'join': $this->handleJoin(); return;
case 'leave': $this->handleLeave(); return;
case 'update': $this->handleUpdate(); return;
case 'message': $this->handleMessage(); return;
}
}
exitWithHttpError(400, 'Unknown command');
}
// The "join" API command registers a user to receive notifications that
// are sent in a specific "chat room". Each chat room is identified by a
// secret code. All the users who register with the same secret code can
// see each other's messages.
//
// This command takes the following POST parameters:
//
// - user_Id: A unique identifier. Must be a string of 40 hexadecimal characters.
// - token: The device's device token. Must be a string of 64 hexadecimal
// characters, or "0" if no token is available yet.
// - name: The nickname of the user. Must be a UTF-8 string of maximum 255
// bytes. Only the first 20 bytes are actually shown in the push
// notifications.
// - code: The secret code that identifies the chat room. Must be a UTF-8
// string of maximum 255 bytes.
//
function handleJoin()
{
//function getUserId;
$userId = $this->getUserId();
$token = $this->getDeviceToken(true);
$name = $this->getString('name', 255);
$code = $this->getString('code', 255);
// When the client sends a "join" command, we add a new record to the
// active_users table. We identify the client by the user_id that it
// provides. When the client sends a "leave" command, we delete its
// record from the active_users table.
// It is theoretically possible that a client sends a "join" command
// while its user_id is still present in active_users (because it did not
// send a "leave" command). In that case, we simply remove the old
// record first and then insert the new one.
$this->pdo->beginTransaction();
$stmt = $this->pdo->prepare('DELETE FROM active_users WHERE user_Id = ?');
$stmt->execute(array($userId));
$stmt = $this->pdo->prepare('INSERT INTO active_users (user_Id, device_token, nickname, secret_code, ip_address) VALUES (?, ?, ?, ?, ?)');
$stmt->execute(array($userId, $token, $name, $code, $_SERVER['REMOTE_ADDR']));
$this->pdo->commit();
}
// The "leave" API command removes a user from a chat room. That user will
// no longer receive push notifications for messages sent to that room.
//
// This command takes the following POST parameters:
//
// - user_id: A unique identifier. Must be a string of 40 hexadecimal characters.
//
function handleLeave()
{
$userId = $this->getUserId();
$stmt = $this->pdo->prepare('DELETE FROM active_users WHERE user_Id = ?');
$stmt->execute(array($userId));
}
// The "update" API command gives a user a new device token.
//
// This command takes the following POST parameters:
//
// - user_id: A unique identifier. Must be a string of 40 hexadecimal characters.
// - token: The device's device token. Must be a string of 64 hexadecimal
// characters.
//
function handleUpdate()
{
$userId = $this->getUserId();
$token = $this->getDeviceToken(false);
$stmt = $this->pdo->prepare('UPDATE active_users SET device_token = ? WHERE user_Id = ?');
$stmt->execute(array($token, $userId));
}
// The "message" API command sends a message to all users who are registered
// with the same secret code as the sender of the message.
//
// This command takes the following POST parameters:
//
// - user_id: A unique identifier. Must be a string of 40 hexadecimal characters.
// - text: The message text. Must be a UTF-8 string of maximum 190 bytes.
//
function handleMessage()
{
$userId = $this->getUserId();
/*$text = $this->getString('text', self::MAX_MESSAGE_LENGTH, true);*/
// First, we get the record for the sender of the message from the
// active_users table. That gives us the nickname, device token, and
// secret code for that user.
$stmt = $this->pdo->prepare('SELECT * FROM active_users WHERE user_Id = ? LIMIT 1');
$stmt->execute(array($userId));
$user = $stmt->fetch(PDO::FETCH_OBJ);
if ($user !== false)
{
// Put the sender's name and the message text into the JSON payload
// for the push notification.
$payload = $this->makePayload($user->nickname/*, $text*/);
// Find the device tokens for all other users who are registered
// for this secret code. We exclude the device token of the sender
// of the message, so he will not get a push notification. We also
// exclude users who have not submitted a valid device token yet.
$stmt = $this->pdo->prepare("SELECT device_token FROM active_users WHERE secret_code = ? AND device_token <> ? AND device_token <> '0'");
$stmt->execute(array($user->secret_code, $user->device_token));
$tokens = $stmt->fetchAll(PDO::FETCH_COLUMN);
// Send out a push notification to each of these devices.
foreach ($tokens as $token)
{
$this->addPushNotification($token, $payload);
}
}
}
// Retrieves the user identifier from the POST data. If the user_id does not
// appear to be valid, the script exits with an error message.
function getUserId()
{
if (!isset($_POST['user_id']))
exitWithHttpError(400, 'Missing user_id');
$userId = trim(urldecode($_POST['user_id']));
if (!$this->isValidUserId($userId))
exitWithHttpError(400, 'Invalid user_id');
return $userId;
}
// Checks whether the format of the user identifier is correct (40 hex
// characters or 32 for the simulator).
function isValidUserId($userId)
{
if (strlen($userId) != 40 && strlen($userId) != 32) // 32 for simulator
return false;
if (preg_match("/^[0-9a-fA-F]+$/", $userId) == 0)
return false;
return true;
}
// Retrieves the device token from the POST data. If the token does not
// appear to be valid, the script exits with an error message.
function getDeviceToken($mayBeEmpty = false)
{
if (!isset($_POST['token']))
exitWithHttpError(400, 'Missing device token');
$token = trim($_POST['token']);
// The "join" command allows a token value of "0" to be specified,
// which is necessary in case the client did not yet obtain a device
// token at that point. We allow such clients to join, but they will
// not receive any notifications until they provide a valid token
// using the "update" command.
if ($mayBeEmpty && $token == "0")
return $token;
if (!$this->isValidDeviceToken($token))
exitWithHttpError(400, 'Invalid device token');
return $token;
}
// Checks whether the format of the device token is correct (64 hexadecimal
// characters). Note: we have no means to verify whether the device token
// was really issued by APNS and corresponds to an actual device.
function isValidDeviceToken($deviceToken)
{
if (strlen($deviceToken) != 64)
return false;
if (preg_match("/^[0-9a-fA-F]{64}$/", $deviceToken) == 0)
return false;
return true;
}
// Looks in the POST data for a field with the given name. If the field
// is not a valid UTF-8 string, or it is too long, the script exits with
// an error message.
function getString($name, $maxLength, $allowNewlines = false)
{
if (!isset($_POST[$name]))
exitWithHttpError(400, "Missing $name");
$string = trim($_POST[$name]);
if (!isValidUtf8String($string, $maxLength, $allowNewlines))
exitWithHttpError(400, "Invalid $name");
return $string;
}
// Creates the JSON payload for the push notification message. The "alert"
// text has the following format: "sender_name: message_text". Recipients
// can obtain the name of the sender by parsing the alert text up to the
// first colon followed by a space.
function makePayload($senderName, $text)
{
// Convert the nickname of the sender to JSON and truncate to a maximum
// length of 20 bytes (which may be less than 20 characters).
$nameJson = $this->jsonEncode($senderName);
$nameJson = truncateUtf8($nameJson, 20);
// Convert and truncate the message text
$textJson = $this->jsonEncode($text);
$textJson = truncateUtf8($textJson, self::MAX_MESSAGE_LENGTH);
// Combine everything into a JSON string
$payload = '{"aps":{"alert":"' . $nameJson . ': ' . $textJson . '","sound":"beep.caf"}}';
return $payload;
}
// We don't use PHP's built-in json_encode() function because it converts
// UTF-8 characters to \uxxxx. That eats up 6 characters in the payload for
// no good reason, as JSON already supports UTF-8 just fine.
function jsonEncode($text)
{
static $from = array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"');
static $to = array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"');
return str_replace($from, $to, $text);
}
// Adds a push notification to the push queue. The notification will not
// be sent immediately. The server runs a separate script, push.php, which
// periodically checks for new entries in this database table and sends
// them to the APNS servers.
function addPushNotification($deviceToken, $payload)
{
// Payloads have a maximum size of 256 bytes. If the payload is too
// large (which shouldn't happen), we won't send this notification.
if (strlen($payload) <= 256)
{
$stmt = $this->pdo->prepare('INSERT INTO push_queue (device_token, payload, time_queued) VALUES (?, ?, NOW())');
$stmt->execute(array($deviceToken, $payload));
}
}
}
?>
Related
I'm calling through Axios a PHP script checking whether a URL passed to it as a parameter can be embedded in an iframe. That PHP script starts with opening the URL with $_GET[].
Strangely, a page with cross-origin-opener-policy: same-origin (like https://twitter.com/) can be opened with $_GET[], whereas a page with Referrer Policy: strict-origin-when-cross-origin (like https://calia.order.liven.com.au/) cannot.
I don't understand why, and it's annoying because for the pages that cannot be opened with $_GET[] I'm unable to perform my checks on them - the script just fails (meaning I get no response and the Axios call runs the catch() block).
So basically there are 3 types of pages: (1) those who allow iframe embeddability, (2) those who don't, and (3) the annoying ones who not only don't but also can't even be opened to perform this check.
Is there a way to open any page with PHP, and if not, what can I do to prevent my script from failing after several seconds?
PHP script:
$source = $_GET['url'];
$response = true;
try {
$headers = get_headers($source, 1);
$headers = array_change_key_case($headers, CASE_LOWER);
if (isset($headers['content-security-policy'])) {
$response = false;
}
else if (isset($headers['x-frame-options']) &&
$headers['x-frame-options'] == 'DENY' ||
$headers['x-frame-options'] == 'SAMEORIGIN'
) {
$response = false;
}
} catch (Exception $ex) {
$response = $ex;
}
echo $response;
EDIT: below is the console error.
Access to XMLHttpRequest at 'https://path.to.cdn/iframeHeaderChecker?url=https://calia.order.liven.com.au/' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CustomLink.vue?b495:61 Error: Network Error
at createError (createError.js?2d83:16)
at XMLHttpRequest.handleError (xhr.js?b50d:84)
VM4758:1 GET https://path.to.cdn/iframeHeaderChecker?url=https://calia.order.com.au/ net::ERR_FAILED
The error you have shown is coming from Javascript, not from PHP. get_headers() returns false on failure, it will not throw an exception - the catch() never happens. get_headers() just makes an http request, like your browser, or curl, and the only reason that would fail is if the URL is malformed, or the remote site is down, etc.
It is the access from http://localhost:3000 to https://path.to.cdn/iframeHeaderChecker with Javascript that has been blocked, not PHP access to the URLs you are passing as parameters in $_GET['url'].
What you're seeing is a standard CORS error when you try to access a different domain than the one the Javascript is running on. CORS means Javascript running on one host cannot make http requests to another host, unless that other host explicitly allows it. In this case, the Javascript running at http://localhost:3000 is making an http request to a remote site https://path.to.cdn/. That's a cross-origin request (localhost !== path.to.cdn), and the server/script receiving that request on path.to.cdn is not returning any specific CORS headers allowing that request, so the request is blocked.
Note though that if the request is classed as "simple", it will actually run. So your PHP is working already, always, but bcs the right headers aren't returned, the result is blocked from being displayed in your browser. This can lead to confusion bcs for eg you might notice a delay while it gets the headers from a slow site, whereas it is super fast for a fast site. Or maybe you have logging which you see is working all the time, despite nothing showing up in your browser.
My understanding is that https://path.to.cdn/iframeHeaderChecker is your PHP script, some of the code of which you have shown in your question? If so, you have 2 choices:
Update iframeHeaderChecker to return the appropriate CORS headers, so that your cross-origin JS request is allowed. As a quick, insecure hack to allow access from anyone and anywhere (not a good idea for the long term!) you could add:
header("Access-Control-Allow-Origin: *");
But it would be better to update that to more specifically restrict access to only your app, and not everyone else. You'll have to evaluate the best way to do that depending on the specifics of your application and infrastructure. There many questions here on SO about CORS/PHP/AJAX to check for reference. You could also configure this at the web server level, rather than the application level, eg here's how to configure Apache to return those headers.
If iframeHeaderChecker is part of the same application as the Javascript calling it, is it also available locally, on http://localhost:3000? If so, update your JS to use the local version, not the remote one on path.to.cdn, and you avoid the whole problem!
This is just my rough guess about what wrong with your code can be.
I noticed you do:
a comparison of values from $headers but without
ensuring they have the same CAPITAL CASE as the values you compare against. Applied: strtoupper().
check with isset() but not test if key_exist before
Applied: key_exist()
check with isset() but perhaps you should use !empty() instead of isset()
compare result:
$value = "";
var_dump(isset($value)); // (bool) true
var_dump(!empty($value)); // (bool) false
$value = "something";
var_dump(isset($value)); // (bool) true
var_dump(!empty($value)); // (bool) true
unset($value);
var_dump(isset($value)); // (bool) false
var_dump(!empty($value)); // (bool) false
The code with applied changes:
<?php
error_reporting(E_ALL);
declare(strict_types=1);
header('Access-Control-Allow-Origin: *');
ob_start();
try {
$response = true;
if (!key_exists('url', $_GET)) {
$msg = '$_GET does not have a key "url"';
throw new \RuntimeException($msg);
}
$source = $_GET['url'];
if ($source !== filter_var($source, \FILTER_SANITIZE_URL)) {
$msg = 'Passed url is invaid, url: ' . $source;
throw new \RuntimeException($msg);
}
if (filter_var($source, \FILTER_VALIDATE_URL) === FALSE) {
$msg = 'Passed url is invaid, url: ' . $source;
throw new \RuntimeException($msg);
}
$headers = get_headers($source, 1);
if (!is_array($headers)) {
$msg = 'Headers should be array but it is: ' . gettype($headers);
throw new \RuntimeException($msg);
}
$headers = array_change_key_case($headers, \CASE_LOWER);
if ( key_exists('content-security-policy', $headers) &&
isset($headers['content-security-policy'])
) {
$response = false;
}
elseif ( key_exists('x-frame-options', $headers) &&
(
strtoupper($headers['x-frame-options']) == 'DENY' ||
strtoupper($headers['x-frame-options']) == 'SAMEORIGIN'
)
) {
$response = false;
}
} catch (Exception $ex) {
$response = "Error: " . $ex->getMessage() . ' at: ' . $ex->getFile() . ':' . $ex->getLine();
}
$phpOutput = ob_get_clean();
if (!empty($phpOutput)) {
$response .= \PHP_EOL . 'PHP Output: ' . $phpOutput;
}
echo $response;
Using Throwable instead of Exception will also catch Errors in PHP7.
Keep in mind that:
$response = true;
echo $response; // prints "1"
but
$response = false;
echo $response; // prints ""
so for the $response = false you'll get an empty string, not 0
if you want to have 0 for false and 1 for true then change the $response = true; to $response = 1; for true and $response = false; to $response = 0; for false everywhere.
I hope that somehow helps
I have a php script handling an incoming ajax request. It looks up some credentials from text files and if they match requirements it sets two cookies, one called username and one called creds on the client machine.
When I do this from my local web server, all three cookies get set and I receive all the php feedback from the echoes.
When I do this from my hosted web server the first setcookie works ("cookies","enabled") but the next two dont! However I get all the echoes confirming that php has reached the point in my script where they should be set. Any ideas please? I am thoroughly stumped.
<?php
//george:bloog
//emeline:sparg
setCookie("primacy[cookies]","enabled", time()+3600*24*30,'/');
//convert string to summed int
function pwdInt($pw)
{
$pwdIntVal = 0;
for($i=0; $i<strlen($pw);$i++)
{
$pwdIntVal = $pwdIntVal + ( ord(strtolower($pw[$i])) - 96 );
}
return $pwdIntVal;
}
//retrieve user account creation date by parsing savefile for accountCreate var
function getACD($aUSR)
{
$saveFileName = "saveFiles/" . $aUSR . ".txt";
echo "Fetched save successfully.<br>";
$lines = file($saveFileName);
foreach($lines as $line)
{
if( explode(":",$line)[0] == "accountCreate");
$lineDate = explode(":",$line)[1];
return $lineDate;
}
}
//accept incoming vars
if(isset($_POST['username']) && !empty($_POST['username']))
{
$uN = strtolower($_POST['username']);
$pwd = strtolower($_POST['password']);
$found = "Invalid user";
//test for presence in creds
$lines = file("creds/creds.txt");
foreach($lines as $line)
{
$lineName = explode("_",$line)[0];
if($uN == $lineName)
{
//matched username before delimiter "_"
$found = $lineName;
echo "Found user, " . explode("_",$line)[0] . " checking password<br>";
//check two: use int of pwd with account creation date from user save
$usrACD = getACD($uN);
echo $usrACD;
if( (pwdInt($pwd) * $usrACD) == (explode("_",$line)[1]) )
{
echo "Tests passed: granting access cookies";
setCookie("uN",$uN, time()+3600*24*30,'/');
setCookie("cred",(pwdInt($pwd) * $usrACD), time()+3600*24*30,'/');
}
else
{
echo "Failed password check for allowed user<br>";
}
}
}
}
else
{
echo $found . pwdInt($pwd) . "<br>";
}
?>
You should either enable output buffering or move echoes after setCookie method. Setting cookies is thing that happens during headers of response. All headers should be sent before content. Echoing things is setting up content, so every header edition (like setting cookies) after first echo will fail.
I am having issue to send messages via WhatsApp PHP Client. Details given below:
Error
Due to length it's given here: http://pastie.org/10794465
```
Debug log
http://pastie.org/10794474
Code
<?php
ini_set('display_errors',1);
error_reporting(E_ALL);
set_time_limit(10);
var_dump(extension_loaded('curve25519'));
var_dump( extension_loaded('protobuf'));
//require_once __DIR__.'../vendor/whatsapp/autoload.php';
date_default_timezone_set('Europe/Madrid');
//require_once __DIR__.'../vendor/whatsapp/chat-api/src/whatsprot.class.php';
require_once 'vendor/whatsapp/chat-api/src/whatsprot.class.php';
require_once 'vendor/whatsapp/chat-api/src/events/MyEvents.php';
//require_once __DIR__.'/../src//events/MyEvents.php';
$username = '92xxxxxxxxx'; // Telephone number including the country code without '+' or '00'.
$password = 't7+YzhqpUd8P7LgeU9NdttaIpc4='; // Use registerTool.php or exampleRegister.php to obtain your password
$nickname = 'ADD Agent'; // This is the username (or nickname) displayed by WhatsApp clients.
$target = "92xxxxxxxxx"; // Destination telephone number including the country code without '+' or '00'.
$target = "92xxxxxxxxx"; // Destination telephone number including the country code without '+' or '00'.
$debug = true; // Set this to true, to see debug mode.
echo "[] Logging in as '$nickname' ($username)\n";
//Create the whatsapp object and setup a connection.
$w = new WhatsProt($username, $nickname, $debug,true,__DIR__.'/wadata/');
$events = new MyEvents($w);
$events->setEventsToListenFor($events->activeEvents);
$w->connect();
// Now loginWithPassword function sends Nickname and (Available) Presence
$w->loginWithPassword($password);
$w->sendMessage($target, 'Salam kia haal hain?!');
echo "<b>Message Sent to $target</b>";
echo "<br>Getting message<br>";
$w->pollMessage();
$msgs = $w->GetMessages();
foreach ($msgs as $m) {
var_dump($m);
}
In MyEvents.php
public function onGetMessage( $mynumber, $from, $id, $type, $time, $name, $body )
{
echo "<br>Message Got from $name:\n$body\n\n<br>"; // NOT being fired.
exit;
}
To receive message you need bind onGetMessage and call pollMessage in a loop
while (1) {
$w->pollMessage();
}
Check this example complete:
<?php
//set_time_limit(10);
require_once __DIR__.'/../src/whatsprot.class.php';
require_once __DIR__.'/../src//events/MyEvents.php';
//Change to your time zone
date_default_timezone_set('Europe/Madrid');
//######### DO NOT COMMIT THIS FILE WITH YOUR CREDENTIALS ###########
///////////////////////CONFIGURATION///////////////////////
//////////////////////////////////////////////////////////
$username = '*************'; // Telephone number including the country code without '+' or '00'.
$password = '*************'; // Use registerTool.php or exampleRegister.php to obtain your password
$nickname = 'LuisN'; // This is the username (or nickname) displayed by WhatsApp clients.
$target = "************"; // Destination telephone number including the country code without '+' or '00'.
$debug = false; // Set this to true, to see debug mode.
///////////////////////////////////////////////////////////
function onPresenceAvailable($username, $from)
{
$dFrom = str_replace(['#s.whatsapp.net', '#g.us'], '', $from);
echo "<$dFrom is online>\n\n";
}
function onPresenceUnavailable($username, $from, $last)
{
$dFrom = str_replace(['#s.whatsapp.net', '#g.us'], '', $from);
echo "<$dFrom is offline> Last seen: $last seconds\n\n";
}
function onGetMessage($mynumber, $from, $id, $type, $time, $name, $body){
echo sprintf("Message from %s: [%s]\r\n",$from,$body);
}
echo "[] Logging in as '$nickname' ($username)\n";
// Create the whatsapp object and setup a connection.
$w = new WhatsProt($username, $nickname, $debug);
$w->connect();
// Now loginWithPassword function sends Nickname and (Available) Presence
$w->loginWithPassword($password);
$w->sendGetServerProperties();
$w->sendGetGroups();
$w->sendGetBroadcastLists();
// Set the profile picture
//$w->sendSetProfilePicture(Constants::PICTURES_FOLDER . '/314484_300x300.jpg');
$w->sendStatusUpdate("La vida es un carnaval \xF0\x9F\x8E\xB6");
// Synchronizes contacts with the server, very important to avoid bans
$w->sendSync([$target]);
// Print when the user goes online/offline (you need to bind a function to the event onPressence
// so the script knows what to do)
$w->eventManager()->bind('onPresenceAvailable', 'onPresenceAvailable');
$w->eventManager()->bind('onPresenceUnavailable', 'onPresenceUnavailable');
// Receives and processes messages, this includes decrypted
$w->eventManager()->bind('onGetMessage','onGetMessage');
echo "[*] Connected to WhatsApp\n\n";
$w->sendMessage($target, 'Guess the number :)');
$w->sendMessage($target, 'Sent from WhatsApi at '.date('H:i'));
while (1) {
$w->pollMessage();
}
PD: I tested this method in 3 environments
php 5.5 Cli NTS VC11 Windows 10
php 5.5 Cli NTS VC11 Windows 7
PHP 5.5.9-1ubuntu4.14 Cli
My application has a production or development setting that I can toggle. When setting up the server, I set up this flag in Applications > Mamp > Conf > Apache > httpd.conf. Its purpose is to give my local API directory a server alias. It also defines the document root etc.
Listen 44447
<VirtualHost *:44447>
DocumentRoot "/Users/user/Desktop/PushChatServer/api"
ServerName 192.168.1.5:44447
ServerAlias pushchat.local
CustomLog "/Users/user/Desktop/PushChatServer/log/apache_access.log" combined
ErrorLog "/Users/user/Desktop/PushChatServer/log/apache_error.log"
SetEnv APPLICATION_ENV development
php_flag magic_quotes_gpc off
<Directory "/Users/user/Desktop/PushChatServer/api">
Options Indexes MultiViews FollowSymLinks
AllowOverride All
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
My goal is to set up a global variable in my api and pass it to another file. The php file my app uses is called api.php and I'd like to use a global variable named $Var1. I set up $Var1 like this in api.php
//In api.php
global $Var1;
$Var1 = '1';
When I try to call it, no out put is received. I am calling it like this:
<?php
//Checks for warnings
error_reporting(E_ALL);
ini_set("display_errors", 1);
error_reporting(E_ALL|E_STRICT); ini_set('display_errors', 'on');
include 'api.php'
echo "Var1";
?>
The thing is no output at all is echoed in the browser. I now realize the code block in api.php that is interfering with the global variable. I still am not sure why though. When I delete this code block from api.php the global variable is successfully displayed in Test.php. How can I keep the code block and successfully display the global in Test.php?
try
{
// Are we running in development or production mode? You can easily switch
// between these two in the Apache VirtualHost configuration.
if (!defined('APPLICATION_ENV'))
define('APPLICATION_ENV', getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production');
// In development mode, we show all errors because we obviously want to
// know about them. We don't do this in production mode because that might
// expose critical details of our app or our database. Critical PHP errors
// will still be logged in the PHP and Apache error logs, so it's always
// a good idea to keep an eye on them.
if (APPLICATION_ENV == 'development')
{
error_reporting(E_ALL|E_STRICT);
ini_set('display_errors', 'on');
}
else
{
error_reporting(0);
ini_set('display_errors', 'off');
}
// Load the config file. I prefer to keep all configuration settings in a
// separate file so you don't have to mess around in the main code if you
// just want to change some settings.
require_once 'api_config.php';
$config = $config[APPLICATION_ENV];
// In development mode, we fake a delay that makes testing more realistic.
// You're probably running this on a fast local server but in production
// mode people will be using it on a mobile device over a slow connection.
if (APPLICATION_ENV == 'development')
sleep(0);
// To keep the code clean, I put the API into its own class. Create an
// instance of that class and let it handle the request.
$api = new API($config);
$api->handleCommand();
echo "OK" . PHP_EOL;
}
catch (Exception $e)
{
// The code throws an exception when something goes horribly wrong; e.g.
// no connection to the database could be made. In development mode, we
// show these exception messages. In production mode, we simply return a
// "500 Server Error" message.
if (APPLICATION_ENV == 'development')
var_dump($e);
else
exitWithHttpError(500);
}
////////////////////////////////////////////////////////////////////////////////
function exitWithHttpError($error_code, $message = '')
{
switch ($error_code)
{
case 400: header("HTTP/1.0 400 Bad Request"); break;
case 403: header("HTTP/1.0 403 Forbidden"); break;
case 404: header("HTTP/1.0 404 Not Found"); break;
case 500: header("HTTP/1.0 500 Server Error"); break;
}
header('Content-Type: text/plain');
if ($message != '')
header('X-Error-Description: ' . $message);
exit;
}
function isValidUtf8String($string, $maxLength, $allowNewlines = false)
{
if (empty($string) || strlen($string) > $maxLength)
return false;
if (mb_check_encoding($string, 'UTF-8') === false)
return false;
// Don't allow control characters, except possibly newlines
for ($t = 0; $t < strlen($string); $t++)
{
$ord = ord($string{$t});
if ($allowNewlines && ($ord == 10 || $ord == 13))
continue;
if ($ord < 32)
return false;
}
return true;
}
function truncateUtf8($string, $maxLength)
{
$origString = $string;
$origLength = $maxLength;
while (strlen($string) > $origLength)
{
$string = mb_substr($origString, 0, $maxLength, 'utf-8');
$maxLength--;
}
return $string;
}
UPDATE
Full source code of api.php. My goal is to declare a global variable and pass it a long using a require statement to a new file. I am only able to do so if I delete the entire try block at the beginning of this file and I need to know why.
<?php
global $Var1;
$Var1 = '1';
// This is the server API for the PushChat iPhone app. To use the API, the app
// sends an HTTP POST request to our URL. The POST data contains a field "cmd"
// that indicates what API command should be executed.
try
{
// Are we running in development or production mode? You can easily switch
// between these two in the Apache VirtualHost configuration.
if (!defined('APPLICATION_ENV'))
define('APPLICATION_ENV', getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production');
// In development mode, we show all errors because we obviously want to
// know about them. We don't do this in production mode because that might
// expose critical details of our app or our database. Critical PHP errors
// will still be logged in the PHP and Apache error logs, so it's always
// a good idea to keep an eye on them.
if (APPLICATION_ENV == 'development')
{
error_reporting(E_ALL|E_STRICT);
ini_set('display_errors', 'on');
}
else
{
error_reporting(0);
ini_set('display_errors', 'off');
}
// Load the config file. I prefer to keep all configuration settings in a
// separate file so you don't have to mess around in the main code if you
// just want to change some settings.
require_once 'api_config.php';
$config = $config[APPLICATION_ENV];
// In development mode, we fake a delay that makes testing more realistic.
// You're probably running this on a fast local server but in production
// mode people will be using it on a mobile device over a slow connection.
if (APPLICATION_ENV == 'development')
sleep(0);
// To keep the code clean, I put the API into its own class. Create an
// instance of that class and let it handle the request.
$api = new API($config);
$api->handleCommand();
echo "OK" . PHP_EOL;
}
catch (Exception $e)
{
// The code throws an exception when something goes horribly wrong; e.g.
// no connection to the database could be made. In development mode, we
// show these exception messages. In production mode, we simply return a
// "500 Server Error" message.
if (APPLICATION_ENV == 'development')
var_dump($e);
else
exitWithHttpError(500);
}
////////////////////////////////////////////////////////////////////////////////
function exitWithHttpError($error_code, $message = '')
{
switch ($error_code)
{
case 400: header("HTTP/1.0 400 Bad Request"); break;
case 403: header("HTTP/1.0 403 Forbidden"); break;
case 404: header("HTTP/1.0 404 Not Found"); break;
case 500: header("HTTP/1.0 500 Server Error"); break;
}
header('Content-Type: text/plain');
if ($message != '')
header('X-Error-Description: ' . $message);
exit;
}
function isValidUtf8String($string, $maxLength, $allowNewlines = false)
{
if (empty($string) || strlen($string) > $maxLength)
return false;
if (mb_check_encoding($string, 'UTF-8') === false)
return false;
// Don't allow control characters, except possibly newlines
for ($t = 0; $t < strlen($string); $t++)
{
$ord = ord($string{$t});
if ($allowNewlines && ($ord == 10 || $ord == 13))
continue;
if ($ord < 32)
return false;
}
return true;
}
function truncateUtf8($string, $maxLength)
{
$origString = $string;
$origLength = $maxLength;
while (strlen($string) > $origLength)
{
$string = mb_substr($origString, 0, $maxLength, 'utf-8');
$maxLength--;
}
return $string;
}
////////////////////////////////////////////////////////////////////////////////
class API
{
// Because the payload only allows for 256 bytes and there is some overhead
// we limit the message text to 190 characters.
const MAX_MESSAGE_LENGTH = 190;
private $pdo;
function __construct($config)
{
// Create a connection to the database.
$this->pdo = new PDO(
'mysql:host=' . $config['db']['host'] . ';dbname=' . $config['db']['dbname'],
$config['db']['username'],
$config['db']['password'],
array());
// If there is an error executing database queries, we want PDO to
// throw an exception. Our exception handler will then exit the script
// with a "500 Server Error" message.
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// We want the database to handle all strings as UTF-8.
$this->pdo->query('SET NAMES utf8');
}
function handleCommand()
{
// Figure out which command the client sent and let the corresponding
// method handle it. If the command is unknown, then exit with an error
// message.
if (isset($_POST['cmd']))
{
switch (trim($_POST['cmd']))
{
case 'join': $this->handleJoin(); return;
case 'leave': $this->handleLeave(); return;
case 'update': $this->handleUpdate(); return;
case 'message': $this->handleMessage(); return;
}
}
exitWithHttpError(400, 'Unknown command');
}
// The "join" API command registers a user to receive notifications that
// are sent in a specific "chat room". Each chat room is identified by a
// secret code. All the users who register with the same secret code can
// see each other's messages.
//
// This command takes the following POST parameters:
//
// - user_Id: A unique identifier. Must be a string of 40 hexadecimal characters.
// - token: The device's device token. Must be a string of 64 hexadecimal
// characters, or "0" if no token is available yet.
// - name: The nickname of the user. Must be a UTF-8 string of maximum 255
// bytes. Only the first 20 bytes are actually shown in the push
// notifications.
// - code: The secret code that identifies the chat room. Must be a UTF-8
// string of maximum 255 bytes.
//
function handleJoin()
{
//function getUserId;
$userId = $this->getUserId();
$token = $this->getDeviceToken(true);
$name = $this->getString('name', 255);
$code = $this->getString('code', 255);
// When the client sends a "join" command, we add a new record to the
// active_users table. We identify the client by the user_id that it
// provides. When the client sends a "leave" command, we delete its
// record from the active_users table.
// It is theoretically possible that a client sends a "join" command
// while its user_id is still present in active_users (because it did not
// send a "leave" command). In that case, we simply remove the old
// record first and then insert the new one.
$this->pdo->beginTransaction();
$stmt = $this->pdo->prepare('DELETE FROM active_users WHERE user_Id = ?');
$stmt->execute(array($userId));
$stmt = $this->pdo->prepare('INSERT INTO active_users (user_Id, device_token, nickname, secret_code, ip_address) VALUES (?, ?, ?, ?, ?)');
$stmt->execute(array($userId, $token, $name, $code, $_SERVER['REMOTE_ADDR']));
$this->pdo->commit();
}
// The "leave" API command removes a user from a chat room. That user will
// no longer receive push notifications for messages sent to that room.
//
// This command takes the following POST parameters:
//
// - user_id: A unique identifier. Must be a string of 40 hexadecimal characters.
//
function handleLeave()
{
$userId = $this->getUserId();
$stmt = $this->pdo->prepare('DELETE FROM active_users WHERE user_Id = ?');
$stmt->execute(array($userId));
}
// The "update" API command gives a user a new device token.
//
// This command takes the following POST parameters:
//
// - user_id: A unique identifier. Must be a string of 40 hexadecimal characters.
// - token: The device's device token. Must be a string of 64 hexadecimal
// characters.
//
function handleUpdate()
{
$userId = $this->getUserId();
$token = $this->getDeviceToken(false);
$stmt = $this->pdo->prepare('UPDATE active_users SET device_token = ? WHERE user_Id = ?');
$stmt->execute(array($token, $userId));
}
// The "message" API command sends a message to all users who are registered
// with the same secret code as the sender of the message.
//
// This command takes the following POST parameters:
//
// - user_id: A unique identifier. Must be a string of 40 hexadecimal characters.
// - text: The message text. Must be a UTF-8 string of maximum 190 bytes.
//
function handleMessage()
{
$userId = $this->getUserId();
/*$text = $this->getString('text', self::MAX_MESSAGE_LENGTH, true);*/
// First, we get the record for the sender of the message from the
// active_users table. That gives us the nickname, device token, and
// secret code for that user.
$stmt = $this->pdo->prepare('SELECT * FROM active_users WHERE user_Id = ? LIMIT 1');
$stmt->execute(array($userId));
$user = $stmt->fetch(PDO::FETCH_OBJ);
if ($user !== false)
{
// Put the sender's name and the message text into the JSON payload
// for the push notification.
$payload = $this->makePayload($user->nickname/*, $text*/);
// Find the device tokens for all other users who are registered
// for this secret code. We exclude the device token of the sender
// of the message, so he will not get a push notification. We also
// exclude users who have not submitted a valid device token yet.
$stmt = $this->pdo->prepare("SELECT device_token FROM active_users WHERE secret_code = ? AND device_token <> ? AND device_token <> '0'");
$stmt->execute(array($user->secret_code, $user->device_token));
$tokens = $stmt->fetchAll(PDO::FETCH_COLUMN);
// Send out a push notification to each of these devices.
foreach ($tokens as $token)
{
$this->addPushNotification($token, $payload);
}
}
}
// Retrieves the user identifier from the POST data. If the user_id does not
// appear to be valid, the script exits with an error message.
function getUserId()
{
if (!isset($_POST['user_id']))
exitWithHttpError(400, 'Missing user_id');
$userId = trim(urldecode($_POST['user_id']));
if (!$this->isValidUserId($userId))
exitWithHttpError(400, 'Invalid user_id');
return $userId;
}
// Checks whether the format of the user identifier is correct (40 hex
// characters or 32 for the simulator).
function isValidUserId($userId)
{
if (strlen($userId) != 40 && strlen($userId) != 32) // 32 for simulator
return false;
if (preg_match("/^[0-9a-fA-F]+$/", $userId) == 0)
return false;
return true;
}
// Retrieves the device token from the POST data. If the token does not
// appear to be valid, the script exits with an error message.
function getDeviceToken($mayBeEmpty = false)
{
if (!isset($_POST['token']))
exitWithHttpError(400, 'Missing device token');
$token = trim($_POST['token']);
// The "join" command allows a token value of "0" to be specified,
// which is necessary in case the client did not yet obtain a device
// token at that point. We allow such clients to join, but they will
// not receive any notifications until they provide a valid token
// using the "update" command.
if ($mayBeEmpty && $token == "0")
return $token;
if (!$this->isValidDeviceToken($token))
exitWithHttpError(400, 'Invalid device token');
return $token;
}
// Checks whether the format of the device token is correct (64 hexadecimal
// characters). Note: we have no means to verify whether the device token
// was really issued by APNS and corresponds to an actual device.
function isValidDeviceToken($deviceToken)
{
if (strlen($deviceToken) != 64)
return false;
if (preg_match("/^[0-9a-fA-F]{64}$/", $deviceToken) == 0)
return false;
return true;
}
// Looks in the POST data for a field with the given name. If the field
// is not a valid UTF-8 string, or it is too long, the script exits with
// an error message.
function getString($name, $maxLength, $allowNewlines = false)
{
if (!isset($_POST[$name]))
exitWithHttpError(400, "Missing $name");
$string = trim($_POST[$name]);
if (!isValidUtf8String($string, $maxLength, $allowNewlines))
exitWithHttpError(400, "Invalid $name");
return $string;
}
// Creates the JSON payload for the push notification message. The "alert"
// text has the following format: "sender_name: message_text". Recipients
// can obtain the name of the sender by parsing the alert text up to the
// first colon followed by a space.
function makePayload($senderName, $text)
{
// Convert the nickname of the sender to JSON and truncate to a maximum
// length of 20 bytes (which may be less than 20 characters).
$nameJson = $this->jsonEncode($senderName);
$nameJson = truncateUtf8($nameJson, 20);
// Convert and truncate the message text
$textJson = $this->jsonEncode($text);
$textJson = truncateUtf8($textJson, self::MAX_MESSAGE_LENGTH);
// Combine everything into a JSON string
$payload = '{"aps":{"alert":"' . $nameJson . ': ' . $textJson . '","sound":"beep.caf"}}';
return $payload;
}
// We don't use PHP's built-in json_encode() function because it converts
// UTF-8 characters to \uxxxx. That eats up 6 characters in the payload for
// no good reason, as JSON already supports UTF-8 just fine.
function jsonEncode($text)
{
static $from = array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"');
static $to = array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"');
return str_replace($from, $to, $text);
}
// Adds a push notification to the push queue. The notification will not
// be sent immediately. The server runs a separate script, push.php, which
// periodically checks for new entries in this database table and sends
// them to the APNS servers.
function addPushNotification($deviceToken, $payload)
{
// Payloads have a maximum size of 256 bytes. If the payload is too
// large (which shouldn't happen), we won't send this notification.
if (strlen($payload) <= 256)
{
$stmt = $this->pdo->prepare('INSERT INTO push_queue (device_token, payload, time_queued) VALUES (?, ?, NOW())');
$stmt->execute(array($deviceToken, $payload));
}
}
}
?>
I'm developing an API to let my users access to files stored on another server.
Let's call my two servers, server 1 and server 2!
server 1 is the server im hosting my web site, and
server 2 is the server im storing my files!
My site is basically Javascript based one, so I will be using Javascript to post data to API when user needs to access files which are stored on server 2.
when users requests to access files, the data will be posted to API URL via Javascript! API is made of PHP. Using that PHP script(API) on server 1, I will made another request to server 2 asking for files so there will be another PHP script(API) on server 2.
I need to know how should I do this authentication between two servers as server 2 has no access to user details on server 1?
I hope to do that like this, I can use the method which is used by most payment gateways.
When API on server 2 received a request with some unique data of the user , post back those unique data through SSL to server 1 API and match them with user data in the database, then post back result through SSL to server 2 so then server 2 knows file request is a genuine request.
In this case what kind of user data/credentials server 1 API should post to server 2 and server 2 API should post back to server 1? and which user data should be matched with the data in the database? like user ID, session, cookies, ip, time stamp, ect!
Any clear and described answer would be nice! Thanks.
I would go with this:
user initiates action, javascript asks Server 1 (ajax) for request for file on Server 2
Server 1 creates URL using hash_hmac with data: file, user ID, user secret
when clicking that URL (server2.com/?file=FILE&user_id=ID&hash=SHA_1_HASH) server 2 asks server 1 for validation (sends file, user_id and hash)
server 1 does the validation, sends response to server 2
server 2 pushes file or sends 403 HTTP response
This way, server 2 only needs to consume API of server 1, server 1 has all the logic.
Pseudocode for hash and url creation:
// getHash($userId, $file) method
$user = getUser($userId);
$hash = hash_hmac('sha1', $userId . $file, $user->getSecret());
// getUrl($userId, $file) method
return sprintf('http://server2.com/get-file?file=%1&user_id=%2&hash=%3',
$userId,
$file,
$security->getHash($userId, $file)
);
Pseudocode for validation:
$hash = $security->getHash($_GET['id'], $_GET['file']);
if ($hash === $_GET['hash']) {
// All is good
}
Edit: getHash() method accepts user ID and file (ID or string, what ever suits your needs). With that data, it produces a hash, using hash_hmac method. For the secret parameter of hash_hmac function, users "secret key" is used. That key would be stored together with users data in the db table. It would be generated with mt_rand or even something stronger as reading /dev/random or using something like https://stackoverflow.com/a/16478556/691850.
A word of advice, use mod_xsendfile on server 2 (if it is Apache) to push files.
Introduction
You can use 2 simple method
Authentication Token
Signed Request
You can also combine both of them by using Token for authentication and using signature to verify integrity of the message sent
Authentication Token
If you are going to consider matching any identification in the database perhaps you can consider creating authentication token rather than user ID, session, cookies, ip, time stamp, etc! as suggested.
Create a random token and save to Database
$token = bin2hex(mcrypt_create_iv(64, MCRYPT_DEV_URANDOM));
This can be easily generated
You can guaranteed it more difficult to guess unlike password
It can easily be deleted if compromised and re generate another key
Signed Request
The concept is simple, For each file uploaded must meat a specific signature crated using a random generated key just like the token for each specific user
This can easily be implemented with HMAC with hash_hmac_file function
Combine Both Authentication & Signed Request
Here is a simple Prof of concept
Server 1
/**
* This should be stored securly
* Only known to User
* Unique to each User
* Eg : mcrypt_create_iv(32, MCRYPT_DEV_URANDOM);
*/
$key = "d767d183315656d90cce5c8a316c596c971246fbc48d70f06f94177f6b5d7174";
$token = "3380cb5229d4737ebe8e92c1c2a90542e46ce288901da80fe8d8c456bace2a9e";
$url = "http://server 2/run.php";
// Start File Upload Manager
$request = new FileManager($key, $token);
// Send Multiple Files
$responce = $request->send($url, [
"file1" => __DIR__ . "/a.png",
"file2" => __DIR__ . "/b.css"
]);
// Decode Responce
$json = json_decode($responce->data, true);
// Output Information
foreach($json as $file) {
printf("%s - %s \n", $file['name'], $file['msg']);
}
Output
temp\14-a.png - OK
temp\14-b.css - OK
Server 2
// Where to store the files
$tmpDir = __DIR__ . "/temp";
try {
$file = new FileManager($key, $token);
echo json_encode($file->recive($tmpDir), 128);
} catch (Exception $e) {
echo json_encode([
[
"name" => "Execption",
"msg" => $e->getMessage(),
"status" => 0
]
], 128);
}
Class Used
class FileManager {
private $key;
function __construct($key, $token) {
$this->key = $key;
$this->token = $token;
}
function send($url, $files) {
$post = [];
// Convert to array fromat
$files = is_array($files) ? $files : [
$files
];
// Build Post Request
foreach($files as $name => $file) {
$file = realpath($file);
if (! (is_file($file) || is_readable($file))) {
throw new InvalidArgumentException("Invalid File");
}
// Add File
$post[$name] = "#" . $file;
// Sign File
$post[$name . "-sign"] = $this->sign($file);
}
// Start Curl ;
$ch = curl_init($url);
$options = [
CURLOPT_HTTPHEADER => [
"X-TOKEN:" . $this->token
],
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_POST => count($post),
CURLOPT_POSTFIELDS => $post
];
curl_setopt_array($ch, $options);
// Get Responce
$responce = [
"data" => curl_exec($ch),
"error" => curl_error($ch),
"error" => curl_errno($ch),
"info" => curl_getinfo($ch)
];
curl_close($ch);
return (object) $responce;
}
function recive($dir) {
if (! isset($_SERVER['HTTP_X_TOKEN'])) {
throw new ErrorException("Missing Security Token");
}
if ($_SERVER['HTTP_X_TOKEN'] !== $this->token) {
throw new ErrorException("Invalid Security Token");
}
if (! isset($_FILES)) {
throw new ErrorException("File was not uploaded");
}
$responce = [];
foreach($_FILES as $name => $file) {
$responce[$name]['status'] = 0;
// check if file is uploaded
if ($file['error'] == UPLOAD_ERR_OK) {
// Check for signatire
if (isset($_POST[$name . '-sign']) && $_POST[$name . '-sign'] === $this->sign($file['tmp_name'])) {
$path = $dir . DIRECTORY_SEPARATOR . $file['name'];
$x = 0;
while(file_exists($path)) {
$x ++;
$path = $dir . DIRECTORY_SEPARATOR . $x . "-" . $file['name'];
}
// Move File to temp folder
move_uploaded_file($file['tmp_name'], $path);
$responce[$name]['name'] = $path;
$responce[$name]['sign'] = $_POST[$name . '-sign'];
$responce[$name]['status'] = 1;
$responce[$name]['msg'] = "OK";
} else {
$responce[$name]['msg'] = sprintf("Invalid File Signature");
}
} else {
$responce[$name]['msg'] = sprintf("Upload Error : %s" . $file['error']);
}
}
return $responce;
}
private function sign($file) {
return hash_hmac_file("sha256", $file, $this->key);
}
}
Other things to consider
For better security you can consider the follow
IP Lock down
File Size Limit
File Type Validation
Public-Key Cryptography
Changing Date Based token generation
Conclusion
The sample class can be extended in so many ways and rather than use URL you can consider a proper json RCP solution
A long enough, single-use, short-lived, random generated key should suffice in this case.
Client requests for a file to Server 1
Server 1 confirms login information and generates a long single-use key and sends it to the user. Server 1 keeps track of this key and matches it with an actual file on Server 2.
Client sends a request to Server 2 along with the key
Server 2 contacts Server 1 and submits the key
Server 1 returns a file path if the key is valid. The key is invalidated (destroyed).
Server 2 sends the file to the client
Server 1 invalidates the key after say 30 seconds, even if it didn't receive a confirmation request from Server 2. Your front-end should account for this case and retry the process a couple of times before returning an error.
I do not think there is a point in sending cookie/session information along, this information can be brute-forced just like the random key.
A 1024-bit long key sounds more than reasonable. This entropy can be obtained with a string of less than 200 alphanumeric characters.
For the absolute best security you would need some communication from server 2 to server 1, to double check if the request is valid. Although this communication could be minimal, its still communication and thus slows down the proces.
If you could live with a marginally less secure solution, I would suggest the following.
Server 1 requestfile.php:
<?php
//check login
if (!$loggedon) {
die('You need to be logged on');
}
$dataKey = array();
$uniqueKey = 'fgsdjk%^347JH$#^%&5ghjksc'; //choose whatever you want.
//check file
$file = isset($_GET['file']) ? $_GET['file'] : '';
if (empty($file)) {
die('Invalid request');
}
//add user data to create a reasonably unique fingerprint.
//It will mostlikely be the same for people in the same office with the same browser, thats mainly where the security drop comes from.
//I double check if all variables are set just to be sure. Most of these will never be missing.
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$dataKey[] = $_SERVER['HTTP_USER_AGENT'];
}
if (isset($_SERVER['REMOTE_ADDR'])) {
$dataKey[] = $_SERVER['REMOTE_ADDR'];
}
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$dataKey[] = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
}
if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
$dataKey[] = $_SERVER['HTTP_ACCEPT_ENCODING'];
}
if (isset($_SERVER['HTTP_ACCEPT'])) {
$dataKey[] = $_SERVER['HTTP_ACCEPT'];
}
//also add the unique key
$dataKey[] = $uniqueKey;
//add the file
$dataKey[] = $file;
//add a timestamp. Since the request will be a different times, dont use the exact second
//make sure its added last
$dataKey[] = date('YmdHi');
//create a hash
$hash = md5(implode('-', $dataKey));
//send to server 2
header('Location: https://server2.com/download.php?file='.urlencode($file).'&key='.$hash);
?>
On server 2 you will do almost the same.
<?php
$valid = false;
$dataKey = array();
$uniqueKey = 'fgsdjk%^347JH$#^%&5ghjksc'; //same as on server one
//check file
$file = isset($_GET['file']) ? $_GET['file'] : '';
if (empty($file)) {
die('Invalid request');
}
//check key
$key = isset($_GET['key']) ? $_GET['key'] : '';
if (empty($key)) {
die('Invalid request');
}
//add user data to create a reasonably unique fingerprint.
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$dataKey[] = $_SERVER['HTTP_USER_AGENT'];
}
if (isset($_SERVER['REMOTE_ADDR'])) {
$dataKey[] = $_SERVER['REMOTE_ADDR'];
}
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$dataKey[] = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
}
if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
$dataKey[] = $_SERVER['HTTP_ACCEPT_ENCODING'];
}
if (isset($_SERVER['HTTP_ACCEPT'])) {
$dataKey[] = $_SERVER['HTTP_ACCEPT'];
}
//also add the unique key
$dataKey[] = $uniqueKey;
//add the file
$dataKey[] = $file;
//add a timestamp. Since the request will be a different times, dont use the exact second
//keep the request time in a variable
$time = time();
$dataKey[] = date('YmdHi', $time);
//create a hash
$hash = md5(implode('-', $dataKey));
if ($hash == $key) {
$valid = true;
} else {
//perhaps the request to server one was made at 2013-06-26 14:59 and the request to server 2 come in at 2013-06-26 15:00
//It would still fail when the request to server 1 and 2 are more then one minute apart, but I think thats an acceptable margin. You could always adjust for more margin though.
//drop the current time
$requesttime = array_pop($dataKey);
//go back one minute
$time -= 60;
//add the time again
$dataKey[] = date('YmdHi', $time);
//create a hash
$hash = md5(implode('-', $dataKey));
if ($hash == $key) {
$valid = true;
}
}
if ($valid!==true) {
die('Invalid request');
}
//all is ok. Put the code to download the file here
?>
You can restrict access to server2. Only server1 will be able to send request to server2. You can do this by whitelisting ip of server1 on server side or using .htaccess file. In php you can do by checking request generated ip and validate it with server1 ip.
Also you can write a algorithm which generates a unique number. Using that algorithm generate a number on server1 and send it to server2 in request. On server2 check if that number is generated by algorithm and if yes then request is valid.
I'd go with a simple symetric encryption, where server 1 encodes the date and the authenticated user using a key known only by server 1 and server 2, sending it to the client who cant read it, but can send it to server 2 as a sort of ticket to authenticate himself. The date is important to not let any client use the same "ticket" over the time. But at least one of the servers must know which user have access to which files, so unless you use dedicated folders or access groups you must keep the user and file infos together.