Class | Description | |
---|---|---|
AssociationRejectionExampleServer |
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.
| |
Program |
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 mr-knee.dcm localhost 5555 SCP SCU StoreSCU.exe mr-knee.dcm localhost 5555 JPEG2000_SCP SCU |
/// <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 mr-knee.dcm localhost 5555 SCP SCU /// </code> /// or /// <code> /// StoreSCU.exe mr-knee.dcm localhost 5555 JPEG2000_SCP SCU /// </code> /// </para> /// </summary> public class Program { /// <summary> /// Main entry point for AssociationRejection. /// </summary> [STAThread] public static void Main() { try { int port = 5555; IList<AllowedPresentationContext> allowedPresentationContexts = new List<AllowedPresentationContext> { new AllowedPresentationContext(Uids.MRImageStorage, Uids.TransferSyntax.JPEG2000ImageCompression, Uids.TransferSyntax.JPEG2000ImageCompressionLosslessOnly, Uids.TransferSyntax.ExplicitVRLittleEndian), new AllowedPresentationContext(Uids.CTImageStorage, Uids.TransferSyntax.ExplicitVRLittleEndian, Uids.TransferSyntax.ImplicitVRLittleEndian) }; 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 dependant 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 callingIP = assoc.AssociationInfo.GetCallingHost(); // reject the association if we don't like the IP address if (!_allowedAddresses.Contains(callingIP)) { Console.WriteLine("bad IP address: calling title={0} address={1} port={2}", callingTitle, callingIP, assoc.AssociationInfo.GetCallingPort()); // 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, callingIP, assoc.AssociationInfo.GetCallingPort()); 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, callingIP, assoc.AssociationInfo.GetCallingPort()); 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) { IList<AllowedPresentationContext> restrictedList = assoc.AssociationInfo.CalledTitle.Equals("JPEG2000_SCP") ? RemoveNonJpeg2000TransferSyntaxes() : _presentationContexts; assoc.RegisterServiceClassProvider(new Dicom.Store.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}.{7}DIMSE read timeout={4}, PDU read timeout={5}, transfer syntax={6}{7}", acceptor.AssociationInfo.CallingTitle, acceptor.AssociationInfo.CalledTitle, acceptor.AssociationInfo.GetCallingHost(), acceptor.AssociationInfo.GetCallingPort(), acceptor.SessionSettings.ReceiveDimseTimeoutSeconds, acceptor.SessionSettings.PduReadTimeoutSeconds, request.Data.OriginalTransferSyntax, Environment.NewLine); // This example doesn't persist the dataset contained in the CStoreRequest, resulting in the dataset being discarded, which is fine for the purposes of this simple example. // See the various StoreSCP examples that demonstrate persisting the data. return new CStoreResponse(request); } }