Skip to content
Snippets Groups Projects
Sender.cs 11.4 KiB
Newer Older
Klaus Fischer's avatar
Klaus Fischer committed
using System.Net.Mime;
using System.Text.RegularExpressions;
using Autofac;
using FitConnect.Encryption;
Klaus Fischer's avatar
Klaus Fischer committed
using FitConnect.Interfaces;
using FitConnect.Models;
using FitConnect.Models.Api.Metadata;
Klaus Fischer's avatar
Klaus Fischer committed
using FitConnect.Services.Models.v1.Submission;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Attachment = FitConnect.Models.Attachment;
using Data = FitConnect.Models.Api.Metadata.Data;
using Metadata = FitConnect.Models.Metadata;
Klaus Fischer's avatar
Klaus Fischer committed
using Route = FitConnect.Services.Models.v1.Routes.Route;

namespace FitConnect;

/// <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)
///     .Subm@it();
/// </code>
/// </example>
Klaus Fischer's avatar
Klaus Fischer committed
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) {
    }
Klaus Fischer's avatar
Klaus Fischer committed

    public string? PublicKey { get; set; }

Klaus Fischer's avatar
Klaus Fischer committed
    public async Task<string> GetPublicKeyForDestinationAsync(string destinationId) {
        var publicKey = await DestinationService.GetPublicKey(destinationId);
Klaus Fischer's avatar
Klaus Fischer committed
        var keyIsValid = new CertificateHelper(Logger).ValidateCertificateJsonWebKeyString(
            publicKey,
Klaus Fischer's avatar
Klaus Fischer committed
            VerifiedCertificatesAreMandatory ? LogLevel.Error : LogLevel.Warning);
Klaus Fischer's avatar
Klaus Fischer committed
        if (VerifiedCertificatesAreMandatory && !await keyIsValid)
Klaus Fischer's avatar
Klaus Fischer committed
            throw new SecurityException("Public key is not trusted");
Klaus Fischer's avatar
Klaus Fischer committed

Klaus Fischer's avatar
Klaus Fischer committed
        return publicKey;
Klaus Fischer's avatar
Klaus Fischer committed
    public async Task<Submission> SendAsync(SendableSubmission submission) {
        var sendable = await CreateSubmission(submission.DestinationId);
        sendable.AddServiceType(submission.ServiceName!, submission.LeikaKey!);
Klaus Fischer's avatar
Klaus Fischer committed
        sendable.Data = submission.Data;
Klaus Fischer's avatar
Klaus Fischer committed
        sendable!.Attachments = new List<Attachment>();
        if (submission.Attachments != null)
            foreach (var attachment in submission.Attachments)
                sendable!.Attachments.Add(attachment);
Klaus Fischer's avatar
Klaus Fischer committed
        var created =
            await SubmissionService.CreateSubmissionAsync((CreateSubmissionDto)sendable);
        sendable.Id = created.SubmissionId;
        sendable.CaseId = created.CaseId;
Klaus Fischer's avatar
Klaus Fischer committed
        Logger?.LogInformation("Submission Id {CreatedSubmissionId}, CaseId {SubmissionCaseId}",
            created.SubmissionId, sendable.CaseId);
Klaus Fischer's avatar
Klaus Fischer committed
        var encryptedAttachments = Encrypt(PublicKey!, sendable.Attachments);
        await UploadAttachmentsAsync(sendable.Id!, encryptedAttachments);
        return await Submit(sendable);
Klaus Fischer's avatar
Klaus Fischer committed
    /// <summary>
    ///     Sender for pre-encrypted data
    /// </summary>
    /// <param name="submission"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    /// <exception cref="ArgumentException"></exception>
    public async Task<Submitted> SendAsync(SendableEncryptedSubmission submission) {
Klaus Fischer's avatar
Klaus Fischer committed
        SubmissionSenderGuards(submission);
Klaus Fischer's avatar
Klaus Fischer committed
        var sendable = await CreateSubmission(submission.DestinationId);
        sendable.AddServiceType(submission.ServiceName!, submission.LeikaKey!);
        sendable.EncryptedData = submission.Data;
        sendable.EncryptedMetadata = submission.Metadata;
Klaus Fischer's avatar
Klaus Fischer committed
        var announcedSubmission = (CreateSubmissionDto)sendable;
Klaus Fischer's avatar
Klaus Fischer committed

Klaus Fischer's avatar
Klaus Fischer committed
        announcedSubmission.AnnouncedAttachments = submission.Attachments?.Keys.ToList();
Klaus Fischer's avatar
Klaus Fischer committed
        var created =
            await SubmissionService.CreateSubmissionAsync(announcedSubmission);
        sendable.Id = created.SubmissionId;
        sendable.CaseId = created.CaseId;
Klaus Fischer's avatar
Klaus Fischer committed
        Logger?.LogInformation("Submission Id {CreatedSubmissionId}, CaseId {SubmissionCaseId}",
            created.SubmissionId, sendable.CaseId);
Klaus Fischer's avatar
Klaus Fischer committed
        Logger?.LogInformation("Uploading pre encrypted attachments");
        await UploadAttachmentsAsync(sendable.Id!, submission.Attachments!);
Klaus Fischer's avatar
Klaus Fischer committed
        return await Submit(sendable);
Klaus Fischer's avatar
Klaus Fischer committed
    public async Task<List<Route>> FindDestinationId(string leiaKey, string? ags = null,
        string? ars = null,
        string? areaId = null) {
        if (ags == null && ars == null && areaId == null)
            throw new ArgumentException("One of the following must be provided: ags, ars, areaId");
Klaus Fischer's avatar
Klaus Fischer committed
        var routes = await RouteService.GetDestinationIdAsync(leiaKey, ags, ars, areaId);
        Logger?.LogInformation("Received destinations: {Destinations}",
            routes.Select(d => d.DestinationId).Aggregate((a, b) => a + "," + b));
        return routes;
    }
Klaus Fischer's avatar
Klaus Fischer committed
    private async Task<Submission> Submit(Submission submission) {
        if (submission == null) {
            Logger?.LogCritical("Submission is null on submit");
            throw new InvalidOperationException("Submission is not ready");
Klaus Fischer's avatar
Klaus Fischer committed
        if (submission.EncryptedMetadata == null) {
            var metadata = CreateMetadata(submission);
            Logger?.LogTrace("MetaData: {Metadata}", metadata);
Klaus Fischer's avatar
Klaus Fischer committed
            var valid = await JsonHelper.VerifyMetadata(metadata, null);
Klaus Fischer's avatar
Klaus Fischer committed
            if (!valid) {
                Logger?.LogError("Sending submission aborted due to validation errors");
                throw new InvalidOperationException("Submission is not ready");
            }
Klaus Fischer's avatar
Klaus Fischer committed
            Logger?.LogInformation("MetaData validation check, done");
            Logger?.LogInformation("Sending submission");
            var encryptedMeta = Encryption.Encrypt(metadata);
            Logger?.LogTrace("Encrypted metadata: {EncryptedMeta}", encryptedMeta);
            submission.EncryptedMetadata = encryptedMeta;
Klaus Fischer's avatar
Klaus Fischer committed
        if (submission.EncryptedData == null)
            if (submission.Data != null)
                submission.EncryptedData = Encryption.Encrypt(submission.Data);
Klaus Fischer's avatar
Klaus Fischer committed
        var result = await SubmissionService
            .SubmitSubmission(submission.Id!, (SubmitSubmissionDto)submission);
Klaus Fischer's avatar
Klaus Fischer committed
        Logger?.LogInformation("Submission sent");
        return submission;
Klaus Fischer's avatar
Klaus Fischer committed
    /// <summary>
Klaus Fischer's avatar
Klaus Fischer committed
    ///     Creates a new <see cref="SendableSubmission" /> on the FitConnect server.
Klaus Fischer's avatar
Klaus Fischer committed
    /// </summary>
    /// <param name="destinationId">The id of the destination the submission has to be sent to</param>
Klaus Fischer's avatar
Klaus Fischer committed
    /// <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> CreateSubmission(string destinationId) {
Klaus Fischer's avatar
Klaus Fischer committed
        PublicKey = await GetPublicKeyForDestinationAsync(destinationId);
        Encryption = new FitEncryption(Logger) { PublicKeyEncryption = PublicKey };

        var submission = new Submission {
            DestinationId = destinationId
        };

        return submission;
    }
Klaus Fischer's avatar
Klaus Fischer committed
    /// <summary>
    ///     Create Metadata incl. Hash
Klaus Fischer's avatar
Klaus Fischer committed
    /// </summary>
    /// <param name="submission"></param>
Klaus Fischer's avatar
Klaus Fischer committed
    /// <param name="dataHash"></param>
Klaus Fischer's avatar
Klaus Fischer committed
    /// <returns></returns>
Klaus Fischer's avatar
Klaus Fischer committed
    internal static string CreateMetadata(Submission submission,
        DataHash? dataHash = null) {
        var data = new Data {
Klaus Fischer's avatar
Klaus Fischer committed
            Hash = dataHash ?? FitEncryption.CalculateHash(submission.Data!),
            SubmissionSchema = new Fachdatenschema {
                SchemaUri = submission.ServiceType.Identifier,
Klaus Fischer's avatar
Klaus Fischer committed
                MimeType = submission.DataMimeType ??
                           MediaTypeNames.Application.Json
        };
        var contentStructure = new ContentStructure {
            Data = data,
Klaus Fischer's avatar
Klaus Fischer committed
            Attachments = submission.Attachments?.Select(a =>
                new Models.Api.Metadata.Attachment {
                    Description = a.Description,
                    AttachmentId = a.Id,
                    MimeType = a.MimeType,
                    Filename = a.Filename,
                    Purpose = "attachment",
                    Hash = new AttachmentHash {
                        Type = "sha512",
Klaus Fischer's avatar
Klaus Fischer committed
                        Content = a.Hash
Klaus Fischer's avatar
Klaus Fischer committed
                }).ToList() ?? null
Klaus Fischer's avatar
Klaus Fischer committed

        var metaData = new Metadata {
Klaus Fischer's avatar
Klaus Fischer committed
            Schema = Metadata.SchemaUrl,
            ContentStructure = contentStructure,
            PublicServiceType = new Verwaltungsleistung {
                Identifier = submission.ServiceType.Identifier,
                Name = submission.ServiceType.Name
            }
Klaus Fischer's avatar
Klaus Fischer committed
        };
        return JsonConvert.SerializeObject(metaData);
Klaus Fischer's avatar
Klaus Fischer committed

    /// <summary>
    ///     Finding Areas
    /// </summary>
    /// <param name="filter"></param>
    /// <param name="offset"></param>
    /// <param name="limit"></param>
    /// <returns></returns>
Klaus Fischer's avatar
Klaus Fischer committed
    public async Task<IEnumerable<Area>> GetAreas(string filter, int offset = 0,
        int limit = 100) {
Klaus Fischer's avatar
Klaus Fischer committed
        var dto = await RouteService.GetAreas(filter, offset, limit);
        // totalCount = dto?.TotalCount ?? 0;
        return dto?.Areas ?? new List<Area>();
    }
Klaus Fischer's avatar
Klaus Fischer committed
    /// <summary>
    ///     Uploading the encrypted data to the server
Klaus Fischer's avatar
Klaus Fischer committed
    /// </summary>
Klaus Fischer's avatar
Klaus Fischer committed
    /// <param name="submissionId">Submissions ID</param>
Klaus Fischer's avatar
Klaus Fischer committed
    /// <param name="encryptedAttachments">Encrypted attachments with id and content</param>
Klaus Fischer's avatar
Klaus Fischer committed
    private async Task<bool> UploadAttachmentsAsync(string submissionId,
Klaus Fischer's avatar
Klaus Fischer committed
        Dictionary<string, string> encryptedAttachments) {
        try {
            foreach (var (id, content) in encryptedAttachments) {
                Logger?.LogInformation("Uploading attachment {Id}", id);
Klaus Fischer's avatar
Klaus Fischer committed
                await SubmissionService.UploadAttachment(submissionId, id, content);
            }

            return true;
        }
        catch (Exception e) {
Klaus Fischer's avatar
Klaus Fischer committed
            Logger?.LogError("Error Uploading attachment {message}", e.Message);
            throw;
Klaus Fischer's avatar
Klaus Fischer committed

    private static void SubmissionSenderGuards(ISendableSubmission submission) {
        if (submission.DestinationId == null)
            throw new ArgumentNullException(nameof(SendableSubmission.DestinationId));
        if (!Regex.IsMatch(submission.DestinationId, GuidPattern))
            throw new ArgumentException("The destination must be a valid GUID");

        if (submission.ServiceName == null)
            throw new ArgumentNullException(nameof(SendableSubmission.ServiceName));

        if (submission.LeikaKey == null)
            throw new ArgumentNullException(nameof(SendableSubmission.LeikaKey));

        if (submission.Data == null)
            throw new ArgumentNullException(nameof(SendableSubmission.Data));
    }
}

public static class SubmissionSenderExtension {
    public static void AddServiceType(this Submission submission, string serviceName,
        string leikaKey) {
        if (string.IsNullOrWhiteSpace(leikaKey) || !Regex.IsMatch(leikaKey,
                FitConnectClient.LeikaKeyPattern))
            throw new ArgumentException("Invalid leika key");

        submission.ServiceType = new ServiceType {
            Name = serviceName,
            Identifier = leikaKey
        };
    }

    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;
    }
Klaus Fischer's avatar
Klaus Fischer committed
}