/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using System.IO.Pipes;
using System.Net;
using System.Runtime.Versioning;
using System.Security.Principal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
#if SUPPORTS_WINDOWS_RUNTIME
using Windows.Security.Authentication.Web;
#endif
namespace OpenIddict.Client.SystemIntegration;
///
/// Contains the logic necessary to handle URI protocol activations (that
/// are typically resolved when launching the application or redirected
/// by other instances using inter-process communication).
///
public sealed class OpenIddictClientSystemIntegrationService
{
private readonly IOptionsMonitor _options;
private readonly IServiceProvider _provider;
///
/// Creates a new instance of the class.
///
/// The OpenIddict client system integration options.
/// The service provider.
/// is .
public OpenIddictClientSystemIntegrationService(
IOptionsMonitor options,
IServiceProvider provider)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
///
/// Handles the specified protocol activation.
///
/// The protocol activation details.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
/// is .
[EditorBrowsable(EditorBrowsableState.Advanced)]
public Task HandleProtocolActivationAsync(
OpenIddictClientSystemIntegrationActivation activation, CancellationToken cancellationToken = default)
=> HandleRequestAsync(activation ?? throw new ArgumentNullException(nameof(activation)), cancellationToken);
///
/// Handles the specified HTTP request.
///
/// The HTTP request received by the embedded web server.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
/// is .
internal Task HandleHttpRequestAsync(HttpListenerContext request, CancellationToken cancellationToken = default)
=> HandleRequestAsync(request ?? throw new ArgumentNullException(nameof(request)), cancellationToken);
#if SUPPORTS_WINDOWS_RUNTIME
///
/// Handles the specified web authentication result.
///
/// The web authentication result.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
/// is .
[SupportedOSPlatform("windows10.0.17763")]
internal Task HandleWebAuthenticationResultAsync(WebAuthenticationResult result, CancellationToken cancellationToken = default)
=> HandleRequestAsync(result ?? throw new ArgumentNullException(nameof(result)), cancellationToken);
#endif
///
/// Handles the request using the specified property.
///
/// The property to add to the transaction.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
/// is .
private async Task HandleRequestAsync(TProperty property, CancellationToken cancellationToken) where TProperty : class
{
if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
cancellationToken.ThrowIfCancellationRequested();
var scope = _provider.CreateScope();
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService();
var factory = scope.ServiceProvider.GetRequiredService();
// Create a client transaction and store the specified instance so
// it can be retrieved by the event handlers that need to access it.
var transaction = await factory.CreateTransactionAsync();
transaction.SetProperty(typeof(TProperty).FullName!, property);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = cancellationToken
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction)
{
CancellationToken = cancellationToken,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
});
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
///
/// Redirects a protocol activation to the specified instance.
///
/// The protocol activation to redirect.
/// The identifier of the target instance.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
/// is .
internal async Task RedirectProtocolActivationAsync(
OpenIddictClientSystemIntegrationActivation activation,
string identifier, CancellationToken cancellationToken = default)
{
if (activation is null)
{
throw new ArgumentNullException(nameof(activation));
}
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier));
}
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
using var stream = new NamedPipeClientStream(
serverName : ".",
pipeName : $@"{_options.CurrentValue.PipeName}\{identifier}",
direction : PipeDirection.Out,
options : PipeOptions.Asynchronous,
impersonationLevel: TokenImpersonationLevel.None,
inheritability : HandleInheritability.None);
// Wait for the target to accept the pipe connection.
await stream.ConnectAsync(cancellationToken);
// Write the type of message stored in the shared memory and the
// version used to identify the binary serialization format.
writer.Write(0x01);
writer.Write(0x01);
// Write the protocol activation URI.
writer.Write(activation.ActivationUri.AbsoluteUri);
// Transfer the payload to the target.
buffer.Seek(0L, SeekOrigin.Begin);
await buffer.CopyToAsync(stream, bufferSize: 81_920, cancellationToken);
}
}