gproxy-protocol 4.1.14

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use std::{
    collections::VecDeque,
    pin::Pin,
    sync::{Arc, Mutex},
    task::{Context, Poll, Waker},
};

use futures_core::Stream;
use futures_sink::Sink;
use gproxy_protocol::connection::{
    Bytes, HeaderMap, HeaderValue, HttpBody, Method, Multipart, MultipartPart, StatusCode,
    TransportError, WebSocket, WireRequest, WireResponse, WsFrame,
};

struct Items<T>(VecDeque<T>);

impl<T: Unpin> Stream for Items<T> {
    type Item = T;

    fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<T>> {
        Poll::Ready(self.0.pop_front())
    }
}

fn next<T>(mut stream: Pin<&mut dyn Stream<Item = T>>) -> Option<T> {
    let mut cx = Context::from_waker(Waker::noop());
    match stream.as_mut().poll_next(&mut cx) {
        Poll::Ready(value) => value,
        Poll::Pending => panic!("fixture stream is immediately ready"),
    }
}

#[test]
fn raw_http_keeps_query_encoding_headers_and_arbitrary_body_chunks() {
    let chunks = [
        Bytes::from_static(b"data: {\"text\":\""),
        Bytes::from_static(&[0xe4]),
        Bytes::from_static(&[0xbd, 0xa0]),
        Bytes::from_static(b"\"}\n\n"),
    ];
    let mut headers = HeaderMap::new();
    headers.append("x-extra", HeaderValue::from_static("first"));
    headers.append("x-extra", HeaderValue::from_static("second"));
    headers.insert(
        "content-type",
        HeaderValue::from_static("text/event-stream"),
    );
    let response = WireResponse {
        status: StatusCode::OK,
        headers,
        body: HttpBody::Stream(Box::pin(Items(chunks.iter().cloned().map(Ok).collect()))),
    };
    let HttpBody::Stream(mut body) = response.body else {
        unreachable!()
    };
    for expected in chunks {
        assert_eq!(next(body.as_mut()).unwrap().unwrap(), expected);
    }
    assert!(next(body.as_mut()).is_none());
    assert_eq!(response.headers.get_all("x-extra").iter().count(), 2);

    let request = WireRequest {
        method: Method::GET,
        path: "/models/a%2Fb".into(),
        query: Some("q=a+b&q=a%20b&flag".into()),
        headers: HeaderMap::new(),
        body: (),
    };
    assert_eq!(request.path, "/models/a%2Fb");
    assert_eq!(request.query.as_deref(), Some("q=a+b&q=a%20b&flag"));
}

#[test]
fn multipart_preserves_repeated_parts_and_streaming_binary_content() {
    let mut parts = VecDeque::new();
    for value in [b"one".as_slice(), &[0, 255, 42]] {
        let mut headers = HeaderMap::new();
        headers.insert(
            "content-disposition",
            HeaderValue::from_static("form-data; name=\"file\"; filename=\"a.bin\""),
        );
        headers.insert(
            "content-type",
            HeaderValue::from_static("application/octet-stream"),
        );
        parts.push_back(Ok(MultipartPart {
            headers,
            body: HttpBody::Stream(Box::pin(Items([Ok(Bytes::copy_from_slice(value))].into()))),
        }));
    }
    let mut headers = HeaderMap::new();
    headers.insert(
        "content-type",
        HeaderValue::from_static("multipart/form-data; boundary=test"),
    );
    let mut request = WireRequest {
        method: Method::POST,
        path: "/v1/files".into(),
        query: None,
        headers,
        body: Multipart {
            parts: Box::pin(Items(parts)),
        },
    };
    for expected in [b"one".as_slice(), &[0, 255, 42]] {
        let part = next(request.body.parts.as_mut()).unwrap().unwrap();
        assert_eq!(
            part.headers["content-disposition"],
            "form-data; name=\"file\"; filename=\"a.bin\""
        );
        let HttpBody::Stream(mut content) = part.body else {
            unreachable!()
        };
        assert_eq!(next(content.as_mut()).unwrap().unwrap().as_ref(), expected);
    }
    assert!(next(request.body.parts.as_mut()).is_none());
}

struct Sent(Arc<Mutex<Vec<WsFrame>>>);

impl Sink<WsFrame> for Sent {
    type Error = TransportError;
    fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }
    fn start_send(self: Pin<&mut Self>, item: WsFrame) -> Result<(), Self::Error> {
        self.0.lock().unwrap().push(item);
        Ok(())
    }
    fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }
    fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }
}

#[test]
fn websocket_can_send_multiple_messages_before_receiving() {
    let sent = Arc::new(Mutex::new(Vec::new()));
    let mut socket = WebSocket {
        incoming: Box::pin(Items(
            [Ok(WsFrame::Ping(Bytes::from_static(b"ping")))].into(),
        )),
        outgoing: Box::pin(Sent(sent.clone())),
    };
    let mut cx = Context::from_waker(Waker::noop());
    for text in ["first", "second"] {
        assert!(matches!(
            socket.outgoing.as_mut().poll_ready(&mut cx),
            Poll::Ready(Ok(()))
        ));
        socket
            .outgoing
            .as_mut()
            .start_send(WsFrame::Text(text.into()))
            .unwrap();
    }
    assert_eq!(sent.lock().unwrap().len(), 2);
    assert_eq!(
        next(socket.incoming.as_mut()).unwrap().unwrap(),
        WsFrame::Ping(Bytes::from_static(b"ping"))
    );
}