You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/Emby.Dlna/Service/BaseControlHandler.cs

248 lines
8.6 KiB

#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<ControlResponse> 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<ControlResponse> 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<ControlRequestInfo> 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<ControlRequestInfo> 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<string, string> 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<string, string> 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<string, string>(StringComparer.OrdinalIgnoreCase);
}
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; }
}
}
}