Skip to content

Commit bd8c3a4

Browse files
committed
📦 Add user install webhook endpoints
1 parent 09bd31a commit bd8c3a4

File tree

18 files changed

+1012
-16
lines changed

18 files changed

+1012
-16
lines changed

‎.env.template‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ API_KEY="key used in Killua API"
88
MODE="dev"
99
HASH_SECRET="hash secret"
1010
GF_SECURITY_ADMIN_USER="admin"
11-
GF_SECURITY_ADMIN_PASSWORD="admin"
11+
GF_SECURITY_ADMIN_PASSWORD="admin"
12+
ADMIN_IDS=""
13+
PUBLIC_KEY=""

‎api/Cargo.lock‎

Lines changed: 117 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎api/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ toml = "0.8.23"
1919
zmq = "0.10.0"
2020
reqwest = { version = "0.11", features = ["json"] }
2121
chrono = { version = "0.4", features = ["serde"] }
22+
ed25519-dalek = "2.0.0"
23+
hex = "0.4.3"
24+
rand = { version = "0.8.5", features = ["std"] }

‎api/Rocket.toml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
## defaults for _all_ profiles
22
[default]
33
address = "0.0.0.0"
4-
limits = { form = "64 kB", json = "1 MiB" }
4+
limits = { form = "64 kB", json = "1 MiB", data-form = "500 MiB" }
55

66
## set only when compiled in debug mode, i.e, `cargo build`
77
[debug]
88
port = 7650
99
## only the `json` key from `default` will be overridden; `form` will remain
10-
limits = { json = "10MiB" }
10+
limits = { json = "10MiB", data-form = "500 MiB" }
1111

1212
## set only when compiled in release mode, i.e, `cargo build --release`
1313
[release]
1414
port = 7650
15-
ip_header = false
15+
ip_header = false
16+
limits = { data-form = "500 MiB" }

‎api/src/main.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod tests;
1313
use routes::cards::{get_cards, get_public_cards};
1414
use routes::commands::get_commands;
1515
use routes::diagnostics::get_diagnostics;
16+
use routes::discord_webhooks::{handle_discord_webhook, webhook_health_check};
1617
use routes::image::{delete, edit, image, list, upload};
1718
use routes::stats::get_stats;
1819
use routes::update::{update, update_cors};
@@ -44,6 +45,8 @@ fn rocket() -> _ {
4445
update_cors,
4546
get_userinfo,
4647
get_userinfo_by_id,
48+
handle_discord_webhook,
49+
webhook_health_check,
4750
],
4851
)
4952
.attach(db::init())
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
2+
use hex;
3+
use rocket::http::Status;
4+
use rocket::request::{FromRequest, Outcome};
5+
use serde::{Deserialize, Serialize};
6+
use std::env;
7+
8+
#[derive(Debug, Serialize, Deserialize)]
9+
pub struct SignatureError {
10+
pub error: String,
11+
}
12+
13+
#[derive(Debug)]
14+
pub struct DiscordSignature {
15+
#[allow(dead_code)]
16+
pub signature: String,
17+
#[allow(dead_code)]
18+
pub timestamp: String,
19+
}
20+
21+
#[rocket::async_trait]
22+
impl<'r> FromRequest<'r> for DiscordSignature {
23+
type Error = SignatureError;
24+
25+
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
26+
// Get required headers
27+
let signature = match request.headers().get_one("X-Signature-Ed25519") {
28+
Some(sig) => sig.to_string(),
29+
None => {
30+
return Outcome::Error((
31+
Status::Unauthorized,
32+
SignatureError {
33+
error: "Missing X-Signature-Ed25519 header".to_string(),
34+
},
35+
));
36+
}
37+
};
38+
39+
let timestamp = match request.headers().get_one("X-Signature-Timestamp") {
40+
Some(ts) => ts.to_string(),
41+
None => {
42+
return Outcome::Error((
43+
Status::Unauthorized,
44+
SignatureError {
45+
error: "Missing X-Signature-Timestamp header".to_string(),
46+
},
47+
));
48+
}
49+
};
50+
51+
Outcome::Success(DiscordSignature {
52+
signature,
53+
timestamp,
54+
})
55+
}
56+
}
57+
58+
// Helper function to verify Discord signature
59+
#[allow(dead_code)]
60+
pub fn verify_discord_signature(
61+
public_key: &str,
62+
signature: &str,
63+
timestamp: &str,
64+
body: &str,
65+
) -> bool {
66+
// Parse the public key
67+
let public_key_bytes = match hex::decode(public_key) {
68+
Ok(bytes) => {
69+
if bytes.len() != 32 {
70+
return false;
71+
}
72+
let mut array = [0u8; 32];
73+
array.copy_from_slice(&bytes);
74+
array
75+
}
76+
Err(_) => return false,
77+
};
78+
79+
// Parse the signature
80+
let signature_bytes = match hex::decode(signature) {
81+
Ok(bytes) => {
82+
if bytes.len() != 64 {
83+
return false;
84+
}
85+
let mut array = [0u8; 64];
86+
array.copy_from_slice(&bytes);
87+
array
88+
}
89+
Err(_) => return false,
90+
};
91+
92+
// Create the message to verify (timestamp + body)
93+
let message = format!("{}{}", timestamp, body);
94+
let message_bytes = message.as_bytes();
95+
96+
// Create the verifying key
97+
let verifying_key = match VerifyingKey::from_bytes(&public_key_bytes) {
98+
Ok(key) => key,
99+
Err(_) => return false,
100+
};
101+
102+
// Create the signature
103+
let signature = Signature::from(signature_bytes);
104+
105+
// Verify the signature
106+
verifying_key.verify(message_bytes, &signature).is_ok()
107+
}
108+
109+
// Function to validate Discord webhook request
110+
#[allow(dead_code)]
111+
pub fn validate_discord_webhook(
112+
signature_header: Option<&str>,
113+
timestamp_header: Option<&str>,
114+
body: &str,
115+
) -> Result<(), Status> {
116+
// Get the public key from environment variable
117+
let public_key = match env::var("PUBLIC_KEY") {
118+
Ok(key) => key,
119+
Err(_) => {
120+
return Err(Status::InternalServerError);
121+
}
122+
};
123+
124+
// Get required headers
125+
let signature = match signature_header {
126+
Some(sig) => sig,
127+
None => {
128+
return Err(Status::Unauthorized);
129+
}
130+
};
131+
132+
let timestamp = match timestamp_header {
133+
Some(ts) => ts,
134+
None => {
135+
return Err(Status::Unauthorized);
136+
}
137+
};
138+
139+
// Verify the signature
140+
if verify_discord_signature(&public_key, signature, timestamp, body) {
141+
Ok(())
142+
} else {
143+
Err(Status::Unauthorized)
144+
}
145+
}

0 commit comments

Comments
 (0)