Skip to main content

fastcgi_client/
client.rs

1// Copyright 2022 jmjoy
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! FastCGI client implementation for async communication with FastCGI servers.
16//!
17//! This module provides the main `Client` struct that handles communication
18//! with FastCGI servers in both short connection and keep-alive modes.
19//! The client can execute requests and receive responses or response streams.
20
21use crate::{
22    ClientError, ClientResult, Response,
23    conn::{KeepAlive, Mode, ShortConn},
24    io::{self, AsyncRead, AsyncWrite, AsyncWriteExt},
25    meta::{BeginRequestRec, EndRequestRec, Header, ParamPairs, RequestType, Role},
26    params::Params,
27    request::Request,
28    response::ResponseStream,
29};
30use std::marker::PhantomData;
31use tracing::debug;
32
33#[cfg(feature = "tokio")]
34use crate::io::{TokioAsyncReadCompatExt, TokioCompat};
35
36/// I refer to nginx fastcgi implementation, found the request id is always 1.
37///
38/// <https://github.com/nginx/nginx/blob/f7ea8c76b55f730daa3b63f5511feb564b44d901/src/http/modules/ngx_http_fastcgi_module.c>
39const REQUEST_ID: u16 = 1;
40
41/// Async client for handling communication between fastcgi server.
42pub struct Client<S, M> {
43    stream: S,
44    _mode: PhantomData<M>,
45}
46
47impl<S, M> Client<S, M> {
48    fn from_stream(stream: S) -> Self {
49        Self {
50            stream,
51            _mode: PhantomData,
52        }
53    }
54}
55
56#[cfg(feature = "tokio")]
57impl<S> Client<TokioCompat<S>, ShortConn>
58where
59    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
60{
61    /// Construct a `Client` Object with a Tokio stream under short connection
62    /// mode.
63    ///
64    /// Requires the optional `tokio` feature, which exists purely as a
65    /// convenience adapter: Tokio defines its own I/O traits, so the stream is
66    /// wrapped with [`tokio_util::compat`](crate::io::TokioAsyncReadCompatExt)
67    /// before being handed to the runtime agnostic core. Without the feature,
68    /// bridge the stream yourself and call [`Client::new`].
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// # #[cfg(feature = "tokio")]
74    /// # async fn example() {
75    /// use fastcgi_client::Client;
76    /// use tokio::net::TcpStream;
77    /// # #[cfg(unix)]
78    /// # use tokio::net::UnixStream;
79    ///
80    /// let tcp_stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap();
81    /// let _tcp_client = Client::new_tokio(tcp_stream);
82    ///
83    /// # #[cfg(unix)]
84    /// # {
85    /// let unix_stream = UnixStream::connect("/run/php-fpm.sock").await.unwrap();
86    /// let _unix_client = Client::new_tokio(unix_stream);
87    /// # }
88    /// # }
89    /// # #[cfg(not(feature = "tokio"))]
90    /// # fn example() {}
91    /// ```
92    pub fn new_tokio(stream: S) -> Self {
93        Self::from_stream(stream.compat())
94    }
95}
96
97impl<S: AsyncRead + AsyncWrite + Unpin> Client<S, ShortConn> {
98    /// Construct a `Client` object under short connection mode.
99    ///
100    /// This constructor is runtime agnostic: it accepts any stream implementing
101    /// the [`futures_io`](crate::io) `AsyncRead` + `AsyncWrite` traits, such as
102    /// `smol::net::TcpStream`, `async_net::TcpStream`, or a Tokio stream
103    /// wrapped by [`tokio_util::compat`](https://docs.rs/tokio-util/latest/tokio_util/compat/index.html).
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// # async fn example() {
109    /// use fastcgi_client::Client;
110    /// use smol::net::TcpStream;
111    ///
112    /// let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap();
113    /// let _client = Client::new(stream);
114    /// # }
115    /// ```
116    pub fn new(stream: S) -> Self {
117        Self::from_stream(stream)
118    }
119
120    /// Send request and receive response from fastcgi server, under short
121    /// connection mode.
122    pub async fn execute_once<I: AsyncRead + Unpin>(
123        mut self, request: Request<'_, I>,
124    ) -> ClientResult<Response> {
125        self.inner_execute(request).await
126    }
127
128    /// Send request and receive response stream from fastcgi server, under
129    /// short connection mode.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// # #[cfg(feature = "tokio")]
135    /// # async fn stream() {
136    /// use fastcgi_client::{io, response::Content, Client, Params, Request, StreamExt};
137    /// use tokio::net::TcpStream;
138    ///
139    ///     let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap();
140    ///     let client = Client::new_tokio(stream);
141    ///     let mut stream = client
142    ///         .execute_once_stream(Request::new(Params::default(), io::empty()))
143    ///         .await
144    ///         .unwrap();
145    ///
146    ///     while let Some(content) = stream.next().await {
147    ///         let content = content.unwrap();
148    ///
149    ///         match content {
150    ///             Content::Stdout(out) => todo!(),
151    ///             Content::Stderr(out) => todo!(),
152    ///         }
153    ///     }
154    /// }
155    /// # #[cfg(not(feature = "tokio"))]
156    /// # fn stream() {}
157    /// ```
158    pub async fn execute_once_stream<I: AsyncRead + Unpin>(
159        mut self, request: Request<'_, I>,
160    ) -> ClientResult<ResponseStream<S>> {
161        self.send_request(request).await?;
162        Ok(ResponseStream::new(self.stream, REQUEST_ID))
163    }
164}
165
166#[cfg(feature = "tokio")]
167impl<S> Client<TokioCompat<S>, KeepAlive>
168where
169    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
170{
171    /// Construct a `Client` Object with a Tokio stream under keep-alive mode.
172    ///
173    /// Requires the optional `tokio` feature, which exists purely as a
174    /// convenience adapter: Tokio defines its own I/O traits, so the stream is
175    /// wrapped with [`tokio_util::compat`](crate::io::TokioAsyncReadCompatExt)
176    /// before being handed to the runtime agnostic core. Without the feature,
177    /// bridge the stream yourself and call [`Client::new_keep_alive`].
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// # #[cfg(feature = "tokio")]
183    /// # async fn example() {
184    /// use fastcgi_client::Client;
185    /// use tokio::net::TcpStream;
186    /// # #[cfg(unix)]
187    /// # use tokio::net::UnixStream;
188    ///
189    /// let tcp_stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap();
190    /// let _tcp_client = Client::new_keep_alive_tokio(tcp_stream);
191    ///
192    /// # #[cfg(unix)]
193    /// # {
194    /// let unix_stream = UnixStream::connect("/run/php-fpm.sock").await.unwrap();
195    /// let _unix_client = Client::new_keep_alive_tokio(unix_stream);
196    /// # }
197    /// # }
198    /// # #[cfg(not(feature = "tokio"))]
199    /// # fn example() {}
200    /// ```
201    pub fn new_keep_alive_tokio(stream: S) -> Self {
202        Self::from_stream(stream.compat())
203    }
204}
205
206impl<S: AsyncRead + AsyncWrite + Unpin> Client<S, KeepAlive> {
207    /// Construct a `Client` object under keep-alive mode.
208    ///
209    /// This constructor is runtime agnostic: it accepts any stream implementing
210    /// the [`futures_io`](crate::io) `AsyncRead` + `AsyncWrite` traits, such as
211    /// `smol::net::TcpStream`, `async_net::TcpStream`, or a Tokio stream
212    /// wrapped by [`tokio_util::compat`](https://docs.rs/tokio-util/latest/tokio_util/compat/index.html).
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// # async fn example() {
218    /// use fastcgi_client::Client;
219    /// use smol::net::TcpStream;
220    ///
221    /// let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap();
222    /// let _client = Client::new_keep_alive(stream);
223    /// # }
224    /// ```
225    pub fn new_keep_alive(stream: S) -> Self {
226        Self::from_stream(stream)
227    }
228
229    /// Send request and receive response from fastcgi server, under keep alive
230    /// connection mode.
231    pub async fn execute<I: AsyncRead + Unpin>(
232        &mut self, request: Request<'_, I>,
233    ) -> ClientResult<Response> {
234        self.inner_execute(request).await
235    }
236
237    /// Send request and receive response stream from fastcgi server, under
238    /// keep alive connection mode.
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// # #[cfg(feature = "tokio")]
244    /// # async fn stream() {
245    /// use fastcgi_client::{io, response::Content, Client, Params, Request, StreamExt};
246    /// use tokio::net::TcpStream;
247    ///
248    ///     let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap();
249    ///     let mut client = Client::new_keep_alive_tokio(stream);
250    ///
251    ///     for _ in (0..3) {
252    ///         let mut stream = client
253    ///             .execute_stream(Request::new(Params::default(), io::empty()))
254    ///             .await
255    ///             .unwrap();
256    ///
257    ///         while let Some(content) = stream.next().await {
258    ///             let content = content.unwrap();
259    ///
260    ///             match content {
261    ///                 Content::Stdout(out) => todo!(),
262    ///                 Content::Stderr(out) => todo!(),
263    ///             }
264    ///         }
265    ///     }
266    /// }
267    /// # #[cfg(not(feature = "tokio"))]
268    /// # fn stream() {}
269    /// ```
270    pub async fn execute_stream<I: AsyncRead + Unpin>(
271        &mut self, request: Request<'_, I>,
272    ) -> ClientResult<ResponseStream<&mut S>> {
273        self.send_request(request).await?;
274        Ok(ResponseStream::new(&mut self.stream, REQUEST_ID))
275    }
276}
277
278impl<S: AsyncRead + AsyncWrite + Unpin, M: Mode> Client<S, M> {
279    /// Internal method to execute a request and return a complete response.
280    ///
281    /// # Arguments
282    ///
283    /// * `request` - The request to execute
284    async fn inner_execute<I: AsyncRead + Unpin>(
285        &mut self, request: Request<'_, I>,
286    ) -> ClientResult<Response> {
287        self.send_request(request).await?;
288        handle_response(&mut self.stream, REQUEST_ID).await
289    }
290
291    /// Sends a complete request, using the `FCGI_KEEP_CONN` flag of the
292    /// connection mode `M`.
293    ///
294    /// This is the only place where the connection mode is observed; everything
295    /// else in the protocol handling is mode agnostic.
296    ///
297    /// # Arguments
298    ///
299    /// * `request` - The request to send
300    async fn send_request<I: AsyncRead + Unpin>(
301        &mut self, request: Request<'_, I>,
302    ) -> ClientResult<()> {
303        handle_request(
304            &mut self.stream,
305            REQUEST_ID,
306            M::KEEP_CONN,
307            request.params,
308            request.stdin,
309        )
310        .await
311    }
312}
313
314/// Handles the complete request process.
315///
316/// # Arguments
317/// * `stream` - The stream to write to
318/// * `id` - The request ID
319/// * `keep_conn` - Value of the `FCGI_KEEP_CONN` flag
320/// * `params` - The request parameters
321/// * `body` - The request body stream
322async fn handle_request<S: AsyncWrite + Unpin, I: AsyncRead + Unpin>(
323    stream: &mut S, id: u16, keep_conn: bool, params: Params<'_>, mut body: I,
324) -> ClientResult<()> {
325    handle_request_start(stream, id, keep_conn).await?;
326    handle_request_params(stream, id, params).await?;
327    handle_request_body(stream, id, &mut body).await?;
328    handle_request_flush(stream).await?;
329    Ok(())
330}
331
332/// Handles the start of a request by sending the begin request record.
333///
334/// # Arguments
335///
336/// * `stream` - The stream to write to
337/// * `id` - The request ID
338/// * `keep_conn` - Value of the `FCGI_KEEP_CONN` flag
339async fn handle_request_start<S: AsyncWrite + Unpin>(
340    stream: &mut S, id: u16, keep_conn: bool,
341) -> ClientResult<()> {
342    debug!(id, "Start handle request");
343
344    let begin_request_rec = BeginRequestRec::new(id, Role::Responder, keep_conn).await?;
345
346    debug!(id, ?begin_request_rec, "Send to stream.");
347
348    begin_request_rec.write_to_stream(stream).await?;
349
350    Ok(())
351}
352
353/// Handles sending request parameters to the stream.
354///
355/// # Arguments
356///
357/// * `stream` - The stream to write to
358/// * `id` - The request ID
359/// * `params` - The request parameters
360async fn handle_request_params<S: AsyncWrite + Unpin>(
361    stream: &mut S, id: u16, params: Params<'_>,
362) -> ClientResult<()> {
363    let param_pairs = ParamPairs::new(params);
364    debug!(id, ?param_pairs, "Params will be sent.");
365
366    Header::write_to_stream_batches(
367        RequestType::Params,
368        id,
369        stream,
370        &mut &param_pairs.to_content().await?[..],
371        Some(|header| {
372            debug!(id, ?header, "Send to stream for Params.");
373            header
374        }),
375    )
376    .await?;
377
378    Header::write_to_stream_batches(
379        RequestType::Params,
380        id,
381        stream,
382        &mut io::empty(),
383        Some(|header| {
384            debug!(id, ?header, "Send to stream for Params.");
385            header
386        }),
387    )
388    .await?;
389
390    Ok(())
391}
392
393/// Handles sending the request body to the stream.
394///
395/// # Arguments
396///
397/// * `stream` - The stream to write to
398/// * `id` - The request ID
399/// * `body` - The request body stream
400async fn handle_request_body<S: AsyncWrite + Unpin, I: AsyncRead + Unpin>(
401    stream: &mut S, id: u16, body: &mut I,
402) -> ClientResult<()> {
403    Header::write_to_stream_batches(
404        RequestType::Stdin,
405        id,
406        stream,
407        body,
408        Some(|header| {
409            debug!(id, ?header, "Send to stream for Stdin.");
410            header
411        }),
412    )
413    .await?;
414
415    Header::write_to_stream_batches(
416        RequestType::Stdin,
417        id,
418        stream,
419        &mut io::empty(),
420        Some(|header| {
421            debug!(id, ?header, "Send to stream for Stdin.");
422            header
423        }),
424    )
425    .await?;
426
427    Ok(())
428}
429
430/// Flushes the stream to ensure all data is sent.
431///
432/// # Arguments
433///
434/// * `stream` - The stream to flush
435async fn handle_request_flush<S: AsyncWrite + Unpin>(stream: &mut S) -> ClientResult<()> {
436    stream.flush().await?;
437
438    Ok(())
439}
440
441/// Handles reading and processing the response from the stream.
442///
443/// # Arguments
444///
445/// * `stream` - The stream to read from
446/// * `id` - The request ID to match
447async fn handle_response<S: AsyncRead + Unpin>(stream: &mut S, id: u16) -> ClientResult<Response> {
448    let mut response = Response::default();
449
450    let mut stderr = Vec::new();
451    let mut stdout = Vec::new();
452
453    loop {
454        let header = Header::new_from_stream(stream).await?;
455        if header.request_id != id {
456            return Err(ClientError::ResponseNotFound { id });
457        }
458        debug!(id, ?header, "Receive from stream.");
459
460        match header.r#type {
461            RequestType::Stdout => {
462                stdout.extend(header.read_content_from_stream(stream).await?);
463            }
464            RequestType::Stderr => {
465                stderr.extend(header.read_content_from_stream(stream).await?);
466            }
467            RequestType::EndRequest => {
468                let end_request_rec = EndRequestRec::from_header(&header, stream).await?;
469                debug!(id, ?end_request_rec, "Receive from stream.");
470
471                end_request_rec
472                    .end_request
473                    .protocol_status
474                    .convert_to_client_result(end_request_rec.end_request.app_status)?;
475
476                response.stdout = if stdout.is_empty() {
477                    None
478                } else {
479                    Some(stdout)
480                };
481                response.stderr = if stderr.is_empty() {
482                    None
483                } else {
484                    Some(stderr)
485                };
486
487                return Ok(response);
488            }
489            r#type => {
490                return Err(ClientError::UnknownRequestType {
491                    request_type: r#type,
492                });
493            }
494        }
495    }
496}