Related
I've developed an API which originally was only used via a browser and never noticed an issue however, I am now trying to connect to it via a third party Android library (OkHttpClient) and I've tested what I am seeing using a REST API test client (Insomnia.rest).
The problem I am having is when I perform the login action of the API I start a session and call session_regenerate_id(true); to avoid sticky session attacks (I'm not sure if that's proper name).
However, when I do this I return two PHPSESSID cookies as shown in the headers below:
< HTTP/1.1 200 OK
< Date: Thu, 18 Apr 2019 22:51:43 GMT
< Server: Apache/2.4.27 (Win64) PHP/7.1.9
< X-Powered-By: PHP/7.1.9
* cookie size: name/val 8 + 6 bytes
* cookie size: name/val 4 + 1 bytes
< Set-Cookie: ClientID=413059; path=/
* cookie size: name/val 9 + 26 bytes
* cookie size: name/val 4 + 1 bytes
< Set-Cookie: PHPSESSID=15u9j1p2oinfl5a8slh518ee9r; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
* cookie size: name/val 9 + 26 bytes
* cookie size: name/val 4 + 1 bytes
* Replaced cookie PHPSESSID="hkkffpj8ta9onsn92pp70r257v" for domain localhost, path /, expire 0
< Set-Cookie: PHPSESSID=hkkffpj8ta9onsn92pp70r257v; path=/
* cookie size: name/val 17 + 1 bytes
* cookie size: name/val 4 + 1 bytes
< Set-Cookie: UsingGoogleSignIn=0; path=/
* cookie size: name/val 6 + 1 bytes
* cookie size: name/val 4 + 1 bytes
< Set-Cookie: UserID=7; path=/
< Access-Control-Allow-Credentials: true
< Content-Length: 47
< Content-Type: application/json
As you can see from the above output there's two Set-Cookies with PHPSESSID. If I remove the session_regenerate_id I then only get the one PHPSESSID cookie and then the Android client successfully works.
I've exhibited this on Apache under Wamp on Windows 10 and Apache in production on a CentOS 7 build.
So question is, how can I generate a new PHP session ID without sending back two different PHPSESSID cookies?
UDPATE
Below is some of the code relating to the login process. I can't include all of the code but it should show the concept of what is going on.
An API request is made to the login function
$email = mysqli_escape_string($this->getDBConn(), $encryption->encrypt($postArray["email"]));
$password = mysqli_escape_string($this->getDBConn(), $encryption->encrypt($postArray["password"]));
$externalDevice = isset($postArray["external_device"]) ? abs($postArray["external_device"]) : 0;
$query = "SELECT * FROM users WHERE Email='$email'";
$result = $this->getDBConn()->query($query);
if ($result)
{
if (mysqli_num_rows($result) > 0 )
{
$myrow = $result->fetch_array();
if ($myrow["UsingGoogleSignIn"] === '1')
{
//We're trying to login as a normal user, but the account was registered using Google Sign In
//so tell the user to login via google instead
return new APIResponse(API_RESULT::SUCCESS, "AccountSigninViaGoogle");
}
else
{
//Check the password matches
if ($myrow["Password"] === $password)
{
$this->getLogger()->writeToLog("Organisation ID: " . $myrow["Organisation"]);
$organisationDetails = $this->getOrganisationDetails(abs($myrow["Organisation"]), false);
$this->getLogger()->writeToLog(print_r($organisationDetails, true));
$this->createLoginSession($myrow, $organisationDetails, false, $paymentRequired, $passwordChangeRequired);
$data = null;
if ($externalDevice === 1)
{
$data = new stdClass();
$data->AuthToken = $_SESSION["AuthToken"];
$data->ClientID = $_SESSION["ClientID"];
$data->UserID = abs($_SESSION["UserID"]);
}
$this->getLogger()->writeToLog("Login Response Headers");
$headers = apache_response_headers();
$this->getLogger()->writeToLog(print_r($headers, true));
At this point an API response is returned which contains JSON object
In the code above, if the email and password matches (not using Google sign in here) it calls createLoginSession which is the following:
private function createLoginSession($myrow, $organisationDetails, $usingGoogleSignIn, &$paymentRequired, &$passwordChangeRequired)
{
require_once 'CommonTasks.php';
require_once 'IPLookup.php';
require_once 'Encryption.php';
try
{
$this->getLogger()->writeToLog("Creating login session");
$paymentRequired = false;
if ($organisationDetails === null)
{
$organisationDetails = $this->getOrganisationDetails($myrow["Organisation"]);
}
$encryption = new Encryption();
$userID = mysqli_escape_string($this->getDBConn(), $myrow["UserID"]);
$organisationID = intval(abs($myrow["Organisation"]));
$commonTasks = new CommonTasks();
$browserDetails = $commonTasks->getBrowserName();
$this->getLogger()->writeToLog("Browser Details");
$this->getLogger()->writeToLog(print_r($browserDetails, true));
$clientName = $browserDetails["name"];
$iplookup = new IPLookup(null, $this->getLogger());
$ipDetails = json_decode($iplookup->getAllIPDetails($commonTasks->getIP()));
if ($ipDetails !== null)
{
$ip = $ipDetails->ip;
$country = $ipDetails->country_name;
$city = $ipDetails->city;
}
else
{
$ip = "";
$country = "";
$city = "";
}
//Create a random client ID and store this as a cookie
if (isset($_COOKIE["ClientID"]))
{
$clientID = $_COOKIE["ClientID"];
}
else
{
$clientID = $commonTasks->generateRandomString(6, "0123456789");
setcookie("ClientID", $clientID, 0, "/");
}
//Create an auth token
$authToken = $commonTasks->generateRandomString(25);
$encryptedAuthToken = $encryption->encrypt($authToken);
$query = "REPLACE INTO client (ClientID, UserID, AuthToken, ClientName, Country, City, IPAddress) " .
"VALUES ('$clientID', '$userID', '$encryptedAuthToken', '$clientName', '$country', '$city', '$ip')";
$result = $this->getDBConn()->query($query);
if ($result)
{
session_start();
$this->getLogger()->writeToLog("Logging in and regnerating session id");
session_regenerate_id(true);
$_SESSION["AuthToken"] = $authToken;
$_SESSION["ClientID"] = $clientID;
$_SESSION["UserID"] = $userID;
$_SESSION["FirstName"] = $this->getEncryption()->decrypt($myrow["FirstName"]);
$_SESSION["LastName"] = $this->getEncryption()->decrypt($myrow["LastName"]);
$passwordChangeRequired = $myrow["PasswordChangeRequired"] === "1" ? true : false;
//Check if the last payment failure reason is set, if so, set a cookie with the message but only
//if the organisation is not on the free plan
//Logger::log("Current Plan: " . $this->getOrganisationDetails(->getPlan()));
if ($organisationDetails->getPlan() !== "Free")
{
if (!empty($organisationDetails->getLastPaymentFailureReason()))
{
$this->getLogger()->writeToLog("Detected last payment as a failure. Setting cookies for organisation id: " . $organisationDetails->getId());
setcookie("HavePaymentFailure", true, 0, "/");
setcookie("PaymentFailureReason", $organisationDetails->getLastPaymentFailureReason(), 0, "/");
}
//Check if the current SubscriptionPeriodEnd is in the past
$subscriptionPeriodEnd = $organisationDetails->getSubscriptionOfPeriod();
$currentTime = DateTimeManager::getEpochFromCurrentTime();
if ($currentTime > $subscriptionPeriodEnd)
{
$this->getLogger()->writeToLog("Detected payment overdue for organisation: " . $organisationDetails->getId());
//The payment was overdue, determine the number of days grace period (there's a 7 day grace period) that's left
$subscriptionPeriodEndGracePeriod = $subscriptionPeriodEnd + (86400 * 7);
$numberOfDaysRemaining = floor((($subscriptionPeriodEndGracePeriod - $currentTime) / 86400));
setcookie("PaymentOverdue", true, 0, "/");
setcookie("DaysGraceRemaining", $numberOfDaysRemaining, 0, "/");
if ($numberOfDaysRemaining <= 0)
{
$paymentRequired = true;
}
}
}
setcookie("UsingGoogleSignIn", $usingGoogleSignIn ? "1" : "0", 0, "/");
if ($organisationDetails->getId() !== 0)
{
$_SESSION["OrganisationDetails"] = array();
$_SESSION["OrganisationDetails"]["id"] = $organisationDetails->getId();
$_SESSION["OrganisationDetails"]["Name"] = $organisationDetails->getName();
}
setcookie("UserID", $userID, 0, "/");
$this->getLogger()->writeToLog("Successfully created login session. User ID '$userID' and Organisation ID '$organisationID'");
return true;
}
else
{
$error = mysqli_error($this->getDBConn());
$this->getLogger()->writeToLog("Failed to create login session. DB Error: $error");
$this->getAlarms()->setAlarm(AlarmLevel::CRITICAL, "AccountManagement", "Failed to create login session. DB Error");
throw new DBException($error);
}
}
catch (DBException $ex)
{
throw $ex;
}
}
In the function above I call session_start() and then regenerate_session_id() and I then get the two PHPSESSID cookies in the response although the log line is only outputted once so its definetely not getting called multiple times.
If I remove regenerate_session_id then the problem goes away. To be safe I've tried swapping the session_start() so it comes after regenerate_session_id but that looks like the session id doesn't get re-created as expected.
UPDATE 2
From the comment from #waterloomatt I've created a PHP script that just has the following:
<?php
session_start();
session_regenerate_id(true);
phpinfo();
and the HTTP header information outputted from phpinfo is as follows
**HTTP Request Headers**
GET /api/session_test.php HTTP/1.1
Host localhost
Connection keep-alive Upgrade-Insecure-Requests 1
User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Accept text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding gzip, deflate, br
Accept-Language en-GB,en-US;q=0.9,en;q=0.8
Cookie _ga=GA1.1.1568991346.1553017442
**HTTP Response Headers**
X-Powered-By PHP/7.2.10
Set-Cookie PHPSESSID=i19irid70cqbvpkrh0ufffi0jk; path=/
Expires Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control no-store, no-cache,> must-revalidate Pragma no-cache
Set-Cookie PHPSESSID=48qvia5e6bpmmk251qfrqs8urd; path=/
This header will actually remove cookie. This is the only way to remove HTTP-only cookie: to make it expire backdate.
Set-Cookie: PHPSESSID=15u9j1p2oinfl5a8slh518ee9r; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
I think I've figured out what's going on. I thought it a cookies expires it will be all on the same line but it looks like - at least the way I'm passing the cookie strings that the cookies expiry is a different line.
I've therefore changed how I parse the cookie string so if it there's expire on the next line I don't include the cookie which seems to work. Below is how I am passing the cookies and not including the cookie if its expired:
for (int i = 0; i < headers.length; i++)
{
Log.d("BaseAPI", "Header: " + headers[i]);
if (headers[i].trim().toLowerCase().startsWith("set-cookie:"))
{
if (headers[i+1].toLowerCase().startsWith("expires"))
{
Log.d("BaseAPI", "Found expired header. The cookie is: " + headers[i+1]);
//Thu, 19 Nov 1981 08:52:00 GMT
long epoch = new SimpleDateFormat("EEE, dd MMM YYYY HH:mm:ss").parse(headers[i+1].replace("Expires: ", "").replace("GMT", "").trim()).getTime();
Log.d("BaseAPI", "Cookie Epoch: " + epoch);
long currentEpoch = new Date().getTime();
Log.d("BaseAPI", "Current Epoch: " + currentEpoch);
if (epoch < currentEpoch)
{
continue;
}
}
cookieBuilder.append(headers[i].trim().replace("Set-Cookie:", "").replace("path=/", ""));
cookieBuilder.append(" ");
}
}
This is a known and documented issue.
Just call session_regenerate_id() without passing true.
The manual clearly says that you should not delete old session data if you want to avoid racing condition and also concurrent access may lead to inconsistent state.
See https://www.php.net/manual/en/function.session-regenerate-id.php
for more info
I'm trying to post a insert on a MySQL database using idHTTP and a PHP script. This is the PHP script to insert in the database:
$mysqli = new mysqli($servidor, $usuario, $senha, $banco);
// Caso algo tenha dado errado, exibe uma mensagem de erro
if (mysqli_connect_errno()) trigger_error(mysqli_connect_error());
$iduser = quoted_printable_decode($_POST['iduser']);
$nome = quoted_printable_decode($_POST['nome']);
$data = quoted_printable_decode($_POST['data']);
$hora = quoted_printable_decode($_POST['hora']);
$mensagem = quoted_printable_decode($_POST['mensagem']);
$latitude = quoted_printable_decode($_POST['latitude']);
$longitude = quoted_printable_decode($_POST['longitude']);
$imagem = $_FILES["imagem"]['tmp_name'];
$tamanho = $_FILES['imagem']['size'];
header($_SERVER["SERVER_PROTOCOL"] . " 200 OK");
header('Content-Type: text/plain; charset="utf-8"');
if ( $imagem != "none" )
{
$fp = fopen($imagem, "rb");
$conteudo = fread($fp, $tamanho);
$conteudo = addslashes($conteudo);
fclose($fp);
$queryInsercao = "INSERT INTO tabpainel (iduser, nome, data, hora, mensagem, latitude, longitude, imagem) VALUES ('$iduser', '$nome', '$data','$hora','$mensagem', '$latitude', '$longitude', '$conteudo')";
mysqli_query($mysqli,$queryInsercao) or die("Algo deu errado ao inserir o registro. Tente novamente.");
if (mysqli_affected_rows($mysqli) > 0)
include 'baixarpainel.php';
else
print utf8_encode("Não foi possível inserir o registro");
}
else
print utf8_encode("Não foi possível carregar a imagem.");
?>
And in Delphi, i'm using this:
FormPHP := TIdMultiPartFormDataStream.Create;
FormPHP.AddFile ('imagem', AImagem, 'image/jpeg');
FormPHP.AddFormField ('iduser', AIDUser, 'utf-8');
FormPHP.AddFormField ('nome', ANome, 'utf-8');
FormPHP.AddFormField ('data', AData, 'utf-8');
FormPHP.AddFormField ('hora', AHora, 'utf-8');
FormPHP.AddFormField ('mensagem', AMensagem, 'utf-8');
FormPHP.AddFormField ('latitude', '1');
FormPHP.AddFormField ('longitude', '1');
Response := TStringStream.Create('',TEncoding.UTF8);
HTTP:= TIdHTTP.Create(self);
HTTP.Post('http://addressexample.com/cadastro.php',FormPHP,Response);
It was working fine until a had to change the hosting company. With Hostinger was ok but with Hostgator it doesn't. With Hostgator the idHTTP raise an exception in the class EIdHTTPProtocalException with the message: "HTTP/1.1 406 Not Acceptable". The Hostgator support has already disabled the mod_security, that could cause the problem.
This exception only occurs on Android. Using the same app on Windows, it works fine.
UPDATE: I've tried another thing. The PHP script is this:
// Conecta-se ao banco de dados MySQL
$mysqli = new mysqli($servidor, $usuario, $senha, $banco);
// Caso algo tenha dado errado, exibe uma mensagem de erro
if (mysqli_connect_errno()) trigger_error(mysqli_connect_error());
# Instanciando o XMLWriter
$xml = new XMLWriter;
$xml->openMemory();
# Definindo o encoding do XML
$xml->startDocument( '1.0', 'UTF-8');
# Primeiro elemento do XML
$xml->startElement("DATAPACKET");
$xml->writeAttribute("version", "2.0");
$xml->StartElement("METADATA");
$xml->startElement("FIELDS");
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "id");
$xml->writeAttribute("fieldtype", "I4");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "iduser");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "30");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "nome");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "200");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "data");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "8");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "hora");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "5");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "mensagem");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "3000");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "latitude");
$xml->writeAttribute("fieldtype", "r8");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "longitude");
$xml->writeAttribute("fieldtype", "r8");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "imagem");
$xml->writeAttribute("fieldtype", "bin.hex");
$xml->writeAttribute("subtype", "Binary");
$xml->endElement();
$xml->endElement(); //FIELDS
$xml->endElement(); //METADATA
$xml->StartElement("ROWDATA");
# Query na tabela escolhida
$rs_table = $mysqli->query("select * from tabpainel ORDER BY id DESC LIMIT 50");
while($table = $rs_table->fetch_array(MYSQLI_ASSOC))
{
# Transformando array em objeto
$table = (object)$table;
# Criando elemento tabela
$xml->StartElement("ROW");
# Setando os atributos
$xml->writeAttribute("id", "$table->id");
$xml->writeAttribute("iduser", "$table->iduser");
$xml->writeAttribute("nome", "$table->nome");
$xml->writeAttribute("data", "$table->data");
$xml->writeAttribute("hora", "$table->hora");
$xml->writeAttribute("mensagem", "$table->mensagem");
$xml->writeAttribute("latitude", "$table->latitude");
$xml->writeAttribute("longitude","$table->longitude");
$xml->writeAttribute("imagem", base64_encode("$table->imagem"));
$xml->endElement();
}
# Fechando o ROWDATA
$xml->endElement();
# Fechando o elemento DATAPACKET
$xml->endElement();
# Encerrando a conexao
//$con->close();
# Definindo cabecalho de saida
header("content-type: application/xml; charset=utf-8");
# Imprimindo a saida do XML
print $xml->outputMemory(true);
?>
And I used a http.get to receive the xml:
Http.HandleRedirects:= true;
Http.request.useragent := 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; MAAU)';
MS.Text:= Http.get('http://addressexample.com/baixarpainel.php');
MS.SaveToFile(FarquivoBaixado);
And this worked just fine on Android too. The problem remains only http.post on Android.
TIdHTTP works exactly the same way on all platforms, as Indy uses a single cross-platform codebase. So the generated HTTP request should be exactly the same on all platforms.
An HTTP 406 error happens when the HTTP request includes an Accept header that does not specify any media type that the server is capable of rendering the response in. Per RFC 2616 Section 14.1:
If no Accept header field is present, then it is assumed that the client accepts all media types. If an Accept header field is present, and if the server cannot send a response which is acceptable according to the combined Accept field value, then the server SHOULD send a 406 (not acceptable) response.
Your PHP script is sending a text/plain response, so if you send an Accept header that does not allow text/plain then that can cause a 406 error. It sounds like Hostgator is enforcing that more than Hostinger does.
By default, TIdHTTP sets its Request.Accept property to the following string value:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
Which technically allows all media types via */*, but just with a lower priority than some other media types. But that default should still be enough to allow a text/plain response, if the server implements Accept handling correctly.
You need to contact Hostgator and discuss the issue with them, as the problem is on their end, not yours.
That being said, since you know the server response is always text/plain, you could just add the following to your code before calling Post():
HTTP.Request.Accept := 'text/plain';
HTTP.Request.AcceptCharset := 'utf-8';
After long hours trying to solve, this is what happened.
On Windows, the application was working fine, and when I tried using HTTP.GET i imagined that couldn't be on server's end the problem, but on my request. So then, i started opening the headers of request and response both on Windows and Android using this code:
Astr.Add('Response text: ' + Http.Response.ResponseText);
Astr.Add(#13);
Astr.Add('Raw Headers Response: ' + HTTP.Response.RawHeaders.Text);
Astr.Add(#13);
Astr.Add('Raw Headers Request: ' + HTTP.Request.RawHeaders.Text);
This was the message i've got on Android:
Response text: HTTP/1.1 406 Not Acceptable
Raw Headers Response: Server: nginx/1.10.2
Date: ...
Content-Type: text/html;
charset=iso-8859-1
Content-Length:226
Connection: keep-alive
Raw Headers Request: Connection: keep-alive
Content-Type: multipart/form-data; boundary=------(somenumbers)
Content-Length: 0
Host: myhost
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Encoding: identity
User-Agent: ...
And this was the message on Windows
Response text: HTTP/1.1 200 OK
Raw Headers Request: Server: nginx/1.10.2
Date: ...
Content-Type: application/xml
Connection: close
Vary: Accept-Encoding,User-Agent
Raw Headers Request: Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------(numebrs)
Content-Length: 0
Host: myhost
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
User-Agent: ...
The same app on different platforms were sending different headers on Request. On Android, the idHTTP was sending an Accept-Encoding that on Windows was not. So I tried adding this: Http.Request.AcceptEncoding:= '*'; before the HTTP.POST and it worked! I received the xml just as expected.
I don't know why the idHTTP was changing the Request Accept-Encoding, but I had to specify another one and I chose this one because of MDN definition:
*
Matches any content encoding not already listed in the header. This is
the default value if the header is not present. It doesn't mean that
any algorithm is supported; merely that no preference is expressed.
I was able to resolve this issue by simple changing user agent string. I think remote machine(server) has some security installed.
I just set the Request -> UserAgent to:
Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0
and now it works !
Using this package for laravel: https://github.com/nikolajlovenhardt/laravel-google-ads
When i try to write a campaign, the following error occur:
SoapFault in AdsSoapClient.php line 115: Unmarshalling Error:
cvc-elt.4.3: Type 'ns1:Budget' is not validly derived from the type
definition, 'Campaign', of element 'ns1:operand'.
soap.log: http://pastebin.com/raw/TmU6XSfp
[2017-03-10 14:33:57] AW_SOAP.INFO: clientCustomerId=556-905-2150
operations=1 service=BudgetService method=mutate responseTime=145
requestId=00054a61407809890a378107c00d151f server=adwords.google.com
isFault=0 faultMessage= [2017-03-10 14:33:58] AW_SOAP.WARNING:
clientCustomerId=556-905-2150 operations= service=CampaignService
method=mutate responseTime= requestId= server=adwords.google.com
isFault=1 faultMessage=Unmarshalling Error: cvc-elt.4.3: Type
'ns1:Budget' is not validly derived from the type definition,
'Campaign', of element 'ns1:operand'. [2017-03-10 14:33:58]
AW_SOAP.NOTICE: POST /api/adwords/cm/v201609/CampaignService?wsdl
HTTP/1.1 Host: adwords.google.com Connection: Keep-Alive User-Agent:
PHP-SOAP/7.1.2-3+deb.sury.org~xenial+1 Content-Type: text/xml;
charset=utf-8 SOAPAction: "" Content-Length: 1436 Authorization:
REDACTED
556-905-2150REDACTEDunknown
(AwApi-PHP, googleads-php-lib/25.2.0,
PHP/7.1.2-3+deb.sury.org~xenial+1)falsefalseADDInterplanetary Cruise Budget
58c2b955417e250000000STANDARDADDInterplanetary Cruise
58c2b955cc997PAUSED1050511653DISPLAYMANUAL_CPC
HTTP/1.1 500 Internal Server Error Content-Type: text/xml;
charset=UTF-8 Date: Fri, 10 Mar 2017 14:33:58 GMT Expires: Fri, 10 Mar
2017 14:33:58 GMT Cache-Control: private, max-age=0
X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block Server: GSE Alt-Svc: quic=":443";
ma=2592000; v="36,35,34" Accept-Ranges: none Vary: Accept-Encoding
Transfer-Encoding: chunked
soap:ClientUnmarshalling
Error: cvc-elt.4.3: Type 'ns1:Budget' is not validly derived from the
type definition, 'Campaign', of element 'ns1:operand'.
Copied from the "Basic Operations Samples"
$customerClientId = 'xxx-xxx-xxxx';
$adWordsService = new AdWordsService();
/** #var BudgetService $budgetService */
$budgetService = $adWordsService->getService(BudgetService::class, $customerClientId);
// Create the shared budget (required).
$budget = new Budget();
$budget->setName('Interplanetary Cruise Budget #' . uniqid());
$money = new Money();
$money->setMicroAmount(50000000);
$budget->setAmount($money);
$budget->setDeliveryMethod(BudgetBudgetDeliveryMethod::STANDARD);
$operations = [];
// Create a budget operation.
$operation = new BudgetOperation();
$operation->setOperand($budget);
$operation->setOperator(Operator::ADD);
$operations[] = $operation;
// Create the budget on the server.
$result = $budgetService->mutate($operations);
$budget = $result->getValue()[0];
// Create a campaign with only required settings.
$campaign = new Campaign();
$campaign->setName('Interplanetary Cruise #' . uniqid());
$campaign->setAdvertisingChannelType(AdvertisingChannelType::DISPLAY);
// Set shared budget (required).
$campaign->setBudget(new Budget());
$campaign->getBudget()->setBudgetId($budget->getBudgetId());
// Set bidding strategy (required).
$biddingStrategyConfiguration = new BiddingStrategyConfiguration();
$biddingStrategyConfiguration->setBiddingStrategyType(
BiddingStrategyType::MANUAL_CPC);
$campaign->setBiddingStrategyConfiguration($biddingStrategyConfiguration);
$campaign->setStatus(CampaignStatus::PAUSED);
// Create a campaign operation and add it to the operations list.
$operation = new CampaignOperation();
$operation->setOperand($campaign);
$operation->setOperator(Operator::ADD);
$operations[] = $operation;
/** #var CampaignService $campaignService */
$campaignService = $adWordsService->getService(CampaignService::class, $customerClientId);
$result = $campaignService->mutate($operations);
foreach ($result->getValue() as $campaign) {
printf("Campaign with name '%s' and ID %d was added.\n",
$campaign->getName(),
$campaign->getId()
);
}
Receiving campaigns with the sample code works...
I fink, it's because, you make $campaignService->mutate($operations); with budget operation
$operations = [];
// Create a budget operation.
$operation = new BudgetOperation();
$operation->setOperand($budget);
$operation->setOperator(Operator::ADD);
$operations[] = $operation;
And
// Create a campaign operation and add it to the operations list.
$operation = new CampaignOperation();
$operation->setOperand($campaign);
$operation->setOperator(Operator::ADD);
$operations[] = $operation;
$result = $campaignService->mutate($operations);
You need to do $operations = []; again before $campaignService->mutate($operations);
$operations = [];
// Create a campaign operation and add it to the operations list.
$operation = new CampaignOperation();
$operation->setOperand($campaign);
$operation->setOperator(Operator::ADD);
$operations[] = $operation;
/** #var CampaignService $campaignService */
$campaignService = $adWordsService->getService(CampaignService::class, $customerClientId);
$result = $campaignService->mutate($operations);
I'm trying to post a insert on a MySQL database using idHTTP and a PHP script. This is the PHP script to insert in the database:
$mysqli = new mysqli($servidor, $usuario, $senha, $banco);
// Caso algo tenha dado errado, exibe uma mensagem de erro
if (mysqli_connect_errno()) trigger_error(mysqli_connect_error());
$iduser = quoted_printable_decode($_POST['iduser']);
$nome = quoted_printable_decode($_POST['nome']);
$data = quoted_printable_decode($_POST['data']);
$hora = quoted_printable_decode($_POST['hora']);
$mensagem = quoted_printable_decode($_POST['mensagem']);
$latitude = quoted_printable_decode($_POST['latitude']);
$longitude = quoted_printable_decode($_POST['longitude']);
$imagem = $_FILES["imagem"]['tmp_name'];
$tamanho = $_FILES['imagem']['size'];
header($_SERVER["SERVER_PROTOCOL"] . " 200 OK");
header('Content-Type: text/plain; charset="utf-8"');
if ( $imagem != "none" )
{
$fp = fopen($imagem, "rb");
$conteudo = fread($fp, $tamanho);
$conteudo = addslashes($conteudo);
fclose($fp);
$queryInsercao = "INSERT INTO tabpainel (iduser, nome, data, hora, mensagem, latitude, longitude, imagem) VALUES ('$iduser', '$nome', '$data','$hora','$mensagem', '$latitude', '$longitude', '$conteudo')";
mysqli_query($mysqli,$queryInsercao) or die("Algo deu errado ao inserir o registro. Tente novamente.");
if (mysqli_affected_rows($mysqli) > 0)
include 'baixarpainel.php';
else
print utf8_encode("Não foi possível inserir o registro");
}
else
print utf8_encode("Não foi possível carregar a imagem.");
?>
And in Delphi, i'm using this:
FormPHP := TIdMultiPartFormDataStream.Create;
FormPHP.AddFile ('imagem', AImagem, 'image/jpeg');
FormPHP.AddFormField ('iduser', AIDUser, 'utf-8');
FormPHP.AddFormField ('nome', ANome, 'utf-8');
FormPHP.AddFormField ('data', AData, 'utf-8');
FormPHP.AddFormField ('hora', AHora, 'utf-8');
FormPHP.AddFormField ('mensagem', AMensagem, 'utf-8');
FormPHP.AddFormField ('latitude', '1');
FormPHP.AddFormField ('longitude', '1');
Response := TStringStream.Create('',TEncoding.UTF8);
HTTP:= TIdHTTP.Create(self);
HTTP.Post('http://addressexample.com/cadastro.php',FormPHP,Response);
It was working fine until a had to change the hosting company. With Hostinger was ok but with Hostgator it doesn't. With Hostgator the idHTTP raise an exception in the class EIdHTTPProtocalException with the message: "HTTP/1.1 406 Not Acceptable". The Hostgator support has already disabled the mod_security, that could cause the problem.
This exception only occurs on Android. Using the same app on Windows, it works fine.
UPDATE: I've tried another thing. The PHP script is this:
// Conecta-se ao banco de dados MySQL
$mysqli = new mysqli($servidor, $usuario, $senha, $banco);
// Caso algo tenha dado errado, exibe uma mensagem de erro
if (mysqli_connect_errno()) trigger_error(mysqli_connect_error());
# Instanciando o XMLWriter
$xml = new XMLWriter;
$xml->openMemory();
# Definindo o encoding do XML
$xml->startDocument( '1.0', 'UTF-8');
# Primeiro elemento do XML
$xml->startElement("DATAPACKET");
$xml->writeAttribute("version", "2.0");
$xml->StartElement("METADATA");
$xml->startElement("FIELDS");
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "id");
$xml->writeAttribute("fieldtype", "I4");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "iduser");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "30");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "nome");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "200");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "data");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "8");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "hora");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "5");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "mensagem");
$xml->writeAttribute("fieldtype", "String");
$xml->writeAttribute("Width", "3000");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "latitude");
$xml->writeAttribute("fieldtype", "r8");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "longitude");
$xml->writeAttribute("fieldtype", "r8");
$xml->endElement();
$xml->startElement("FIELD");
$xml->writeAttribute("attrname", "imagem");
$xml->writeAttribute("fieldtype", "bin.hex");
$xml->writeAttribute("subtype", "Binary");
$xml->endElement();
$xml->endElement(); //FIELDS
$xml->endElement(); //METADATA
$xml->StartElement("ROWDATA");
# Query na tabela escolhida
$rs_table = $mysqli->query("select * from tabpainel ORDER BY id DESC LIMIT 50");
while($table = $rs_table->fetch_array(MYSQLI_ASSOC))
{
# Transformando array em objeto
$table = (object)$table;
# Criando elemento tabela
$xml->StartElement("ROW");
# Setando os atributos
$xml->writeAttribute("id", "$table->id");
$xml->writeAttribute("iduser", "$table->iduser");
$xml->writeAttribute("nome", "$table->nome");
$xml->writeAttribute("data", "$table->data");
$xml->writeAttribute("hora", "$table->hora");
$xml->writeAttribute("mensagem", "$table->mensagem");
$xml->writeAttribute("latitude", "$table->latitude");
$xml->writeAttribute("longitude","$table->longitude");
$xml->writeAttribute("imagem", base64_encode("$table->imagem"));
$xml->endElement();
}
# Fechando o ROWDATA
$xml->endElement();
# Fechando o elemento DATAPACKET
$xml->endElement();
# Encerrando a conexao
//$con->close();
# Definindo cabecalho de saida
header("content-type: application/xml; charset=utf-8");
# Imprimindo a saida do XML
print $xml->outputMemory(true);
?>
And I used a http.get to receive the xml:
Http.HandleRedirects:= true;
Http.request.useragent := 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; MAAU)';
MS.Text:= Http.get('http://addressexample.com/baixarpainel.php');
MS.SaveToFile(FarquivoBaixado);
And this worked just fine on Android too. The problem remains only http.post on Android.
TIdHTTP works exactly the same way on all platforms, as Indy uses a single cross-platform codebase. So the generated HTTP request should be exactly the same on all platforms.
An HTTP 406 error happens when the HTTP request includes an Accept header that does not specify any media type that the server is capable of rendering the response in. Per RFC 2616 Section 14.1:
If no Accept header field is present, then it is assumed that the client accepts all media types. If an Accept header field is present, and if the server cannot send a response which is acceptable according to the combined Accept field value, then the server SHOULD send a 406 (not acceptable) response.
Your PHP script is sending a text/plain response, so if you send an Accept header that does not allow text/plain then that can cause a 406 error. It sounds like Hostgator is enforcing that more than Hostinger does.
By default, TIdHTTP sets its Request.Accept property to the following string value:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
Which technically allows all media types via */*, but just with a lower priority than some other media types. But that default should still be enough to allow a text/plain response, if the server implements Accept handling correctly.
You need to contact Hostgator and discuss the issue with them, as the problem is on their end, not yours.
That being said, since you know the server response is always text/plain, you could just add the following to your code before calling Post():
HTTP.Request.Accept := 'text/plain';
HTTP.Request.AcceptCharset := 'utf-8';
After long hours trying to solve, this is what happened.
On Windows, the application was working fine, and when I tried using HTTP.GET i imagined that couldn't be on server's end the problem, but on my request. So then, i started opening the headers of request and response both on Windows and Android using this code:
Astr.Add('Response text: ' + Http.Response.ResponseText);
Astr.Add(#13);
Astr.Add('Raw Headers Response: ' + HTTP.Response.RawHeaders.Text);
Astr.Add(#13);
Astr.Add('Raw Headers Request: ' + HTTP.Request.RawHeaders.Text);
This was the message i've got on Android:
Response text: HTTP/1.1 406 Not Acceptable
Raw Headers Response: Server: nginx/1.10.2
Date: ...
Content-Type: text/html;
charset=iso-8859-1
Content-Length:226
Connection: keep-alive
Raw Headers Request: Connection: keep-alive
Content-Type: multipart/form-data; boundary=------(somenumbers)
Content-Length: 0
Host: myhost
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Encoding: identity
User-Agent: ...
And this was the message on Windows
Response text: HTTP/1.1 200 OK
Raw Headers Request: Server: nginx/1.10.2
Date: ...
Content-Type: application/xml
Connection: close
Vary: Accept-Encoding,User-Agent
Raw Headers Request: Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------(numebrs)
Content-Length: 0
Host: myhost
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
User-Agent: ...
The same app on different platforms were sending different headers on Request. On Android, the idHTTP was sending an Accept-Encoding that on Windows was not. So I tried adding this: Http.Request.AcceptEncoding:= '*'; before the HTTP.POST and it worked! I received the xml just as expected.
I don't know why the idHTTP was changing the Request Accept-Encoding, but I had to specify another one and I chose this one because of MDN definition:
*
Matches any content encoding not already listed in the header. This is
the default value if the header is not present. It doesn't mean that
any algorithm is supported; merely that no preference is expressed.
I was able to resolve this issue by simple changing user agent string. I think remote machine(server) has some security installed.
I just set the Request -> UserAgent to:
Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0
and now it works !
I'm trying to update a QB invoice with a payment using the PHP API by Keith Palmer. What I'm doing is:
1) Grab the invoice information (See code bellow)
2) Add a payment
Following this code example:
https://github.com/consolibyte/quickbooks-php/blob/master/docs/partner_platform/example_app_ipp_v3/example_payment_add.php
I've created this:
function updateQBInvoice($invoiceNumber = null, $depositAmount = null) {
$this->autoRender = false;
Configure::write('debug', 2);
include(APP."webroot".DS."quickbooks-php".DS."docs".DS."example_app_ipp_v3".DS."config.php");
$IPP = new QuickBooks_IPP($dsn);
$creds = $IntuitAnywhere->load($the_username, $the_tenant);
$IPP->authMode(QuickBooks_IPP::AUTHMODE_OAUTH, $the_username, $creds);
$realm = $creds['qb_realm'];
if ($Context = $IPP->context()) {
$IPP->version(QuickBooks_IPP_IDS::VERSION_3);
$InvoiceService = new QuickBooks_IPP_Service_Invoice();
$invoices = $InvoiceService->query($Context, $realm, "SELECT * FROM Invoice WHERE DocNumber='".$invoiceNumber."'");
$Invoice = $invoices[0];
$docNumber = $Invoice->getDocNumber();
$CustomerRef = $Invoice->getCustomerRef();
$invoiceId = $Invoice->getId();
$PaymentService = new QuickBooks_IPP_Service_Payment();
$Payment = new QuickBooks_IPP_Object_Payment();
$Payment->setPaymentRefNum($invoiceNumber);
$Payment->setTxnDate(date('Y-m-d'));
$Payment->setTotalAmt($depositAmount);
$Line = new QuickBooks_IPP_Object_Line();
$Line->setAmount($depositAmount);
$LinkedTxn = new QuickBooks_IPP_Object_LinkedTxn();
$LinkedTxn->setTxnId($invoiceId);
$LinkedTxn->setTxnType('Invoice');
$Line->setLinkedTxn($LinkedTxn);
$Payment->addLine($Line);
$Payment->setCustomerRef($CustomerRef);
debug($Payment);
if ($resp = $PaymentService->add($Context, $realm, $Payment)) {
print('Our new Payment ID is: [' . $resp . ']');
} else {
print($PaymentService->lastError());
}
debug('Request [' . $PaymentService->lastRequest() . ']');
debug('Response [' . $PaymentService->lastResponse() . ']');
} else {
echo 'Unable to load a context...?';
}
}
What I've noticed, when getting the invoice info is, all numbers are negatives, and if I assigned them to variable they are {-123}. My response is always:
2030: [Invalid ID, Id should be a valid number. Supplied value:{-10643}]
I imagine I'm setting something wrong somewhere. Would someone care to explain what I'm doing wrong please? Thanks!
UPDATE:
Request:
POST https://quickbooks.api.intuit.com/v3/company/210896252/payment HTTP/1.1
Content-Type: application/xml
Authorization: OAuth realm="", oauth_signature_method="HMAC-SHA1", oauth_signature="jxw2JuAGtvcg9ynRxXApi8VjcWk%3D", oauth_nonce="uvJB5", oauth_timestamp="1400786567", oauth_token="xxx", oauth_consumer_key="xxx", oauth_version="1.0"
Content-Length: 414
<Payment xmlns="http://schema.intuit.com/finance/v3">
<Line xmlns="http://schema.intuit.com/finance/v3">
<Amount>45</Amount>
<LinkedTxn xmlns="http://schema.intuit.com/finance/v3">
<TxnId>{-10643}</TxnId>
<TxnType>Invoice</TxnType>
</LinkedTxn>
</Line>
<PaymentRefNum>2060</PaymentRefNum>
<TxnDate>2014-05-22</TxnDate>
<TotalAmt>45</TotalAmt>
<CustomerRef>627</CustomerRef>
</Payment>
Response:
HTTP/1.1 400 Bad Request
Date: Thu, 22 May 2014 19:22:47 GMT
Content-Type: application/xml
Content-Length: 278
intuit_tid: c53e5e81-fd5e-4a24-b511-cd6b3449d012
Via: 1.1 ipp-gateway-.net
Vary: Accept-Encoding
Content-Encoding: gzip
Connection: close
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2014-05-22T12:22:47.834-07:00"><Fault type="ValidationFault"><Error code="2030" element="LinkedTxn.TxnId"><Message>Invalid ID</Message><Detail>Id should be a valid number. Supplied value:{-10643}</Detail></Error></Fault></IntuitResponse>
This highlights your problem here:
<LinkedTxn xmlns="http://schema.intuit.com/finance/v3">
<TxnId>0</TxnId>
<TxnType>Invoice</TxnType>
</LinkedTxn>
You're not setting a valid Id value for the transaction for some reason.
This is probably because of one of two reasons:
You're just not setting a valid TxnId in your code somewhere (if you print out print($Invoice->getId()); what do you see?)
OR
You're using an outdated version of the code from GitHub, which had a but related to this in it (make sure you're using the very latest code from GitHub, as of today)