· last year · Apr 21, 2024, 12:20 AM
1use std::{collections::HashMap, sync::Arc};
2
3use mysql_async::{prelude::Queryable, Pool};
4use axum::{
5 extract::State, response::IntoResponse, routing::{get, post}, Json, Router
6};
7use serde::Deserialize;
8use sha1::{Digest, Sha1};
9use tokio::sync::{Mutex, RwLock};
10
11#[derive(Clone)]
12struct User {
13 lock: Arc<Mutex<()>>,
14 secret: u32
15}
16
17impl User {
18 fn new() -> User {
19 User {
20 lock: Arc::new(Mutex::new(())),
21 secret: rand::random::<u32>()
22 }
23 }
24}
25
26#[derive(Clone)]
27struct AppState {
28 users: Arc<RwLock<HashMap<u64, User>>>,
29 pool: Arc<Pool>
30}
31
32impl AppState {
33 fn new(pool: Pool) -> AppState {
34 AppState {
35 users: Arc::new(RwLock::new(HashMap::new())),
36 pool: Arc::new(pool)
37 }
38 }
39}
40
41#[tokio::main]
42async fn main() {
43 tracing_subscriber::fmt::init();
44
45 let url = "mysql://fearless_concurrency:fearless_concurrency@database:3306/fearless_concurrency";
46
47 let pool = Pool::new(url);
48
49 let mut conn = pool.get_conn().await.unwrap();
50 conn.exec_drop("CREATE TABLE IF NOT EXISTS info (body varchar(255))", ()).await.unwrap();
51 conn.exec_drop("INSERT INTO info VALUES ('Hello, world')", ()).await.unwrap();
52
53 let state = AppState::new(pool);
54 let app = Router::new()
55 .route("/", get(root))
56 .route("/register", post(register))
57 .route("/query", post(query))
58 .route("/flag", post(flag))
59 .with_state(state);
60
61 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
62 println!("Listener started on port 3000");
63 axum::serve(listener, app).await.unwrap();
64}
65
66async fn root() -> &'static str {
67 "Hello, World!"
68}
69
70async fn register(State(state): State<AppState>) -> impl IntoResponse {
71 let uid = rand::random::<u64>();
72 let mut users = state.users.write().await;
73 let user = User::new();
74 users.insert(uid, user);
75 uid.to_string()
76}
77
78#[derive(Deserialize)]
79struct Query {
80 user_id: u64,
81 query_string: String
82}
83
84async fn query(State(state): State<AppState>, Json(body): Json<Query>) -> axum::response::Result<String> {
85 let users = state.users.read().await;
86 let user = users.get(&body.user_id).ok_or_else(|| "User not found! Register first!")?;
87 let user = user.clone();
88
89 // Prevent registrations from being blocked while query is running
90 // Fearless concurrency :tm:
91 drop(users);
92
93 // Prevent concurrent access to the database!
94 // Don't even try any race condition thingies
95 // They don't exist in rust!
96 let _lock = user.lock.lock().await;
97 let mut conn = state.pool.get_conn().await.map_err(|_| "Failed to acquire connection")?;
98
99 // Unguessable table name (requires knowledge of user id and random table id)
100 let table_id = rand::random::<u32>();
101 let mut hasher = Sha1::new();
102 hasher.update(b"fearless_concurrency");
103 hasher.update(body.user_id.to_le_bytes());
104 let table_name = format!("tbl_{}_{}", hex::encode(hasher.finalize()), table_id);
105
106 let table_name = dbg!(table_name);
107 let qs = dbg!(body.query_string);
108
109 // Create temporary, unguessable table to store user secret
110 conn.exec_drop(
111 format!("CREATE TABLE {} (secret int unsigned)", table_name), ()
112 ).await.map_err(|_| "Failed to create table")?;
113
114 conn.exec_drop(
115 format!("INSERT INTO {} values ({})", table_name, user.secret), ()
116 ).await.map_err(|_| "Failed to insert secret")?;
117
118
119 // Secret can't be leaked here since table name is unguessable!
120 let res = conn.exec_first::<String, _, _>(
121 format!("SELECT * FROM info WHERE body LIKE '{}'", qs),
122 ()
123 ).await;
124
125 // You'll never get the secret!
126 conn.exec_drop(
127 format!("DROP TABLE {}", table_name), ()
128 ).await.map_err(|_| "Failed to drop table")?;
129
130 let res = res.map_err(|_| "Failed to run query")?;
131
132 // _lock is automatically dropped when function exits, releasing the user lock
133
134 if let Some(result) = res {
135 return Ok(result);
136 }
137 Ok(String::from("No results!"))
138}
139
140
141#[derive(Deserialize)]
142struct ClaimFlag {
143 user_id: u64,
144 secret: u32
145}
146
147async fn flag(State(state): State<AppState>, Json(body): Json<ClaimFlag>) -> axum::response::Result<String> {
148 let users = state.users.read().await;
149 let user = users.get(&body.user_id).ok_or_else(|| "User not found! Register first!")?;
150
151 if user.secret == body.secret {
152 return Ok(String::from("grey{fake_flag_for_testing}"));
153 }
154 Ok(String::from("Wrong!"))
155}