Я пытаюсь сделать запрос к файлам 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 и у меня нет опыта работы с двоичными протоколами, поэтому мне очень жаль, если я упустил что-то, что кажется очевидным.
@ Робби О да, я понимаю, ты не имеешь в виду это в негативном смысле. Это то, что привлекает мое внимание, потому что предполагается, что именно так работает nginx, и, поскольку именно так работают несколько http-серверов, я хотел знать, как они работают на «низком уровне».
Я не знаю ни протокола rust, ни протокола fastcgi, но, возможно, вам следует использовать socket.read_to_end (или read_to_string, если это лучше для ваших целей) doc.rust-lang.org/stable/std/io/… вместо socket .read_exact это должно читаться до тех пор, пока удаленный хост не закроет соединение (конец выполнения скрипта)






Есть несколько проблем с вашим кодом.
Последний байт FCGI_Record называется padding. Он говорит, сколько байтов следует пропустить после этой полезной нагрузки. Вы устанавливаете его на 0, поэтому там это не имеет значения, но при чтении с сервера вы должны пропустить столько-то байтов:
let mut pad = vec![0; responseHeader[7] as usize];
socket.read_exact (&mut pad).unwrap();
Это объясняет, что если вы попытаетесь выполнить цикл чтения, вы получите мусор вместо кадра FCGI_END_REQUEST.
Для части параметров требуется заголовок, но вы отправляете их в необработанном виде. Вы должны вычислить полную длину сериализованных параметров и отправить заголовок перед этим:
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 (¶msRequest).unwrap();
Минимальных параметров, требуемых 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 (¶msRequest).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")
Вы должны зацикливать код ответа на чтение, объединяя любые FCGI_STDOUT данные, регистрируя или игнорируя другие сообщения, возможно, регистрируя FCGI_STDERR сообщения, пока не получите FCGI_END_REQUEST.
В этом конкретном примере вывод настолько короткий, что умещается в одно сообщение, но, не выполняя цикл, вы не видите FCGI_END_REQUEST.
После FCGI_END_REQUEST, в зависимости от того, использовали ли вы FCGI_KEEP_CONN, вы либо закроете сокет, либо оставите его открытым для отправки нового запроса.
Вау, большое спасибо за ответ, этим ответом вы ответили на многие другие вопросы, которые у меня были.
Не имеет строгого отношения, поэтому, пожалуйста, не воспринимайте это негативно. Если у вас уже есть Apache или NGINX на сервере для доставки PHP, рассмотрите возможность использования этого как обычного HTTP-запроса от Rust. Мы рассмотрели возможность прямого обращения к fast-cgi (кроме как из C#, а не из Rust), но решили, что дополнительная защита, ведение журнала, повторное использование сокета/поддержание активности и т. д. имеют больше смысла для прохождения через веб-сервер в виде локального HTTP-запроса (с защитой). Сказав это, я слежу, так как хочу увидеть ответ!