Newer
Older
using System.Security;
using System.Text.RegularExpressions;
using Autofac;
using FitConnect.Models;
using FitConnect.Models.Api.Metadata;
using FitConnect.Services.Models.v1.Destination;
using Microsoft.Extensions.Logging;
using Attachment = FitConnect.Models.Attachment;
using Metadata = FitConnect.Models.Metadata;
/// <summary>
/// The fluent implementation for the <see cref="Sender" /> to encapsulate the FitConnect API.
/// </summary>
/// <example>
/// Reference for FluentSender
/// <code>
/// client.Sender
/// .Authenticate(clientId!, clientSecret!)
/// .CreateSubmission(new Submission { Attachments = new List() })
/// .UploadAttachments()
/// .WithData(data)
public class Sender : FitConnectClient, ISender {
internal Sender(FitConnectEnvironment environment, string clientId, string clientSecret,
ILogger? logger = null) : base(environment, clientId, clientSecret, logger) {
public Sender(FitConnectEnvironment environment, string clientId, string clientSecret,
IContainer container) : base(environment, clientId, clientSecret,
container) {
}
public async Task<string> GetPublicKeyForDestinationAsync(Guid destinationId) {
var publicKey = await DestinationService.GetPublicKey(destinationId);
var keyIsValid = new CertificateHelper(Logger).ValidateCertificateJsonWebKeyString(
publicKey,
VerifiedCertificatesAreMandatory ? LogLevel.Error : LogLevel.Warning);
public async Task<SubmissionStatus> GetStatusForSubmissionAsync(SentSubmission submission)
=> await base.GetStatusForSubmissionAsync(submission, true);
public async Task<SentSubmission> SendAsync(SendableSubmission submission) {
await SubmissionSenderGuards(submission, submission.DataMimeType);
var introducedSubmission = await IntroduceSubmission(submission);
return (SentSubmission)await FinishSubmissionTransmission(introducedSubmission);
private async Task<Submission> IntroduceSubmission(SendableSubmission sendable) {
var submission = await CreateSubmissionFromSendable(sendable);
submission.AddServiceType(sendable.ServiceName!, sendable.ServiceIdentifier!);
submission!.Attachments = new List<Attachment>();
if (sendable.Attachments != null)
foreach (var attachment in sendable.Attachments)
submission!.Attachments.Add(attachment);
await SubmissionService.CreateSubmissionOnServerAsync((CreateSubmissionDto)submission);
submission.Id = created.SubmissionId;
submission.CaseId = created.CaseId;
Logger?.LogInformation("Submission Id {CreatedSubmissionId}, CaseId {SubmissionCaseId}",
created.SubmissionId, submission.CaseId);
var encryptedAttachments = Encrypt(PublicKey!, submission.Attachments);
await UploadAttachmentsAsync(submission.Id!, encryptedAttachments);
foreach (var (key, value) in encryptedAttachments) {
var item = sendable.Attachments!.Single(attachment => attachment.Id == key);
item.AttachmentAuthentication = value.Split('.').Last();
}
internal override Task<List<SecurityEventToken>> GetEventLogAsync(Guid caseId,
Guid destinationId, bool skipTest = false) {
return base.GetEventLogAsync(caseId, destinationId, true);
}
public override Task<List<SecurityEventToken>> GetEventLogAsync(
SubmissionForPickup submission, bool skipTest = false) {
return base.GetEventLogAsync(submission, true);
}
/// <summary>
/// Sender for pre-encrypted data
/// </summary>
/// <param name="submission"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public async Task<SentSubmission> SendAsync(SendableEncryptedSubmission submission) {
await SubmissionSenderGuards(submission, null);
var sendable = await PrepareEncryptedSubmission(submission);
return (SentSubmission)(await FinishSubmissionTransmission(sendable));
private async Task<Submission> PrepareEncryptedSubmission(
SendableEncryptedSubmission submission) {
var sendable = await CreateSubmissionFromSendable(submission);
sendable.AddServiceType(submission.ServiceName!, submission.ServiceIdentifier!);
var announcedSubmission = (CreateSubmissionDto)sendable;
announcedSubmission.AnnouncedAttachments = submission.Attachments?.Keys.ToList();
await SubmissionService.CreateSubmissionOnServerAsync(announcedSubmission);
sendable.Id = created.SubmissionId;
sendable.CaseId = created.CaseId;
Logger?.LogInformation("Submission Id {CreatedSubmissionId}, CaseId {SubmissionCaseId}",
created.SubmissionId, sendable.CaseId);
Logger?.LogInformation("Uploading pre encrypted attachments");
await UploadAttachmentsAsync(sendable.Id!, submission.Attachments!);
if (submission.Attachments != null) {
sendable.Attachments ??= new List<Attachment>();
foreach (var (key, value) in submission.Attachments) {
sendable.Attachments.Add(
Attachment.FromPreEncryptedAttachment(key, value.Split('.').Last()));
}
}
private async Task<Submission> FinishSubmissionTransmission(Submission submission) {
if (submission == null) {
Logger?.LogCritical("Submission is null on submit");
throw new InvalidOperationException("Submission is not ready");
await HandleMetadata(submission);
if (submission.EncryptedData == null)
if (submission.Data != null)
submission.EncryptedData = Encryption.Encrypt(submission.Data);
await SubmissionService
.SubmitSubmission(submission.Id!, (SubmitSubmissionDto)submission);
Logger?.LogInformation("Submission sent");
return submission;
}
private async Task HandleMetadata(Submission submission) {
if (submission.EncryptedMetadata == null) {
var metadata = CreateMetadata(submission);
submission.GeneratedMetadata = metadata;
Logger?.LogTrace("Metadata: {Metadata}", metadata);
var valid = await JsonHelper.VerifyMetadata(metadata, null, Logger);
if (!valid) {
Logger?.LogError("Sending submission aborted due to validation errors");
throw new InvalidOperationException("Submission is not ready");
}
Logger?.LogInformation("Metadata validation check, done");
Logger?.LogInformation("Sending submission");
var encryptedMeta = Encryption.Encrypt(metadata);
Logger?.LogTrace("Encrypted metadata: {EncryptedMeta}", encryptedMeta);
submission.EncryptedMetadata = encryptedMeta;
/// Creates a new <see cref="SendableSubmission" /> on the FitConnect server.
/// <exception cref="InvalidOperationException">If sender is not authenticated</exception>
/// <exception cref="ArgumentException">If submission is not ready to be sent</exception>
private async Task<Submission> CreateSubmissionFromSendable(
SendableSubmission sendableSubmission) {
PublicKey = await GetPublicKeyForDestinationAsync(destinationId);
Encryption = new FitEncryption(Logger) { PublicEncryptionKey = PublicKey };
return sendableSubmission.ToSubmission();
}
/// <summary>
/// Creates a new <see cref="SendableSubmission" /> on the FitConnect server.
/// </summary>
/// <param name="sendableSubmission"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">If sender is not authenticated</exception>
/// <exception cref="ArgumentException">If submission is not ready to be sent</exception>
private async Task<Submission>
CreateSubmissionFromSendable(ISendableSubmission sendableSubmission) {
var destinationId = sendableSubmission.DestinationId;
PublicKey = await GetPublicKeyForDestinationAsync(destinationId);
Encryption = new FitEncryption(Logger) { PublicEncryptionKey = PublicKey };
return new Submission {
DestinationId = destinationId,
EncryptedData = sendableSubmission.Data,
/// Create Metadata incl. Hash
/// <param name="submission"></param>
internal static string CreateMetadata(Submission submission,
DataHash? dataHash = null) {
Hash = dataHash ?? FitEncryption.CalculateHash(submission.Data!),
SubmissionSchema = new SubmissionSchema {
SchemaUri = submission.SchemaUri,
}
};
var contentStructure = new ContentStructure {
Data = data,
Attachments = submission.Attachments?.Select(attachment =>
new Models.Api.Metadata.AttachmentMetadata {
Description = attachment.Description,
AttachmentId = attachment.Id,
MimeType = attachment.MimeType,
Filename = attachment.Filename,
Purpose = attachment.Purpose,
Hash = attachment.Hash
}).ToList() ?? new List<Models.Api.Metadata.AttachmentMetadata>()
var metadata = new Metadata {
SchemaUri = new Uri(Metadata.SchemaUrl),
// PublicServiceType = new Verwaltungsleistung {
// Identifier = submission.ServiceType.Identifier,
// Name = submission.ServiceType.Name
// },
ReplyChannel = submission.ReplyChannel,
PaymentInformation = submission.PaymentInformation,
AuthenticationInformation = submission.AuthenticationInformation?.ToList()
return JsonConvert.SerializeObject(metadata, Formatting.None, new JsonSerializerSettings() {
Converters = new List<JsonConverter>() {
new StatusConverter(),
new PaymentMethodConverter(),
new AuthenticationInformationTypeConverter()
},
NullValueHandling = NullValueHandling.Ignore,
});
/// </summary>
/// <param name="filter"></param>
/// <param name="offset">How many entries of the result should be skipped</param>
/// <param name="limit">How many result sets should be loaded</param>
public async Task<IEnumerable<Area>> GetAreas(string filter, int offset = 0,
var dto = await RouteService.FindAreas(new List<string>() { filter }, offset, limit);
return dto?.Areas ?? new List<Area>();
}
/// Uploading the encrypted data to the server
/// <param name="submissionId">Submissions ID</param>
/// <param name="encryptedAttachments">Encrypted attachments with id and content</param>
private async Task<bool> UploadAttachmentsAsync(Guid submissionId,
Dictionary<Guid, string> encryptedAttachments) {
try {
foreach (var (id, content) in encryptedAttachments) {
Logger?.LogInformation("Uploading attachment {Id}", id);
await SubmissionService.UploadAttachment(submissionId, id, content);
}
return true;
}
catch (Exception e) {
Logger?.LogError("Error Uploading attachment {Message}", e.Message);
/// <summary>
/// Checking the submission if it is ready to be sent
/// Locally identified errors are thrown as exceptions
/// </summary>
private async Task
SubmissionSenderGuards(ISendableSubmission submission, string? dataMimeType) {
if (submission.ServiceName == null)
throw new ArgumentNullException(nameof(SendableSubmission.ServiceName));
if (submission.ServiceIdentifier == null)
throw new ArgumentNullException(nameof(SendableSubmission.ServiceIdentifier));
if (submission.Data == null)
throw new ArgumentNullException(nameof(SendableSubmission.Data));
if (new Regex(ServiceIdentifierPattern).IsMatch(submission.ServiceIdentifier) == false)
throw new ArgumentException("The service identifier is not valid. " +
"It must match the pattern " + ServiceIdentifierPattern);
await CheckServiceWithMimeType(submission, dataMimeType);
}
/// <summary>
/// Checks the destination for service for ServiceIdentifier, SchemaUri and MimeType
/// </summary>
private async Task CheckServiceWithMimeType(ISendableSubmission submission,
string? dataMimeType) {
var destination = await DestinationService.GetDestinationAsync(submission.DestinationId);
if (destination == null)
throw new Exception("Destination not found");
destination?.Services?.Any(s => s.Identifier == submission.ServiceIdentifier) ??
false;
if (!serviceIdentifierMatch)
throw new ArgumentException("The service identifier is not valid for the destination");
if (submission is SendableSubmission && submission.SchemaUri == null)
throw new ArgumentException("Argument SchemaUri is required");
// Searching a service with the given service identifier
var matchingServiceIdentifier = destination?.Services?.Where(s =>
s.Identifier == submission.ServiceIdentifier);
// Searching a service with the given mime type
var matchingService = matchingServiceIdentifier?.FirstOrDefault(s =>
s.SubmissionSchemas?.Any(schema =>
dataMimeType == null || schema.mimeType == dataMimeType) ?? false);
if (matchingService == null)
throw new ArgumentException(
"The service identifier with the given mime type is not valid for this destination");
if (matchingService.SubmissionSchemas == null)
throw new ArgumentException("Schemas property for service is empty");
if (submission is SendableEncryptedSubmission) return;
var matchingSchema = matchingService.SubmissionSchemas.FirstOrDefault(schema =>
schema.SchemaUri.ToString() == submission.SchemaUri?.ToString()
);
if (matchingSchema == null)
throw new ArgumentException("The schema uri is not valid for the destination");
if (dataMimeType != null) {
if (matchingSchema.mimeType == dataMimeType) return;
throw new ArgumentException("The data mime type is not valid for the destination");
}
public static void AddServiceType(this Submission submission, string serviceName,
string serviceIdentifier) {
if (string.IsNullOrWhiteSpace(serviceIdentifier) || !Regex.IsMatch(serviceIdentifier,
FitConnectClient.ServiceIdentifierPattern))
throw new ArgumentException("Invalid service identifier");
submission.ServiceType = new ServiceType {
Name = serviceName,
};
}
public static void AddData(this Submission submission, string data) {
try {
JsonConvert.DeserializeObject(data);
}
catch (Exception e) {
throw new ArgumentException("The data must be valid JSON string", e);
}
submission.Data = data;
}