licensegate_rs/
lib.rs

1pub mod licensegate_config;
2
3use base64::{Engine, engine::general_purpose};
4use licensegate_config::LicenseGateConfig;
5use reqwest::Client;
6use std::sync::Arc;
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum ValidationType {
11    Valid,
12    NotFound,
13    NotActive,
14    Expired,
15    LicenseScopeFailed,
16    IpLimitExceeded,
17    RateLimitExceeded,
18    FailedChallenge,
19    ServerError,
20    ConnectionError,
21}
22
23#[derive(Debug, Error)]
24pub enum LicenseGateError {
25    #[error("server error")]
26    ServerError,
27    #[error("connection error")]
28    ConnectionError,
29    #[error("challenge verification failed")]
30    ChallengeFailed,
31    #[error("validation result: {0:?}")]
32    Validation(ValidationType),
33}
34
35pub struct LicenseGate {
36    user_id: String,
37    public_rsa_key: Option<String>,
38    validation_server: String,
39    use_challenge: bool,
40    debug_mode: bool,
41    client: Arc<Client>,
42}
43
44impl LicenseGate {
45    pub fn new(user_id: impl Into<String>) -> Self {
46        Self {
47            user_id: user_id.into(),
48            public_rsa_key: None,
49            validation_server: "https://api.licensegate.io".into(),
50            use_challenge: false,
51            debug_mode: false,
52            client: Arc::new(Client::new()),
53        }
54    }
55
56    pub fn set_public_rsa_key(mut self, key: impl Into<String>) -> Self {
57        self.public_rsa_key = Some(key.into());
58        self.use_challenge = true;
59        self
60    }
61
62    pub fn set_validation_server(mut self, server: impl Into<String>) -> Self {
63        self.validation_server = server.into();
64        self
65    }
66
67    pub fn use_challenges(mut self) -> Self {
68        self.use_challenge = true;
69        self
70    }
71
72    pub fn debug(mut self) -> Self {
73        self.debug_mode = true;
74        self
75    }
76
77    pub async fn verify(
78        &self,
79        config: LicenseGateConfig,
80    ) -> Result<ValidationType, LicenseGateError> {
81        let challenge = if self.use_challenge {
82            Some(format!("{}", chrono::Utc::now().timestamp_millis()))
83        } else {
84            None
85        };
86
87        let url = self.build_url(
88            &config.license,
89            config.scope,
90            config.metadata,
91            challenge.as_deref(),
92        );
93
94        let res = self
95            .client
96            .get(&url)
97            .header("User-Agent", "RohitSangwan/LicenseGate-Rust")
98            .send()
99            .await
100            .map_err(|_| LicenseGateError::ConnectionError)?;
101
102        let status = res.status();
103        let data: serde_json::Value = res
104            .json()
105            .await
106            .map_err(|_| LicenseGateError::ServerError)?;
107
108        if self.debug_mode {
109            println!("\nRequest URL: {}", url);
110            println!("Status: {}", status);
111            println!("Response: {}", data);
112        }
113
114        if let Some(error) = data.get("error") {
115            if self.debug_mode {
116                println!("Error: {}", error);
117            }
118            return Err(LicenseGateError::ServerError);
119        }
120
121        let valid = data.get("valid").and_then(|v| v.as_bool()).unwrap_or(true);
122        let result_str = data
123            .get("result")
124            .and_then(|r| r.as_str())
125            .unwrap_or("SERVER_ERROR");
126
127        let result = match result_str {
128            "VALID" if !valid => ValidationType::ServerError,
129            "VALID" => ValidationType::Valid,
130            "NOT_FOUND" => ValidationType::NotFound,
131            "NOT_ACTIVE" => ValidationType::NotActive,
132            "EXPIRED" => ValidationType::Expired,
133            "LICENSE_SCOPE_FAILED" => ValidationType::LicenseScopeFailed,
134            "IP_LIMIT_EXCEEDED" => ValidationType::IpLimitExceeded,
135            "RATE_LIMIT_EXCEEDED" => ValidationType::RateLimitExceeded,
136            _ => ValidationType::ServerError,
137        };
138
139        if !valid {
140            return Err(LicenseGateError::Validation(result));
141        }
142
143        if self.use_challenge {
144            if let Some(signed) = data.get("signedChallenge").and_then(|s| s.as_str()) {
145                if !self.verify_challenge(challenge.unwrap().as_bytes(), signed) {
146                    return Err(LicenseGateError::ChallengeFailed);
147                }
148            } else {
149                return Err(LicenseGateError::ChallengeFailed);
150            }
151        }
152
153        Ok(result)
154    }
155
156    pub async fn verify_simple(&self, config: LicenseGateConfig) -> bool {
157        matches!(self.verify(config).await, Ok(ValidationType::Valid))
158    }
159
160    fn build_url(
161        &self,
162        license_key: &str,
163        scope: Option<String>,
164        metadata: Option<String>,
165        challenge: Option<&str>,
166    ) -> String {
167        let mut query = vec![];
168        if let Some(meta) = metadata {
169            query.push(format!("metadata={}", urlencoding::encode(&meta)));
170        }
171        if let Some(sc) = scope {
172            query.push(format!("scope={}", urlencoding::encode(&sc)));
173        }
174        if let Some(ch) = challenge {
175            query.push(format!("challenge={}", urlencoding::encode(ch)));
176        }
177        let query_str = if query.is_empty() {
178            "".into()
179        } else {
180            format!("?{}", query.join("&"))
181        };
182        format!(
183            "{}/license/{}/{}/verify{}",
184            self.validation_server, self.user_id, license_key, query_str
185        )
186    }
187
188    fn verify_challenge(&self, challenge: &[u8], signed_base64: &str) -> bool {
189        use openssl::hash::MessageDigest;
190        use openssl::rsa::Rsa;
191        use openssl::sign::Verifier;
192
193        if let Some(pub_key_pem) = &self.public_rsa_key {
194            let pub_key_pem = Self::normalize_pem_format(pub_key_pem);
195            match Rsa::public_key_from_pem(pub_key_pem.as_bytes()) {
196                Ok(rsa) => {
197                    let pub_key = rsa.public_key_to_pem().unwrap();
198                    let pkey = openssl::pkey::PKey::public_key_from_pem(&pub_key).unwrap();
199                    let mut verifier = Verifier::new(MessageDigest::sha256(), &pkey).unwrap();
200                    verifier.update(challenge).unwrap();
201                    if let Ok(sig) = general_purpose::STANDARD.decode(signed_base64) {
202                        return verifier.verify(&sig).unwrap_or(false);
203                    }
204                }
205                Err(e) => {
206                    if self.debug_mode {
207                        eprintln!("RSA decode error: {}", e);
208                    }
209                }
210            }
211        }
212
213        false
214    }
215
216    fn normalize_pem_format(key: &str) -> String {
217        let key = key.replace("\\n", "\n").trim().to_string();
218        if key.contains("-----BEGIN") {
219            println!("Formatting key");
220            key.replace("-----BEGIN PUBLIC KEY-----", "-----BEGIN PUBLIC KEY-----\n")
221                .replace("-----END PUBLIC KEY-----", "\n-----END PUBLIC KEY-----")
222        } else {
223            key
224        }
225    }
226}