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 -i mr-knee.dcm -h localhost -p 5555 -d SCP1 -s SCU99 StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP99 -s SCU1 StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d JPEG2000_SCP -s SCU1 StoreSCU.exe -i mr-knee.dcm -h localhost -p 5555 -d SCP1 -s SCU1 |
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
/// <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); } }