binaryninja/
enterprise.rs

1use crate::rc::Array;
2use crate::string::{BnString, IntoCStr};
3use std::ffi::c_void;
4use std::marker::PhantomData;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum EnterpriseCheckoutError {
10    #[error("enterprise server returned error: {0}")]
11    ServerError(String),
12    #[error("no credentials set for authentication")]
13    NoCredentials,
14    #[error("failed to authenticate with username and password")]
15    NotAuthenticated,
16    #[error("failed to refresh expired license: {0}")]
17    RefreshExpiredLicenseFailed(String),
18}
19
20#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
21pub enum EnterpriseCheckoutStatus {
22    /// The UI is managing the enterprise checkout.
23    AlreadyManaged,
24    /// Checkout was successful, attached duration is the duration of the license, if any.
25    Success(Option<Duration>),
26}
27
28/// Initialize the enterprise server connection to check out a floating license.
29/// Result value is if we actually checked out a license (i.e. Ok(false) means we already have a
30/// license checked out and will not need to release it later)
31pub fn checkout_license(
32    duration: Duration,
33) -> Result<EnterpriseCheckoutStatus, EnterpriseCheckoutError> {
34    if crate::is_ui_enabled() {
35        // We only need to check out a license if running headlessly.
36        return Ok(EnterpriseCheckoutStatus::AlreadyManaged);
37    }
38
39    // The disparate core functions we call here might already have mutexes to guard.
40    static CHECKOUT_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
41    let _mtx = CHECKOUT_MUTEX.lock().unwrap();
42
43    #[allow(clippy::collapsible_if)]
44    if !is_server_initialized() {
45        // We need to first initialize the server.
46        if !initialize_server() && is_server_floating_license() {
47            let last_error = server_last_error().to_string();
48            return Err(EnterpriseCheckoutError::ServerError(last_error));
49        }
50    }
51
52    if is_server_floating_license() {
53        if !is_server_connected() && !connect_server() {
54            let last_error = server_last_error().to_string();
55            return Err(EnterpriseCheckoutError::ServerError(last_error));
56        }
57
58        #[allow(clippy::collapsible_if)]
59        if !is_server_authenticated() {
60            'auth: {
61                // We have yet to authenticate with the server, we should try all available authentication methods.
62                if authenticate_server_with_method("Keychain", false) {
63                    break 'auth;
64                }
65
66                // We could not authenticate with the system keychain, we should try with credentials.
67                let username = std::env::var("BN_ENTERPRISE_USERNAME");
68                let password = std::env::var("BN_ENTERPRISE_PASSWORD");
69                if let Ok(username) = username {
70                    if let Ok(password) = password {
71                        // Having creds that don't work is a hard error
72                        if !authenticate_server_with_credentials(&username, &password, true) {
73                            return Err(EnterpriseCheckoutError::NotAuthenticated);
74                        }
75                        // Otherwise, if the creds worked, we got auth
76                        break 'auth;
77                    }
78                }
79
80                let token = std::env::var("BN_ENTERPRISE_TOKEN");
81                if let Ok(token) = token {
82                    if !authenticate_server_with_token(&token, true) {
83                        return Err(EnterpriseCheckoutError::NotAuthenticated);
84                    }
85                    break 'auth;
86                }
87
88                // If we're still here, we don't have any credentials for authentication.
89                return Err(EnterpriseCheckoutError::NoCredentials);
90            }
91        }
92    }
93
94    #[allow(clippy::collapsible_if)]
95    if !is_server_license_still_activated()
96        || (!is_server_floating_license() && crate::license_expiration_time() < SystemTime::now())
97    {
98        // If the license is expired, we should refresh the license.
99        if !update_server_license(duration) {
100            let last_error = server_last_error().to_string();
101            return Err(EnterpriseCheckoutError::RefreshExpiredLicenseFailed(
102                last_error,
103            ));
104        }
105    }
106
107    Ok(EnterpriseCheckoutStatus::Success(license_duration()))
108}
109
110pub fn release_license(release_floating: bool) {
111    if !crate::is_ui_enabled() {
112        // This might look dumb, why would we want to connect to the server, would that not just mean
113        // we don't need to release the license? Well no, you could have run a script, acquired a license for 10 hours
114        // then you WOULD want to call release license, and your expectation is that acquired license
115        // will now be released. To release that you must have an active connection which is what this does.
116        if !is_server_initialized() {
117            initialize_server();
118        }
119        if !is_server_connected() {
120            connect_server();
121        }
122        // We optionally release floating licenses as users typically want to keep them around.
123        if is_server_floating_license() && !release_floating {
124            return;
125        }
126        // We should only release the license if we are running headlessly.
127        release_server_license();
128    }
129}
130
131// TODO: If "" string return None
132pub fn server_username() -> String {
133    unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerUsername()) }
134}
135
136// TODO: If "" string return None
137pub fn server_url() -> String {
138    unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerUrl()) }
139}
140
141pub fn set_server_url(url: &str) -> Result<(), ()> {
142    let url = url.to_cstr();
143    let result = unsafe {
144        binaryninjacore_sys::BNSetEnterpriseServerUrl(
145            url.as_ref().as_ptr() as *const std::os::raw::c_char
146        )
147    };
148    if result {
149        Ok(())
150    } else {
151        Err(())
152    }
153}
154
155pub fn server_name() -> String {
156    unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerName()) }
157}
158
159pub fn server_id() -> String {
160    unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerId()) }
161}
162
163pub fn server_version() -> u64 {
164    unsafe { binaryninjacore_sys::BNGetEnterpriseServerVersion() }
165}
166
167pub fn server_build_id() -> String {
168    unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerBuildId()) }
169}
170
171pub fn server_token() -> String {
172    unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerToken()) }
173}
174
175pub fn license_duration() -> Option<Duration> {
176    let duration =
177        Duration::from_secs(unsafe { binaryninjacore_sys::BNGetEnterpriseServerLicenseDuration() });
178    match duration {
179        // If the core returns 0 there is no license duration.
180        Duration::ZERO => None,
181        _ => Some(duration),
182    }
183}
184
185pub fn license_expiration_time() -> SystemTime {
186    let m = Duration::from_secs(unsafe {
187        binaryninjacore_sys::BNGetEnterpriseServerLicenseExpirationTime()
188    });
189    UNIX_EPOCH + m
190}
191
192pub fn server_reservation_time_limit() -> Duration {
193    Duration::from_secs(unsafe { binaryninjacore_sys::BNGetEnterpriseServerReservationTimeLimit() })
194}
195
196pub fn is_server_floating_license() -> bool {
197    unsafe { binaryninjacore_sys::BNIsEnterpriseServerFloatingLicense() }
198}
199
200pub fn is_server_license_still_activated() -> bool {
201    unsafe { binaryninjacore_sys::BNIsEnterpriseServerLicenseStillActivated() }
202}
203
204pub fn authenticate_server_with_token(token: &str, remember: bool) -> bool {
205    let token = token.to_cstr();
206    unsafe {
207        binaryninjacore_sys::BNAuthenticateEnterpriseServerWithToken(
208            token.as_ref().as_ptr() as *const std::os::raw::c_char,
209            remember,
210        )
211    }
212}
213
214pub fn authenticate_server_with_credentials(
215    username: &str,
216    password: &str,
217    remember: bool,
218) -> bool {
219    let username = username.to_cstr();
220    let password = password.to_cstr();
221    unsafe {
222        binaryninjacore_sys::BNAuthenticateEnterpriseServerWithCredentials(
223            username.as_ref().as_ptr() as *const std::os::raw::c_char,
224            password.as_ref().as_ptr() as *const std::os::raw::c_char,
225            remember,
226        )
227    }
228}
229
230pub fn authenticate_server_with_method(method: &str, remember: bool) -> bool {
231    let method = method.to_cstr();
232    unsafe {
233        binaryninjacore_sys::BNAuthenticateEnterpriseServerWithMethod(
234            method.as_ref().as_ptr() as *const std::os::raw::c_char,
235            remember,
236        )
237    }
238}
239
240pub fn connect_server() -> bool {
241    unsafe { binaryninjacore_sys::BNConnectEnterpriseServer() }
242}
243
244pub fn deauthenticate_server() -> bool {
245    unsafe { binaryninjacore_sys::BNDeauthenticateEnterpriseServer() }
246}
247
248pub fn cancel_server_authentication() {
249    unsafe { binaryninjacore_sys::BNCancelEnterpriseServerAuthentication() }
250}
251
252pub fn update_server_license(timeout: Duration) -> bool {
253    unsafe { binaryninjacore_sys::BNUpdateEnterpriseServerLicense(timeout.as_secs()) }
254}
255
256pub fn release_server_license() -> bool {
257    unsafe { binaryninjacore_sys::BNReleaseEnterpriseServerLicense() }
258}
259
260pub fn is_server_connected() -> bool {
261    unsafe { binaryninjacore_sys::BNIsEnterpriseServerConnected() }
262}
263
264pub fn is_server_authenticated() -> bool {
265    unsafe { binaryninjacore_sys::BNIsEnterpriseServerAuthenticated() }
266}
267
268pub fn is_server_initialized() -> bool {
269    unsafe { binaryninjacore_sys::BNIsEnterpriseServerInitialized() }
270}
271
272pub fn initialize_server() -> bool {
273    unsafe { binaryninjacore_sys::BNInitializeEnterpriseServer() }
274}
275
276pub fn server_last_error() -> String {
277    unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerLastError()) }
278}
279
280pub fn server_authentication_methods() -> (Array<BnString>, Array<BnString>) {
281    let mut methods = core::ptr::null_mut();
282    let mut names = core::ptr::null_mut();
283    let count = unsafe {
284        binaryninjacore_sys::BNGetEnterpriseServerAuthenticationMethods(&mut methods, &mut names)
285    };
286    unsafe { (Array::new(methods, count, ()), Array::new(names, count, ())) }
287}
288
289// NOTE don't implement Clone, Copy, so each callback can only be
290// register/unregistered only once
291#[repr(transparent)]
292#[derive(Debug)]
293pub struct EnterpriseServerCallback<'a> {
294    handle: binaryninjacore_sys::BNEnterpriseServerCallbacks,
295    lifetime: PhantomData<&'a ()>,
296}
297
298pub fn register_license_changed_callback<'a, F: FnMut(bool) + 'a>(
299    callback: F,
300) -> EnterpriseServerCallback<'a> {
301    unsafe extern "C" fn cb_license_status_changed<F: FnMut(bool)>(
302        ctxt: *mut c_void,
303        still_valid: bool,
304    ) {
305        let ctxt: &mut F = &mut *(ctxt as *mut F);
306        ctxt(still_valid)
307    }
308    let mut handle = binaryninjacore_sys::BNEnterpriseServerCallbacks {
309        context: Box::leak(Box::new(callback)) as *mut F as *mut c_void,
310        licenseStatusChanged: Some(cb_license_status_changed::<F>),
311    };
312    unsafe { binaryninjacore_sys::BNRegisterEnterpriseServerNotification(&mut handle) }
313    EnterpriseServerCallback {
314        handle,
315        lifetime: PhantomData,
316    }
317}
318
319pub fn unregister_license_changed_callback(mut callback_handle: EnterpriseServerCallback) {
320    unsafe {
321        binaryninjacore_sys::BNUnregisterEnterpriseServerNotification(&mut callback_handle.handle)
322    }
323}
324
325impl<'a> EnterpriseServerCallback<'a> {
326    /// register the license changed callback
327    pub fn register<F: FnMut(bool) + 'a>(callback: F) -> Self {
328        register_license_changed_callback(callback)
329    }
330
331    /// deregister the license changed callback, equivalent to drop the struct
332    pub fn deregister(self) {
333        // Nothing, just drop self
334    }
335}
336
337impl Drop for EnterpriseServerCallback<'_> {
338    fn drop(&mut self) {
339        unregister_license_changed_callback(EnterpriseServerCallback {
340            handle: self.handle,
341            lifetime: PhantomData,
342        })
343    }
344}