/// <summary> /// Provide the fault /// </summary> public bool ProvideFault(Exception error, RestResponseMessage response) { var errCode = WebErrorUtility.ClassifyException(error, true); var hdlr = ApplicationContext.Current.GetService <IAppletManagerService>().Applets.SelectMany(o => o.ErrorAssets).FirstOrDefault(o => o.ErrorCode == errCode); #if DEBUG var ie = error; while (ie != null) { this.m_tracer.TraceError("{0} - ({1}){2} - {3}", error == ie ? "" : "Caused By", RestOperationContext.Current.EndpointOperation?.Description.InvokeMethod.Name, ie.GetType().FullName, ie.Message); ie = ie.InnerException; } #else if (error is TargetInvocationException) { this.m_tracer.TraceError("{0} - {1} / {2}", RestOperationContext.Current.EndpointOperation.Description.InvokeMethod.Name, error.Message, error.InnerException?.Message); } else { this.m_tracer.TraceError("{0} - {1}", RestOperationContext.Current.EndpointOperation.Description.InvokeMethod.Name, error.Message); } #endif // Grab the asset handler try { if (hdlr != null) { response.Body = new MemoryStream(new byte[0]); RestOperationContext.Current.OutgoingResponse.Redirect(hdlr.Asset); } else { RestOperationContext.Current.OutgoingResponse.StatusCode = errCode; using (var sr = new StreamReader(typeof(AgsWebErrorHandlerServiceBehavior).Assembly.GetManifestResourceStream("SanteDB.DisconnectedClient.Ags.Resources.GenericError.html"))) { string errRsp = sr.ReadToEnd().Replace("{status}", response.StatusCode.ToString()) .Replace("{description}", response.StatusDescription) .Replace("{type}", error.GetType().Name) .Replace("{message}", error.Message) .Replace("{details}", error.ToString()) .Replace("{trace}", error.StackTrace); RestOperationContext.Current.OutgoingResponse.ContentType = "text/html"; response.Body = new MemoryStream(Encoding.UTF8.GetBytes(errRsp)); } } AuditUtil.AuditNetworkRequestFailure(error, RestOperationContext.Current.IncomingRequest.Url, RestOperationContext.Current.IncomingRequest.Headers.AllKeys.ToDictionary(o => o, o => RestOperationContext.Current.IncomingRequest.Headers[o]), RestOperationContext.Current.OutgoingResponse.Headers.AllKeys.ToDictionary(o => o, o => RestOperationContext.Current.OutgoingResponse.Headers[o])); return(true); } catch (Exception e) { Tracer.GetTracer(typeof(AgsWebErrorHandlerServiceBehavior)).TraceError("Could not provide fault: {0}", e.ToString()); throw; } }
/// <summary> /// Apply the authorization policy rule /// </summary> public void Apply(RestRequestMessage request) { try { this.m_traceSource.TraceInfo("CheckAccess"); // Http message inbound var httpMessage = RestOperationContext.Current.IncomingRequest; // Get the authorize header String authorization = httpMessage.Headers["Authorization"]; if (authorization == null) { if (httpMessage.HttpMethod == "OPTIONS" || httpMessage.HttpMethod == "PING") { return; } else { throw new SecuritySessionException(SessionExceptionType.NotEstablished, "Missing Authorization header", null); } } // Authorization method var auth = authorization.Split(' ').Select(o => o.Trim()).ToArray(); switch (auth[0].ToLowerInvariant()) { case "bearer": var contextToken = this.CheckBearerAccess(auth[1]); RestOperationContext.Current.Disposed += (o, e) => contextToken.Dispose(); break; default: throw new SecuritySessionException(SessionExceptionType.TokenType, "Invalid authentication scheme", null); } } catch (UnauthorizedAccessException e) { this.m_traceSource.TraceError("Token Error (From: {0}) : {1}", RestOperationContext.Current.IncomingRequest.RemoteEndPoint, e); AuditUtil.AuditNetworkRequestFailure(e, RestOperationContext.Current.IncomingRequest.Url, RestOperationContext.Current.IncomingRequest.Headers, null); throw; } catch (KeyNotFoundException e) { this.m_traceSource.TraceError("Token Error (From: {0}) : {1}", RestOperationContext.Current.IncomingRequest.RemoteEndPoint, e); AuditUtil.AuditNetworkRequestFailure(e, RestOperationContext.Current.IncomingRequest.Url, RestOperationContext.Current.IncomingRequest.Headers, null); throw new SecuritySessionException(SessionExceptionType.NotEstablished, e.Message, e); } catch (Exception e) { this.m_traceSource.TraceError("Token Error (From: {0}) : {1}", RestOperationContext.Current.IncomingRequest.RemoteEndPoint, e); AuditUtil.AuditNetworkRequestFailure(e, RestOperationContext.Current.IncomingRequest.Url, RestOperationContext.Current.IncomingRequest.Headers, null); throw new SecuritySessionException(SessionExceptionType.Other, e.Message, e); } }
/// <summary> /// Provide fault /// </summary> public bool ProvideFault(Exception error, RestResponseMessage faultMessage) { var uriMatched = RestOperationContext.Current.IncomingRequest.Url; while (error.InnerException != null) { error = error.InnerException; } var fault = new RestServiceFault(error); var authScheme = RestOperationContext.Current.AppliedPolicies.OfType <BasicAuthorizationAccessBehavior>().Any() ? "Basic" : "Bearer"; var authRealm = RestOperationContext.Current.IncomingRequest.Url.Host; // Formulate appropriate response if (error is DomainStateException) { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.ServiceUnavailable; } else if (error is ObjectLockedException lockException) { faultMessage.StatusCode = 423; fault.Data.Add(lockException.LockedUser); } else if (error is PolicyViolationException) { var pve = error as PolicyViolationException; if (pve.PolicyDecision == PolicyGrantType.Elevate) { // Ask the user to elevate themselves faultMessage.StatusCode = 401; faultMessage.AddAuthenticateHeader(authScheme, authRealm, "insufficient_scope", pve.PolicyId, error.Message); } else { faultMessage.StatusCode = 403; } } else if (error is SecurityException) { faultMessage.StatusCode = (int)HttpStatusCode.Forbidden; } else if (error is SecurityTokenException) { // TODO: Audit this faultMessage.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized; var authHeader = $"Bearer realm=\"{RestOperationContext.Current.IncomingRequest.Url.Host}\" error=\"invalid_token\" error_description=\"{error.Message}\""; faultMessage.AddAuthenticateHeader(authScheme, authRealm, error: "invalid_token", description: error.Message); } else if (error is LimitExceededException) { faultMessage.StatusCode = (int)(HttpStatusCode)429; faultMessage.StatusDescription = "Too Many Requests"; faultMessage.Headers.Add("Retry-After", "1200"); } else if (error is AuthenticationException) { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized; faultMessage.AddAuthenticateHeader(authScheme, authRealm, "invalid_token", description: error.Message); } else if (error is UnauthorizedAccessException) { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.Forbidden; } else if (error is SecuritySessionException ses) { switch (ses.Type) { case SessionExceptionType.Expired: case SessionExceptionType.NotYetValid: case SessionExceptionType.NotEstablished: faultMessage.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized; faultMessage.AddAuthenticateHeader(authScheme, authRealm, error: "unauthorized"); break; default: faultMessage.StatusCode = (int)System.Net.HttpStatusCode.Forbidden; break; } } else if (error is FaultException) { faultMessage.StatusCode = (int)(error as FaultException).StatusCode; } else if (error is Newtonsoft.Json.JsonException || error is System.Xml.XmlException) { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.BadRequest; } else if (error is DuplicateKeyException || error is DuplicateNameException) { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.Conflict; } else if (error is FileNotFoundException || error is KeyNotFoundException) { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.NotFound; } else if (error is DomainStateException) { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.ServiceUnavailable; } else if (error is DetectedIssueException) { faultMessage.StatusCode = (int)(System.Net.HttpStatusCode) 422; } else if (error is NotImplementedException) { faultMessage.StatusCode = (int)HttpStatusCode.NotImplemented; } else if (error is NotSupportedException) { faultMessage.StatusCode = (int)HttpStatusCode.MethodNotAllowed; } else if (error is PatchException) { faultMessage.StatusCode = (int)HttpStatusCode.Conflict; } else { faultMessage.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError; } switch (faultMessage.StatusCode) { case 409: case 429: case 503: this.m_traceSource.TraceInfo("Issue on REST pipeline: {0}", error); break; case 401: case 403: case 501: case 405: this.m_traceSource.TraceWarning("Warning on REST pipeline: {0}", error); break; default: this.m_traceSource.TraceError("Error on REST pipeline: {0}", error); break; } RestMessageDispatchFormatter.CreateFormatter(RestOperationContext.Current.ServiceEndpoint.Description.Contract.Type).SerializeResponse(faultMessage, null, fault); AuditUtil.AuditNetworkRequestFailure(error, uriMatched, RestOperationContext.Current.IncomingRequest.Headers.AllKeys.ToDictionary(o => o, o => RestOperationContext.Current.IncomingRequest.Headers[o]), RestOperationContext.Current.OutgoingResponse.Headers.AllKeys.ToDictionary(o => o, o => RestOperationContext.Current.OutgoingResponse.Headers[o])); return(true); }
/// <summary> /// Provide the fault /// </summary> /// <param name="error"></param> /// <param name="response"></param> /// <returns></returns> public bool ProvideFault(Exception error, RestResponseMessage faultMessage) { try { #if DEBUG this.m_tracer.TraceWarning("Error on pipeline: {0}", error); #else if (error is TargetInvocationException tie) { this.m_tracer.TraceWarning("{0} - {1} / {2}", RestOperationContext.Current?.EndpointOperation?.Description?.InvokeMethod?.Name, error.Message, error.InnerException?.Message); } else { this.m_tracer.TraceWarning("{0} - {1}", RestOperationContext.Current?.EndpointOperation?.Description?.InvokeMethod?.Name, error.Message); } #endif if (faultMessage == null) { if (RestOperationContext.Current.OutgoingResponse == null) { this.m_tracer.TraceWarning("Client hangup"); return(false); } this.m_tracer.TraceWarning("For some reason the fault message is null - "); faultMessage = new RestResponseMessage(RestOperationContext.Current.OutgoingResponse); } var ie = error; while (ie != null) { this.m_tracer.TraceWarning("{0} - ({1}){2} - {3}", error == ie ? "" : "Caused By", RestOperationContext.Current.EndpointOperation?.Description.InvokeMethod.Name, ie?.GetType().FullName, ie.Message); // TODO: Do we need this or can we just capture the innermost exception as the cause? if (ie is RestClientException <RestServiceFault> || ie is SecurityException || ie is DetectedIssueException || ie is FileNotFoundException || ie is KeyNotFoundException) { error = ie; } ie = ie.InnerException; } faultMessage.StatusCode = WebErrorUtility.ClassifyException(error); object fault = (error as RestClientException <RestServiceFault>)?.Result ?? new RestServiceFault(error); if (error is FaultException && error?.GetType() != typeof(FaultException)) // Special classification { fault = error?.GetType().GetRuntimeProperty("Body").GetValue(error); } var formatter = RestMessageDispatchFormatter.CreateFormatter(RestOperationContext.Current.ServiceEndpoint.Description.Contract.Type); if (formatter != null) { formatter.SerializeResponse(faultMessage, null, fault); } else { RestOperationContext.Current.OutgoingResponse.OutputStream.Write(System.Text.Encoding.UTF8.GetBytes(error.Message), 0, System.Text.Encoding.UTF8.GetByteCount(error.Message)); } try { if (ApplicationServiceContext.Current.GetService <IOperatingSystemInfoService>()?.OperatingSystem != OperatingSystemID.Android) { AuditUtil.AuditNetworkRequestFailure(error, RestOperationContext.Current.IncomingRequest.Url, RestOperationContext.Current.IncomingRequest.Headers.AllKeys.ToDictionary(o => o, o => RestOperationContext.Current.IncomingRequest.Headers[o]), RestOperationContext.Current.OutgoingResponse.Headers.AllKeys.ToDictionary(o => o, o => RestOperationContext.Current.OutgoingResponse.Headers[o])); } } catch (Exception e) { this.m_tracer.TraceError("Could not send network request failure - {0}", e); } } catch (Exception e) { this.m_tracer.TraceError("Error providing fault: {0}", e); } return(true); }
/// <summary> /// Receive and process message /// </summary> protected virtual void OnReceiveMessage(object client) { using (TcpClient tcpClient = client as TcpClient) { this.m_traceSource.TraceEvent(EventLevel.Verbose, "Accepted connection on {0} from {1}", this.m_listener.LocalEndpoint, tcpClient.Client.RemoteEndPoint); NetworkStream stream = tcpClient.GetStream(); try { // Now read to a string DateTime lastReceive = DateTime.Now; while (DateTime.Now.Subtract(lastReceive) < this.m_timeout) { if (!stream.DataAvailable) { Thread.Sleep(10); continue; } // Read LLP head byte int llpByte = stream.ReadByte(); if (llpByte != START_TX) // first byte must be HT { throw new InvalidOperationException("Invalid LLP First Byte"); } // Standard stream stuff, read until the stream is exhausted StringBuilder messageData = new StringBuilder(); byte[] buffer = new byte[1024]; bool receivedEOF = false, scanForCr = false; while (!receivedEOF) { if (DateTime.Now.Subtract(lastReceive) > this.m_timeout) { throw new TimeoutException("Data not received in the specified amount of time. Increase the timeout or check the network connection"); } if (!stream.DataAvailable) { Thread.Sleep(10); continue; } int br = stream.Read(buffer, 0, 1024); messageData.Append(System.Text.Encoding.UTF8.GetString(buffer, 0, br)); // Need to check for CR? if (scanForCr) { receivedEOF = buffer[0] == END_TXNL; } else { // Look for FS int fsPos = Array.IndexOf(buffer, (byte)END_TX); if (fsPos == -1) // not found { continue; } else if (fsPos < buffer.Length - 1) // more room to read { receivedEOF = buffer[fsPos + 1] == END_TXNL; } else { scanForCr = true; // Cannot check the end of message for CR because there is no more room in the message buffer } // so need to check on the next loop } } // Use the nHAPI parser to process the data Hl7MessageReceivedEventArgs messageArgs = null; String originalVersion = null; // Setup local and remote receive endpoint data for auditing var localEp = tcpClient.Client.LocalEndPoint as IPEndPoint; var remoteEp = tcpClient.Client.RemoteEndPoint as IPEndPoint; Uri localEndpoint = new Uri(String.Format("llp://{0}:{1}", localEp.Address, localEp.Port)); Uri remoteEndpoint = new Uri(String.Format("llp://{0}:{1}", remoteEp.Address, remoteEp.Port)); foreach (var messagePart in messageData.ToString().Split((char)END_TX)) { if (messagePart == "\r") { continue; } try { this.m_traceSource.TraceInfo("Received message from llp://{0}:{1} : {2}", remoteEp.Address, remoteEp.Port, messagePart); // HACK: nHAPI doesn't like URLs ... Will fix this later string messageString = messagePart.Replace("|URL|", "|ST|"); var message = MessageUtils.ParseMessage(messageString, out originalVersion); messageArgs = new Hl7MessageReceivedEventArgs(message, localEndpoint, remoteEndpoint, DateTime.Now); HL7OperationContext.Current = new HL7OperationContext(messageArgs); // Call any bound event handlers that there is a message available OnMessageReceived(messageArgs); } catch (Exception e) { this.m_traceSource.TraceError("Error processing HL7 message: {0}", e); if (messageArgs != null) { var nack = new NHapi.Model.V25.Message.ACK(); nack.MSH.SetDefault(messageArgs.Message.GetStructure("MSH") as NHapi.Model.V25.Segment.MSH); nack.MSA.AcknowledgmentCode.Value = "AE"; nack.MSA.TextMessage.Value = $"FATAL - {e.Message}"; nack.MSA.MessageControlID.Value = (messageArgs.Message.GetStructure("MSH") as NHapi.Model.V25.Segment.MSH).MessageControlID.Value; messageArgs.Response = nack; var icomps = PipeParser.Encode(messageArgs.Message.GetStructure("MSH") as NHapi.Base.Model.ISegment, new EncodingCharacters('|', "^~\\&")).Split('|'); var ocomps = PipeParser.Encode(messageArgs.Response.GetStructure("MSH") as NHapi.Base.Model.ISegment, new EncodingCharacters('|', "^~\\&")).Split('|'); AuditUtil.AuditNetworkRequestFailure(e, messageArgs.ReceiveEndpoint, Enumerable.Range(1, icomps.Length).ToDictionary(o => $"MSH-{o}", o => icomps[o - 1]), Enumerable.Range(1, icomps.Length).ToDictionary(o => $"MSH-{o}", o => ocomps[o - 1])); } else { AuditUtil.AuditNetworkRequestFailure(e, localEndpoint, new System.Collections.Specialized.NameValueCollection(), new System.Collections.Specialized.NameValueCollection()); } } finally { // Send the response back using (MemoryStream memoryWriter = new MemoryStream()) { using (StreamWriter streamWriter = new StreamWriter(memoryWriter)) { memoryWriter.Write(new byte[] { START_TX }, 0, 1); // header if (messageArgs != null && messageArgs.Response != null) { var strMessage = MessageUtils.EncodeMessage(messageArgs.Response, originalVersion); this.m_traceSource.TraceInfo("Sending message to llp://{0} : {1}", tcpClient.Client.RemoteEndPoint, strMessage); // Since nHAPI only emits a string we just send that along the stream streamWriter.Write(strMessage); streamWriter.Flush(); } memoryWriter.Write(new byte[] { END_TX, END_TXNL }, 0, 2); // Finish the stream with FSCR stream.Write(memoryWriter.ToArray(), 0, (int)memoryWriter.Position); stream.Flush(); } } lastReceive = DateTime.Now; // Update the last receive time so the timeout function works } } if (!stream.DataAvailable) { return; } } } catch (Exception e) { this.m_traceSource.TraceEvent(EventLevel.Error, e.ToString()); } finally { stream.Close(); tcpClient.Close(); HL7OperationContext.Current = null; } } }
/// <summary> /// Create a negative acknolwedgement from the specified exception /// </summary> /// <param name="request">The request message</param> /// <param name="error">The exception that occurred</param> /// <returns>NACK message</returns> protected virtual IMessage CreateNACK(Type nackType, IMessage request, Exception error, Hl7MessageReceivedEventArgs receiveData) { // Extract TIE into real cause while (error is TargetInvocationException) { error = error.InnerException; } IMessage retVal = null; if (error is DomainStateException) { retVal = this.CreateACK(nackType, request, "AR", "Domain Error"); } else if (error is PolicyViolationException || error is SecurityException) { retVal = this.CreateACK(nackType, request, "AR", "Security Error"); } else if (error is AuthenticationException || error is UnauthorizedAccessException) { retVal = this.CreateACK(nackType, request, "AR", "Unauthorized"); } else if (error is Newtonsoft.Json.JsonException || error is System.Xml.XmlException) { retVal = this.CreateACK(nackType, request, "AR", "Messaging Error"); } else if (error is DuplicateNameException) { retVal = this.CreateACK(nackType, request, "CR", "Duplicate Data"); } else if (error is FileNotFoundException || error is KeyNotFoundException) { retVal = this.CreateACK(nackType, request, "CE", "Data not found"); } else if (error is DetectedIssueException) { retVal = this.CreateACK(nackType, request, "CR", "Business Rule Violation"); } else if (error is DataPersistenceException) { // Data persistence failed because of D/I/E if (error.InnerException is DetectedIssueException) { error = error.InnerException; retVal = this.CreateACK(nackType, request, "CR", "Business Rule Violation"); } else { retVal = this.CreateACK(nackType, request, "CE", "Error committing data"); } } else if (error is NotImplementedException) { retVal = this.CreateACK(nackType, request, "AE", "Not Implemented"); } else if (error is NotSupportedException) { retVal = this.CreateACK(nackType, request, "AR", "Not Supported"); } else if (error is HL7ProcessingException || error is HL7DatatypeProcessingException) { retVal = this.CreateACK(nackType, request, "AR", "Invalid Message"); } else { retVal = this.CreateACK(nackType, request, "AE", "General Error"); } var msa = retVal.GetStructure("MSA") as MSA; msa.ErrorCondition.Identifier.Value = this.MapErrCode(error); msa.ErrorCondition.Text.Value = error.Message; int erc = 0; // Detected issue exception if (error is DetectedIssueException) { foreach (var itm in (error as DetectedIssueException).Issues) { var err = retVal.GetStructure("ERR", erc) as ERR; if (retVal.IsRepeating("ERR")) { erc++; } err.HL7ErrorCode.Identifier.Value = "207"; err.Severity.Value = itm.Priority == Core.BusinessRules.DetectedIssuePriorityType.Error ? "E" : itm.Priority == Core.BusinessRules.DetectedIssuePriorityType.Warning ? "W" : "I"; err.GetErrorCodeAndLocation(err.ErrorCodeAndLocationRepetitionsUsed).CodeIdentifyingError.Text.Value = itm.Text; } } else { var ex = error; while (ex != null) { var err = retVal.GetStructure("ERR", erc) as ERR; if (retVal.IsRepeating("ERR")) { erc++; } err.HL7ErrorCode.Identifier.Value = this.MapErrCode(ex); err.Severity.Value = "E"; err.GetErrorCodeAndLocation(err.ErrorCodeAndLocationRepetitionsUsed).CodeIdentifyingError.Text.Value = ex.Message; if (ex is HL7ProcessingException) { var hle = ex as HL7ProcessingException; var erl = err.GetErrorLocation(err.ErrorLocationRepetitionsUsed); erl.SegmentID.Value = hle.Segment; erl.SegmentSequence.Value = hle.Repetition ?? "1"; erl.FieldPosition.Value = hle.Field.ToString(); erl.FieldRepetition.Value = "1"; erl.ComponentNumber.Value = hle.Component.ToString(); var ihle = (hle.InnerException as HL7DatatypeProcessingException)?.InnerException as HL7DatatypeProcessingException; // Nested DTE if (ihle != null) { erl.SubComponentNumber.Value = ihle.Component.ToString(); } } ex = ex.InnerException; } } var icomps = PipeParser.Encode(request.GetStructure("MSH") as MSH, new EncodingCharacters('|', "^~\\&")).Split('|'); var ocomps = PipeParser.Encode(retVal.GetStructure("MSH") as MSH, new EncodingCharacters('|', "^~\\&")).Split('|'); AuditUtil.AuditNetworkRequestFailure(error, receiveData.ReceiveEndpoint, Enumerable.Range(0, icomps.Length).ToDictionary(o => $"MSH-{o}", o => icomps[o]), Enumerable.Range(0, ocomps.Length).ToDictionary(o => $"MSA-{o}", o => ocomps[o])); return(retVal); }