#pragma warning disable CS1591 using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; using System.Xml; using Diacritics.Extensions; using Emby.Dlna.Didl; using MediaBrowser.Controller.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Dlna.Service { public abstract class BaseControlHandler { private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/"; protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) { Config = config; Logger = logger; } protected IServerConfigurationManager Config { get; } protected ILogger Logger { get; } public async Task ProcessControlRequestAsync(ControlRequest request) { try { LogRequest(request); var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false); LogResponse(response); return response; } catch (Exception ex) { Logger.LogError(ex, "Error processing control request"); return ControlErrorHandler.GetResponse(ex); } } private async Task ProcessControlRequestInternalAsync(ControlRequest request) { ControlRequestInfo? requestInfo = null; using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8)) { var readerSettings = new XmlReaderSettings() { ValidationType = ValidationType.None, CheckCharacters = false, IgnoreProcessingInstructions = true, IgnoreComments = true, Async = true }; using var reader = XmlReader.Create(streamReader, readerSettings); requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); } Logger.LogDebug("Received control request {0}", requestInfo.LocalName); var settings = new XmlWriterSettings { Encoding = Encoding.UTF8, CloseOutput = false }; StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); using (var writer = XmlWriter.Create(builder, settings)) { writer.WriteStartDocument(true); writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv); writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/"); writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv); writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI); WriteResult(requestInfo.LocalName, requestInfo.Headers, writer); writer.WriteFullEndElement(); writer.WriteFullEndElement(); writer.WriteFullEndElement(); writer.WriteEndDocument(); } var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal); var controlResponse = new ControlResponse(xml, true); controlResponse.Headers.Add("EXT", string.Empty); return controlResponse; } private async Task ParseRequestAsync(XmlReader reader) { await reader.MoveToContentAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { if (reader.NodeType == XmlNodeType.Element) { switch (reader.LocalName) { case "Body": { if (!reader.IsEmptyElement) { using var subReader = reader.ReadSubtree(); return await ParseBodyTagAsync(subReader).ConfigureAwait(false); } else { await reader.ReadAsync().ConfigureAwait(false); } break; } default: { await reader.SkipAsync().ConfigureAwait(false); break; } } } else { await reader.ReadAsync().ConfigureAwait(false); } } throw new EndOfStreamException("Stream ended but no body tag found."); } private async Task ParseBodyTagAsync(XmlReader reader) { string? namespaceURI = null, localName = null; await reader.MoveToContentAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { if (reader.NodeType == XmlNodeType.Element) { localName = reader.LocalName; namespaceURI = reader.NamespaceURI; if (!reader.IsEmptyElement) { var result = new ControlRequestInfo(localName, namespaceURI); using var subReader = reader.ReadSubtree(); await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); return result; } else { await reader.ReadAsync().ConfigureAwait(false); } } else { await reader.ReadAsync().ConfigureAwait(false); } } if (localName != null && namespaceURI != null) { return new ControlRequestInfo(localName, namespaceURI); } throw new EndOfStreamException("Stream ended but no control found."); } private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary headers) { await reader.MoveToContentAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false); // Loop through each element while (!reader.EOF && reader.ReadState == ReadState.Interactive) { if (reader.NodeType == XmlNodeType.Element) { // TODO: Should we be doing this here, or should it be handled earlier when decoding the request? headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false); } else { await reader.ReadAsync().ConfigureAwait(false); } } } protected abstract void WriteResult(string methodName, IReadOnlyDictionary methodParams, XmlWriter xmlWriter); private void LogRequest(ControlRequest request) { if (!Config.GetDlnaConfiguration().EnableDebugLog) { return; } Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers); } private void LogResponse(ControlResponse response) { if (!Config.GetDlnaConfiguration().EnableDebugLog) { return; } Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml); } private class ControlRequestInfo { public ControlRequestInfo(string localName, string namespaceUri) { LocalName = localName; NamespaceURI = namespaceUri; Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); } public string LocalName { get; set; } public string NamespaceURI { get; set; } public Dictionary Headers { get; } } } }