Ресурс PHP cUrl Handle заменен целым числом

Я использую PHP curl для отправки серии запросов на сторонний сервер, для которого требуется вход в систему, а затем сохранение файла cookie сеанса для этого входа.

Поэтому я обернул операцию curl в этот класс:

class SoapCli {
    private $ch;
    private $id;
    private $rc;

    function __construct() {
        $this->rc=0;
        $this->id=bin2hex(random_bytes(8));
        $this->ch = curl_init();
        $time=microtime(true);
        error_log(PHP_EOL.PHP_EOL."Instance id $this->id created ($time): \$this->ch = ".print_r($this->ch,true).PHP_EOL,3,"log.txt");
        curl_setopt($this->ch, CURLOPT_AUTOREFERER,1);
        curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, 120);
        curl_setopt($this->ch, CURLOPT_COOKIEFILE, "");
        curl_setopt($this->ch, CURLOPT_ENCODING, "");
        curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($this->ch, CURLOPT_MAXREDIRS, 10);
        curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($this->ch, CURLOPT_VERBOSE, 1);
    }

    function Request(string $method, string $url, array $headers = array(), $postdata = "", $referer = null) {
        $resp = new stdClass();
        $resp->id = $this->id;
        $this->rc++;
        $time=microtime(true);
        error_log("Instance id $this->id before request $this->rc ($time): \$this->ch = ".print_r($this->ch,true).PHP_EOL,3,"log.txt");
        try {
            curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method);
            curl_setopt($this->ch, CURLOPT_URL, $url);
            curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
            if (isset($referer)) curl_setopt($this->ch, CURLOPT_REFERER, $referer);
            if (preg_match("/^POST$/i",$method)===1) curl_setopt($this->ch, CURLOPT_POSTFIELDS, $postdata);
            $resp->body = curl_exec($this->ch);
            $resp->err_message = curl_error($this->ch);
            $resp->err_number = curl_errno($this->ch);
            $resp->info = curl_getinfo($this->ch);
        }
        catch (Exception $exception) {
            $resp->err_message = $exception->getMessage();
            $resp->err_number = $exception->getCode();
            $resp->info = $exception->getTrace();
        }
        $time=microtime(true);
        error_log("Instance id $this->id before request $this->rc ($time): \$this->ch = ".print_r($this->ch,true).PHP_EOL,3,"log.txt");
        return $resp;
    }
}

Однако после третьего запроса содержимое защищенной переменной, в которой хранится ресурс дескриптора завитка, заменено значением 0 (целое число), и я действительно не могу понять, почему. Я смог собрать только этот лог:

Instance id 1cb893bc5b7369bd created (1547852391.7976): $this->ch = Resource id #3
Instance id 1cb893bc5b7369bd before request 1 (1547852391.8025): $this->ch = Resource id #3
Instance id 1cb893bc5b7369bd before request 1 (1547852392.0723): $this->ch = Resource id #3
Instance id 1cb893bc5b7369bd before request 2 (1547852392.0778): $this->ch = Resource id #3
Instance id 1cb893bc5b7369bd before request 2 (1547852392.357): $this->ch = Resource id #3
Instance id 1cb893bc5b7369bd before request 3 (1547852392.3616): $this->ch = Resource id #3
Instance id 1cb893bc5b7369bd before request 3 (1547852392.6225): $this->ch = Resource id #3
Instance id 1cb893bc5b7369bd before request 4 (1547852393.0264): $this->ch = 0
Instance id 1cb893bc5b7369bd before request 4 (1547852393.0758): $this->ch = 0
Instance id 1cb893bc5b7369bd before request 5 (1547852394.8992): $this->ch = 0
Instance id 1cb893bc5b7369bd before request 5 (1547852394.9461): $this->ch = 0

EDIT: Это код, который использует класс SoapCli:

// index.php

$postdata = filter_input_array(INPUT_POST);
if ($_SESSION["logged_in"]===true) {
    echo file_get_contents("main.html");
} else if (isset($postdata) && isset($postdata["action"])) {
    $action = $postdata["action"];
    if ($action==="Login" && isset($postdata["usrcpf"]) && isset($postdata["usrpwd"])) {
        $username=$postdata["username"];
        $password=$postdata["password"];
        $sc=new SoapCli();   //instantiated here
        $_SESSION["sc"]=$sc;
        $login_response = $sc->Request(
            "GET",
            BASEURL."/login",
            array(
                "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0",
                "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                "Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
                "Connection: keep-alive",
                "Upgrade-Insecure-Requests: 1",
                "Cache-Control: max-age=0"
                )
            );
        if ($login_response->err_number) {
            echo file_get_contents("login_server_error.html");
        } else {
            $dom = new DOMDocument;
            $dom->loadHTML($login_response->body);
            $xdom  = new DOMXPath($dom);
            $csrf_token_nodes = $xdom->query("//input[@name='_csrf_token']/@value");
            if ($csrf_token_nodes->length<1) {
                echo file_get_contents("login_server_error.html");
            } else {
                $csrf_token = $csrf_token_nodes->item(0)->textContent;
                $postdata = "_csrf_token=$csrf_token&_username=$username&_password=$password&_submit=Login";
                $login_check_response = $sc->Request(
                    "POST",
                    BASEURL."/login_check",
                    array(
                        "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0",
                        "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                        "Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
                        "Content-Type: application/x-www-form-urlencoded",
                        "Connection: keep-alive",
                        "Upgrade-Insecure-Requests: 1"
                    ),
                    $postdata,
                    BASEURL."/login"
                    );
                if ($login_check_response->err_number) {
                    echo file_get_contents("login_server_error.html");
                } elseif (strpos($login_check_response->body, "api.js")) {
                    echo file_get_contents("login_auth_error.html");
                } else {
                    $route_userinfo = $sc->Request(
                        "POST",
                        BASEURL."/route",
                        array(
                             "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0",
                             "Accept: */*",
                             "Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
                             "Content-Type: application/json",
                             "X-Requested-With: XMLHttpRequest",
                             "Connection: keep-alive",
                             "Upgrade-Insecure-Requests: 1",
                         ),
                        USERINFO_JSON,
                        BASEURL."/"
                        );
                    if ($route_userinfo->err_number) {
                        echo file_get_contents("login_server_error.html");
                    } else {
                        $_SESSION["logged_in"]=true;
                        $_SESSION["user_info"]=json_decode($route_userinfo->body);
                        header("Location: ".$_SERVER["PHP_SELF"], true, 303);
                    }
                }
            }
        }
    } else {
        http_response_code(400);
    }
} else {
    echo file_get_contents("login.html");
}

а также

// ajax.php (called by JS in main.html, which is loaded after login)

if ($_SESSION["logged_in"]===true) {
    $postdata = filter_input_array(INPUT_POST);
    if (isset($postdata)) {
        if (isset($postdata["content"])) {
            if ($postdata["content"]==="tasks") {
                $sc=$_SESSION["sc"];
                $route_tasks = $sc->Request(
                    "POST",
                    BASEURL."/route",
                    array(
                         "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0",
                         "Accept: */*",
                         "Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
                         "Content-Type: application/json",
                         "X-Requested-With: XMLHttpRequest",
                         "Connection: keep-alive",
                         "Upgrade-Insecure-Requests: 1",
                     ),
                    TAKS_JSON,
                    BASEURL."/"
                    );
                if ($route_tasks->err_number) {
                    echo file_get_contents("ajax_server_error.html");
                } else {
                    $tarefas=json_decode($route_tasks->body);
                    if (isset($tarefas) && is_array($tarefas->records)) {
                        foreach($tarefas->records as $i=>$tarefa){
                            echo "<p>".$tarefa->especieTarefa->nome."</p>";
                        }
                    } else {
                        http_response_code(500);
                    }
                }
            }
        } else {
            http_response_code(400);
        }
    } else {
        http_response_code(400);
    }
} else {
    http_response_code(403);
}

Поскольку переменная SoapCli::ch недоступна вне класса, я действительно не понимаю, как ее содержимое может быть изменено без оператора. Я не нашел никакой информации о HTTP-запросе/ответе, который также уничтожил бы дескриптор.

Дополнительная информация

Что бы это ни было, это не имеет отношения к запросу, потому что я пытался повторить запрос № 3, который является действительным и получает действительный ответ, и его повторение не удается, потому что дескриптор исчез.

Кроме того, то, что я пытаюсь реализовать в PHP, уже сделано полнофункциональным настольным приложением .NET (winforms), поэтому это не похоже на то, что это невозможно сделать по внешним причинам. Я просто пытаюсь сделать с PHP curl то же, что и с System.Net.HttpWebRequest, и наткнулся на проблему, описанную в этом посте.

Как я могу сохранить ручку, пока она мне нужна?

Я использую PHP 7.2 в IIS Express/Windows 10.


person VBobCat    schedule 18.01.2019    source источник
comment
Вы уверены, что опубликовали фактический код, который используете? В коде указано New SoapCli instance, но в сообщении журнала указано только created.   -  person Barmar    schedule 19.01.2019
comment
Я изменил только текст лога   -  person VBobCat    schedule 19.01.2019
comment
Доступ к защищенным свойствам и их изменение можно получить в подклассах, в этом разница между защищенными и частными. У вас есть подклассы SoapCli?   -  person Barmar    schedule 19.01.2019
comment
Нет, только этот класс   -  person VBobCat    schedule 19.01.2019
comment
Что произойдет, если вы измените его на частный?   -  person Barmar    schedule 19.01.2019
comment
Я попробую это и вернусь с ответом   -  person VBobCat    schedule 19.01.2019
comment
Ничего другого не происходит. @Barmar, я отредактировал сообщение, чтобы обновить свой код и журнал по вашему предыдущему наблюдению.   -  person VBobCat    schedule 19.01.2019
comment
Если это действительно весь код класса, я не вижу другого способа, кроме серьезной ошибки PHP. Переменные не меняют значения сами по себе.   -  person Barmar    schedule 19.01.2019
comment
Клянусь, это весь код... Я сделал класс именно для того, чтобы запечатать переменную curl, потому что до этого она действовала мне на нервы.   -  person VBobCat    schedule 19.01.2019
comment
Не выключайте CURLOPT_SSL_VERIFYPEER   -  person Dharman    schedule 19.01.2019
comment
Попробуйте установить curl_setopt($this->ch, CURLOPT_FAILONERROR, true); и вывести curl_getinfo($this->ch, CURLINFO_HTTP_CODE);, возможно, curl закрывается после сбоя. Я не уверен, что блок try catch действительно работает с curl   -  person Solrac    schedule 19.01.2019
comment
@Solrac Даже если curl закроется, как это изменит значение $this->ch?   -  person Barmar    schedule 19.01.2019
comment
Потому что 0 == false правда, черт возьми, я думаю, что даже null == false правда   -  person Solrac    schedule 19.01.2019
comment
Таким образом, вы также можете попробовать var_dump() или var_export() вместо print_r().   -  person Solrac    schedule 19.01.2019
comment
@Solrac, никаких изменений после реализации вашего предложения curl_setopt($this->ch, CURLOPT_FAILONERROR, true);, и если я заменю print_r на var_export, будет печататься $this->ch = NULL до запроса № 3 и $this->ch = 0 после этого.   -  person VBobCat    schedule 19.01.2019
comment
Получаете ли вы http 200 за каждый вызов, у которого есть ресурс?   -  person Solrac    schedule 19.01.2019
comment
Да, 200 с №1 по №3, и после этого запрос даже не отправляется.   -  person VBobCat    schedule 19.01.2019
comment
Вы не включаете весь код, потому что класс, который вы определили, бесполезен. А как вы думаете, что try блок идет в catch?   -  person miken32    schedule 19.01.2019
comment
@ miken32, я не использовал этот класс для краткости: он создается без параметров, а затем вызывает эти ресурсы на одном и том же сервере: 1. GET /login' (receives text/html); 2. POST /login_check` (отправляет application/x-www-form-urlencoded имя пользователя и пароль, получает text/html); 3. POST /route (отправляет параметры приложения/json, получает tex/html); 4. POST /route (отправляет параметры приложения/json, получает tex/html); и т. д. #3 и #4 похожи, но ручка исчезает после успешного #3. Что касается блока try, я хотел поймать, что происходит, но ничего не вышло.   -  person VBobCat    schedule 19.01.2019
comment
@Dharman, ты прав, я настрою правильный cacerts.pem, прежде чем переходить к производству, если смогу решить проблему этого поста.   -  person VBobCat    schedule 19.01.2019
comment
Я не могу воспроизвести вашу ситуацию, я смог сделать 100 вызовов с использованием цикла for без проблем, все с использованием GET на одну и ту же страницу. Можете ли вы предоставить код, который вы используете для использования этого класса?   -  person Solrac    schedule 20.01.2019
comment
@Solrac, вот оно. Однако мне не разрешено предоставлять фактические URL-адреса и информацию для входа. Я надеюсь, вы понимаете.   -  person VBobCat    schedule 20.01.2019


Ответы (2)


Короткий ответ: дескриптор не существует, когда вы пытаетесь использовать его внутри ajax.php

Внутри ajax.php взгляните на следующую строку:

                $sc=$_SESSION["sc"];

И тогда вы звоните:

                $route_tasks = $sc->Request(
                   ...
                    );

Итак, вы создали свой класс внутри index.php и все 3 вызова, сделанные там, были успешными, затем вы записываете объект в переменную $_SESSION["sc"], и, по-видимому, объект правильно кодируется и декодируется обработчиком сеанса php, поэтому вы все еще можете вызвать метод Request внутри ajax.php после получения объекта.

Хотя вы действительно используете объект в ajax.php, это не тот экземпляр объекта, который был создан index.php, поскольку этот экземпляр принадлежит потоку index.php вместе с дескриптором curl; вызов ajax.php из index.php создаст другой поток для его обработки, а также потребует новый дескриптор curl.

Измените $sc=$_SESSION["sc"]; на $sc=new SoapCli();, чтобы дескриптор curl можно было создать перед использованием.

person Solrac    schedule 21.01.2019
comment
Привет, @Solrac, спасибо. Я понимаю, что вы говорите. Я нашел обходной путь, установив файл cookie с CURLOPT_COOKIEJAR и CURLOPT_COOKIEFILE и перестраивая дескриптор каждый раз, когда он исчезал, и он начинал работать, и это имеет смысл с вашим объяснением. Однако есть кое-что, что меня озадачивает. Как может быть еще один экземпляр SoapCli в ajax.php, если конструктор не вызывался? Потому что если бы это было так, то был бы новый дескриптор, и он бы не работал из-за отсутствия файла cookie сеанса (полученного первым экземпляром)... - person VBobCat; 21.01.2019
comment
$_SESSION["sc"] он содержит уже созданную копию исходного объекта. Для этой копии конструктор никогда не вызывается; объект был извлечен из хранилища сеанса и построен на session_start(), и поэтому каждый раз, когда новый php-скрипт запускает этот объект, будет отсутствовать дескриптор curl. Однако необычно использовать хранилище сеансов в качестве транспорта класса. - person Solrac; 21.01.2019
comment
Я новичок в PHP и веб-разработке, но теперь я понимаю, почему необычно хранить сконструированные объекты в переменных сеанса. Они могут стать зомби, как в этом случае. Большое спасибо за Вашу помощь! - person VBobCat; 21.01.2019
comment
С удовольствием @VbobCat Рад, что смог помочь :) - person Solrac; 21.01.2019

Я публикую этот ответ только для того, чтобы показать, как я решил проблему, описанную и объясненную @Solrac в его ответе (что правильно, и я соглашусь):

class SoapCli {
    private $ch;
    private $cookiepot;

    function __construct() {
        $this->cookiepot=tempnam(sys_get_temp_dir(),"CookieJar");
        $this->reconstruct();
    }

    function reconstruct() {
        $this->ch = curl_init();
        curl_setopt($this->ch, CURLOPT_AUTOREFERER, true);
        curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, 300);
        curl_setopt($this->ch, CURLOPT_COOKIEFILE, $this->cookiepot);
        curl_setopt($this->ch, CURLOPT_COOKIEJAR, $this->cookiepot);
        curl_setopt($this->ch, CURLOPT_ENCODING, "");
        curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($this->ch, CURLOPT_HEADER, true);
        curl_setopt($this->ch, CURLINFO_HEADER_OUT, true);
        curl_setopt($this->ch, CURLOPT_MAXREDIRS, 32);
        curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->ch, CURLOPT_VERBOSE, true);
    }

    function Request(string $method, string $url, array $headers = array(), $postdata = "", $referer = "") {
        if (!is_resource($this->ch)) {
            $this->reconstruct();
        }
        curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($this->ch, CURLOPT_URL, $url);
        curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($this->ch, CURLOPT_REFERER, $referer);
        if (preg_match("/^POST$/i",$method)===1) curl_setopt($this->ch, CURLOPT_POSTFIELDS, $postdata);
        $response=curl_exec($this->ch);
        list($headers,$body)=preg_split("/\r\n\r\n(?!HTTP)/", $response, 2);
        $resp_obj = (object) array(
            "body"=>$body,
            "headers"=>$headers,
            "err_number"=>curl_errno($this->ch),
            "err_message"=>curl_error($this->ch),
            "info"=>curl_getinfo($this->ch)
        );
        return $resp_obj;
    }

    function log(string $text) {
        file_put_contents($this->id."log.txt",$text.PHP_EOL,FILE_APPEND|FILE_TEXT|LOCK_EX);
    }
}
person VBobCat    schedule 21.01.2019