Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
492 changes: 424 additions & 68 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"supervisor",
"web"
"web",
"cli",
]
18 changes: 18 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "cli"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = "3.0.0-beta.5"
dialoguer = "0.9.0"
lazy_static = "1.4.0"
anyhow = "1.0.48"
reqwest = { version = "0.11.6", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
thiserror = "1.0.30"

[dev-dependencies]
serde_json = "1.0.72"
4 changes: 4 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pCTF CLI

This is the command-line interface for the pCTF GraphQL API.
It enables you to easily list challenges, submit flags and recieve prices!
46 changes: 46 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use anyhow::Result;
use clap::Parser;
use lazy_static::lazy_static;
use reqwest::blocking::Client;

use crate::oauth2::DeviceAuthorizationRequest;

mod oauth2;

// Constaints that allow compiling with cargo-install
/// The host/domian used for openid connect discovery
// const OIDC_HOST: &str = "https://pwnhub-dev.eu.auth0.com/";
/// The id of the oidc client
pub const CLIENT_ID: &str = "XOUgCp9H7k0rkRknnAf8ID6Fz4skI3Wi";
// pub const OAUTH_AUTH: &str = "https://pwnhub-dev.eu.auth0.com/authorize";
pub const DEVICE_AUTH: &str = "https://pwnhub-dev.eu.auth0.com/oauth/device/code";
pub const OAUTH_TOKEN: &str = "https://pwnhub-dev.eu.auth0.com/oauth/token";
pub const AUDIENCE: &str = "http://localhost:8000";

#[derive(Parser)]
enum Commands {
/// Login using discord
Login,
}

fn main() -> Result<()> {
let cmd = Commands::parse();
lazy_static! {
static ref CLIENT: Client = Client::new();
}
// You can handle information about subcommands by requesting their matches by name
// (as below), requesting just the name used, or both at the same time
match cmd {
Commands::Login => {
let request = DeviceAuthorizationRequest::new(&CLIENT)?;
println!(
"Please follow this link: {}\nYour code is {}",
request.verification_uri_complete, request.user_code
);
println!("Waiting ...");
let response = request.poll(&CLIENT)?;
println!("Done!\nYour access token is {}", response.access_token);
}
};
Ok(())
}
87 changes: 87 additions & 0 deletions cli/src/oauth2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! An wrapper for the oauth2 [Device Authorization Grant][1]
//!
//! [1]: https://datatracker.ietf.org/doc/html/rfc8628

mod error;
use std::{
collections::HashMap, result::Result as StdResult, str::FromStr, thread::sleep, time::Duration,
};

use anyhow::Result;
#[doc(inline)]
pub use error::PollResponseError;
use reqwest::{blocking::Client, Url};
use serde::Deserialize;

use crate::{AUDIENCE, CLIENT_ID, DEVICE_AUTH, OAUTH_TOKEN};

#[non_exhaustive]
#[derive(Deserialize, Debug)]
/// A Device Authorization Request as defined in [RFC 8628 section 3.1][1]
///
/// The server will return an url that the user should follow to authorize this
/// application
/// [1]: https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
pub struct DeviceAuthorizationRequest {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: String,
pub expires_in: u64,
pub interval: u64,
}

#[derive(Debug, Deserialize)]
pub struct DeviceAccessTokenResponse {
pub access_token: String,
pub refresh_token: Option<String>,
pub id_token: Option<String>,
pub token_type: Option<String>,
pub expires_in: u64,
}

impl DeviceAuthorizationRequest {
pub fn new(client: &Client) -> Result<Self> {
let mut params = HashMap::new();
params.insert("client_id", CLIENT_ID);
params.insert("scope", "openid profile");
params.insert("audience", AUDIENCE);
client
.post(Url::from_str(DEVICE_AUTH)?)
.form(&params)
.send()?
.json()
.map_err(Into::into)
}

pub fn poll(self, client: &Client) -> Result<DeviceAccessTokenResponse> {
let mut params = HashMap::new();
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
params.insert("device_code", &self.device_code);
params.insert("client_id", CLIENT_ID);
loop {
#[derive(Deserialize)]
#[serde(untagged)]
/// Enum to determine if a [`DeviceAuthorizationRequest`] was successful or not.
/// This way, we can return a [`std::result::Result`] directly
enum ResponseMatcher {
Ok(DeviceAccessTokenResponse),
Err(PollResponseError),
}
let response: ResponseMatcher = client
.post(Url::from_str(OAUTH_TOKEN)?)
.form(&params)
.send()?
.json()?;
match response {
ResponseMatcher::Ok(d) => return StdResult::Ok(d),
ResponseMatcher::Err(e) => match e {
PollResponseError::AuthorizationPending {
error_description: _,
} => sleep(Duration::from_secs(self.interval)),
_ => return StdResult::Err(e.into()),
},
}
}
}
}
153 changes: 153 additions & 0 deletions cli/src/oauth2/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Typed errors for the oauth2 [Device Authorization Grant][1]
//!
//! [1]: https://datatracker.ietf.org/doc/html/rfc8628
use serde::Deserialize;
use thiserror::Error;

macro_rules! poll_response_error {
(
$(
$(#[$attr:meta])*
$v:ident
),*$(,)?
) => {
#[derive(Debug, Error, Deserialize)]
#[serde(tag = "error")]
#[serde(rename_all = "snake_case")]
/// An error type that represents the possible error types defined in
/// [RFC 8628 section 7.3][1]
///
/// [1]: https://datatracker.ietf.org/doc/html/rfc8628#section-7.3
pub enum PollResponseError {
$(
#[error("{error_description}")]
$(#[$attr])*
$v {
error_description: String,
},
)*
}
};
}

// documentation for variants taken from
// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
// and https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
// note that only invalid_grant is taken from rfc 6749
poll_response_error! {
/// authorization_pending
///
/// The authorization request is still pending. The client SHOULD repeat
/// the access token request to the token endpoint (a process known as
/// polling).
AuthorizationPending,
/// slow_down
///
/// A variant of "authorization_pending", the authorization request is
/// still pending and polling should continue, but the interval MUST
/// be increased by 5 seconds for this and all subsequent requests.
SlowDown,
/// expired_token
///
/// The "device_code" has expired, and the device authorization
/// session has concluded. The client MAY commence a new device
/// authorization request but SHOULD wait for user interaction before
/// restarting to avoid unnecessary polling.
ExpiredToken,
/// invalid_grant
///
/// Defined as part of the standard oauth2 errors in [RFC 6749 section 5.2][1]:
/// The provided authorization grant (e.g., authorization
/// code, resource owner credentials) or refresh token is
/// invalid, expired, revoked, does not match the redirection
/// URI used in the authorization request, or was issued to
/// another client.
///
/// [1]: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
InvalidGrant,
/// access_denied
///
/// The authorization request was denied.
AccessDenied,
}

#[cfg(test)]
mod tests {
use super::PollResponseError;

#[allow(non_upper_case_globals)]
#[test]
fn parse_errors() {
const authorization_pending: &str = r#"
{
"error": "authorization_pending",
"error_description": "User did not approve the request yet"
}
"#;
const slow_down: &str = r#"
{
"error": "slow_down",
"error_description": "Please poll slower"
}
"#;
const expired_token: &str = r#"
{
"error": "expired_token",
"error_description": "Token expired"
}
"#;
const invalid_grant: &str = r#"
{
"error": "invalid_grant",
"error_description": "Token is invalid"
}
"#;
const access_denied: &str = r#"
{
"error": "access_denied",
"error_description": "User denied the request"
}
"#;

let ap: PollResponseError = serde_json::from_str(authorization_pending).unwrap();
assert!(matches!(
ap,
PollResponseError::AuthorizationPending {
error_description: _
}
));

let sd: PollResponseError = serde_json::from_str(slow_down).unwrap();
println!("{:#?}", sd);
assert!(matches!(
sd,
PollResponseError::SlowDown {
error_description: _
}
));

let et: PollResponseError = serde_json::from_str(expired_token).unwrap();
assert!(matches!(
et,
PollResponseError::ExpiredToken {
error_description: _
}
));

let ig: PollResponseError = serde_json::from_str(invalid_grant).unwrap();
assert!(matches!(
ig,
PollResponseError::InvalidGrant {
error_description: _
}
));

let ad: PollResponseError = serde_json::from_str(access_denied).unwrap();
assert!(matches!(
ad,
PollResponseError::AccessDenied {
error_description: _
}
));
}
}
1 change: 1 addition & 0 deletions web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ uuid = { version = "0.8.2", features = ["serde"] }
sqlx = { version = "0.5.9", features = ["chrono", "macros", "migrate", "offline", "postgres", "runtime-tokio-rustls", "uuid"] }
serde = { version = "1.0.130", features = ["derive"] }
async-trait = "0.1.51"
tokio = { version = "1.15.0", features = ["rt", "macros"]}
futures = "0.3.17"
chrono = { version = "0.4.19", features = ["serde"] }
6 changes: 0 additions & 6 deletions web/migrations/20211113170055_challenge_type_enum.sql

This file was deleted.

This file was deleted.

7 changes: 7 additions & 0 deletions web/migrations/20220103134712_add_challenge_type_enum.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- a enum to define the type of challenge
CREATE TYPE challenge_type AS ENUM (
'pwn',
'web',
'crypto',
'reversing'
)
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
CREATE TABLE ctf_challenges (
-- stores the ctf challenges
CREATE TABLE challenges (
-- unique id of this challenge
"id" UUID NOT NULL UNIQUE DEFAULT uuid_generate_v4() PRIMARY KEY,
-- for `gen_random_uuid()` ensure that the `pgcrypto` extension is present
-- or else enable it with `CREATE EXTENSION EXISTS pgcrypto`
"id" UUID NOT NULL UNIQUE DEFAULT gen_random_uuid() PRIMARY KEY,
-- the name of the challenge displayed to the user
"name" VARCHAR(32) NOT NULL UNIQUE CHECK (char_length(name) > 0),
-- the type of this challenge
"type" CHALLENGE_TYPE NOT NULL,
-- a short description (max length 120 chars)
"short_description" VARCHAR(120) CHECK (char_length(short_description) > 0),
-- the long description of the challenge
"long_description" TEXT CHECK (char_length(long_description) > 0),
-- hints for the challenge
-- hints for the challenge to help players
"hints" TEXT CHECK (char_length(hints) > 0),
-- then this challenge was created
"created_at" TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,
-- if the challenge is being deployed by the supervisor
"active" BOOLEAN NOT NULL DEFAULT false
)
Loading