public static byte[] ConvertToMqtt(MqttSession session, EventMessage message) { if (message.Protocol == ProtocolType.MQTT) { return(MqttConversion(session, message.Message)); } if (message.Protocol == ProtocolType.COAP) { CoapMessage msg = CoapMessage.DecodeMessage(message.Message); CoapUri curi = new CoapUri(msg.ResourceUri.ToString()); QualityOfServiceLevelType qos = QualityOfServiceLevelType.AtLeastOnce; QualityOfServiceLevelType?qosType = session.GetQoS(curi.Resource); qos = qosType ?? QualityOfServiceLevelType.AtLeastOnce; PublishMessage pub = new PublishMessage(false, qos, false, session.NewId(), curi.Resource, msg.Payload); return(pub.Encode()); } if (message.Protocol == ProtocolType.REST) { PublishMessage pubm = new PublishMessage(false, session.GetQoS(message.ResourceUri).Value, false, session.NewId(), message.ResourceUri, message.Message); return(pubm.Encode()); } return(MqttConversion(session, message.Message)); }
public async Task <CoapMessage> PutAsync(CoapMessage message) { CoapUri uri = new CoapUri(message.ResourceUri.ToString()); ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; //EventValidator.Validate(false, resourceUriString, Channel, graphManager, context).Validated //if (!await adapter.CanSubscribeAsync(uri.Resource, channel.IsEncrypted)) if (EventValidator.Validate(false, uri.Resource, channel, graphManager).Validated) { return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Unauthorized, message.Token)); } if (coapObserved.ContainsKey(uri.Resource) || coapUnobserved.Contains(uri.Resource)) { //resource previously subscribed return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.NotAcceptable, message.Token)); } //this point the resource is not being observed, so we can // #1 subscribe to it // #2 add to unobserved resources (means not coap observed) SubscriptionMetadata metadata = new SubscriptionMetadata() { IsEphemeral = true, Identity = session.Identity, Indexes = session.Indexes }; string subscriptionUriString = await adapter.SubscribeAsync(uri.Resource, metadata); coapUnobserved.Add(uri.Resource); return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Created, message.Token)); }
public async Task <CoapMessage> DeleteAsync(CoapMessage message) { Exception error = null; CoapUri uri = new CoapUri(message.ResourceUri.ToString()); try { await adapter.UnsubscribeAsync(uri.Resource); coapObserved.Remove(uri.Resource); } catch (Exception ex) { Trace.TraceError("{0} - CoAP Delete fault '{1}' ", DateTime.UtcNow.ToString("yyyy-MM-ddTHH-MM-ss.fffff"), ex.Message); error = ex; } if (error == null) { ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Deleted, message.Token)); } else { return(new CoapResponse(message.MessageId, ResponseMessageType.Reset, ResponseCodeType.EmptyMessage)); } }
public async Task <CoapMessage> ObserveAsync(CoapMessage message) { if (!message.Observe.HasValue) { //RST because GET needs to be observe/unobserve await logger?.LogWarningAsync($"CoAP observe received without Observe flag and will return RST for {session.Identity}"); await logger?.LogDebugAsync($"Returning RST because GET needs to be observe/unobserve for {session.Identity}"); return(new CoapResponse(message.MessageId, ResponseMessageType.Reset, ResponseCodeType.EmptyMessage)); } CoapUri uri = new CoapUri(message.ResourceUri.ToString()); ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; ValidatorResult result = EventValidator.Validate(false, uri.Resource, channel, graphManager); if (!result.Validated) { await logger?.LogErrorAsync($"{result.ErrorMessage} for {session.Identity}"); return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Unauthorized, message.Token)); } if (!message.Observe.Value) { //unsubscribe await logger?.LogInformationAsync($"CoAP unobserve '{message.ResourceUri.ToString()}' for {session.Identity}."); await adapter.UnsubscribeAsync(uri.Resource); await logger?.LogDebugAsync($"CoAP unsubscribed '{message.ResourceUri.ToString()} for {session.Identity}'."); coapObserved.Remove(uri.Resource); } else { //subscribe SubscriptionMetadata metadata = new SubscriptionMetadata() { IsEphemeral = true, Identity = session.Identity, Indexes = session.Indexes }; await logger?.LogInformationAsync($"CoAP subscribed '{message.ResourceUri.ToString()}' for {session.Identity}"); string subscriptionUriString = await adapter.SubscribeAsync(uri.Resource, metadata); if (!coapObserved.ContainsKey(uri.Resource)) //add resource to observed list { coapObserved.Add(uri.Resource, message.Token); await logger?.LogDebugAsync("Key added to observable resource."); } } return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Valid, message.Token)); }
public static byte[] ConvertToCoap(CoapSession session, EventMessage message, byte[] observableToken = null) { CoapMessage coapMessage = null; CoapToken token = CoapToken.Create(); ushort id = observableToken == null?session.CoapSender.NewId(token.TokenBytes) : session.CoapSender.NewId(observableToken); string uriString = CoapUri.Create(session.Config.Authority, message.ResourceUri, IsEncryptedChannel); if (message.Protocol == ProtocolType.MQTT) { MqttMessage msg = MqttMessage.DecodeMessage(message.Message); PublishMessage pub = msg as PublishMessage; MqttUri uri = new MqttUri(pub.Topic); if (observableToken == null) { RequestMessageType messageType = msg.QualityOfService == QualityOfServiceLevelType.AtMostOnce ? RequestMessageType.NonConfirmable : RequestMessageType.Confirmable; //request coapMessage = new CoapRequest(id, messageType, MethodType.POST, new Uri(uriString), MediaTypeConverter.ConvertToMediaType(message.ContentType)); } else { //response coapMessage = new CoapResponse(id, ResponseMessageType.NonConfirmable, ResponseCodeType.Content, observableToken, MediaTypeConverter.ConvertToMediaType(uri.ContentType), msg.Payload); } } else if (message.Protocol == ProtocolType.COAP) { CoapMessage msg = CoapMessage.DecodeMessage(message.Message); if (observableToken == null) { //request coapMessage = new CoapRequest(id, msg.MessageType == CoapMessageType.Confirmable ? RequestMessageType.Confirmable : RequestMessageType.NonConfirmable, MethodType.POST, new Uri(uriString), MediaTypeConverter.ConvertToMediaType(message.ContentType), msg.Payload); } else { //response coapMessage = new CoapResponse(id, ResponseMessageType.NonConfirmable, ResponseCodeType.Content, observableToken, MediaTypeConverter.ConvertToMediaType(message.ContentType), msg.Payload); } } else { if (observableToken == null) { //request coapMessage = new CoapRequest(id, RequestMessageType.NonConfirmable, MethodType.POST, new Uri(uriString), MediaTypeConverter.ConvertToMediaType(message.ContentType), message.Message); } else { //response coapMessage = new CoapResponse(id, ResponseMessageType.NonConfirmable, ResponseCodeType.Content, observableToken, MediaTypeConverter.ConvertToMediaType(message.ContentType), message.Message); } } return(coapMessage.Encode()); }
public async Task <CoapMessage> PostAsync(CoapMessage message) { try { CoapUri uri = new CoapUri(message.ResourceUri.ToString()); ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; EventMetadata metadata = await GraphManager.GetPiSystemMetadataAsync(uri.Resource); if (!await adapter.CanPublishAsync(metadata, channel.IsEncrypted)) { if (metadata.Audit) { await auditor?.WriteAuditRecordAsync(new MessageAuditRecord("XXXXXXXXXXXX", session.Identity, this.channel.TypeId, "COAP", message.Payload.Length, MessageDirectionType.In, false, DateTime.UtcNow, "Not authorized, missing resource metadata, or channel encryption requirements")); } return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Unauthorized, message.Token)); } string contentType = message.ContentType.HasValue ? message.ContentType.Value.ConvertToContentType() : "application/octet-stream"; EventMessage msg = new EventMessage(contentType, uri.Resource, ProtocolType.COAP, message.Encode(), DateTime.UtcNow, metadata.Audit); if (!string.IsNullOrEmpty(uri.CacheKey)) { msg.CacheKey = uri.CacheKey; } if (uri.Indexes == null) { await adapter.PublishAsync(msg); } else { List <KeyValuePair <string, string> > indexes = new List <KeyValuePair <string, string> >(uri.Indexes); Task task = Retry.ExecuteAsync(async() => { await adapter.PublishAsync(msg, indexes); }); task.LogExceptions(); } return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Created, message.Token)); } catch (Exception ex) { Trace.TraceError("{0} - CoAP publish error on channel '{1}'", DateTime.UtcNow.ToString("yyyy-MM-ddTHH-MM-ss.fffff"), channel.Id); throw ex; } }
public Task <CoapMessage> PostAsync(CoapMessage message) { TaskCompletionSource <CoapMessage> tcs = new TaskCompletionSource <CoapMessage>(); CoapUri uri = new CoapUri(message.ResourceUri.ToString()); ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; registry.GetAction("POST", uri.Resource)?.Invoke(MediaTypeConverter.ConvertFromMediaType(message.ContentType), message.Payload); CoapMessage response = new CoapResponse(message.MessageId, rmt, ResponseCodeType.Created, message.Token); tcs.SetResult(response); return(tcs.Task); }
public void EnsureAuthentication(CoapMessage message, bool force = false) { if (!IsAuthenticated || force) { CoapUri coapUri = new CoapUri(message.ResourceUri.ToString()); if (!Authenticate(coapUri.TokenType, coapUri.SecurityToken)) { throw new SecurityException("CoAP session not authenticated."); } IdentityDecoder decoder = new IdentityDecoder(Config.IdentityClaimType, context, Config.Indexes); Identity = decoder.Id; Indexes = decoder.Indexes; } }
public async Task <CoapMessage> PostAsync(CoapMessage message) { try { CoapUri uri = new CoapUri(message.ResourceUri.ToString()); ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; EventMetadata metadata = await graphManager.GetPiSystemMetadataAsync(uri.Resource); ValidatorResult result = EventValidator.Validate(true, metadata, null, graphManager); if (!result.Validated) { if (metadata.Audit) { await auditor?.WriteAuditRecordAsync(new MessageAuditRecord("XXXXXXXXXXXX", session.Identity, this.channel.TypeId, "COAP", message.Payload.Length, MessageDirectionType.In, false, DateTime.UtcNow, "Not authorized, missing resource metadata, or channel encryption requirements")).LogExceptions(logger); } logger?.LogErrorAsync(result.ErrorMessage); return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Unauthorized, message.Token)); } string contentType = message.ContentType.HasValue ? message.ContentType.Value.ConvertToContentType() : "application/octet-stream"; EventMessage msg = new EventMessage(contentType, uri.Resource, ProtocolType.COAP, message.Encode(), DateTime.UtcNow, metadata.Audit); if (!string.IsNullOrEmpty(uri.CacheKey)) { msg.CacheKey = uri.CacheKey; } if (uri.Indexes == null) { await adapter.PublishAsync(msg); } else { List <KeyValuePair <string, string> > indexes = GetIndexes(uri); await adapter.PublishAsync(msg, indexes); } return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Created, message.Token)); } catch (Exception ex) { logger?.LogErrorAsync(ex, $"CoAP POST fault for {session.Identity}."); throw ex; } }
private void Channel_OnOpen(object sender, ChannelOpenEventArgs e) { session.IsAuthenticated = Channel.IsAuthenticated; logger?.LogDebugAsync( $"CoAP protocol channel opening with session authenticated '{session.IsAuthenticated}'.").GetAwaiter(); try { if (!Channel.IsAuthenticated && e.Message != null) { CoapMessage msg = CoapMessage.DecodeMessage(e.Message); CoapUri coapUri = new CoapUri(msg.ResourceUri.ToString()); session.IsAuthenticated = session.Authenticate(coapUri.TokenType, coapUri.SecurityToken); logger?.LogDebugAsync( $"CoAP protocol channel opening session authenticated '{session.IsAuthenticated}' by authenticator.") .GetAwaiter(); } if (session.IsAuthenticated) { IdentityDecoder decoder = new IdentityDecoder(session.Config.IdentityClaimType, context, session.Config.Indexes); session.Identity = decoder.Id; session.Indexes = decoder.Indexes; logger?.LogDebugAsync($"CoAP protocol channel opening with session identity '{session.Identity}'.") .GetAwaiter(); UserAuditRecord record = new UserAuditRecord(Channel.Id, session.Identity, session.Config.IdentityClaimType, Channel.TypeId, "COAP", "Granted", DateTime.UtcNow); userAuditor?.WriteAuditRecordAsync(record).Ignore(); } } catch (Exception ex) { logger?.LogErrorAsync(ex, $"CoAP adapter opening channel '{Channel.Id}'.").GetAwaiter(); OnError?.Invoke(this, new ProtocolAdapterErrorEventArgs(Channel.Id, ex)); } if (!session.IsAuthenticated && e.Message != null) { logger?.LogWarningAsync("CoAP adpater closing due to unauthenticated user."); Channel.CloseAsync().Ignore(); } else { dispatcher = new CoapRequestDispatcher(session, Channel, config, graphManager, logger); } }
public async Task <CoapMessage> ObserveAsync(CoapMessage message) { if (!message.Observe.HasValue) { //RST because GET needs to be observe/unobserve Trace.TraceWarning("{0} - CoAP observe received without Observe flag set on channel '{1}', returning RST", DateTime.UtcNow.ToString("yyyy-MM-ddTHH-MM-ss.fffff"), channel.Id); return(new CoapResponse(message.MessageId, ResponseMessageType.Reset, ResponseCodeType.EmptyMessage)); } CoapUri uri = new CoapUri(message.ResourceUri.ToString()); ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; if (!await adapter.CanSubscribeAsync(uri.Resource, channel.IsEncrypted)) { //not authorized Trace.TraceWarning("{0} - CoAP observe not authorized on channel '{1}'", DateTime.UtcNow.ToString("yyyy-MM-ddTHH-MM-ss.fffff"), channel.Id); return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Unauthorized, message.Token)); } if (!message.Observe.Value) { //unsubscribe Trace.TraceWarning("{0} - CoAP observe with value on channel '{1}', unsubscribing.", DateTime.UtcNow.ToString("yyyy-MM-ddTHH-MM-ss.fffff"), channel.Id); await adapter.UnsubscribeAsync(uri.Resource); coapObserved.Remove(uri.Resource); } else { //subscribe SubscriptionMetadata metadata = new SubscriptionMetadata() { IsEphemeral = true, Identity = session.Identity, Indexes = session.Indexes }; string subscriptionUriString = await adapter.SubscribeAsync(uri.Resource, metadata); if (!coapObserved.ContainsKey(uri.Resource)) //add resource to observed list { coapObserved.Add(uri.Resource, message.Token); } } return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Valid, message.Token)); }
private List <KeyValuePair <string, string> > GetIndexes(CoapUri coapUri) { List <KeyValuePair <string, string> > list = new List <KeyValuePair <string, string> >(coapUri.Indexes); if (coapUri.Indexes.Contains(new KeyValuePair <string, string>("~", "~"))) { list.Remove(new KeyValuePair <string, string>("~", "~")); var query = config.GetClientIndexes().Where((ck) => ck.Key == session.Config.IdentityClaimType); if (query.Count() == 1) { query.GetEnumerator().MoveNext(); list.Add(new KeyValuePair <string, string>(query.GetEnumerator().Current.Value, "~" + session.Identity)); } } return(list.Count > 0 ? list : null); }
private void Channel_OnOpen(object sender, ChannelOpenEventArgs e) { session.IsAuthenticated = Channel.IsAuthenticated; try { if (!Channel.IsAuthenticated && e.Message != null) { CoapMessage msg = CoapMessage.DecodeMessage(e.Message); CoapUri coapUri = new CoapUri(msg.ResourceUri.ToString()); session.IsAuthenticated = session.Authenticate(coapUri.TokenType, coapUri.SecurityToken); } if (session.IsAuthenticated) { IdentityDecoder decoder = new IdentityDecoder(session.Config.IdentityClaimType, context, session.Config.Indexes); session.Identity = decoder.Id; session.Indexes = decoder.Indexes; UserAuditRecord record = new UserAuditRecord(Channel.Id, session.Identity, session.Config.IdentityClaimType, Channel.TypeId, "COAP", "Granted", DateTime.UtcNow); userAuditor?.WriteAuditRecordAsync(record).Ignore(); } } catch (Exception ex) { logger?.LogError(ex, $"CoAP adapter opening channel '{Channel.Id}'."); OnError?.Invoke(this, new ProtocolAdapterErrorEventArgs(Channel.Id, ex)); } if (!session.IsAuthenticated && e.Message != null) { //close the channel logger?.LogInformation($"CoAP adapter user not authenticated; must close channel '{Channel.Id}'."); Channel.CloseAsync().Ignore(); } else { dispatcher = new CoapRequestDispatcher(session, Channel); } }
public async Task <CoapMessage> DeleteAsync(CoapMessage message) { Exception error = null; CoapUri uri = new CoapUri(message.ResourceUri.ToString()); try { await adapter.UnsubscribeAsync(uri.Resource); await logger?.LogDebugAsync($"CoAP delete unsubscribe '{uri.Resource}' for {session.Identity}."); coapObserved.Remove(uri.Resource); await logger?.LogDebugAsync($"CoAP delete removed '{uri.Resource}' for {session.Identity}."); } catch (Exception ex) { await logger?.LogErrorAsync(ex, $"CoAP delete fault during unsubscribe process for {session.Identity}"); error = ex; } if (error == null) { ResponseMessageType rmt = message.MessageType == CoapMessageType.Confirmable ? ResponseMessageType.Acknowledgement : ResponseMessageType.NonConfirmable; await logger?.LogDebugAsync($"CoAP delete returning response for '{uri.Resource}' with {rmt.ToString()} for {session.Identity}."); return(new CoapResponse(message.MessageId, rmt, ResponseCodeType.Deleted, message.Token)); } else { await logger?.LogDebugAsync($"CoAP delete returning response for '{uri.Resource}' with {ResponseCodeType.EmptyMessage.ToString()} for {session.Identity}."); return(new CoapResponse(message.MessageId, ResponseMessageType.Reset, ResponseCodeType.EmptyMessage)); } }
public static byte[] ConvertToMqtt(MqttSession session, EventMessage message) { if (message.Protocol == ProtocolType.MQTT) { return(MqttConversion(session, message.Message)); } else if (message.Protocol == ProtocolType.COAP) { CoapMessage msg = CoapMessage.DecodeMessage(message.Message); CoapUri curi = new CoapUri(msg.ResourceUri.ToString()); QualityOfServiceLevelType qos = QualityOfServiceLevelType.AtLeastOnce; try { QualityOfServiceLevelType?qosType = session.GetQoS(curi.Resource); qos = qosType.HasValue ? qosType.Value : QualityOfServiceLevelType.AtLeastOnce; } catch (Exception ex) { Trace.TraceWarning("{0} - Fault in ProtocolTransition.ConvertToMqtt", DateTime.UtcNow.ToString()); Trace.TraceError("{0} - {1} - {2}", DateTime.UtcNow.ToString(""), "ProtocolTransition", ex.Message); } PublishMessage pub = new PublishMessage(false, qos, false, session.NewId(), curi.Resource, msg.Payload); return(pub.Encode()); } else if (message.Protocol == ProtocolType.REST) { PublishMessage pubm = new PublishMessage(false, session.GetQoS(message.ResourceUri).Value, false, session.NewId(), message.ResourceUri, message.Message); return(pubm.Encode()); } else { return(MqttConversion(session, message.Message, message.ContentType)); } }
public static string ToCoreLinkFormat(CoapResourceMetadata resource, Uri baseUri = null) { var message = new StringBuilder(); try { if (baseUri == null) { baseUri = _throwAwayUri; } var uri = resource.UriReference.IsAbsoluteUri ? resource.UriReference : new Uri(baseUri, resource.UriReference); message.Append(CoapUri.Compare(uri, baseUri, UriComponents.HostAndPort, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == 0 ? $"<{uri.AbsolutePath}>" : $"<{uri}>"); if (resource.InterfaceDescription.Count > 0) { message.AppendFormat(";if=\"{0}\"", string.Join(" ", resource.InterfaceDescription)); } if (resource.SuggestedContentTypes.Count == 1) { message.Append($";ct={(int)resource.SuggestedContentTypes.First():D}"); } if (resource.SuggestedContentTypes.Count > 1) { message.AppendFormat(";ct=\"{0}\"", string.Join(" ", resource.SuggestedContentTypes.Select(ct => ((int)ct).ToString("D")))); } if (resource.ResourceTypes.Count > 0) { message.AppendFormat(";rt=\"{0}\"", string.Join(" ", resource.ResourceTypes)); } if (resource.Rel.Count == 1) { message.AppendFormat(";rel={0}", resource.Rel.First()); } if (resource.Rel.Count > 1) { message.AppendFormat(";rel=\"{0}\"", string.Join(" ", resource.Rel)); } if (!string.IsNullOrEmpty(resource.Anchor)) { message.Append($";anchor=\"{resource.Anchor}\""); } if ((resource.HrefLang ?? string.Empty) != string.Empty) { message.Append($";hreflang={resource.HrefLang?.ToLower()}"); } if ((resource.Media ?? string.Empty) != string.Empty) { message.Append(resource.Media.Any(c => c == ';' || c == ',') ? $";media=\"{resource.Media}\"" : $";media={resource.Media}"); } if ((resource.Title ?? string.Empty) != string.Empty) { message.Append($";title=\"{resource.Title}\""); } //case "title*": // // TODO: No idea what to do here...? // var charset = value.Substring(0, value.IndexOf('\'')); // var lang = value.Substring(charset.Length + 1, // value.IndexOf('\'', charset.Length + 1) - charset.Length - 1); // value = value.Substring(charset.Length + lang.Length + 3, // value.Length - charset.Length - lang.Length - 4); // //System.Diagnostics.Debug.WriteLine("title* = {3}\n\tCharset: {0}\n\tLanguage: {1}\n\tValue: {2}", // // charset, lang, value, Uri.UnescapeDataString(value)); // resource.TitleExt = Uri.UnescapeDataString(value); // break; if ((resource.Type ?? string.Empty) != string.Empty) { message.Append(resource.Type.Contains(" ") ? $";type=\"{resource.Type}\"" : $";type={resource.Type}"); } if (resource.MaxSize != 0) { message.Append($";sz={resource.MaxSize:D}"); } //default: // if (value.Length == 1) // { // if (!char.IsLetterOrDigit(value[0]) && !new[] // { // '!', '#', '$', '%', '&', '\'', '(', ')', '*', '+', '-', '.', '/', ':', // '<', '=', '>', '?', '@', '[', ']', '^', '_', '`', '{', '|', '}', '~' // }.Contains(value[0])) // throw new ArgumentException( // $"PToken contains invalid character '{value[0]}' at pos {mSeek}"); // resource.Extentions.Add(param, value); // } // else // { // if (value[0] != '"') // throw new ArgumentException( // $"Expected QuotedString DQUOTE '\"' at pos {mPos}"); // if (value[value.Length - 1] != '"') // throw new ArgumentException( // $"Expected QuotedString DQUOTE '\"' at pos {mSeek}"); // resource.Extentions.Add(param, // value.Substring(1, value.Length - 2)); // } // break; } catch (IndexOutOfRangeException) { // Moving on... } return(message.ToString()); }