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 ¶m_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}