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