diff --git a/Cargo.lock b/Cargo.lock index 5397271ce..0f98a3684 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -557,6 +557,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "tokio", ] [[package]] diff --git a/crates/bitwarden-c/src/c.rs b/crates/bitwarden-c/src/c.rs index 158c38025..76fed0073 100644 --- a/crates/bitwarden-c/src/c.rs +++ b/crates/bitwarden-c/src/c.rs @@ -1,6 +1,11 @@ -use std::{ffi::CStr, os::raw::c_char, str}; +use std::{ + ffi::{CStr, CString}, + os::raw::c_char, + str, +}; use bitwarden_json::client::Client; +use tokio::task::JoinHandle; use crate::{box_ptr, ffi_ref}; @@ -28,6 +33,46 @@ pub extern "C" fn run_command(c_str_ptr: *const c_char, client_ptr: *const CClie } } +type OnCompletedCallback = unsafe extern "C" fn(result: *mut c_char) -> (); + +#[no_mangle] +pub extern "C" fn run_command_async( + c_str_ptr: *const c_char, + client_ptr: *const CClient, + on_completed_callback: OnCompletedCallback, + is_cancellable: bool, +) -> *mut JoinHandle<()> { + let client = unsafe { ffi_ref!(client_ptr) }; + let input_str = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr) }.to_bytes()) + .expect("Input should be a valid string") + // Languages may assume that the string is collectable as soon as this method exits + // but it's not since the request will be run in the background + // so we need to make our own copy. + .to_owned(); + + let join_handle = client.runtime.spawn(async move { + let result = client.client.run_command(input_str.as_str()).await; + let str_result = match std::ffi::CString::new(result) { + Ok(cstr) => cstr.into_raw(), + Err(_) => panic!("failed to return comment result: null encountered"), + }; + + // run completed function + unsafe { + on_completed_callback(str_result); + let _ = CString::from_raw(str_result); + } + }); + + // We only want to box the join handle the caller has said that they may want to cancel, + // essentially promising to us that they will take care of the returned pointer. + if is_cancellable { + box_ptr!(join_handle) + } else { + std::ptr::null_mut() + } +} + // Init client, potential leak! You need to call free_mem after this! #[no_mangle] pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient { @@ -56,3 +101,15 @@ pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient { pub extern "C" fn free_mem(client_ptr: *mut CClient) { std::mem::drop(unsafe { Box::from_raw(client_ptr) }); } + +#[no_mangle] +pub extern "C" fn abort_and_free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () { + let join_handle = unsafe { Box::from_raw(join_handle_ptr) }; + join_handle.abort(); + std::mem::drop(join_handle); +} + +#[no_mangle] +pub extern "C" fn free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () { + std::mem::drop(unsafe { Box::from_raw(join_handle_ptr) }); +} diff --git a/crates/bitwarden-json/Cargo.toml b/crates/bitwarden-json/Cargo.toml index ed1b39580..dd703b9d7 100644 --- a/crates/bitwarden-json/Cargo.toml +++ b/crates/bitwarden-json/Cargo.toml @@ -26,5 +26,8 @@ schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +[target.'cfg(debug_assertions)'.dependencies] +tokio = { version = "1.36.0", features = ["time"] } + [lints] workspace = true diff --git a/crates/bitwarden-json/src/client.rs b/crates/bitwarden-json/src/client.rs index dc84c559d..55c08c2e5 100644 --- a/crates/bitwarden-json/src/client.rs +++ b/crates/bitwarden-json/src/client.rs @@ -99,6 +99,34 @@ impl Client { client.generator().password(req).into_string() } }, + #[cfg(debug_assertions)] + Command::Debug(cmd) => { + use bitwarden::Error; + + use crate::command::DebugCommand; + + match cmd { + DebugCommand::CancellationTest { duration_millis } => { + use tokio::time::sleep; + let duration = std::time::Duration::from_millis(duration_millis); + sleep(duration).await; + println!("After wait #1"); + sleep(duration).await; + println!("After wait #2"); + sleep(duration).await; + println!("After wait #3"); + Ok::<i32, Error>(42).into_string() + } + DebugCommand::ErrorTest {} => { + use bitwarden::Error; + + Err::<i32, Error>(Error::Internal(std::borrow::Cow::Borrowed( + "This is an error.", + ))) + .into_string() + } + } + } } } diff --git a/crates/bitwarden-json/src/command.rs b/crates/bitwarden-json/src/command.rs index 7483b90cf..132925b0c 100644 --- a/crates/bitwarden-json/src/command.rs +++ b/crates/bitwarden-json/src/command.rs @@ -79,6 +79,8 @@ pub enum Command { Projects(ProjectsCommand), #[cfg(feature = "secrets")] Generators(GeneratorsCommand), + #[cfg(debug_assertions)] + Debug(DebugCommand), } #[cfg(feature = "secrets")] @@ -188,3 +190,11 @@ pub enum GeneratorsCommand { /// Returns: [String] GeneratePassword(PasswordGeneratorRequest), } + +#[cfg(debug_assertions)] +#[derive(Serialize, Deserialize, JsonSchema, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub enum DebugCommand { + CancellationTest { duration_millis: u64 }, + ErrorTest {}, +} diff --git a/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj b/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj index 5b189d8ca..ab616ab83 100644 --- a/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj +++ b/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> diff --git a/languages/csharp/Bitwarden.Sdk.Samples/Program.cs b/languages/csharp/Bitwarden.Sdk.Samples/Program.cs index e80d12017..64837ee76 100644 --- a/languages/csharp/Bitwarden.Sdk.Samples/Program.cs +++ b/languages/csharp/Bitwarden.Sdk.Samples/Program.cs @@ -1,4 +1,4 @@ -using Bitwarden.Sdk; +using Bitwarden.Sdk; // Get environment variables var identityUrl = Environment.GetEnvironmentVariable("IDENTITY_URL")!; @@ -15,10 +15,10 @@ }); // Authenticate -bitwardenClient.Auth.LoginAccessToken(accessToken, stateFile); +await bitwardenClient.Auth.LoginAccessTokenAsync(accessToken, stateFile); // Projects List -var projectsList = bitwardenClient.Projects.List(organizationId).Data; +var projectsList = (await bitwardenClient.Projects.ListAsync(organizationId)).Data; Console.WriteLine("A list of all projects:"); foreach (ProjectResponse pr in projectsList) { @@ -30,9 +30,9 @@ // Projects Create, Update, & Get Console.WriteLine("Creating and updating a project"); -var projectResponse = bitwardenClient.Projects.Create(organizationId, "NewTestProject"); -projectResponse = bitwardenClient.Projects.Update(organizationId, projectResponse.Id, "NewTestProject Renamed"); -projectResponse = bitwardenClient.Projects.Get(projectResponse.Id); +var projectResponse = await bitwardenClient.Projects.CreateAsync(organizationId, "NewTestProject"); +projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed"); +projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id); Console.WriteLine("Here is the project we created and updated:"); Console.WriteLine(projectResponse.Name); @@ -40,7 +40,7 @@ Console.ReadLine(); // Secrets list -var secretsList = bitwardenClient.Secrets.List(organizationId).Data; +var secretsList = (await bitwardenClient.Secrets.ListAsync(organizationId)).Data; Console.WriteLine("A list of all secrets:"); foreach (SecretIdentifierResponse sr in secretsList) { @@ -52,9 +52,9 @@ // Secrets Create, Update, Get Console.WriteLine("Creating and updating a secret"); -var secretResponse = bitwardenClient.Secrets.Create(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id }); -secretResponse = bitwardenClient.Secrets.Update(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id }); -secretResponse = bitwardenClient.Secrets.Get(secretResponse.Id); +var secretResponse = await bitwardenClient.Secrets.CreateAsync(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id }); +secretResponse = await bitwardenClient.Secrets.UpdateAsync(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id }); +secretResponse = await bitwardenClient.Secrets.GetAsync(secretResponse.Id); Console.WriteLine("Here is the secret we created and updated:"); Console.WriteLine(secretResponse.Key); @@ -62,12 +62,12 @@ Console.ReadLine(); // Secrets GetByIds -var secretsResponse = bitwardenClient.Secrets.GetByIds(new[] { secretResponse.Id }); +var secretsResponse = await bitwardenClient.Secrets.GetByIdsAsync(new[] { secretResponse.Id }); // Secrets Sync -var syncResponse = bitwardenClient.Secrets.Sync(organizationId, null); +var syncResponse = await bitwardenClient.Secrets.SyncAsync(organizationId, null); // Secrets & Projects Delete Console.WriteLine("Deleting our secret and project"); -bitwardenClient.Secrets.Delete(new[] { secretResponse.Id }); -bitwardenClient.Projects.Delete(new[] { projectResponse.Id }); +await bitwardenClient.Secrets.DeleteAsync(new[] { secretResponse.Id }); +await bitwardenClient.Projects.DeleteAsync(new[] { projectResponse.Id }); diff --git a/languages/csharp/Bitwarden.Sdk.Tests/Bitwarden.Sdk.Tests.csproj b/languages/csharp/Bitwarden.Sdk.Tests/Bitwarden.Sdk.Tests.csproj new file mode 100644 index 000000000..3dbd8c99e --- /dev/null +++ b/languages/csharp/Bitwarden.Sdk.Tests/Bitwarden.Sdk.Tests.csproj @@ -0,0 +1,29 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + <IsTestProject>true</IsTestProject> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> + <PackageReference Include="xunit" Version="2.4.2" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="coverlet.collector" Version="6.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Bitwarden.Sdk\Bitwarden.Sdk.csproj" /> + </ItemGroup> + +</Project> diff --git a/languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs b/languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs new file mode 100644 index 000000000..c802f4480 --- /dev/null +++ b/languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/languages/csharp/Bitwarden.Sdk.Tests/InteropTests.cs b/languages/csharp/Bitwarden.Sdk.Tests/InteropTests.cs new file mode 100644 index 000000000..d6903d58d --- /dev/null +++ b/languages/csharp/Bitwarden.Sdk.Tests/InteropTests.cs @@ -0,0 +1,35 @@ +using Bitwarden.Sdk; +using System.Diagnostics; + +namespace Bitwarden.Sdk.Tests; + +public class InteropTests +{ + [Fact] + public async void CancelingTest_ThrowsTaskCanceledException() + { + var client = new BitwardenClient(); + + var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)); + + await Assert.ThrowsAsync<TaskCanceledException>(async () => await client.CancellationTestAsync(cts.Token)); + } + + [Fact] + public async void NoCancel_TaskCompletesSuccessfully() + { + var client = new BitwardenClient(); + + var result = await client.CancellationTestAsync(CancellationToken.None); + Assert.Equal(42, result); + } + + [Fact] + public async void Error_ThrowsException() + { + var client = new BitwardenClient(); + + var bitwardenException = await Assert.ThrowsAsync<BitwardenException>(async () => await client.ErrorTestAsync()); + Assert.Equal("Internal error: This is an error.", bitwardenException.Message); + } +} diff --git a/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs b/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs new file mode 100644 index 000000000..6432f9ea1 --- /dev/null +++ b/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs @@ -0,0 +1,56 @@ +namespace Bitwarden.Sdk.Tests; + +public class SampleTests +{ + [SecretsManagerFact] + public async Task RunSample_Works() + { + // Get environment variables + var identityUrl = Environment.GetEnvironmentVariable("IDENTITY_URL")!; + var apiUrl = Environment.GetEnvironmentVariable("API_URL")!; + var organizationId = Guid.Parse(Environment.GetEnvironmentVariable("ORGANIZATION_ID")!); + var accessToken = Environment.GetEnvironmentVariable("ACCESS_TOKEN")!; + var stateFile = Environment.GetEnvironmentVariable("STATE_FILE")!; + + // Create the SDK Client + using var bitwardenClient = new BitwardenClient(new BitwardenSettings + { + ApiUrl = apiUrl, + IdentityUrl = identityUrl + }); + + // Authenticate + await bitwardenClient.Auth.LoginAccessTokenAsync(accessToken, stateFile); + + // Projects Create, Update, & Get + var projectResponse = await bitwardenClient.Projects.CreateAsync(organizationId, "NewTestProject"); + projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed"); + projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id); + + Assert.Equal("NewTestProject Renamed", projectResponse.Name); + + var projectList = await bitwardenClient.Projects.ListAsync(organizationId); + + Assert.True(projectList.Data.Count() >= 1); + + // Secrets list + var secretsList = await bitwardenClient.Secrets.ListAsync(organizationId); + + // Secrets Create, Update, Get + var secretResponse = await bitwardenClient.Secrets.CreateAsync(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id }); + secretResponse = await bitwardenClient.Secrets.UpdateAsync(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id }); + secretResponse = await bitwardenClient.Secrets.GetAsync(secretResponse.Id); + + Assert.Equal("New Secret Name", secretResponse.Key); + + // Secrets GetByIds + var secretsResponse = await bitwardenClient.Secrets.GetByIdsAsync(new[] { secretResponse.Id }); + + // Secrets Sync + var syncResponse = await bitwardenClient.Secrets.SyncAsync(organizationId, null); + + // Secrets & Projects Delete + await bitwardenClient.Secrets.DeleteAsync(new[] { secretResponse.Id }); + await bitwardenClient.Projects.DeleteAsync(new[] { projectResponse.Id }); + } +} diff --git a/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs b/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs new file mode 100644 index 000000000..e5bcebfc2 --- /dev/null +++ b/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs @@ -0,0 +1,54 @@ +namespace Bitwarden.Sdk.Tests; + +public class SecretsManagerFactAttribute : FactAttribute +{ + public SecretsManagerFactAttribute() + { + if (!TryGetEnvironment("IDENTITY_URL", out var identityUrl)) + { + Skip = "Environment variable IDENTITY_URL was not provided."; + } + + if (!Uri.TryCreate(identityUrl, UriKind.Absolute, out _)) + { + Skip = $"The identity url {identityUrl} provided in IDENTITY_URL is not a valid URL."; + } + + if (!TryGetEnvironment("API_URL", out var apiUrl)) + { + Skip = "Environment variable API_URL was not provided."; + } + + if (!Uri.TryCreate(apiUrl, UriKind.Absolute, out _)) + { + Skip = $"The identity url {apiUrl} provided in API_URL is not a valid URL."; + } + + if (!TryGetEnvironment("ORGANIZATION_ID", out var organizationId)) + { + Skip = "Environment variable ORGANIZATION_ID was not provided."; + } + + if (!Guid.TryParse(organizationId, out _)) + { + Skip = $"The organization id {organizationId} provided in ORGANIZATION_ID is not a valid GUID."; + } + + if (!TryGetEnvironment("ACCESS_TOKEN", out _)) + { + Skip = "Environment variable ACCESS_TOKEN was not provided."; + } + } + + private static bool TryGetEnvironment(string variable, out string value) + { + value = Environment.GetEnvironmentVariable(variable); + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + return true; + } +} diff --git a/languages/csharp/Bitwarden.Sdk/AuthClient.cs b/languages/csharp/Bitwarden.Sdk/AuthClient.cs index e801f2aee..7b7f25d2b 100644 --- a/languages/csharp/Bitwarden.Sdk/AuthClient.cs +++ b/languages/csharp/Bitwarden.Sdk/AuthClient.cs @@ -9,10 +9,10 @@ internal AuthClient(CommandRunner commandRunner) _commandRunner = commandRunner; } - public void LoginAccessToken(string accessToken, string stateFile = "") + public async Task LoginAccessTokenAsync(string accessToken, string stateFile = "", CancellationToken cancellationToken = default) { var command = new Command { LoginAccessToken = new AccessTokenLoginRequest { AccessToken = accessToken, StateFile = stateFile } }; - var response = _commandRunner.RunCommand<ResponseForApiKeyLoginResponse>(command); + var response = await _commandRunner.RunCommandAsync<ResponseForApiKeyLoginResponse>(command, cancellationToken); if (response is not { Success: true }) { throw new BitwardenAuthException(response != null ? response.ErrorMessage : "Login failed"); diff --git a/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj index a7c82e4b5..2dd16dd82 100644 --- a/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj +++ b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj @@ -1,17 +1,19 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <RootNamespace>Bitwarden.Sdk</RootNamespace> - + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <Title>Bitwarden Secrets Manager SDK</Title> <Authors>Bitwarden Inc.</Authors> <Description>.NET bindings for interacting with the Bitwarden Secrets Manager</Description> <Copyright>Bitwarden Inc.</Copyright> <Product>SDK</Product> + <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> + <RepositoryUrl>https://github.com/bitwarden/sdk/tree/main/languages/csharp</RepositoryUrl> <RepositoryType>Git</RepositoryType> @@ -74,4 +76,4 @@ <PackagePath>runtimes/win-x64/native</PackagePath> </Content> </ItemGroup> -</Project> \ No newline at end of file +</Project> diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs b/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs new file mode 100644 index 000000000..0b1e95409 --- /dev/null +++ b/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs @@ -0,0 +1,58 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +namespace Bitwarden.Sdk; + +[Obsolete("DebugCommand is intended for tests only, using any of these commands will throw errors in production code.")] +[EditorBrowsable(EditorBrowsableState.Never)] +partial class DebugCommand +{ + +} + +#if DEBUG +public sealed partial class BitwardenClient +{ + public async Task<int> CancellationTestAsync(CancellationToken token) + { + var result = await _commandRunner.RunCommandAsync<JsonElement>( + new Command + { + Debug = new DebugCommand + { + CancellationTest = new CancellationTest + { + DurationMillis = 200, + }, + }, + }, token); + + return ParseResult(result).GetInt32(); + } + + public async Task<int> ErrorTestAsync() + { + var result = await _commandRunner.RunCommandAsync<JsonElement>( + new Command + { + Debug = new DebugCommand + { + ErrorTest = new ErrorTest(), + }, + }, CancellationToken.None); + + return ParseResult(result).GetInt32(); + } + + private JsonElement ParseResult(JsonElement result) + { + if (result.GetProperty("success").GetBoolean()) + { + return result.GetProperty("data"); + } + + throw new BitwardenException(result.GetProperty("errorMessage").GetString()); + } +} +#endif diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs b/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs index 2f10e0cf9..636a100b1 100644 --- a/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs +++ b/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs @@ -1,6 +1,8 @@ -namespace Bitwarden.Sdk; +using System.Text.Json; -public sealed class BitwardenClient : IDisposable +namespace Bitwarden.Sdk; + +public sealed partial class BitwardenClient : IDisposable { private readonly CommandRunner _commandRunner; private readonly BitwardenSafeHandle _handle; diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs b/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs index ada399401..e18d3976b 100644 --- a/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs +++ b/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs @@ -2,20 +2,71 @@ namespace Bitwarden.Sdk; -internal static class BitwardenLibrary +internal static partial class BitwardenLibrary { - [DllImport("bitwarden_c", CallingConvention = CallingConvention.Cdecl)] - private static extern BitwardenSafeHandle init(string settings); + [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)] + private static partial BitwardenSafeHandle init(string settings); - [DllImport("bitwarden_c", CallingConvention = CallingConvention.Cdecl)] - private static extern void free_mem(IntPtr handle); + [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)] + private static partial void free_mem(IntPtr handle); - [DllImport("bitwarden_c", CallingConvention = CallingConvention.Cdecl)] - private static extern string run_command(string json, BitwardenSafeHandle handle); + [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)] + private static partial string run_command(string json, BitwardenSafeHandle handle); + + internal delegate void OnCompleteCallback(IntPtr json); + + [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr run_command_async(string json, + BitwardenSafeHandle handle, + OnCompleteCallback onCompletedCallback, + [MarshalAs(UnmanagedType.U1)] bool isCancellable); + + [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)] + private static partial void abort_and_free_handle(IntPtr joinHandle); + + [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)] + private static partial void free_handle(IntPtr joinHandle); internal static BitwardenSafeHandle Init(string settings) => init(settings); internal static void FreeMemory(IntPtr handle) => free_mem(handle); internal static string RunCommand(string json, BitwardenSafeHandle handle) => run_command(json, handle); + + internal static Task<string> RunCommandAsync(string json, BitwardenSafeHandle handle, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); + + IntPtr abortPointer = IntPtr.Zero; + + try + { + + abortPointer = run_command_async(json, handle, (resultPointer) => + { + var stringResult = Marshal.PtrToStringUTF8(resultPointer); + tcs.SetResult(stringResult); + + if (abortPointer != IntPtr.Zero) + { + free_handle(abortPointer); + } + }, cancellationToken.CanBeCanceled); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + + cancellationToken.Register((state) => + { + // This register delegate will never be called unless the token is cancelable + // therefore we know that the abortPointer is a valid pointer. + abort_and_free_handle((IntPtr)state); + tcs.SetCanceled(cancellationToken); + }, abortPointer); + + return tcs.Task; + } } diff --git a/languages/csharp/Bitwarden.Sdk/CommandRunner.cs b/languages/csharp/Bitwarden.Sdk/CommandRunner.cs index fbd6b7e31..a62e0877e 100644 --- a/languages/csharp/Bitwarden.Sdk/CommandRunner.cs +++ b/languages/csharp/Bitwarden.Sdk/CommandRunner.cs @@ -17,4 +17,17 @@ internal CommandRunner(BitwardenSafeHandle handle) var result = BitwardenLibrary.RunCommand(req, _handle); return JsonSerializer.Deserialize<T>(result, Converter.Settings); } + + internal async Task<T?> RunCommandAsync<T>(Command command, CancellationToken cancellationToken) + { + var req = JsonSerializer.Serialize(command, Converter.Settings); + var result = await BitwardenLibrary.RunCommandAsync(req, _handle, cancellationToken); + return JsonSerializer.Deserialize<T>(result, Converter.Settings); + } + + internal async Task<T?> RunCommandAsync<T>(string command, CancellationToken cancellationToken) + { + var result = await BitwardenLibrary.RunCommandAsync(command, _handle, cancellationToken); + return JsonSerializer.Deserialize<T>(result, Converter.Settings); + } } diff --git a/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs b/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs index 47a419364..efa651518 100644 --- a/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs +++ b/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs @@ -9,10 +9,10 @@ internal ProjectsClient(CommandRunner commandRunner) _commandRunner = commandRunner; } - public ProjectResponse Get(Guid id) + public async Task<ProjectResponse> GetAsync(Guid id, CancellationToken cancellationToken = default) { var command = new Command { Projects = new ProjectsCommand { Get = new ProjectGetRequest { Id = id } } }; - var result = _commandRunner.RunCommand<ResponseForProjectResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForProjectResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -22,7 +22,7 @@ public ProjectResponse Get(Guid id) throw new BitwardenException(result != null ? result.ErrorMessage : "Project not found"); } - public ProjectResponse Create(Guid organizationId, string name) + public async Task<ProjectResponse> CreateAsync(Guid organizationId, string name, CancellationToken cancellationToken = default) { var command = new Command { @@ -31,7 +31,7 @@ public ProjectResponse Create(Guid organizationId, string name) Create = new ProjectCreateRequest { OrganizationId = organizationId, Name = name } } }; - var result = _commandRunner.RunCommand<ResponseForProjectResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForProjectResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -41,7 +41,7 @@ public ProjectResponse Create(Guid organizationId, string name) throw new BitwardenException(result != null ? result.ErrorMessage : "Project create failed"); } - public ProjectResponse Update(Guid organizationId, Guid id, string name) + public async Task<ProjectResponse> UpdateAsync(Guid organizationId, Guid id, string name, CancellationToken cancellationToken = default) { var command = new Command { @@ -50,7 +50,7 @@ public ProjectResponse Update(Guid organizationId, Guid id, string name) Update = new ProjectPutRequest { Id = id, OrganizationId = organizationId, Name = name } } }; - var result = _commandRunner.RunCommand<ResponseForProjectResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForProjectResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -60,13 +60,13 @@ public ProjectResponse Update(Guid organizationId, Guid id, string name) throw new BitwardenException(result != null ? result.ErrorMessage : "Project update failed"); } - public ProjectsDeleteResponse Delete(Guid[] ids) + public async Task<ProjectsDeleteResponse> DeleteAsync(Guid[] ids, CancellationToken cancellationToken = default) { var command = new Command { Projects = new ProjectsCommand { Delete = new ProjectsDeleteRequest { Ids = ids } } }; - var result = _commandRunner.RunCommand<ResponseForProjectsDeleteResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForProjectsDeleteResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -76,13 +76,13 @@ public ProjectsDeleteResponse Delete(Guid[] ids) throw new BitwardenException(result != null ? result.ErrorMessage : "Project delete failed"); } - public ProjectsResponse List(Guid organizationId) + public async Task<ProjectsResponse> ListAsync(Guid organizationId, CancellationToken cancellationToken = default) { var command = new Command { Projects = new ProjectsCommand { List = new ProjectsListRequest { OrganizationId = organizationId } } }; - var result = _commandRunner.RunCommand<ResponseForProjectsResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForProjectsResponse>(command, cancellationToken); if (result is { Success: true }) { diff --git a/languages/csharp/Bitwarden.Sdk/SecretsClient.cs b/languages/csharp/Bitwarden.Sdk/SecretsClient.cs index 5dd77fc6b..e5e8cbc26 100644 --- a/languages/csharp/Bitwarden.Sdk/SecretsClient.cs +++ b/languages/csharp/Bitwarden.Sdk/SecretsClient.cs @@ -9,10 +9,10 @@ internal SecretsClient(CommandRunner commandRunner) _commandRunner = commandRunner; } - public SecretResponse Get(Guid id) + public async Task<SecretResponse> GetAsync(Guid id, CancellationToken cancellationToken = default) { var command = new Command { Secrets = new SecretsCommand { Get = new SecretGetRequest { Id = id } } }; - var result = _commandRunner.RunCommand<ResponseForSecretResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForSecretResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -22,10 +22,10 @@ public SecretResponse Get(Guid id) throw new BitwardenException(result != null ? result.ErrorMessage : "Secret not found"); } - public SecretsResponse GetByIds(Guid[] ids) + public async Task<SecretsResponse> GetByIdsAsync(Guid[] ids, CancellationToken cancellationToken = default) { var command = new Command { Secrets = new SecretsCommand { GetByIds = new SecretsGetRequest { Ids = ids } } }; - var result = _commandRunner.RunCommand<ResponseForSecretsResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForSecretsResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -35,7 +35,7 @@ public SecretsResponse GetByIds(Guid[] ids) throw new BitwardenException(result != null ? result.ErrorMessage : "Secret not found"); } - public SecretResponse Create(Guid organizationId, string key, string value, string note, Guid[] projectIds) + public async Task<SecretResponse> CreateAsync(Guid organizationId, string key, string value, string note, Guid[] projectIds, CancellationToken cancellationToken = default) { var command = new Command { @@ -52,7 +52,7 @@ public SecretResponse Create(Guid organizationId, string key, string value, stri } }; - var result = _commandRunner.RunCommand<ResponseForSecretResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForSecretResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -62,7 +62,7 @@ public SecretResponse Create(Guid organizationId, string key, string value, stri throw new BitwardenException(result != null ? result.ErrorMessage : "Secret create failed"); } - public SecretResponse Update(Guid organizationId, Guid id, string key, string value, string note, Guid[] projectIds) + public async Task<SecretResponse> UpdateAsync(Guid organizationId, Guid id, string key, string value, string note, Guid[] projectIds, CancellationToken cancellationToken = default) { var command = new Command { @@ -80,7 +80,7 @@ public SecretResponse Update(Guid organizationId, Guid id, string key, string va } }; - var result = _commandRunner.RunCommand<ResponseForSecretResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForSecretResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -90,10 +90,10 @@ public SecretResponse Update(Guid organizationId, Guid id, string key, string va throw new BitwardenException(result != null ? result.ErrorMessage : "Secret update failed"); } - public SecretsDeleteResponse Delete(Guid[] ids) + public async Task<SecretsDeleteResponse> DeleteAsync(Guid[] ids, CancellationToken cancellationToken = default) { var command = new Command { Secrets = new SecretsCommand { Delete = new SecretsDeleteRequest { Ids = ids } } }; - var result = _commandRunner.RunCommand<ResponseForSecretsDeleteResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForSecretsDeleteResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -103,13 +103,13 @@ public SecretsDeleteResponse Delete(Guid[] ids) throw new BitwardenException(result != null ? result.ErrorMessage : "Secrets delete failed"); } - public SecretIdentifiersResponse List(Guid organizationId) + public async Task<SecretIdentifiersResponse> ListAsync(Guid organizationId, CancellationToken cancellationToken = default) { var command = new Command { Secrets = new SecretsCommand { List = new SecretIdentifiersRequest { OrganizationId = organizationId } } }; - var result = _commandRunner.RunCommand<ResponseForSecretIdentifiersResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForSecretIdentifiersResponse>(command, cancellationToken); if (result is { Success: true }) { @@ -119,7 +119,7 @@ public SecretIdentifiersResponse List(Guid organizationId) throw new BitwardenException(result != null ? result.ErrorMessage : "No secrets for given organization"); } - public SecretsSyncResponse Sync(Guid organizationId, DateTimeOffset? lastSyncedDate) + public async Task<SecretsSyncResponse> SyncAsync(Guid organizationId, DateTimeOffset? lastSyncedDate, CancellationToken cancellationToken = default) { var command = new Command { @@ -133,7 +133,7 @@ public SecretsSyncResponse Sync(Guid organizationId, DateTimeOffset? lastSyncedD } }; - var result = _commandRunner.RunCommand<ResponseForSecretsSyncResponse>(command); + var result = await _commandRunner.RunCommandAsync<ResponseForSecretsSyncResponse>(command, cancellationToken); if (result is { Success: true }) { diff --git a/languages/csharp/Bitwarden.sln b/languages/csharp/Bitwarden.sln index 4cf8d147f..d57218979 100644 --- a/languages/csharp/Bitwarden.sln +++ b/languages/csharp/Bitwarden.sln @@ -1,9 +1,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00 +# Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Sdk", "Bitwarden.Sdk\Bitwarden.Sdk.csproj", "{DADE59E5-E573-430A-8EB2-BC21D8E8C1D3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Sdk.Samples", "Bitwarden.Sdk.Samples\Bitwarden.Sdk.Samples.csproj", "{CA9F8EDC-643F-4624-AC00-F741E1F30CA4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Sdk.Tests", "Bitwarden.Sdk.Tests\Bitwarden.Sdk.Tests.csproj", "{6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +21,9 @@ Global {CA9F8EDC-643F-4624-AC00-F741E1F30CA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA9F8EDC-643F-4624-AC00-F741E1F30CA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {CA9F8EDC-643F-4624-AC00-F741E1F30CA4}.Release|Any CPU.Build.0 = Release|Any CPU + {6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal