summaryrefslogtreecommitdiffstats
path: root/src/launcher/request.rs
blob: df89a8b3ef1583fa2f140594b3944a3040189f40 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use std::error::Error;
use std::fmt::Display;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use curl::easy::{Easy};
use tokio::sync::oneshot;
use tokio::sync::oneshot::Receiver;
use tokio::task;
use crate::launcher::constants::USER_AGENT;

// yeah this is basically reqwest but bad (I did not want to rely on both reqwest and curl)

#[derive(Clone, Copy)]
enum FetchState {
    Primed,
    Running,
    Complete
}

pub struct EasyFetch {
    easy: Option<Easy>,
    state: FetchState,
    response: Option<Receiver<Result<FetchResult, curl::Error>>>
}

impl EasyFetch {
    fn new(easy: Easy) -> Self {
        EasyFetch {
            easy: Some(easy),
            state: FetchState::Primed,
            response: None
        }
    }

    pub fn get(url: &str) -> Self {
        let mut easy = Easy::new();
        easy.useragent(USER_AGENT).expect("couldn't set user agent");
        easy.get(true).expect("couldn't set request method");
        easy.url(url).expect("couldn't set url");

        Self::new(easy)
    }
}

#[derive(Debug)]
pub struct FetchResult {
    easy: Easy,
    response_code: u32,
    data: Vec<u8>,
}

#[derive(Debug)]
pub struct FetchResponseError(u32);

impl FetchResponseError {
    pub fn get_code(&self) -> u32 {
        self.0
    }
}

impl Display for FetchResponseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "http response: {}", self.0)
    }
}

impl Error for FetchResponseError {}

impl FetchResult {
    pub fn get_response_code(&self) -> u32 {
        self.response_code
    }

    pub fn get_data(&self) -> &[u8] {
        &self.data
    }

    pub fn get_data_string(&self) -> String {
        String::from_utf8_lossy(&self.data).to_string()
    }

    pub fn error_for_status(self) -> Result<Self, FetchResponseError> {
        if self.response_code / 100 == 2 {
            Ok(self)
        } else {
            Err(FetchResponseError(self.response_code))
        }
    }
}

impl Future for EasyFetch {
    type Output = Result<FetchResult, curl::Error>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let self_ref = self.get_mut();

        match &self_ref.state {
            FetchState::Primed => {
                self_ref.state = FetchState::Running;
                let mut easy = self_ref.easy.take().unwrap();
                let waker = cx.waker().clone();

                let (tx, rx) = oneshot::channel::<Result<FetchResult, curl::Error>>();
                self_ref.response.replace(rx);

                task::spawn_blocking(move || {
                    let mut out_data: Vec<u8> = Vec::new();
                    let mut transfer = easy.transfer();

                    transfer.write_function(|data| {
                        out_data.extend_from_slice(data);
                        Ok(data.len())
                    }).expect("infallible curl operation failed");

                    let res = transfer.perform();
                    drop(transfer); // have to explicitly drop to release borrow on "easy"

                    out_data.shrink_to_fit();

                    tx.send(res.map(|_| FetchResult {
                        response_code: easy.response_code().expect("querying response code should not fail"),
                        data: out_data,
                        easy
                    })).expect("curl fetch reader hangup (this shouldn't happen)");
                    waker.wake();
                });

                Poll::Pending
            },
            FetchState::Running => {
                self_ref.state = FetchState::Complete;
                Poll::Ready(self_ref.response.take().unwrap().try_recv()
                    .expect("curl fetch writer hangup or not ready (this shouldn't happen)"))
            },
            FetchState::Complete => panic!("fetch polled after completion")
        }
    }
}