cch24_validator/
lib.rs

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
140
141
142
143
144
145
146
147
148
149
150
pub mod args;

use reqwest::{
    header::{self, HeaderValue},
    redirect::Policy,
    StatusCode,
};
use shuttlings::{SubmissionState, SubmissionUpdate};
use tokio::{
    sync::mpsc::Sender,
    time::{sleep, Duration},
};
use tracing::info;
use uuid::Uuid;

pub const SUPPORTED_CHALLENGES: &[&str] = &[
    "-1",
    // "2",
    // "5",
    // "9",
    // "12",
    // "16",
    // "19",
    // "23",
];
pub const SUBMISSION_TIMEOUT: u64 = 60;

pub async fn run(url: String, id: Uuid, number: &str, tx: Sender<SubmissionUpdate>) {
    info!(%id, %url, %number, "Starting submission");

    tx.send(SubmissionState::Running.into()).await.unwrap();
    tx.send(SubmissionUpdate::Save).await.unwrap();

    tokio::select! {
        _ = validate(url.as_str(), number, tx.clone()) => (),
        _ = sleep(Duration::from_secs(SUBMISSION_TIMEOUT)) => {
            // if the validation task timed out
            info!(%id, %url, %number, "Submission timed out");
            tx.send("Timed out".to_owned().into()).await.unwrap();
            tx.send(SubmissionState::Done.into()).await.unwrap();
            tx.send(SubmissionUpdate::Save).await.unwrap();
        },
    };
    info!(%id, %url, %number, "Completed submission");
}

/// Task number and Test number in the current challenge
type TaskTest = (i32, i32);
/// If failure, return tuple with task number and test number that failed
type ValidateResult = std::result::Result<(), TaskTest>;

pub async fn validate(url: &str, number: &str, tx: Sender<SubmissionUpdate>) {
    let txc = tx.clone();
    if let Err((task, test)) = match number {
        "-1" => validate_minus1(url, txc).await,
        // "2" => validate_2(url, txc).await,
        // "5" => validate_5(url, txc).await,
        // "9" => validate_9(url, txc).await,
        // "12" => validate_12(url, txc).await,
        // "16" => validate_16(url, txc).await,
        // "19" => validate_19(url, txc).await,
        // "23" => validate_23(url, txc).await,
        _ => {
            tx.send(
                format!("Validating Challenge {number} is not supported yet! Check for updates.")
                    .into(),
            )
            .await
            .unwrap();
            return;
        }
    } {
        info!(%url, %number, %task, %test, "Submission failed");
        tx.send(format!("Task {task}: test #{test} failed 🟥").into())
            .await
            .unwrap();
    }
    tx.send(SubmissionState::Done.into()).await.unwrap();
    tx.send(SubmissionUpdate::Save).await.unwrap();
}

fn new_client() -> reqwest::Client {
    reqwest::ClientBuilder::new()
        .http1_only()
        .connect_timeout(Duration::from_secs(3))
        .redirect(Policy::limited(3))
        .referer(false)
        .timeout(Duration::from_secs(60))
        .build()
        .unwrap()
}

async fn validate_minus1(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {
    let client = new_client();
    let mut test: TaskTest;
    // TASK 1: respond 200 with Hello, bird!
    test = (1, 1);
    let url = &format!("{}/", base_url);
    let res = client.get(url).send().await.map_err(|_| test)?;
    if res.status() != StatusCode::OK {
        return Err(test);
    }
    if res.text().await.map_err(|_| test)? != "Hello, bird!" {
        return Err(test);
    }
    // TASK 1 DONE
    tx.send((true, 0).into()).await.unwrap();
    tx.send(SubmissionUpdate::Save).await.unwrap();

    // TASK 2: respond 302
    test = (2, 1);
    let url = &format!("{}/-1/seek", base_url);
    let client_no_redir = reqwest::ClientBuilder::new()
        .http1_only()
        .connect_timeout(Duration::from_secs(3))
        .redirect(Policy::none())
        .referer(false)
        .timeout(Duration::from_secs(60))
        .build()
        .unwrap();
    let res = client_no_redir.get(url).send().await.map_err(|_| test)?;
    if res.status() != StatusCode::FOUND {
        return Err(test);
    }
    if res.headers().get(header::LOCATION)
        != Some(&HeaderValue::from_static(
            "https://www.youtube.com/watch?v=9Gc4QTqslN4",
        ))
    {
        return Err(test);
    }
    // TASK 2 DONE
    tx.send((false, 0).into()).await.unwrap();

    Ok(())
}

// async fn validate_2(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {}

// async fn validate_5(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {}

// async fn validate_9(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {}

// async fn validate_12(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {}

// async fn validate_16(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {}

// async fn validate_19(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {}

// async fn validate_23(base_url: &str, tx: Sender<SubmissionUpdate>) -> ValidateResult {}