Как прочитать весь вывод из запроса FastCGI?

Я пытаюсь сделать запрос к файлам PHP из Rust, используя протокол FastCGI . У меня есть этот код благодаря ответу на мой предыдущий вопрос:

use std::os::unix::net::{UnixStream};
use std::io::{Read, Write};
use std::str;

fn main() {
    const FCGI_VERSION_1: u8    = 1;

    const FCGI_BEGIN_REQUEST:u8 = 1;
    const FCGI_END_REQUEST: u8  = 3;
    const FCGI_STDIN: u8        = 5;
    const FCGI_STDOUT: u8       = 6;

    const FCGI_RESPONDER: u16  = 1;

    const FCGI_PARAMS: u8 = 4;

    const FCGI_GET_VALUES: u8 = 9;

    let socket_path = "/run/php-fpm/php-fpm.sock";

    let mut socket = match UnixStream::connect(socket_path) {
        Ok(sock) => sock,
        Err(e) => {
            println!("Couldn't connect: {e:?}");
            return
        }
    };

    let requestId: u16 = 1;
    let role: u16 = FCGI_RESPONDER;
    let beginRequest = vec![
       // FCGI_Header
       FCGI_VERSION_1, FCGI_BEGIN_REQUEST,
       (requestId >> 8) as u8, (requestId & 0xFF) as u8,
       0x00, 0x08, // This is the size of `FCGI_BeginRequestBody`
       0, 0,
       // FCGI_BeginRequestBody
       (role >> 8) as u8, (role & 0xFF) as u8,
       0, // Flags
       0, 0, 0, 0, 0, // Reserved
    ];
    socket.write_all(&beginRequest).unwrap();

    let param_name = "SCRIPT_FILENAME".as_bytes();
    let param_value = "/var/www/html/index.php".as_bytes();
    let lengths = [ param_name.len() as u8, param_value.len() as u8 ];
    socket.write_all (&lengths).unwrap();
    socket.write_all (param_name).unwrap();
    socket.write_all (param_value).unwrap();

    let requestHeader = vec![
       FCGI_VERSION_1, FCGI_STDIN,
       (requestId >> 8) as u8, (requestId & 0xFF) as u8,
       0, 0,
       0, 0,
    ];
    socket.write_all(&requestHeader).unwrap();

    let mut responseHeader = [0u8; 8];
    socket.read_exact (&mut responseHeader).unwrap();
    assert!(responseHeader[1] == FCGI_STDOUT);  // TODO: proper handling of message type
    let responseLength = ((responseHeader[4] as usize) << 8) | (responseHeader[5] as usize);
    let mut responseBody = Vec::new();
    responseBody.resize (responseLength, 0);
    socket.read_exact (&mut responseBody).unwrap();
    println!("{:?}", std::str::from_utf8(responseBody));
}

Этот код запускает запрос, затем записывает параметр SCRIPT_FILENAME, а затем считывает вывод файла PHP. Это файл PHP:

<?php

echo "First file";

?>

Когда я запускаю файл Rust, консоль показывает мне следующее:

Ok("X-Powered-By: PHP/8.1.11\r\nContent-type: text/html; charset=UTF-8\r\n\r\n")

Консоль не показывает вывод моей программы, поэтому я думаю, что проблема может заключаться в том, что сказал Jmb в ответе, который он мне дал:

Ответ может быть разделен на несколько сообщений FCGI_STDOUT, поэтому вам понадобится цикл, чтобы собрать их все заново.

Теперь, когда я вижу пример 3 спецификации протокола FastCGI, я понимаю, что в этом примере вывод программы все еще разделен (по крайней мере, я так думаю). Итак, как я могу прочитать весь вывод моей программы? Я знаю, что Jmb сказал, что для этого мне нужен цикл, так как же мне его реализовать? Или где? Я хотел бы уточнить, что я довольно новичок в этом FastCGI и у меня нет опыта работы с двоичными протоколами, поэтому мне очень жаль, если я упустил что-то, что кажется очевидным.

Не имеет строгого отношения, поэтому, пожалуйста, не воспринимайте это негативно. Если у вас уже есть Apache или NGINX на сервере для доставки PHP, рассмотрите возможность использования этого как обычного HTTP-запроса от Rust. Мы рассмотрели возможность прямого обращения к fast-cgi (кроме как из C#, а не из Rust), но решили, что дополнительная защита, ведение журнала, повторное использование сокета/поддержание активности и т. д. имеют больше смысла для прохождения через веб-сервер в виде локального HTTP-запроса (с защитой). Сказав это, я слежу, так как хочу увидеть ответ!

Robbie 11.10.2022 07:02

@ Робби О да, я понимаю, ты не имеешь в виду это в негативном смысле. Это то, что привлекает мое внимание, потому что предполагается, что именно так работает nginx, и, поскольку именно так работают несколько http-серверов, я хотел знать, как они работают на «низком уровне».

DFG 11.10.2022 17:51

Я не знаю ни протокола rust, ни протокола fastcgi, но, возможно, вам следует использовать socket.read_to_end (или read_to_string, если это лучше для ваших целей) doc.rust-lang.org/stable/std/io/… вместо socket .read_exact это должно читаться до тех пор, пока удаленный хост не закроет соединение (конец выполнения скрипта)

Roberto Braga 12.10.2022 13:24
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
1
3
436
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть несколько проблем с вашим кодом.

Выпуск №1

Последний байт FCGI_Record называется padding. Он говорит, сколько байтов следует пропустить после этой полезной нагрузки. Вы устанавливаете его на 0, поэтому там это не имеет значения, но при чтении с сервера вы должны пропустить столько-то байтов:

    let mut pad = vec![0; responseHeader[7] as usize];
    socket.read_exact (&mut pad).unwrap();

Это объясняет, что если вы попытаетесь выполнить цикл чтения, вы получите мусор вместо кадра FCGI_END_REQUEST.

Выпуск №2

Для части параметров требуется заголовок, но вы отправляете их в необработанном виде. Вы должны вычислить полную длину сериализованных параметров и отправить заголовок перед этим:

    let params_len: u16 = (param_name.len() + param_value.len() + lengths.len()) as u16;
    let paramsRequest = vec![
       FCGI_VERSION_1, FCGI_PARAMS,
       (requestId >> 8) as u8, (requestId & 0xFF) as u8,
       (params_len >> 8) as u8, (params_len & 0xFF) as u8,
       0, 0,
    ];
    socket.write_all (&paramsRequest).unwrap();

Выпуск №3

Минимальных параметров, требуемых php-fpm, я нигде не нашел, но бегло просматривая исходный код, нужно как минимум добавить REQUEST_METHOD:

    let param1_name = "SCRIPT_FILENAME".as_bytes();
    let param1_value = "/var/www/html/index.php".as_bytes();
    let lengths1 = [ param1_name.len() as u8, param1_value.len() as u8 ];
    let params1_len: u16 = (param1_name.len() + param1_value.len() + lengths1.len()) as u16;

    let param2_name = b"REQUEST_METHOD";
    let param2_value = b"GET";
    let lengths2 = [ param2_name.len() as u8, param2_value.len() as u8 ];
    let params2_len: u16 = (param2_name.len() + param2_value.len() + lengths2.len()) as u16;

    let params_len = params1_len + params2_len;
    let paramsRequest = vec![
       FCGI_VERSION_1, FCGI_PARAMS,
       (requestId >> 8) as u8, (requestId & 0xFF) as u8,
       (params_len >> 8) as u8, (params_len & 0xFF) as u8,
       0, 0,
    ];
    socket.write_all (&paramsRequest).unwrap();
    socket.write_all (&lengths1).unwrap();
    socket.write_all (param1_name).unwrap();
    socket.write_all (param1_value).unwrap();
    socket.write_all (&lengths2).unwrap();
    socket.write_all (param2_name).unwrap();
    socket.write_all (param2_value).unwrap();

На этом этапе я думаю, что вам следует подумать о написании правильных типов и примитивов сериализации для этих вещей вместо необработанных массивов байтов...

С этими изменениями мы уже получаем полный вывод:

Ok("X-Powered-By: PHP/8.1.11\r\nContent-type: text/html; charset=UTF-8\r\n\r\nFirst file")

Выпуск №4

Вы должны зацикливать код ответа на чтение, объединяя любые FCGI_STDOUT данные, регистрируя или игнорируя другие сообщения, возможно, регистрируя FCGI_STDERR сообщения, пока не получите FCGI_END_REQUEST.

В этом конкретном примере вывод настолько короткий, что умещается в одно сообщение, но, не выполняя цикл, вы не видите FCGI_END_REQUEST.

После FCGI_END_REQUEST, в зависимости от того, использовали ли вы FCGI_KEEP_CONN, вы либо закроете сокет, либо оставите его открытым для отправки нового запроса.

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

DFG 15.10.2022 00:48

Другие вопросы по теме