LAUREL BRIDGE

LaurelBridge.DCFExamples.AssociationRejection Namespace

DICOM Connectivity Framework V3.4
The AssociationRejection example demonstrates how to use the DCF to make a programmatic decision to accept or reject an association.
Classes

  ClassDescription
Public classAssociationRejectionExampleServer
This server utilizes hardcoded lists of calling AE titles, called AE titles, and IP addresses to demonstrate how to behave differently as a function of those values, which includes accepting or rejecting the association.
Public classProgram
Accept and/or reject associations based on AE titles and/or IP address.

Please note that while this example shows how an SCP can make decisions based on information presented to it, that information may not always be accurate; spoofing an IP address and/or AE title is feasible. Therefore, the end user should take the steps necessary to ensure they are operating within the confines of a secure network.

This example server implements Store SCP in order to demonstrate settings when an association is successfully established and DIMSE messages are received/sent. The StoreSCU example may be used to experiment with this test SCP using command line invocations similar to the following:

StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP1 -s SCU99
Results in AssociationRejection Server displaying bad calling AE:...
StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP99 -s SCU1
Results in AssociationRejection Server displaying bad called AE:...
StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d JPEG2000_SCP -s SCU1
Results in Association Rejected back to the SCU
StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP1 -s SCU1
Results in successful c-store response

Remarks

Supported OS Platforms:

  • Windows - .Net Framework 4.7.2 64-bit and 32-bit
  • Windows - .Net Core 2.1 64-bit and 32-bit
  • Linux - .Net Core 2.1 64-bit

Examples

AssociationRejection Sample Code
/// <summary>
/// Accept and/or reject associations based on AE titles and/or IP address.
/// <para>
/// Please note that while this example shows how an SCP can make decisions based on information presented to it,
/// that information may not always be accurate; spoofing an IP address and/or AE title is feasible. Therefore, the
/// end user should take the steps necessary to ensure they are operating within the confines of a secure network.
/// </para>
/// <para>
/// This example server implements Store SCP in order to demonstrate settings when an association is successfully
/// established and DIMSE messages are received/sent.  The StoreSCU example may be used to experiment with this
/// test SCP using command line invocations similar to the following:
/// <code>
/// StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP1 -s SCU99
/// </code>
/// Results in AssociationRejection Server displaying bad calling AE:...
/// <code>
/// StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP99 -s SCU1
/// </code>
/// Results in AssociationRejection Server displaying bad called AE:...
/// <code>
/// StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d JPEG2000_SCP -s SCU1
/// </code>
/// Results in Association Rejected back to the SCU
/// <code>
/// StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP1 -s SCU1
/// </code>
/// Results in successful c-store response
/// </para>
/// </summary>
public class Program
{
    /// <summary>
    /// Main entry point for AssociationRejection.
    /// </summary>
    [STAThread]
    public static void Main()
    {
        try
        {
            int port = 5555;
            // This SCP will accept MR and CT ImageStorage
            // Allow j2k90, j2k91, ele and ile transfer syntaxes for each allowed presentation context
            string[] transferSyntaxList = new string[] { Uids.J2k90, Uids.J2k91, Uids.ELE, Uids.ILE };
            IList<AllowedPresentationContext> allowedPresentationContexts = new List<AllowedPresentationContext>
            {
                new AllowedPresentationContext(Uids.MRImageStorage, transferSyntaxList),
                new AllowedPresentationContext(Uids.CTImageStorage, transferSyntaxList)
            };
            // set our default debug flags to dump verbose information about connections to this server
            DCF.Framework.SetConfigurationValue("DCF.Dicom/default_session_cfg/log_debug_flags", "Connection,AcsePdu,DimseWire");
            AssociationRejectionExampleServer exampleServer = new AssociationRejectionExampleServer(port, allowedPresentationContexts);
            exampleServer.BeginListening();
            Console.WriteLine("listening on {0}...{1}", port, Environment.NewLine);

        }
        catch (Exception e)
        {
            Console.WriteLine("Error during execution: {0}", e);
            Environment.ExitCode = 1;
        }
    }
}

/// <summary>
/// This server utilizes hardcoded lists of calling AE titles, called AE titles, and IP addresses to demonstrate how to behave differently
/// as a function of those values, which includes accepting or rejecting the association.
/// </summary>
public class AssociationRejectionExampleServer : AssociationListenerAdapter, IAssociationConfigPolicyManager
{
    readonly AssociationManager _manager;
    readonly IList<AllowedPresentationContext> _presentationContexts;
    private readonly string[] _allowedAddresses = { "127.0.0.1", "192.168.14.45" };
    private readonly string[] _allowedCallingTitles = { "SCU1", "SCU2", "SCU3" };
    private readonly string[] _allowedCalledTitles = { "SCP1", "SCP2", "SCP3", "SLOW_CONNECTION", "JPEG2000_SCP" };

    /// <summary>
    /// Callback store server constructor.
    /// </summary>
    /// <param name="port">server port</param>
    public AssociationRejectionExampleServer(int port)
        : this(port, null)
    {
    }

    /// <summary>
    /// Callback store server with allowed presentation contexts.
    /// </summary>
    /// <param name="port">server port</param>
    /// <param name="allowedPresentationContexts">allowed presentation context list</param>
    public AssociationRejectionExampleServer(int port, IList<AllowedPresentationContext> allowedPresentationContexts)
    {
        _presentationContexts = allowedPresentationContexts;
        _manager = new AssociationManager();
        _manager.ServerTcpPort = port;
        _manager.AssociationConfigPolicyMgr = this;
        _manager.AddAssociationListener(this);
    }

    /// <summary>
    /// Return the session settings for the given association acceptor.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This contrived example chooses to reject the association if the SCU's IP address, calling AE title, and called AE title are not present in
    /// hardcoded arrays for the sake of simplicity. A more robust solution might allow only certain AE titles from certain IP address ranges. Moreover,
    /// this information can be persisted in another manner (eg, a separate configuration file, database, etc.).
    /// </para>
    /// <para>
    /// Also note that this example shows how to restrict the AllowedPresentationContexts as a function of an AE title, and this functionality happens
    /// in the BeginAssociation method.
    /// </para>
    /// <para>
    /// Although it is common to see a DICOM application/device restricted to a single AE title, this example shows that an application can be called 
    /// with different AE titles, allowing AE title dependent functionality.
    /// </para>
    /// </remarks>
    /// <param name="assoc">The AssociationException</param>
    /// <returns>the session settings</returns>
    public DicomSessionSettings GetSessionSettings(AssociationAcceptor assoc)
    {
        string callingTitle = assoc.AssociationInfo.CallingTitle;
        string calledTitle = assoc.AssociationInfo.CalledTitle;
        string callingHost = assoc.AssociationInfo.GetCallingHost();
        int callingPort = assoc.AssociationInfo.GetCallingPort();

        // reject the association if we don't like the IP address
        if (!_allowedAddresses.Contains(callingHost))
        {
            Console.WriteLine("bad IP address: calling title={0} address={1} port={2}", callingTitle, callingHost, callingPort);
            // PduAssociateReject.Reason.NoReason is the best (ie, least-worst) Reason when rejecting because of the IP address.
            throw new AssociationRejectedException(PduAssociateReject.Result.Permanent, PduAssociateReject.Source.ServiceUser, PduAssociateReject.Reason.NoReason);
        }

        // reject the association if we don't like the calling AE title
        if (!_allowedCallingTitles.Contains(callingTitle))
        {
            Console.WriteLine("bad calling AE: calling title={0} address={1} port={2}", callingTitle, callingHost, callingPort);
            throw new AssociationRejectedException(PduAssociateReject.Result.Permanent, PduAssociateReject.Source.ServiceUser, PduAssociateReject.Reason.BadCalling);
        }

        // reject the association if we don't like the called AE title
        if (!_allowedCalledTitles.Contains(calledTitle))
        {
            Console.WriteLine("bad called AE: called title={0} address={1} port={2}", calledTitle, callingHost, callingPort);
            throw new AssociationRejectedException(PduAssociateReject.Result.Permanent, PduAssociateReject.Source.ServiceUser, PduAssociateReject.Reason.BadCalled);
        }

        DicomSessionSettings dss = new DicomSessionSettings();

        // change session settings values based on who the association is with
        if (assoc.AssociationInfo.CalledTitle.Equals("SLOW_CONNECTION"))
        {
            dss.PduReadTimeoutSeconds = (int)TimeSpan.FromMinutes(30).TotalSeconds; // allow each Pdu to take up to 30 minutes from this SCU
            dss.ReceiveDimseTimeoutSeconds = (int)TimeSpan.FromHours(2).TotalSeconds; // allow each DIMSE message to take up to 2 hours from this SCU
        }

        return dss;
    }

    /// <summary>
    /// Start listening for associations via the AssociationManager.
    /// </summary>
    public void BeginListening()
    {
        Thread t = new Thread(_manager.Run);
        t.Start();
        if (!_manager.WaitForRunning(2000))
        {
            throw new TimeoutException("AssociationManager did not start in an acceptable amount of time");
        }
    }

    /// <summary>
    /// Stop the server.
    /// </summary>
    public void Stop()
    {
        _manager.Stop();
    }

    /// <summary>
    /// Call back for begin association.
    /// </summary>
    /// <param name="assoc">the AssociationAcceptor</param>
    public override void BeginAssociation(AssociationAcceptor assoc)
    {
        // Dump some info about the association to the console
        AssociationInfo aInfo = assoc.AssociationInfo;
        string connectionId = assoc.ConnectionID;
        DicomSessionSettings ss = assoc.SessionSettings;
        string sessionName = ss.SessionName;
        SessionId sessionId = ss.SessionId;

        Console.WriteLine("{0}BeginAssociation: ConnectionID={1}{0}SessionName={2}{0}SessionId={3}",
            Environment.NewLine, connectionId, sessionName, sessionId);

        IList<AllowedPresentationContext> restrictedList = aInfo.CalledTitle.Equals("JPEG2000_SCP")
            ? RemoveNonJpeg2000TransferSyntaxes()
            : _presentationContexts;
        assoc.RegisterServiceClassProvider(new StoreSCP(assoc, restrictedList, CStore));
    }

    /// <summary>
    /// Reduce the list of AllowedPresentationContexts to a list of AllowedPresentationContexts that only
    /// contain the transfer syntaxes JPEG2000ImageCompressionLosslessOnly and JPEG2000ImageCompression.
    /// </summary>
    /// <returns>A potentially shortened list of AllowedPresentationContexts. This list may be empty.</returns>
    private IList<AllowedPresentationContext> RemoveNonJpeg2000TransferSyntaxes()
    {
        IList<AllowedPresentationContext> restrictedList = new List<AllowedPresentationContext>(_presentationContexts.Count);
        foreach (AllowedPresentationContext ctx in _presentationContexts)
        {
            IList<string> allowedTransferSyntaxes = ctx.TransferSyntaxes.Where
                (
                    x => x.Equals(Uids.TransferSyntax.JPEG2000ImageCompressionLosslessOnly)
                    ||
                    x.Equals(Uids.TransferSyntax.JPEG2000ImageCompression)
                ).ToList();
            if (allowedTransferSyntaxes.Count > 0)
                restrictedList.Add(new AllowedPresentationContext(ctx.AbstractSyntax, allowedTransferSyntaxes));
        }

        return restrictedList;
    }

    /// <summary>
    /// The callback method that will handle the CStoreRequest DIMSE message and reply with a CStoreResponse DIMSE message.
    /// </summary>
    /// <param name="acceptor">maintains the server side of the association.</param>
    /// <param name="request">a CStoreRequest DIMSE message.</param>
    /// <returns></returns>
    private CStoreResponse CStore(AssociationAcceptor acceptor, CStoreRequest request)
    {
        Console.WriteLine("received CStoreRequest: callingAE={0}, calledAE={1}, client address={2}, client port={3}",
            acceptor.AssociationInfo.CallingTitle,
            acceptor.AssociationInfo.CalledTitle,
            acceptor.AssociationInfo.GetCallingHost(),
            acceptor.AssociationInfo.GetCallingPort());
        Console.WriteLine(
            "DIMSE read timeout={0}, PDU read timeout={1}, transfer syntax={2}",
            acceptor.SessionSettings.ReceiveDimseTimeoutSeconds,
            acceptor.SessionSettings.PduReadTimeoutSeconds,
            request.Data.OriginalTransferSyntax);

        // This example doesn't persist the dataset contained in the CStoreRequest which
        // results in the dataset being discarded.
        // See the various StoreSCP examples that demonstrate persisting the data.

        return new CStoreResponse(request);
    }
}