Imports System.Net
Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Security.Authentication
Imports System.Text.Json
''' <summary>
''' Provides utility methods for interacting with the <c>kvdb.io</c> key-value storage service.
''' </summary>
Public NotInheritable Class UtilKvdb
#Region " Constructors "
''' <summary>
''' Prevents a default instance of the <see cref="UtilKvdb"/> class from being created.
''' </summary>
Private Sub New()
End Sub
#End Region
#Region " Public Methods "
''' <summary>
''' Asynchronously creates and retrieves a newly server-generated bucket storage
''' with random ID on the remote <c>kvdb.io</c> service.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim mailAddress As String = "your_address@gmail.com"
'''
''' Dim policy As New KvdbBucketPolicy With {
''' .SecretKey = Nothing,
''' .ReadKey = Nothing,
''' .WriteKey = Nothing,
''' .SigningKey = Nothing,
''' .KeyExpiration = TimeSpan.FromHours(1)
''' }
'''
''' Dim newlyBucketId As String = Await GenerateBucketAsync(mailAddress, policy)
'''
''' Console.WriteLine($"Server-generated bucket with random ID : {newlyBucketId}")
''' Console.WriteLine($"Access URL to keys collection in bucket: https://kvdb.io/{newlyBucketId}")
''' </code>
''' </example>
'''
''' <param name="associatedEmailAddress">
''' The associated email address required by <c>kvdb.io</c> to generate a new bucket.
''' <para></para>
''' If the email address is not registered on the server, you will receive
''' a notification email from <c>kvdb.io</c> with a confirmation link
''' to validate the email address and activate your (free)account.
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' and key expiration time to be applied to the newly server-generated bucket.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the server-generated bucket will have no configured access keys.
''' </param>
'''
''' <returns>
''' A <see cref="Task(Of String)"/> containing the unique identifier for the newly server-generated bucket.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function GenerateBucketAsync(associatedEmailAddress As String,
Optional policy As KvdbBucketPolicy = Nothing) As Task(Of String)
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(associatedEmailAddress, paramName:=NameOf(associatedEmailAddress))
#Else
If String.IsNullOrWhiteSpace(associatedEmailAddress) Then : Throw New ArgumentNullException(paramName:=NameOf(associatedEmailAddress)) : End If
#End If
Dim query As String = $"email={Uri.EscapeDataString(associatedEmailAddress)}"
If policy IsNot Nothing Then
query &= $"&{policy.ToString(escaped:=True)}"
End If
Using content As New StringContent(query, Encoding.UTF8, "application/x-www-form-urlencoded"),
client As HttpClient = UtilKvdb.BuildHttpClient(policy:=Nothing),
response As HttpResponseMessage = Await client.PostAsync(requestUri:=String.Empty, content).
ConfigureAwait(continueOnCapturedContext:=False)
If response.IsSuccessStatusCode Then
Dim rawId As String = Await response.Content.ReadAsStringAsync().
ConfigureAwait(continueOnCapturedContext:=False)
Return rawId?.Trim()
Else
Throw New HttpRequestException(
$"Failed to create bucket on the server. Status Code: {CInt(response.StatusCode)} ({response.StatusCode})")
End If
End Using
End Function
''' <summary>
''' Asynchronously updates the policy for an existing bucket storage
''' on the remote <c>kvdb.io</c> service using the provided <see cref="KvdbBucketPolicy"/>.
''' </summary>
'''
''' <remarks>
''' <b>Important Limitation:</b>
''' Once a <c>secret key</c>, <c>read key</c>, <c>write key</c> or <c>signing key</c> has been established for a bucket,
''' <para></para>
''' the <c>kvdb.io</c> API does not seem to support clearing any of these keys to null (public state).
''' </remarks>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim mailAddress As String = "your_address@gmail.com"
'''
''' Dim defaultPolicy As New KvdbBucketPolicy()
'''
''' Dim newlyBucketId As String = Await GenerateBucketAsync(mailAddress, defaultPolicy)
'''
''' Console.WriteLine($"Server-generated bucket with random ID: {newlyBucketId}")
'''
''' Dim newPolicy As New KvdbBucketPolicy With {
''' .SecretKey = "My Secret Key",
''' .ReadKey = "My Read Key",
''' .WriteKey = "My Write Key",
''' .SigningKey = Nothing,
''' .KeyExpiration = TimeSpan.FromHours(1)
''' }
'''
''' Await UpdateBucketPolicyAsync(newlyBucketId, newPolicy)
'''
''' Console.WriteLine($"Policty updated for bucket: https://kvdb.io/{newlyBucketId}")
''' </code>
''' </example>
'''
''' <param name="policy">
''' The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' and key expiration time to be applied to the specified bucket.
''' </param>
'''
''' <returns>
''' A <see cref="Task(Of String)"/> representing the asynchronous operation.
''' <para></para>
''' If the task completes successfully, it indicates that
''' the bucket policy was successfully updated on the server.
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function UpdateBucketPolicyAsync(bucketId As String, policy As KvdbBucketPolicy) As Task
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(bucketId, paramName:=NameOf(bucketId))
ArgumentNullException.ThrowIfNull(policy, paramName:=NameOf(policy))
#Else
If String.IsNullOrWhiteSpace(bucketId) Then : Throw New ArgumentNullException(paramName:=NameOf(bucketId)) : End If
If policy Is Nothing Then : Throw New ArgumentNullException(paramName:=NameOf(policy)) : End If
#End If
Dim policyQuery As String = policy.ToString(escaped:=True)
Dim relativeUrl As String = $"{bucketId}?{policyQuery}"
Using content As New StringContent(policyQuery, Encoding.UTF8, "application/x-www-form-urlencoded"),
client As HttpClient = UtilKvdb.BuildHttpClient(policy),
request As New HttpRequestMessage(New HttpMethod("PATCH"), relativeUrl) With {.Content = content},
response As HttpResponseMessage = Await client.SendAsync(request).
ConfigureAwait(continueOnCapturedContext:=False)
If response.IsSuccessStatusCode Then
Return
Else
Throw New HttpRequestException(
$"Failed to update bucket policy on the server. Status Code: {CInt(response.StatusCode)} ({response.StatusCode})")
End If
End Using
End Function
''' <summary>
''' Asynchronously deletes an existing bucket storage and its associated collection of keys
''' from the remote <c>kvdb.io</c> service.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim bucketId As String = "JjSDG9KgFrBMbKgjRwmnAd"
'''
''' Await DeleteBucketAsync(bucketId)
''' </code>
''' </example>
'''
''' <param name="bucketId">
''' The unique identifier pointing to the bucket storage to delete. The bucket storage must exist on the server.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' to authorize the deletion request for the specified bucket.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the deletion request will be sent without authentication.
''' </param>
'''
''' <returns>
''' A <see cref="Task"/> representing the asynchronous operation.
''' <para></para>
''' If the task completes successfully, it indicates that the server accepted the deletion request
''' (<see cref="HttpStatusCode.Accepted"/>) to remove the bucket storage.
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function DeleteBucketAsync(bucketId As String,
Optional policy As KvdbBucketPolicy = Nothing) As Task
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(bucketId, paramName:=NameOf(bucketId))
#Else
If String.IsNullOrWhiteSpace(bucketId) Then : Throw New ArgumentNullException(paramName:=NameOf(bucketId)) : End If
#End If
Using client As HttpClient = UtilKvdb.BuildHttpClient(policy),
response As HttpResponseMessage = Await client.DeleteAsync(bucketId).
ConfigureAwait(continueOnCapturedContext:=False)
If response.IsSuccessStatusCode Then
Return
Else
Throw New HttpRequestException(
$"Failed to delete bucket from the server. Status Code: {CInt(response.StatusCode)} ({response.StatusCode})")
End If
End Using
End Function
''' <summary>
''' Asynchronously creates or overwrites a key-value slot inside the specified bucket storage
''' from the remote <c>kvdb.io</c> service using the provided <see cref="StringContent"/>.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim bucketId As String = "JjSDG9KgFrBMbKgjRwmnAd"
''' Dim key As String = "Name of the key to create"
''' Dim value As String = "Value of the key"
'''
''' Dim payloadData As New Dictionary(Of String, Object)(StringComparer.Ordinal) From {
''' {"Key", key},
''' {"Value", value}
''' }
'''
''' Dim jsonString As String = JsonSerializer.Serialize(payloadData)
'''
''' Using content As New StringContent(jsonString, Encoding.UTF8, "application/json")
'''
''' Await CreateKeyAsync(bucketId, key, content)
''' End Using
''' </code>
''' </example>
'''
''' <param name="bucketId">
''' The unique identifier pointing to the bucket storage. The bucket storage must exist on the server.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </param>
'''
''' <param name="key">
''' The name of the key to create or overwrite.
''' </param>
'''
''' <param name="content">
''' The <see cref="StringContent"/> object containing the data to be stored in the key.
''' <para></para>
''' For example, to store JSON data, the <see cref="StringContent"/> can be initialized as follows:
''' <code>
''' Dim jsonString As String = JsonSerializer.Serialize(myObject)
''' Dim content As New StringContent(jsonString, Encoding.UTF8, "application/json")
''' </code>
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' to authorize the creation or overwriting request for the specified key,
''' and the expiration time to be applied to the key.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the creation request will be sent without authentication.
''' </param>
'''
''' <returns>
''' A <see cref="Task"/> representing the asynchronous operation.
''' <para></para>
''' If the task completes successfully, it indicates that
''' the key was successfully created or overwritten on the server.
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function CreateKeyAsync(bucketId As String, key As String, content As StringContent,
Optional policy As KvdbBucketPolicy = Nothing) As Task
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(bucketId, paramName:=NameOf(bucketId))
ArgumentNullException.ThrowIfNullOrWhiteSpace(key, paramName:=NameOf(key))
ArgumentNullException.ThrowIfNull(content, paramName:=NameOf(content))
#Else
If String.IsNullOrWhiteSpace(bucketId) Then : Throw New ArgumentNullException(paramName:=NameOf(bucketId)) : End If
If String.IsNullOrWhiteSpace(key) Then : Throw New ArgumentNullException(paramName:=NameOf(key)) : End If
If content Is Nothing Then : Throw New ArgumentNullException(paramName:=NameOf(content)) : End If
#End If
UtilKvdb.ValidateTokenSize(key, NameOf(key))
Dim relativeUrl As String = UtilKvdb.BuildRelativeKeyUrl(bucketId, key, isWriteOperation:=True, policy)
Using client As HttpClient = UtilKvdb.BuildHttpClient(policy),
response As HttpResponseMessage = Await client.PostAsync(relativeUrl, content).
ConfigureAwait(continueOnCapturedContext:=False)
If response.IsSuccessStatusCode Then
Return
Else
Throw New HttpRequestException(
$"Failed to create or overwrite key on the server. Status Code: {CInt(response.StatusCode)} ({response.StatusCode})")
End If
End Using
End Function
''' <summary>
''' Asynchronously creates or overwrites a key-value slot inside the specified bucket storage
''' from the remote <c>kvdb.io</c> service using the provided <see cref="String"/> value.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim bucketId As String = "JjSDG9KgFrBMbKgjRwmnAd"
''' Dim key As String = "Name of the key to create"
''' Dim value As String = "Value of the key"
'''
''' Await CreateKeyAsync(bucketId, key, value)
''' </code>
''' </example>
'''
''' <param name="bucketId">
''' The unique identifier pointing to the bucket storage. The bucket storage must exist on the server.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </param>
'''
''' <param name="key">
''' The name of the key to create or overwrite.
''' </param>
'''
''' <param name="value">
''' The value to be stored in the key.
''' <para></para>
''' This value will be sent to the server as UTF-8 encoded plain text with media type <c>"text/plain"</c>.
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' to authorize the creation or overwriting request for the specified key,
''' and the expiration time to be applied to the key.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the creation request will be sent without authentication.
''' </param>
'''
''' <returns>
''' A <see cref="Task"/> representing the asynchronous operation.
''' <para></para>
''' If the task completes successfully, it indicates that
''' the key was successfully created or overwritten on the server.
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function CreateKeyAsync(bucketId As String, key As String, value As String,
Optional policy As KvdbBucketPolicy = Nothing) As Task
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrEmpty(value, paramName:=NameOf(value))
#Else
If String.IsNullOrEmpty(value) Then : Throw New ArgumentNullException(paramName:=NameOf(value)) : End If
#End If
Using content As New StringContent(value, Encoding.UTF8, "text/plain")
Await UtilKvdb.CreateKeyAsync(bucketId, key, content, policy).
ConfigureAwait(continueOnCapturedContext:=False)
End Using
End Function
''' <summary>
''' Asynchronously retrieves the value stored in a existing key-value slot from the specified bucket storage
''' on the remote <c>kvdb.io</c> service as a raw plain text.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim bucketId As String = "JjSDG9KgFrBMbKgjRwmnAd"
''' Dim key As String = "Name of the key to create"
'''
''' Dim value As String = Await GetKeyValueAsync(bucketId, key)
'''
''' Console.WriteLine($"Raw value retrieved from key: {value}")
''' </code>
''' </example>
'''
''' <param name="bucketId">
''' The unique identifier pointing to the bucket storage. The bucket storage must exist on the server.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </param>
'''
''' <param name="key">
''' The name of the key from which to retrieve its value.
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' to authorize read-access for the specified key.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the read-access request will be sent without authentication.
''' </param>
'''
''' <returns>
''' A <see cref="Task(Of String)"/> containing the value stored in the specified key as a raw plain text string.
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function GetKeyValueAsync(bucketId As String, key As String,
Optional policy As KvdbBucketPolicy = Nothing) As Task(Of String)
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(bucketId, paramName:=NameOf(bucketId))
ArgumentNullException.ThrowIfNullOrWhiteSpace(key, paramName:=NameOf(key))
#Else
If String.IsNullOrWhiteSpace(bucketId) Then : Throw New ArgumentNullException(paramName:=NameOf(bucketId)) : End If
If String.IsNullOrWhiteSpace(key) Then : Throw New ArgumentNullException(paramName:=NameOf(key)) : End If
#End If
Dim relativeUrl As String = UtilKvdb.BuildRelativeKeyUrl(bucketId, key, isWriteOperation:=False, policy)
Using client As HttpClient = UtilKvdb.BuildHttpClient(policy),
response As HttpResponseMessage = Await client.GetAsync(relativeUrl).
ConfigureAwait(continueOnCapturedContext:=False)
If response.StatusCode = HttpStatusCode.OK Then
Dim extractedContent As String = Await response.Content.ReadAsStringAsync().
ConfigureAwait(continueOnCapturedContext:=False)
Return extractedContent
Else
Throw New HttpRequestException(
$"Failed to read key from the server. Status Code: {CInt(response.StatusCode)} ({response.StatusCode})")
End If
End Using
End Function
''' <summary>
''' Asynchronously retrieves the value stored in a existing key-value slot from the specified bucket storage
''' on the remote <c>kvdb.io</c> service as a <see cref="JsonDocument"/>.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim bucketId As String = "JjSDG9KgFrBMbKgjRwmnAd"
''' Dim key As String = "Name of the key to create"
'''
''' Dim value As JsonDocument = Await GetKeyValueJsonAsync(bucketId, key)
'''
''' Console.WriteLine($"JSON value retrieved from key: {value.RootElement}")
''' </code>
''' </example>
'''
''' <param name="bucketId">
''' The unique identifier pointing to the bucket storage. The bucket storage must exist on the server.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </param>
'''
''' <param name="key">
''' The name of the key from which to retrieve its value.
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' to authorize read-access for the specified key.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the read-access request will be sent without authentication.
''' </param>
'''
''' <returns>
''' A <see cref="Task(Of JsonDocument)"/> containing the value stored in the specified key as a <see cref="JsonDocument"/>.
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
'''
''' <exception cref="JsonException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function GetKeyValueJsonAsync(bucketId As String, key As String,
Optional policy As KvdbBucketPolicy = Nothing) As Task(Of JsonDocument)
Dim jsonString As String = Await UtilKvdb.GetKeyValueAsync(bucketId, key, policy).
ConfigureAwait(continueOnCapturedContext:=False)
' Throws a JsonException if the remote payload content is invalid JSON.
Dim parsedDocument As JsonDocument = JsonDocument.Parse(jsonString)
Return parsedDocument
End Function
''' <summary>
''' Asynchronously deletes a existing key-value slot in the specified bucket storage
''' from the remote <c>kvdb.io</c> service.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim bucketId As String = "JjSDG9KgFrBMbKgjRwmnAd"
''' Dim key As String = "Name of the key to delete"
'''
''' Await DeleteKeyAsync(bucketId, key)
''' </code>
''' </example>
'''
''' <param name="bucketId">
''' The unique identifier pointing to the bucket storage. The bucket storage must exist on the server.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </param>
'''
''' <param name="key">
''' The name of the key to delete.
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the access keys
''' to authorize the deletion for the specified key.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the deletion request will be sent without authentication.
''' </param>
'''
''' <returns>
''' A <see cref="Task"/> representing the asynchronous operation.
''' <para></para>
''' If the task completes successfully, it indicates that the server accepted the deletion request
''' (<see cref="HttpStatusCode.Accepted"/>) to remove the key, regardless of whether the key exists or not.
''' </returns>
'''
''' <exception cref="HttpRequestException">
''' </exception>
<DebuggerStepThrough>
Public Shared Async Function DeleteKeyAsync(bucketId As String, key As String,
Optional policy As KvdbBucketPolicy = Nothing) As Task
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(bucketId, paramName:=NameOf(bucketId))
ArgumentNullException.ThrowIfNullOrWhiteSpace(key, paramName:=NameOf(key))
#Else
If String.IsNullOrWhiteSpace(bucketId) Then : Throw New ArgumentNullException(paramName:=NameOf(bucketId)) : End If
If String.IsNullOrWhiteSpace(key) Then : Throw New ArgumentNullException(paramName:=NameOf(key)) : End If
#End If
Dim relativeUrl As String = UtilKvdb.BuildRelativeKeyUrl(bucketId, key, isWriteOperation:=True, policy)
Using client As HttpClient = UtilKvdb.BuildHttpClient(policy),
response As HttpResponseMessage = Await client.DeleteAsync(relativeUrl).
ConfigureAwait(continueOnCapturedContext:=False)
If response.IsSuccessStatusCode Then
Return
Else
Throw New HttpRequestException(
$"Failed to delete key from the server. Status Code: {CInt(response.StatusCode)} ({response.StatusCode})")
End If
End Using
End Function
#End Region
#Region " Private Methods "
''' <summary>
''' Gets a new instance of <see cref="HttpClient"/> configured for use with the <c>kvdb.io</c> service,
''' applying the appropriate authentication header if a <see cref="KvdbBucketPolicy.SecretKey"/> is provided.
''' </summary>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing the <c>SecretKey</c> to be applied for
''' authentication in the HTTP requests sent by the returned <see cref="HttpClient"/> instance.
''' <para></para>
''' If no policy is provided or does not contain a <c>SecretKey</c>,
''' the returned <see cref="HttpClient"/> will not have any authentication header applied.
''' </param>
'''
''' <returns>
''' Returns the resulting <see cref="HttpClient"/> instance configured for use with the <c>kvdb.io</c> service,
''' and with the appropriate authentication header if applicable.
''' </returns>
<DebuggerStepThrough>
Private Shared Function BuildHttpClient(Optional policy As KvdbBucketPolicy = Nothing) As HttpClient
' clientHandler is automatically disposed by HttpClient object on disposal.
Dim clientHandler As New HttpClientHandler() With {
.AllowAutoRedirect = False,
.UseCookies = False,
.UseProxy = False,
.DefaultProxyCredentials = Nothing,
.UseDefaultCredentials = False,
.Credentials = Nothing,
.PreAuthenticate = False,
.CheckCertificateRevocationList = True,
.ClientCertificateOptions = ClientCertificateOption.Automatic,
.SslProtocols = SslProtocols.Tls12 Or SslProtocols.Tls13,
.AutomaticDecompression = DecompressionMethods.GZip Or DecompressionMethods.Deflate
}
' The BaseAddress for the <c>kvdb.io</c> service API,
' so that we can use relative URLs for the requests.
Const kvdbBaseAddress As String = "https://kvdb.io"
Dim client As New HttpClient(clientHandler, disposeHandler:=True) With {
.BaseAddress = New Uri(kvdbBaseAddress),
.Timeout = TimeSpan.FromSeconds(60)
}
If (policy IsNot Nothing) AndAlso Not String.IsNullOrEmpty(policy.SecretKey) Then
client.DefaultRequestHeaders.Authorization = New AuthenticationHeaderValue("Bearer", policy.SecretKey)
End If
Return client
End Function
''' <summary>
''' Builds the relative URL for accessing a specific key in a bucket storage on the remote <c>kvdb.io</c> service,
''' including the appropriate query string parameters for authentication and key expiration based on
''' the provided <see cref="KvdbBucketPolicy"/> and operation context (create / read / write / delete).
''' </summary>
'''
''' <param name="bucketId">
''' The unique identifier pointing to the bucket storage. The bucket storage must exist on the server.
''' <para></para>
''' <i>Typically, the bucket identifier is an alphanumeric string with 22 character length,
''' such as: <c>"JjSDG9KgFrBMbKgjRwmnAd"</c></i>
''' </param>
'''
''' <param name="key">
''' The name of the key for which to build the access URL.
''' </param>
'''
''' <param name="isWriteOperation">
''' A value indicating whether the URL is being built for a write or a read operation.
''' <para></para>
''' This parameter is used to determine which access key to attach in the query string,
''' and whether to include the TTL parameter for key expiration.
''' </param>
'''
''' <param name="policy">
''' Optional. The <see cref="KvdbBucketPolicy"/> object containing
''' the access keys to authorize the request for the specified key,
''' and the expiration time to be applied to the key if applicable.
''' <para></para>
''' If no policy is provided or does not contain access keys,
''' the URL will be built without authentication parameters.
''' </param>
'''
''' <returns>
''' Returns the resulting complete URL for accessing the specified key in the bucket storage.
''' </returns>
<DebuggerStepThrough>
Private Shared Function BuildRelativeKeyUrl(bucketId As String, key As String,
isWriteOperation As Boolean,
Optional policy As KvdbBucketPolicy = Nothing) As String
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(bucketId, paramName:=NameOf(bucketId))
ArgumentNullException.ThrowIfNullOrWhiteSpace(key, paramName:=NameOf(key))
#Else
If String.IsNullOrWhiteSpace(bucketId) Then : Throw New ArgumentNullException(paramName:=NameOf(bucketId)) : End If
If String.IsNullOrWhiteSpace(key) Then : Throw New ArgumentNullException(paramName:=NameOf(key)) : End If
#End If
Dim escapedKey As String = Uri.EscapeDataString(key)
Dim relativePath As String = $"{bucketId}/{escapedKey}"
Dim queryParameters As New List(Of String)(capacity:=2)
If policy IsNot Nothing Then
' Determine whether to attach the WriteKey or ReadKey based on the execution context.
If isWriteOperation Then
If Not String.IsNullOrEmpty(policy.WriteKey) Then
Dim escapedWriteKey As String = Uri.EscapeDataString(policy.WriteKey)
queryParameters.Add($"write_key={escapedWriteKey}")
End If
' The TTL parameter is only applicable during write operations.
If policy.KeyExpiration <> TimeSpan.Zero Then
Dim ttlValue As String = CInt(policy.KeyExpiration.TotalSeconds).ToString()
queryParameters.Add($"ttl={ttlValue}")
End If
Else
If Not String.IsNullOrEmpty(policy.ReadKey) Then
Dim escapedReadKey As String = Uri.EscapeDataString(policy.ReadKey)
queryParameters.Add($"read_key={escapedReadKey}")
End If
End If
' If there are parameters, we append them manually to the relative path using a '?' character.
If queryParameters.Any() Then
Dim queryString As String = String.Join("&", queryParameters)
relativePath = $"{relativePath}?{queryString}"
End If
End If
Return relativePath
End Function
''' <summary>
''' Validates that the provided token value does not exceed the maximum allowed byte size limit when encoded in <c>UTF-8</c>,
''' which is 128 bytes (at least for key names) according to the <c>kvdb.io</c> service specifications.
''' <para></para>
''' Throws an <see cref="ArgumentException"/> if the byte size of the token exceeds the maximum allowed limit.
''' </summary>
'''
''' <param name="token">
''' The token value to validate. This value can be null.
''' </param>
'''
''' <param name="paramName">
''' The name of the parameter being validated for error tracing.
''' </param>
'''
''' <exception cref="ArgumentException">
''' </exception>
<DebuggerStepThrough>
Friend Shared Sub ValidateTokenSize(token As String, paramName As String)
Const MaxKvdbTokenByteSize As Integer = 128 ' In bytes, as per kvdb.io specifications.
#If NETCOREAPP Then
ArgumentNullException.ThrowIfNullOrWhiteSpace(paramName, paramName:=NameOf(paramName))
#Else
If String.IsNullOrWhiteSpace(paramName) Then : Throw New ArgumentNullException(paramName:=NameOf(paramName)) : End If
#End If
If token IsNot Nothing Then
Dim byteCount As Integer = Encoding.UTF8.GetByteCount(token)
If byteCount > MaxKvdbTokenByteSize Then
Throw New ArgumentException(
$"The token size ({byteCount} bytes) for parameter '{paramName}' exceeds the" &
" maximum allowed limit of {MaxKvdbTokenByteSize} bytes when encoded in UTF-8.",
paramName:=paramName)
End If
End If
End Sub
#End Region
End Class