diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml new file mode 100644 index 0000000000..4cd19dc838 --- /dev/null +++ b/.ci/azure-pipelines.yml @@ -0,0 +1,183 @@ +name: $(Date:yyyyMMdd)$(Rev:.r) + +variables: + - name: TestProjects + value: 'Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj' + - name: RestoreBuildProjects + value: 'Jellyfin.Server/Jellyfin.Server.csproj' + +pr: + autoCancel: true + +trigger: + batch: true + branches: + include: + - master + +jobs: + - job: main_build + displayName: Main Build + pool: + vmImage: ubuntu-16.04 + strategy: + matrix: + release: + BuildConfiguration: Release + debug: + BuildConfiguration: Debug + maxParallel: 2 + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: false + + - task: DotNetCoreCLI@2 + displayName: Restore + inputs: + command: restore + projects: '$(RestoreBuildProjects)' + + - task: DotNetCoreCLI@2 + displayName: Build + inputs: + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' + + - task: DotNetCoreCLI@2 + displayName: Test + inputs: + command: test + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration)' + enabled: false + + - task: DotNetCoreCLI@2 + displayName: Publish + inputs: + command: publish + publishWebProjects: false + projects: '$(RestoreBuildProjects)' + arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' + zipAfterPublish: false + + # - task: PublishBuildArtifacts@1 + # displayName: 'Publish Artifact' + # inputs: + # PathtoPublish: '$(build.artifactstagingdirectory)' + # artifactName: 'jellyfin-build-$(BuildConfiguration)' + # zipAfterPublish: true + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Naming' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll' + artifactName: 'Jellyfin.Naming' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Controller' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' + artifactName: 'Jellyfin.Controller' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Model' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll' + artifactName: 'Jellyfin.Model' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact Common' + condition: eq(variables['BuildConfiguration'], 'Release') + inputs: + PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll' + artifactName: 'Jellyfin.Common' + + - job: dotnet_compat + displayName: Compatibility Check + pool: + vmImage: ubuntu-16.04 + dependsOn: main_build + condition: succeeded() + strategy: + matrix: + Naming: + NugetPackageName: Jellyfin.Naming + AssemblyFileName: Emby.Naming.dll + Controller: + NugetPackageName: Jellyfin.Controller + AssemblyFileName: MediaBrowser.Controller.dll + Model: + NugetPackageName: Jellyfin.Model + AssemblyFileName: MediaBrowser.Model.dll + Common: + NugetPackageName: Jellyfin.Common + AssemblyFileName: MediaBrowser.Common.dll + maxParallel: 2 + steps: + - checkout: none + + - task: NuGetCommand@2 + displayName: 'Download $(NugetPackageName)' + inputs: + command: custom + arguments: 'install $(NugetPackageName) -OutputDirectory $(System.ArtifactsDirectory)/packages -ExcludeVersion -DirectDownload' + + - task: CopyFiles@2 + displayName: Copy Nuget Assembly to current-release folder + inputs: + sourceFolder: $(System.ArtifactsDirectory)/packages/$(NugetPackageName) # Optional + contents: '**/*.dll' + targetFolder: $(System.ArtifactsDirectory)/current-release + cleanTargetFolder: true # Optional + overWrite: true # Optional + flattenFolders: true # Optional + + - task: DownloadBuildArtifacts@0 + displayName: Download the Assembly Build Artifact + inputs: + buildType: 'current' # Options: current, specific + allowPartiallySucceededBuilds: false # Optional + downloadType: 'single' # Options: single, specific + artifactName: '$(NugetPackageName)' # Required when downloadType == Single + downloadPath: '$(System.ArtifactsDirectory)/new-artifacts' + + - task: CopyFiles@2 + displayName: Copy Artifact Assembly to new-release folder + inputs: + sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional + contents: '**/*.dll' + targetFolder: $(System.ArtifactsDirectory)/new-release + cleanTargetFolder: true # Optional + overWrite: true # Optional + flattenFolders: true # Optional + + - task: DownloadGitHubRelease@0 + displayName: Download ABI compatibility check tool from GitHub + inputs: + connection: Jellyfin GitHub + userRepository: EraYaN/dotnet-compatibility + defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag + #version: # Required when defaultVersionType != Latest + itemPattern: '**-ci.zip' # Optional + downloadPath: '$(System.ArtifactsDirectory)' + + - task: ExtractFiles@1 + displayName: Extract ABI compatibility check tool + inputs: + archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip' + destinationFolder: $(System.ArtifactsDirectory)/tools + cleanDestinationFolder: true + + - task: CmdLine@2 + displayName: Execute ABI compatibility check tool + inputs: + script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)' + workingDirectory: $(System.ArtifactsDirectory) # Optional + #failOnStderr: false # Optional + + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b397b3280..81857e57c7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -23,6 +23,7 @@ - [fruhnow](https://github.com/fruhnow) - [Lynxy](https://github.com/Lynxy) - [fasheng](https://github.com/fasheng) + - [ploughpuff](https://github.com/ploughpuff) # Emby Contributors diff --git a/DvdLib/Ifo/AudioAttributes.cs b/DvdLib/Ifo/AudioAttributes.cs deleted file mode 100644 index b76f9fc05e..0000000000 --- a/DvdLib/Ifo/AudioAttributes.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace DvdLib.Ifo -{ - public enum AudioCodec - { - AC3 = 0, - MPEG1 = 2, - MPEG2ext = 3, - LPCM = 4, - DTS = 6, - } - - public enum ApplicationMode - { - Unspecified = 0, - Karaoke = 1, - Surround = 2, - } - - public class AudioAttributes - { - public readonly AudioCodec Codec; - public readonly bool MultichannelExtensionPresent; - public readonly ApplicationMode Mode; - public readonly byte QuantDRC; - public readonly byte SampleRate; - public readonly byte Channels; - public readonly ushort LanguageCode; - public readonly byte LanguageExtension; - public readonly byte CodeExtension; - } - - public class MultiChannelExtension - { - - } -} diff --git a/DvdLib/Ifo/PgcCommandTable.cs b/DvdLib/Ifo/PgcCommandTable.cs deleted file mode 100644 index d329fcba2a..0000000000 --- a/DvdLib/Ifo/PgcCommandTable.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; - -namespace DvdLib.Ifo -{ - public class ProgramChainCommandTable - { - public readonly ushort LastByteAddress; - public readonly List PreCommands; - public readonly List PostCommands; - public readonly List CellCommands; - } - - public class VirtualMachineCommand - { - public readonly byte[] Command; - } -} diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs index 80889738f1..7b003005b9 100644 --- a/DvdLib/Ifo/ProgramChain.cs +++ b/DvdLib/Ifo/ProgramChain.cs @@ -25,13 +25,10 @@ namespace DvdLib.Ifo public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries private ushort _nextProgramNumber; - public readonly ProgramChain Next; private ushort _prevProgramNumber; - public readonly ProgramChain Previous; private ushort _goupProgramNumber; - public readonly ProgramChain Goup; // ?? maybe Group public ProgramPlaybackMode PlaybackMode { get; private set; } public uint ProgramCount { get; private set; } @@ -40,7 +37,6 @@ namespace DvdLib.Ifo public byte[] Palette { get; private set; } // 16*4 entries private ushort _commandTableOffset; - public readonly ProgramChainCommandTable CommandTable; private ushort _programMapOffset; private ushort _cellPlaybackOffset; diff --git a/DvdLib/Ifo/VideoAttributes.cs b/DvdLib/Ifo/VideoAttributes.cs deleted file mode 100644 index 8b3996715c..0000000000 --- a/DvdLib/Ifo/VideoAttributes.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace DvdLib.Ifo -{ - public enum VideoCodec - { - MPEG1 = 0, - MPEG2 = 1, - } - - public enum VideoFormat - { - NTSC = 0, - PAL = 1, - } - - public enum AspectRatio - { - ar4to3 = 0, - ar16to9 = 3 - } - - public enum FilmMode - { - None = -1, - Camera = 0, - Film = 1, - } - - public class VideoAttributes - { - public readonly VideoCodec Codec; - public readonly VideoFormat Format; - public readonly AspectRatio Aspect; - public readonly bool AutomaticPanScan; - public readonly bool AutomaticLetterBox; - public readonly bool Line21CCField1; - public readonly bool Line21CCField2; - public readonly int Width; - public readonly int Height; - public readonly bool Letterboxed; - public readonly FilmMode FilmMode; - - public VideoAttributes() - { - } - } -} diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs index 68bf801637..8bf3797f85 100644 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ b/Emby.Dlna/Api/DlnaServerService.cs @@ -136,7 +136,7 @@ namespace Emby.Dlna.Api { var url = Request.AbsoluteUri; var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers.ToDictionary(), request.UuId, serverAddress); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress); var cacheLength = TimeSpan.FromDays(1); var cacheKey = Request.RawUrl.GetMD5(); @@ -147,21 +147,21 @@ namespace Emby.Dlna.Api public object Get(GetContentDirectory request) { - var xml = ContentDirectory.GetServiceXml(Request.Headers.ToDictionary()); + var xml = ContentDirectory.GetServiceXml(); return _resultFactory.GetResult(Request, xml, XMLContentType); } public object Get(GetMediaReceiverRegistrar request) { - var xml = MediaReceiverRegistrar.GetServiceXml(Request.Headers.ToDictionary()); + var xml = MediaReceiverRegistrar.GetServiceXml(); return _resultFactory.GetResult(Request, xml, XMLContentType); } public object Get(GetConnnectionManager request) { - var xml = ConnectionManager.GetServiceXml(Request.Headers.ToDictionary()); + var xml = ConnectionManager.GetServiceXml(); return _resultFactory.GetResult(Request, xml, XMLContentType); } @@ -193,7 +193,7 @@ namespace Emby.Dlna.Api return service.ProcessControlRequest(new ControlRequest { - Headers = Request.Headers.ToDictionary(), + Headers = Request.Headers, InputXml = requestStream, TargetServerUuId = id, RequestedUrl = Request.AbsoluteUri diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs index 679eecc891..83011fbabd 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -21,7 +20,7 @@ namespace Emby.Dlna.ConnectionManager _logger = logger; } - public string GetServiceXml(IDictionary headers) + public string GetServiceXml() { return new ConnectionManagerXmlBuilder().GetXml(); } diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs index 3e2927845e..5175898ab7 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs @@ -67,7 +67,7 @@ namespace Emby.Dlna.ContentDirectory } } - public string GetServiceXml(IDictionary headers) + public string GetServiceXml() { return new ContentDirectoryXmlBuilder().GetXml(); } diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index afd9a0b874..8c227159c4 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; using System.IO; +using Microsoft.AspNetCore.Http; namespace Emby.Dlna { public class ControlRequest { - public IDictionary Headers { get; set; } + public IHeaderDictionary Headers { get; set; } public Stream InputXml { get; set; } @@ -15,7 +15,7 @@ namespace Emby.Dlna public ControlRequest() { - Headers = new Dictionary(); + Headers = new HeaderDictionary(); } } } diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index d6ee5d13ac..2b76d27025 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -17,7 +17,9 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace Emby.Dlna { @@ -203,16 +205,13 @@ namespace Emby.Dlna } } - public DeviceProfile GetProfile(IDictionary headers) + public DeviceProfile GetProfile(IHeaderDictionary headers) { if (headers == null) { throw new ArgumentNullException(nameof(headers)); } - // Convert to case insensitive - headers = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); - var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification)); if (profile != null) @@ -228,12 +227,12 @@ namespace Emby.Dlna return profile; } - private bool IsMatch(IDictionary headers, DeviceIdentification profileInfo) + private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo) { return profileInfo.Headers.Any(i => IsMatch(headers, i)); } - private bool IsMatch(IDictionary headers, HttpHeaderInfo header) + private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header) { // Handle invalid user setup if (string.IsNullOrEmpty(header.Name)) @@ -241,14 +240,14 @@ namespace Emby.Dlna return false; } - if (headers.TryGetValue(header.Name, out string value)) + if (headers.TryGetValue(header.Name, out StringValues value)) { switch (header.Match) { case HeaderMatchType.Equals: return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase); case HeaderMatchType.Substring: - var isMatch = value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; + var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; //_logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch); return isMatch; case HeaderMatchType.Regex: @@ -494,7 +493,7 @@ namespace Emby.Dlna internal string Path { get; set; } } - public string GetServerDescriptionXml(IDictionary headers, string serverUuId, string serverAddress) + public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) { var profile = GetProfile(headers) ?? GetDefaultProfile(); diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 71ded23373..4c07087c53 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -58,4 +58,9 @@ + + + + + diff --git a/Emby.Dlna/IUpnpService.cs b/Emby.Dlna/IUpnpService.cs index ab8aa46192..ae90e95c79 100644 --- a/Emby.Dlna/IUpnpService.cs +++ b/Emby.Dlna/IUpnpService.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Emby.Dlna { public interface IUpnpService @@ -7,9 +5,8 @@ namespace Emby.Dlna /// /// Gets the content directory XML. /// - /// The headers. /// System.String. - string GetServiceXml(IDictionary headers); + string GetServiceXml(); /// /// Processes the control request. diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs index eeae3acb6f..b565cb631e 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Emby.Dlna.Service; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -16,7 +15,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar _config = config; } - public string GetServiceXml(IDictionary headers) + public string GetServiceXml() { return new MediaReceiverRegistrarXmlBuilder().GetXml(); } diff --git a/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs b/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs deleted file mode 100644 index fdf435bcf3..0000000000 --- a/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Emby.Dlna.PlayTo -{ - public class CurrentIdEventArgs : EventArgs - { - public string Id { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index b62c5e1d4c..0c5ddee654 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1126,6 +1126,11 @@ namespace Emby.Dlna.PlayTo private void OnPlaybackStart(uBaseObject mediaInfo) { + if (string.IsNullOrWhiteSpace(mediaInfo.Url)) + { + return; + } + PlaybackStart?.Invoke(this, new PlaybackStartEventArgs { MediaInfo = mediaInfo @@ -1134,8 +1139,7 @@ namespace Emby.Dlna.PlayTo private void OnPlaybackProgress(uBaseObject mediaInfo) { - var mediaUrl = mediaInfo.Url; - if (string.IsNullOrWhiteSpace(mediaUrl)) + if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { return; } @@ -1148,7 +1152,6 @@ namespace Emby.Dlna.PlayTo private void OnPlaybackStop(uBaseObject mediaInfo) { - PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs { MediaInfo = mediaInfo diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index be86dde16a..67d5cfef42 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.Didl; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; @@ -17,8 +18,8 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Services; using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo @@ -847,13 +848,13 @@ namespace Emby.Dlna.PlayTo if (index == -1) return request; var query = url.Substring(index + 1); - QueryParamCollection values = MyHttpUtility.ParseQueryString(query); + Dictionary values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); - request.DeviceProfileId = values.Get("DeviceProfileId"); - request.DeviceId = values.Get("DeviceId"); - request.MediaSourceId = values.Get("MediaSourceId"); - request.LiveStreamId = values.Get("LiveStreamId"); - request.IsDirectStream = string.Equals("true", values.Get("Static"), StringComparison.OrdinalIgnoreCase); + request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId"); + request.DeviceId = values.GetValueOrDefault("DeviceId"); + request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); + request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); + request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); @@ -867,9 +868,9 @@ namespace Emby.Dlna.PlayTo } } - private static int? GetIntValue(QueryParamCollection values, string name) + private static int? GetIntValue(IReadOnlyDictionary values, string name) { - var value = values.Get(name); + var value = values.GetValueOrDefault(name); if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { @@ -879,9 +880,9 @@ namespace Emby.Dlna.PlayTo return null; } - private static long GetLongValue(QueryParamCollection values, string name) + private static long GetLongValue(IReadOnlyDictionary values, string name) { - var value = values.Get(name); + var value = values.GetValueOrDefault(name); if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index aceb634e33..446d8e1e6e 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -9,8 +9,6 @@ namespace Emby.Dlna.PlayTo { public class PlaylistItemFactory { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public PlaylistItem Create(Photo item, DeviceProfile profile) { var playlistItem = new PlaylistItem diff --git a/Emby.Dlna/PlayTo/TransportStateEventArgs.cs b/Emby.Dlna/PlayTo/TransportStateEventArgs.cs deleted file mode 100644 index 7dcd39e107..0000000000 --- a/Emby.Dlna/PlayTo/TransportStateEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Emby.Dlna.PlayTo -{ - public class TransportStateEventArgs : EventArgs - { - public TRANSPORTSTATE State { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/uParser.cs b/Emby.Dlna/PlayTo/uParser.cs deleted file mode 100644 index 3a0ffffd41..0000000000 --- a/Emby.Dlna/PlayTo/uParser.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public class uParser - { - public static IList ParseBrowseXml(XDocument doc) - { - if (doc == null) - { - throw new ArgumentException("doc"); - } - - var list = new List(); - - var document = doc.Document; - - if (document == null) - return list; - - var item = (from result in document.Descendants("Result") select result).FirstOrDefault(); - - if (item == null) - return list; - - var uPnpResponse = XElement.Parse((string)item); - - var uObjects = from container in uPnpResponse.Elements(uPnpNamespaces.containers) - select new uParserObject { Element = container }; - - var uObjects2 = from container in uPnpResponse.Elements(uPnpNamespaces.items) - select new uParserObject { Element = container }; - - list.AddRange(uObjects.Concat(uObjects2).Select(CreateObjectFromXML).Where(uObject => uObject != null)); - - return list; - } - - public static uBaseObject CreateObjectFromXML(uParserObject uItem) - { - return UpnpContainer.Create(uItem.Element); - } - } -} diff --git a/Emby.Dlna/PlayTo/uParserObject.cs b/Emby.Dlna/PlayTo/uParserObject.cs deleted file mode 100644 index 87a7f69c62..0000000000 --- a/Emby.Dlna/PlayTo/uParserObject.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public class uParserObject - { - public XElement Element { get; set; } - } -} diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 739f687678..98cd97c318 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -30,13 +30,10 @@ namespace Emby.Server.Implementations.Activity public class ActivityLogEntryPoint : IServerEntryPoint { private readonly IInstallationManager _installationManager; - - //private readonly ILogger _logger; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; - private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; @@ -61,41 +58,37 @@ namespace Emby.Server.Implementations.Activity public Task RunAsync() { - _taskManager.TaskCompleted += _taskManager_TaskCompleted; + _taskManager.TaskCompleted += OnTaskCompleted; - _installationManager.PluginInstalled += _installationManager_PluginInstalled; - _installationManager.PluginUninstalled += _installationManager_PluginUninstalled; - _installationManager.PluginUpdated += _installationManager_PluginUpdated; - _installationManager.PackageInstallationFailed += _installationManager_PackageInstallationFailed; + _installationManager.PluginInstalled += OnPluginInstalled; + _installationManager.PluginUninstalled += OnPluginUninstalled; + _installationManager.PluginUpdated += OnPluginUpdated; + _installationManager.PackageInstallationFailed += OnPackageInstallationFailed; - _sessionManager.SessionStarted += _sessionManager_SessionStarted; - _sessionManager.AuthenticationFailed += _sessionManager_AuthenticationFailed; - _sessionManager.AuthenticationSucceeded += _sessionManager_AuthenticationSucceeded; - _sessionManager.SessionEnded += _sessionManager_SessionEnded; + _sessionManager.SessionStarted += OnSessionStarted; + _sessionManager.AuthenticationFailed += OnAuthenticationFailed; + _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded; + _sessionManager.SessionEnded += OnSessionEnded; - _sessionManager.PlaybackStart += _sessionManager_PlaybackStart; - _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; + _sessionManager.PlaybackStart += OnPlaybackStart; + _sessionManager.PlaybackStopped += OnPlaybackStopped; - //_subManager.SubtitlesDownloaded += _subManager_SubtitlesDownloaded; - _subManager.SubtitleDownloadFailure += _subManager_SubtitleDownloadFailure; + _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; - _userManager.UserCreated += _userManager_UserCreated; - _userManager.UserPasswordChanged += _userManager_UserPasswordChanged; - _userManager.UserDeleted += _userManager_UserDeleted; - _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated; - _userManager.UserLockedOut += _userManager_UserLockedOut; + _userManager.UserCreated += OnUserCreated; + _userManager.UserPasswordChanged += OnUserPasswordChanged; + _userManager.UserDeleted += OnUserDeleted; + _userManager.UserPolicyUpdated += OnUserPolicyUpdated; + _userManager.UserLockedOut += OnUserLockedOut; - //_config.ConfigurationUpdated += _config_ConfigurationUpdated; - //_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; + _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - _deviceManager.CameraImageUploaded += _deviceManager_CameraImageUploaded; - - _appHost.ApplicationUpdated += _appHost_ApplicationUpdated; + _appHost.ApplicationUpdated += OnApplicationUpdated; return Task.CompletedTask; } - void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs e) + private void OnCameraImageUploaded(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -104,7 +97,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserLockedOut(object sender, GenericEventArgs e) + private void OnUserLockedOut(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -114,7 +107,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _subManager_SubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) + private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -125,7 +118,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e) + private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) { var item = e.MediaInfo; @@ -146,7 +139,7 @@ namespace Emby.Server.Implementations.Activity return; } - var user = e.Users.First(); + var user = e.Users[0]; CreateLogEntry(new ActivityLogEntry { @@ -156,7 +149,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) + private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) { var item = e.MediaInfo; @@ -232,7 +225,7 @@ namespace Emby.Server.Implementations.Activity return null; } - void _sessionManager_SessionEnded(object sender, SessionEventArgs e) + private void OnSessionEnded(object sender, SessionEventArgs e) { string name; var session = e.SessionInfo; @@ -258,7 +251,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_AuthenticationSucceeded(object sender, GenericEventArgs e) + private void OnAuthenticationSucceeded(object sender, GenericEventArgs e) { var user = e.Argument.User; @@ -271,7 +264,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _sessionManager_AuthenticationFailed(object sender, GenericEventArgs e) + private void OnAuthenticationFailed(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -282,7 +275,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _appHost_ApplicationUpdated(object sender, GenericEventArgs e) + private void OnApplicationUpdated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -292,25 +285,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("MessageNamedServerConfigurationUpdatedWithValue"), e.Key), - Type = "NamedConfigurationUpdated" - }); - } - - void _config_ConfigurationUpdated(object sender, EventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = _localization.GetLocalizedString("MessageServerConfigurationUpdated"), - Type = "ServerConfigurationUpdated" - }); - } - - void _userManager_UserPolicyUpdated(object sender, GenericEventArgs e) + private void OnUserPolicyUpdated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -320,7 +295,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserDeleted(object sender, GenericEventArgs e) + private void OnUserDeleted(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -329,7 +304,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserPasswordChanged(object sender, GenericEventArgs e) + private void OnUserPasswordChanged(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -339,7 +314,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _userManager_UserCreated(object sender, GenericEventArgs e) + private void OnUserCreated(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -349,18 +324,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _subManager_SubtitlesDownloaded(object sender, SubtitleDownloadEventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("SubtitlesDownloadedForItem"), Notifications.Notifications.GetItemName(e.Item)), - Type = "SubtitlesDownloaded", - ItemId = e.Item.Id.ToString("N"), - ShortOverview = string.Format(_localization.GetLocalizedString("ProviderValue"), e.Provider) - }); - } - - void _sessionManager_SessionStarted(object sender, SessionEventArgs e) + private void OnSessionStarted(object sender, SessionEventArgs e) { string name; var session = e.SessionInfo; @@ -386,7 +350,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PluginUpdated(object sender, GenericEventArgs> e) + private void OnPluginUpdated(object sender, GenericEventArgs> e) { CreateLogEntry(new ActivityLogEntry { @@ -397,7 +361,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PluginUninstalled(object sender, GenericEventArgs e) + private void OnPluginUninstalled(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -406,7 +370,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PluginInstalled(object sender, GenericEventArgs e) + private void OnPluginInstalled(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -416,7 +380,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _installationManager_PackageInstallationFailed(object sender, InstallationFailedEventArgs e) + private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) { var installationInfo = e.InstallationInfo; @@ -429,7 +393,7 @@ namespace Emby.Server.Implementations.Activity }); } - void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e) + private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) { var result = e.Result; var task = e.Task; @@ -468,48 +432,36 @@ namespace Emby.Server.Implementations.Activity } private void CreateLogEntry(ActivityLogEntry entry) - { - try - { - _activityManager.Create(entry); - } - catch - { - // Logged at lower levels - } - } + => _activityManager.Create(entry); public void Dispose() { - _taskManager.TaskCompleted -= _taskManager_TaskCompleted; + _taskManager.TaskCompleted -= OnTaskCompleted; - _installationManager.PluginInstalled -= _installationManager_PluginInstalled; - _installationManager.PluginUninstalled -= _installationManager_PluginUninstalled; - _installationManager.PluginUpdated -= _installationManager_PluginUpdated; - _installationManager.PackageInstallationFailed -= _installationManager_PackageInstallationFailed; + _installationManager.PluginInstalled -= OnPluginInstalled; + _installationManager.PluginUninstalled -= OnPluginUninstalled; + _installationManager.PluginUpdated -= OnPluginUpdated; + _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed; - _sessionManager.SessionStarted -= _sessionManager_SessionStarted; - _sessionManager.AuthenticationFailed -= _sessionManager_AuthenticationFailed; - _sessionManager.AuthenticationSucceeded -= _sessionManager_AuthenticationSucceeded; - _sessionManager.SessionEnded -= _sessionManager_SessionEnded; + _sessionManager.SessionStarted -= OnSessionStarted; + _sessionManager.AuthenticationFailed -= OnAuthenticationFailed; + _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded; + _sessionManager.SessionEnded -= OnSessionEnded; - _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart; - _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped; + _sessionManager.PlaybackStart -= OnPlaybackStart; + _sessionManager.PlaybackStopped -= OnPlaybackStopped; - _subManager.SubtitleDownloadFailure -= _subManager_SubtitleDownloadFailure; + _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; - _userManager.UserCreated -= _userManager_UserCreated; - _userManager.UserPasswordChanged -= _userManager_UserPasswordChanged; - _userManager.UserDeleted -= _userManager_UserDeleted; - _userManager.UserPolicyUpdated -= _userManager_UserPolicyUpdated; - _userManager.UserLockedOut -= _userManager_UserLockedOut; + _userManager.UserCreated -= OnUserCreated; + _userManager.UserPasswordChanged -= OnUserPasswordChanged; + _userManager.UserDeleted -= OnUserDeleted; + _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; + _userManager.UserLockedOut -= OnUserLockedOut; - _config.ConfigurationUpdated -= _config_ConfigurationUpdated; - _config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated; + _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; - _deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded; - - _appHost.ApplicationUpdated -= _appHost_ApplicationUpdated; + _appHost.ApplicationUpdated -= OnApplicationUpdated; } /// @@ -531,6 +483,7 @@ namespace Emby.Server.Implementations.Activity values.Add(CreateValueString(years, "year")); days = days % DaysInYear; } + // Number of months if (days >= DaysInMonth) { @@ -538,25 +491,39 @@ namespace Emby.Server.Implementations.Activity values.Add(CreateValueString(months, "month")); days = days % DaysInMonth; } + // Number of days if (days >= 1) + { values.Add(CreateValueString(days, "day")); + } + // Number of hours if (span.Hours >= 1) + { values.Add(CreateValueString(span.Hours, "hour")); + } // Number of minutes if (span.Minutes >= 1) + { values.Add(CreateValueString(span.Minutes, "minute")); + } + // Number of seconds (include when 0 if no other components included) if (span.Seconds >= 1 || values.Count == 0) + { values.Add(CreateValueString(span.Seconds, "second")); + } // Combine values into string var builder = new StringBuilder(); for (int i = 0; i < values.Count; i++) { if (builder.Length > 0) + { builder.Append(i == values.Count - 1 ? " and " : ", "); + } + builder.Append(values[i]); } // Return result diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ec1ad6dbdf..87447a55ae 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -28,20 +28,20 @@ using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Diagnostics; using Emby.Server.Implementations.Dto; -using Emby.Server.Implementations.FFMpeg; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; +using Emby.Server.Implementations.Middleware; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; -using Emby.Server.Implementations.Reflection; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; +using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using MediaBrowser.Api; @@ -91,7 +91,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; @@ -103,11 +102,15 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.TV.TheTVDB; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using ServiceStack; -using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate; namespace Emby.Server.Implementations { @@ -533,7 +536,7 @@ namespace Emby.Server.Implementations ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; - MediaEncoder.Init(); + MediaEncoder.SetFFmpegPath(); //if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) //{ @@ -610,6 +613,68 @@ namespace Emby.Server.Implementations await RegisterResources(serviceCollection); FindParts(); + + string contentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; + if (string.IsNullOrEmpty(contentRoot)) + { + contentRoot = Path.Combine(ServerConfigurationManager.ApplicationPaths.ApplicationResourcesPath, "jellyfin-web", "src"); + } + + var host = new WebHostBuilder() + .UseKestrel(options => + { + options.ListenAnyIP(HttpPort); + + if (EnableHttps && Certificate != null) + { + options.ListenAnyIP(HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); + } + }) + .UseContentRoot(contentRoot) + .ConfigureServices(services => + { + services.AddResponseCompression(); + services.AddHttpContextAccessor(); + }) + .Configure(app => + { + app.UseWebSockets(); + + app.UseResponseCompression(); + // TODO app.UseMiddleware(); + app.Use(ExecuteWebsocketHandlerAsync); + app.Use(ExecuteHttpHandlerAsync); + }) + .Build(); + + await host.StartAsync(); + } + + private async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func next) + { + if (!context.WebSockets.IsWebSocketRequest) + { + await next().ConfigureAwait(false); + return; + } + + await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false); + } + + private async Task ExecuteHttpHandlerAsync(HttpContext context, Func next) + { + if (context.WebSockets.IsWebSocketRequest) + { + await next().ConfigureAwait(false); + return; + } + + var request = context.Request; + var response = context.Response; + var localPath = context.Request.Path.ToString(); + + var req = new WebSocketSharpRequest(request, response, request.Path, Logger); + await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); } protected virtual IHttpClient CreateHttpClient() @@ -631,6 +696,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ApplicationPaths); + serviceCollection.AddSingleton(_configuration); + serviceCollection.AddSingleton(JsonSerializer); serviceCollection.AddSingleton(LoggerFactory); @@ -672,7 +739,7 @@ namespace Emby.Server.Implementations ZipClient = new ZipClient(); serviceCollection.AddSingleton(ZipClient); - HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, CreateBrotliCompressor()); + HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper); serviceCollection.AddSingleton(HttpResultFactory); serviceCollection.AddSingleton(this); @@ -680,9 +747,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ServerConfigurationManager); - var assemblyInfo = new AssemblyInfo(); - serviceCollection.AddSingleton(assemblyInfo); - LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LoggerFactory); await LocalizationManager.LoadAll(); serviceCollection.AddSingleton(LocalizationManager); @@ -699,7 +763,7 @@ namespace Emby.Server.Implementations var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager); serviceCollection.AddSingleton(displayPreferencesRepo); - ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, assemblyInfo); + ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory); serviceCollection.AddSingleton(ItemRepository); AuthenticationRepository = GetAuthenticationRepository(); @@ -729,7 +793,8 @@ namespace Emby.Server.Implementations _configuration, NetworkManager, JsonSerializer, - XmlSerializer); + XmlSerializer, + CreateHttpListener()); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); serviceCollection.AddSingleton(HttpServer); @@ -786,7 +851,18 @@ namespace Emby.Server.Implementations ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); serviceCollection.AddSingleton(ChapterManager); - RegisterMediaEncoder(serviceCollection); + MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( + LoggerFactory, + JsonSerializer, + StartupOptions.FFmpegPath, + StartupOptions.FFprobePath, + ServerConfigurationManager, + FileSystemManager, + () => SubtitleEncoder, + () => MediaSourceManager, + ProcessFactory, + 5000); + serviceCollection.AddSingleton(MediaEncoder); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); serviceCollection.AddSingleton(EncodingManager); @@ -822,11 +898,6 @@ namespace Emby.Server.Implementations _serviceProvider = serviceCollection.BuildServiceProvider(); } - protected virtual IBrotliCompressor CreateBrotliCompressor() - { - return null; - } - public virtual string PackageRuntime => "netcore"; public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths, EnvironmentInfo.EnvironmentInfo environmentInfo) @@ -859,11 +930,9 @@ namespace Emby.Server.Implementations } } - protected virtual bool SupportsDualModeSockets => true; - - private X509Certificate GetCertificate(CertificateInfo info) + private X509Certificate2 GetCertificate(CertificateInfo info) { - var certificateLocation = info == null ? null : info.Path; + var certificateLocation = info?.Path; if (string.IsNullOrWhiteSpace(certificateLocation)) { @@ -902,85 +971,6 @@ namespace Emby.Server.Implementations return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); } - protected virtual FFMpegInstallInfo GetFfmpegInstallInfo() - { - var info = new FFMpegInstallInfo(); - - // Windows builds: http://ffmpeg.zeranoe.com/builds/ - // Linux builds: http://johnvansickle.com/ffmpeg/ - // OS X builds: http://ffmpegmac.net/ - // OS X x64: http://www.evermeet.cx/ffmpeg/ - - if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux) - { - info.FFMpegFilename = "ffmpeg"; - info.FFProbeFilename = "ffprobe"; - info.ArchiveType = "7z"; - info.Version = "20170308"; - } - else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) - { - info.FFMpegFilename = "ffmpeg.exe"; - info.FFProbeFilename = "ffprobe.exe"; - info.Version = "20170308"; - info.ArchiveType = "7z"; - } - else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) - { - info.FFMpegFilename = "ffmpeg"; - info.FFProbeFilename = "ffprobe"; - info.ArchiveType = "7z"; - info.Version = "20170308"; - } - - return info; - } - - protected virtual FFMpegInfo GetFFMpegInfo() - { - return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo()) - .GetFFMpegInfo(StartupOptions); - } - - /// - /// Registers the media encoder. - /// - /// Task. - private void RegisterMediaEncoder(IServiceCollection serviceCollection) - { - string encoderPath = null; - string probePath = null; - - var info = GetFFMpegInfo(); - - encoderPath = info.EncoderPath; - probePath = info.ProbePath; - var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase); - - var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( - LoggerFactory, - JsonSerializer, - encoderPath, - probePath, - hasExternalEncoder, - ServerConfigurationManager, - FileSystemManager, - LiveTvManager, - IsoManager, - LibraryManager, - ChannelManager, - SessionManager, - () => SubtitleEncoder, - () => MediaSourceManager, - HttpClient, - ZipClient, - ProcessFactory, - 5000); - - MediaEncoder = mediaEncoder; - serviceCollection.AddSingleton(MediaEncoder); - } - /// /// Gets the user repository. /// @@ -1017,7 +1007,7 @@ namespace Emby.Server.Implementations /// private void SetStaticProperties() { - ((SqliteItemRepository)ItemRepository).ImageProcessor = ImageProcessor; + ItemRepository.ImageProcessor = ImageProcessor; // For now there's no real way to inject these properly BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem"); @@ -1059,9 +1049,7 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - HttpServer.Init(GetExports(false), GetExports()); - - StartServer(); + HttpServer.Init(GetExports(false), GetExports(), GetUrlPrefixes()); LibraryManager.AddParts(GetExports(), GetExports(), @@ -1145,15 +1133,12 @@ namespace Emby.Server.Implementations AllConcreteTypes = GetComposablePartAssemblies() .SelectMany(x => x.ExportedTypes) - .Where(type => - { - return type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType; - }) + .Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType) .ToArray(); } private CertificateInfo CertificateInfo { get; set; } - protected X509Certificate Certificate { get; private set; } + protected X509Certificate2 Certificate { get; private set; } private IEnumerable GetUrlPrefixes() { @@ -1175,45 +1160,7 @@ namespace Emby.Server.Implementations }); } - protected abstract IHttpListener CreateHttpListener(); - - /// - /// Starts the server. - /// - private void StartServer() - { - try - { - ((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener()); - return; - } - catch (Exception ex) - { - var msg = string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase) - ? "The http server is unable to start due to a Socket error. This can occasionally happen when the operating system takes longer than usual to release the IP bindings from the previous session. This can take up to five minutes. Please try waiting or rebooting the system." - : "Error starting Http Server"; - - Logger.LogError(ex, msg); - - if (HttpPort == ServerConfiguration.DefaultHttpPort) - { - throw; - } - } - - HttpPort = ServerConfiguration.DefaultHttpPort; - - try - { - ((HttpListenerHost)HttpServer).StartServer(GetUrlPrefixes().ToArray(), CreateHttpListener()); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error starting http server"); - - throw; - } - } + protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(Logger); private CertificateInfo GetCertificateInfo(bool generateCertificate) { @@ -1456,7 +1403,7 @@ namespace Emby.Server.Implementations ServerName = FriendlyName, LocalAddress = localAddress, SupportsLibraryMonitor = true, - EncoderLocationType = MediaEncoder.EncoderLocationType, + EncoderLocation = MediaEncoder.EncoderLocation, SystemArchitecture = EnvironmentInfo.SystemArchitecture, SystemUpdateLevel = SystemUpdateLevel, PackageName = StartupOptions.PackageName @@ -1650,7 +1597,7 @@ namespace Emby.Server.Implementations LogErrorResponseBody = false, LogErrors = logPing, LogRequest = logPing, - TimeoutMs = 30000, + TimeoutMs = 5000, BufferContent = false, CancellationToken = cancellationToken diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index cdfb5cadf1..0244c4a684 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Collections return base.Supports(item); } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var playlist = (BoxSet)item; @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Collections .ToList(); } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 30bfd87498..9bc60972a1 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -6,7 +6,8 @@ namespace Emby.Server.Implementations { public static readonly Dictionary Configuration = new Dictionary { - {"HttpListenerHost:DefaultRedirectPath", "web/index.html"} + {"HttpListenerHost:DefaultRedirectPath", "web/index.html"}, + {"MusicBrainz:BaseUrl", "https://www.musicbrainz.org"} }; } } diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 09fdbc856d..982bba625d 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,13 +1,49 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; +using System.Linq; using MediaBrowser.Model.Cryptography; namespace Emby.Server.Implementations.Cryptography { public class CryptographyProvider : ICryptoProvider { + private static readonly HashSet _supportedHashMethods = new HashSet() + { + "MD5", + "System.Security.Cryptography.MD5", + "SHA", + "SHA1", + "System.Security.Cryptography.SHA1", + "SHA256", + "SHA-256", + "System.Security.Cryptography.SHA256", + "SHA384", + "SHA-384", + "System.Security.Cryptography.SHA384", + "SHA512", + "SHA-512", + "System.Security.Cryptography.SHA512" + }; + + public string DefaultHashMethod => "PBKDF2"; + + private RandomNumberGenerator _randomNumberGenerator; + + private const int _defaultIterations = 1000; + + public CryptographyProvider() + { + //FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto + //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 + //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one + //Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1 + _randomNumberGenerator = RandomNumberGenerator.Create(); + } + public Guid GetMD5(string str) { return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); @@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography return provider.ComputeHash(bytes); } } + + public IEnumerable GetSupportedHashMethods() + { + return _supportedHashMethods; + } + + private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) + { + //downgrading for now as we need this library to be dotnetstandard compliant + //with this downgrade we'll add a check to make sure we're on the downgrade method at the moment + if (method == DefaultHashMethod) + { + using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) + { + return r.GetBytes(32); + } + } + + throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); + } + + public byte[] ComputeHash(string hashMethod, byte[] bytes) + { + return ComputeHash(hashMethod, bytes, Array.Empty()); + } + + public byte[] ComputeHashWithDefaultMethod(byte[] bytes) + { + return ComputeHash(DefaultHashMethod, bytes); + } + + public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) + { + if (hashMethod == DefaultHashMethod) + { + return PBKDF2(hashMethod, bytes, salt, _defaultIterations); + } + else if (_supportedHashMethods.Contains(hashMethod)) + { + using (var h = HashAlgorithm.Create(hashMethod)) + { + if (salt.Length == 0) + { + return h.ComputeHash(bytes); + } + else + { + byte[] salted = new byte[bytes.Length + salt.Length]; + Array.Copy(bytes, salted, bytes.Length); + Array.Copy(salt, 0, salted, bytes.Length, salt.Length); + return h.ComputeHash(salted); + } + } + } + else + { + throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); + } + } + + public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) + { + return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); + } + + public byte[] ComputeHash(PasswordHash hash) + { + int iterations = _defaultIterations; + if (!hash.Parameters.ContainsKey("iterations")) + { + hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture)); + } + else + { + try + { + iterations = int.Parse(hash.Parameters["iterations"]); + } + catch (Exception e) + { + throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); + } + } + + return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); + } + + public byte[] GenerateSalt() + { + byte[] salt = new byte[64]; + _randomNumberGenerator.GetBytes(salt); + return salt; + } } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 06f6563a32..3ae63279b6 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -24,7 +24,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -65,8 +64,7 @@ namespace Emby.Server.Implementations.Data IServerConfigurationManager config, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, - ILoggerFactory loggerFactory, - IAssemblyInfo assemblyInfo) + ILoggerFactory loggerFactory) : base(loggerFactory.CreateLogger(nameof(SqliteItemRepository))) { if (config == null) @@ -82,7 +80,7 @@ namespace Emby.Server.Implementations.Data _appHost = appHost; _config = config; _jsonSerializer = jsonSerializer; - _typeMapper = new TypeMapper(assemblyInfo); + _typeMapper = new TypeMapper(); DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); } diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index db359d7ddc..182df0edc9 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data { TryMigrateToLocalUsersTable(connection); } + + RemoveEmptyPasswordHashes(); } } @@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data } } + private void RemoveEmptyPasswordHashes() + { + foreach (var user in RetrieveAllUsers()) + { + // If the user password is the sha1 hash of the empty string, remove it + if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) + || !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) + { + continue; + } + + user.Password = null; + var serialized = _jsonSerializer.SerializeToBytes(user); + + using (WriteLock.Write()) + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId")) + { + statement.TryBind("@InternalId", user.InternalId); + statement.TryBind("@data", serialized); + statement.MoveNext(); + } + + }, TransactionMode); + } + } + + } + /// /// Save a user in the repo /// diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs index 37c952e88e..0e67affbfc 100644 --- a/Emby.Server.Implementations/Data/TypeMapper.cs +++ b/Emby.Server.Implementations/Data/TypeMapper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Linq; -using MediaBrowser.Model.Reflection; namespace Emby.Server.Implementations.Data { @@ -10,16 +9,13 @@ namespace Emby.Server.Implementations.Data /// public class TypeMapper { - private readonly IAssemblyInfo _assemblyInfo; - /// /// This holds all the types in the running assemblies so that we can de-serialize properly when we don't have strong types /// private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); - public TypeMapper(IAssemblyInfo assemblyInfo) + public TypeMapper() { - _assemblyInfo = assemblyInfo; } /// @@ -45,8 +41,7 @@ namespace Emby.Server.Implementations.Data /// Type. private Type LookupType(string typeName) { - return _assemblyInfo - .GetCurrentAssemblies() + return AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(typeName)) .FirstOrDefault(t => t != null); } diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs index 78b22bda3f..2c4ef170dc 100644 --- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs +++ b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Diagnostics public void Dispose() { - _process.Dispose(); + _process?.Dispose(); } } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index bbf165d627..3cbd2792d9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -9,7 +9,6 @@ - @@ -22,6 +21,14 @@ + + + + + + + + @@ -40,6 +47,21 @@ false + + true + + + + + + + + + + + ../jellyfin.ruleset + + diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs deleted file mode 100644 index 60cd7b3d72..0000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Emby.Server.Implementations.FFMpeg -{ - /// - /// Class FFMpegInfo - /// - public class FFMpegInfo - { - /// - /// Gets or sets the path. - /// - /// The path. - public string EncoderPath { get; set; } - /// - /// Gets or sets the probe path. - /// - /// The probe path. - public string ProbePath { get; set; } - /// - /// Gets or sets the version. - /// - /// The version. - public string Version { get; set; } - } -} diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs deleted file mode 100644 index fa9cb5e01b..0000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Emby.Server.Implementations.FFMpeg -{ - public class FFMpegInstallInfo - { - public string Version { get; set; } - public string FFMpegFilename { get; set; } - public string FFProbeFilename { get; set; } - public string ArchiveType { get; set; } - - public FFMpegInstallInfo() - { - Version = "Path"; - FFMpegFilename = "ffmpeg"; - FFProbeFilename = "ffprobe"; - } - } -} diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs deleted file mode 100644 index bbf51dd246..0000000000 --- a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.FFMpeg -{ - public class FFMpegLoader - { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly FFMpegInstallInfo _ffmpegInstallInfo; - - public FFMpegLoader(IApplicationPaths appPaths, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - _ffmpegInstallInfo = ffmpegInstallInfo; - } - - public FFMpegInfo GetFFMpegInfo(IStartupOptions options) - { - var customffMpegPath = options.FFmpegPath; - var customffProbePath = options.FFprobePath; - - if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath)) - { - return new FFMpegInfo - { - ProbePath = customffProbePath, - EncoderPath = customffMpegPath, - Version = "external" - }; - } - - var downloadInfo = _ffmpegInstallInfo; - - var prebuiltFolder = _appPaths.ProgramSystemPath; - var prebuiltffmpeg = Path.Combine(prebuiltFolder, downloadInfo.FFMpegFilename); - var prebuiltffprobe = Path.Combine(prebuiltFolder, downloadInfo.FFProbeFilename); - if (File.Exists(prebuiltffmpeg) && File.Exists(prebuiltffprobe)) - { - return new FFMpegInfo - { - ProbePath = prebuiltffprobe, - EncoderPath = prebuiltffmpeg, - Version = "external" - }; - } - - var version = downloadInfo.Version; - - if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase)) - { - return new FFMpegInfo(); - } - - var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg"); - var versionedDirectoryPath = Path.Combine(rootEncoderPath, version); - - var info = new FFMpegInfo - { - ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename), - EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename), - Version = version - }; - - Directory.CreateDirectory(versionedDirectoryPath); - - var excludeFromDeletions = new List { versionedDirectoryPath }; - - if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath)) - { - // ffmpeg not present. See if there's an older version we can start with - var existingVersion = GetExistingVersion(info, rootEncoderPath); - - // No older version. Need to download and block until complete - if (existingVersion == null) - { - return new FFMpegInfo(); - } - else - { - info = existingVersion; - versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath); - excludeFromDeletions.Add(versionedDirectoryPath); - } - } - - // Allow just one of these to be overridden, if desired. - if (!string.IsNullOrWhiteSpace(customffMpegPath)) - { - info.EncoderPath = customffMpegPath; - } - if (!string.IsNullOrWhiteSpace(customffProbePath)) - { - info.ProbePath = customffProbePath; - } - - return info; - } - - private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath) - { - var encoderFilename = Path.GetFileName(info.EncoderPath); - var probeFilename = Path.GetFileName(info.ProbePath); - - foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath)) - { - var allFiles = _fileSystem.GetFilePaths(directory, true).ToList(); - - var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase)); - var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrWhiteSpace(encoder) && - !string.IsNullOrWhiteSpace(probe)) - { - return new FFMpegInfo - { - EncoderPath = encoder, - ProbePath = probe, - Version = Path.GetFileName(Path.GetDirectoryName(probe)) - }; - } - } - - return null; - } - } -} diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 2e07281369..1bebdd1637 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -15,6 +15,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpClientManager { @@ -179,11 +180,11 @@ namespace Emby.Server.Implementations.HttpClientManager foreach (var header in options.RequestHeaders) { - if (string.Equals(header.Key, "Accept", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase)) { request.Accept = header.Value; } - else if (string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)) { SetUserAgent(request, header.Value); hasUserAgent = true; @@ -327,7 +328,6 @@ namespace Emby.Server.Implementations.HttpClientManager } httpWebRequest.ContentType = contentType; - httpWebRequest.ContentLength = bytes.Length; (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length); } catch (Exception ex) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 7aedba9b31..c4d2a70e23 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -5,15 +5,19 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { public class FileWriter : IHttpResult { + private readonly IStreamHelper _streamHelper; private ILogger Logger { get; set; } + private readonly IFileSystem _fileSystem; private string RangeHeader { get; set; } private bool IsHeadRequest { get; set; } @@ -42,25 +46,27 @@ namespace Emby.Server.Implementations.HttpServer public string Path { get; set; } - public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem) + public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) { if (string.IsNullOrEmpty(contentType)) { throw new ArgumentNullException(nameof(contentType)); } + _streamHelper = streamHelper; + _fileSystem = fileSystem; + Path = path; Logger = logger; RangeHeader = rangeHeader; - Headers["Content-Type"] = contentType; + Headers[HeaderNames.ContentType] = contentType; TotalContentLength = fileSystem.GetFileInfo(path).Length; - Headers["Accept-Ranges"] = "bytes"; + Headers[HeaderNames.AcceptRanges] = "bytes"; if (string.IsNullOrWhiteSpace(rangeHeader)) { - Headers["Content-Length"] = TotalContentLength.ToString(UsCulture); StatusCode = HttpStatusCode.OK; } else @@ -93,13 +99,10 @@ namespace Emby.Server.Implementations.HttpServer RangeStart = requestedRange.Key; RangeLength = 1 + RangeEnd - RangeStart; - // Content-Length is the length of what we're serving, not the original content - var lengthString = RangeLength.ToString(UsCulture); - Headers["Content-Length"] = lengthString; - var rangeString = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); - Headers["Content-Range"] = rangeString; + var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; + Headers[HeaderNames.ContentRange] = rangeString; - Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); + Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString); } /// @@ -145,8 +148,7 @@ namespace Emby.Server.Implementations.HttpServer } } - private string[] SkipLogExtensions = new string[] - { + private static readonly string[] SkipLogExtensions = { ".js", ".html", ".css" @@ -163,8 +165,10 @@ namespace Emby.Server.Implementations.HttpServer } var path = Path; + var offset = RangeStart; + var count = RangeLength; - if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)) + if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1) { var extension = System.IO.Path.GetExtension(path); @@ -173,20 +177,15 @@ namespace Emby.Server.Implementations.HttpServer Logger.LogDebug("Transmit file {0}", path); } - //var count = FileShare == FileShareMode.ReadWrite ? TotalContentLength : 0; - - await response.TransmitFile(path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false); - return; + offset = 0; + count = 0; } - await response.TransmitFile(path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false); + await response.TransmitFile(path, offset, count, FileShare, _fileSystem, _streamHelper, cancellationToken).ConfigureAwait(false); } finally { - if (OnComplete != null) - { - OnComplete(); - } + OnComplete?.Invoke(); } } @@ -203,8 +202,5 @@ namespace Emby.Server.Implementations.HttpServer get => (HttpStatusCode)Status; set => Status = (int)value; } - - public string StatusDescription { get; set; } - } } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index ee746c6696..e8d47cad52 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; +using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -20,6 +21,9 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ServiceStack.Text.Jsv; @@ -29,12 +33,8 @@ namespace Emby.Server.Implementations.HttpServer public class HttpListenerHost : IHttpServer, IDisposable { private string DefaultRedirectPath { get; set; } - - private readonly ILogger _logger; public string[] UrlPrefixes { get; private set; } - private IHttpListener _listener; - public event EventHandler> WebSocketConnected; private readonly IServerConfigurationManager _config; @@ -42,6 +42,7 @@ namespace Emby.Server.Implementations.HttpServer private readonly IServerApplicationHost _appHost; private readonly IJsonSerializer _jsonSerializer; private readonly IXmlSerializer _xmlSerializer; + private readonly IHttpListener _socketListener; private readonly Func> _funcParseFn; public Action[] ResponseFilters { get; set; } @@ -59,15 +60,18 @@ namespace Emby.Server.Implementations.HttpServer IConfiguration configuration, INetworkManager networkManager, IJsonSerializer jsonSerializer, - IXmlSerializer xmlSerializer) + IXmlSerializer xmlSerializer, + IHttpListener socketListener) { _appHost = applicationHost; - _logger = loggerFactory.CreateLogger("HttpServer"); + Logger = loggerFactory.CreateLogger("HttpServer"); _config = config; DefaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; + _socketListener = socketListener; + _socketListener.WebSocketConnected = OnWebSocketConnected; _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); @@ -77,7 +81,7 @@ namespace Emby.Server.Implementations.HttpServer public string GlobalResponse { get; set; } - protected ILogger Logger => _logger; + protected ILogger Logger { get; } public object CreateInstance(Type type) { @@ -143,11 +147,11 @@ namespace Emby.Server.Implementations.HttpServer return; } - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, Logger) { OnReceive = ProcessWebSocketMessageReceived, Url = e.Url, - QueryString = e.QueryString ?? new QueryParamCollection() + QueryString = e.QueryString ?? new QueryCollection() }; connection.Closed += Connection_Closed; @@ -212,16 +216,16 @@ namespace Emby.Server.Implementations.HttpServer if (logExceptionStackTrace) { - _logger.LogError(ex, "Error processing request"); + Logger.LogError(ex, "Error processing request"); } else if (logExceptionMessage) { - _logger.LogError(ex.Message); + Logger.LogError(ex.Message); } var httpRes = httpReq.Response; - if (httpRes.IsClosed) + if (httpRes.OriginalResponse.HasStarted) { return; } @@ -234,7 +238,7 @@ namespace Emby.Server.Implementations.HttpServer } catch (Exception errorEx) { - _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)"); + Logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)"); } } @@ -277,14 +281,6 @@ namespace Emby.Server.Implementations.HttpServer } } - - if (_listener != null) - { - _logger.LogInformation("Stopping HttpListener..."); - var task = _listener.Stop(); - Task.WaitAll(task); - _logger.LogInformation("HttpListener stopped"); - } } public static string RemoveQueryStringByKey(string url, string key) @@ -292,7 +288,7 @@ namespace Emby.Server.Implementations.HttpServer var uri = new Uri(url); // this gets all the query string key value pairs as a collection - var newQueryString = MyHttpUtility.ParseQueryString(uri.Query); + var newQueryString = QueryHelpers.ParseQuery(uri.Query); var originalCount = newQueryString.Count; @@ -313,7 +309,7 @@ namespace Emby.Server.Implementations.HttpServer string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; return newQueryString.Count > 0 - ? string.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString) + ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString())) : pagePathWithoutQueryString; } @@ -422,7 +418,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// Overridable method that can be used to implement a custom hnandler /// - protected async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) + public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) { var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -599,17 +595,15 @@ namespace Emby.Server.Implementations.HttpServer } finally { - httpRes.Close(); - stopWatch.Stop(); var elapsed = stopWatch.Elapsed; if (elapsed.TotalMilliseconds > 500) { - _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); + Logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } else { - _logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); + Logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } } } @@ -622,7 +616,7 @@ namespace Emby.Server.Implementations.HttpServer var pathParts = pathInfo.TrimStart('/').Split('/'); if (pathParts.Length == 0) { - _logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); + Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl); return null; } @@ -636,15 +630,13 @@ namespace Emby.Server.Implementations.HttpServer }; } - _logger.LogError("Could not find handler for {PathInfo}", pathInfo); + Logger.LogError("Could not find handler for {PathInfo}", pathInfo); return null; } private static Task Write(IResponse response, string text) { var bOutput = Encoding.UTF8.GetBytes(text); - response.SetContentLength(bOutput.Length); - return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); } @@ -663,6 +655,7 @@ namespace Emby.Server.Implementations.HttpServer } else { + // TODO what is this? var httpsUrl = url .Replace("http://", "https://", StringComparison.OrdinalIgnoreCase) .Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); @@ -683,13 +676,15 @@ namespace Emby.Server.Implementations.HttpServer /// Adds the rest handlers. /// /// The services. - public void Init(IEnumerable services, IEnumerable listeners) + /// + /// + public void Init(IEnumerable services, IEnumerable listeners, IEnumerable urlPrefixes) { _webSocketListeners = listeners.ToArray(); - + UrlPrefixes = urlPrefixes.ToArray(); ServiceController = new ServiceController(); - _logger.LogInformation("Calling ServiceStack AppHost.Init"); + Logger.LogInformation("Calling ServiceStack AppHost.Init"); var types = services.Select(r => r.GetType()).ToArray(); @@ -697,7 +692,7 @@ namespace Emby.Server.Implementations.HttpServer ResponseFilters = new Action[] { - new ResponseFilter(_logger).FilterResponse + new ResponseFilter(Logger).FilterResponse }; } @@ -759,8 +754,12 @@ namespace Emby.Server.Implementations.HttpServer return _jsonSerializer.DeserializeFromStreamAsync(stream, type); } - //TODO Add Jellyfin Route Path Normalizer + public Task ProcessWebSocketRequest(HttpContext context) + { + return _socketListener.ProcessWebSocketRequest(context); + } + //TODO Add Jellyfin Route Path Normalizer private static string NormalizeEmbyRoutePath(string path) { if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) @@ -793,6 +792,7 @@ namespace Emby.Server.Implementations.HttpServer private bool _disposed; private readonly object _disposeLock = new object(); + protected virtual void Dispose(bool disposing) { if (_disposed) return; @@ -821,7 +821,7 @@ namespace Emby.Server.Implementations.HttpServer return Task.CompletedTask; } - _logger.LogDebug("Websocket message received: {0}", result.MessageType); + Logger.LogDebug("Websocket message received: {0}", result.MessageType); var tasks = _webSocketListeners.Select(i => Task.Run(async () => { @@ -831,7 +831,7 @@ namespace Emby.Server.Implementations.HttpServer } catch (Exception ex) { - _logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty); + Logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty); } })); @@ -842,18 +842,5 @@ namespace Emby.Server.Implementations.HttpServer { Dispose(true); } - - public void StartServer(string[] urlPrefixes, IHttpListener httpListener) - { - UrlPrefixes = urlPrefixes; - - _listener = httpListener; - - _listener.WebSocketConnected = OnWebSocketConnected; - _listener.ErrorHandler = ErrorHandler; - _listener.RequestHandler = RequestHandler; - - _listener.Start(UrlPrefixes); - } } } diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 070717d489..4632658626 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -16,6 +16,8 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using IRequest = MediaBrowser.Model.Services.IRequest; using MimeTypes = MediaBrowser.Model.Net.MimeTypes; @@ -32,17 +34,16 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; - - private IBrotliCompressor _brotliCompressor; + private readonly IStreamHelper _streamHelper; /// /// Initializes a new instance of the class. /// - public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IBrotliCompressor brotliCompressor) + public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper) { _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; - _brotliCompressor = brotliCompressor; + _streamHelper = streamHelper; _logger = loggerfactory.CreateLogger("HttpResultFactory"); } @@ -76,7 +77,7 @@ namespace Emby.Server.Implementations.HttpServer public object GetRedirectResult(string url) { var responseHeaders = new Dictionary(); - responseHeaders["Location"] = url; + responseHeaders[HeaderNames.Location] = url; var result = new HttpResult(Array.Empty(), "text/plain", HttpStatusCode.Redirect); @@ -97,9 +98,9 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires)) + if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires)) { - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; } AddResponseHeaders(result, responseHeaders); @@ -131,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer content = Array.Empty(); } - result = new StreamWriter(content, contentType, contentLength); + result = new StreamWriter(content, contentType); } else { @@ -143,9 +144,9 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _)) + if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) { - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; } AddResponseHeaders(result, responseHeaders); @@ -175,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer bytes = Array.Empty(); } - result = new StreamWriter(bytes, contentType, contentLength); + result = new StreamWriter(bytes, contentType); } else { @@ -187,9 +188,9 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _)) + if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) { - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; } AddResponseHeaders(result, responseHeaders); @@ -214,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); } - responseHeaders["Expires"] = "-1"; + responseHeaders[HeaderNames.Expires] = "-1"; return ToOptimizedResultInternal(requestContext, result, responseHeaders); } @@ -246,9 +247,9 @@ namespace Emby.Server.Implementations.HttpServer private static string GetCompressionType(IRequest request) { - var acceptEncoding = request.Headers["Accept-Encoding"]; + var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString(); - if (acceptEncoding != null) + if (string.IsNullOrEmpty(acceptEncoding)) { //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) // return "br"; @@ -326,21 +327,21 @@ namespace Emby.Server.Implementations.HttpServer } content = Compress(content, requestedCompressionType); - responseHeaders["Content-Encoding"] = requestedCompressionType; + responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType; - responseHeaders["Vary"] = "Accept-Encoding"; + responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding; var contentLength = content.Length; if (isHeadRequest) { - var result = new StreamWriter(Array.Empty(), contentType, contentLength); + var result = new StreamWriter(Array.Empty(), contentType); AddResponseHeaders(result, responseHeaders); return result; } else { - var result = new StreamWriter(content, contentType, contentLength); + var result = new StreamWriter(content, contentType); AddResponseHeaders(result, responseHeaders); return result; } @@ -348,11 +349,6 @@ namespace Emby.Server.Implementations.HttpServer private byte[] Compress(byte[] bytes, string compressionType) { - if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase)) - { - return CompressBrotli(bytes); - } - if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) { return Deflate(bytes); @@ -366,11 +362,6 @@ namespace Emby.Server.Implementations.HttpServer throw new NotSupportedException(compressionType); } - private byte[] CompressBrotli(byte[] bytes) - { - return _brotliCompressor.Compress(bytes); - } - private static byte[] Deflate(byte[] bytes) { // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream @@ -424,12 +415,12 @@ namespace Emby.Server.Implementations.HttpServer /// private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, StaticResultOptions options) { - bool noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; + bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified); if (!noCache) { - DateTime.TryParse(requestContext.Headers.Get("If-Modified-Since"), out var ifModifiedSinceHeader); + DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) { @@ -530,7 +521,7 @@ namespace Emby.Server.Implementations.HttpServer options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); var contentType = options.ContentType; - if (!string.IsNullOrEmpty(requestContext.Headers.Get("If-Modified-Since"))) + if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince])) { // See if the result is already cached in the browser var result = GetCachedResult(requestContext, options.ResponseHeaders, options); @@ -548,11 +539,11 @@ namespace Emby.Server.Implementations.HttpServer AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); AddAgeHeader(responseHeaders, options.DateLastModified); - var rangeHeader = requestContext.Headers.Get("Range"); + var rangeHeader = requestContext.Headers[HeaderNames.Range]; if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) { - var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem) + var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper) { OnComplete = options.OnComplete, OnError = options.OnError, @@ -590,11 +581,6 @@ namespace Emby.Server.Implementations.HttpServer } else { - if (totalContentLength.HasValue) - { - responseHeaders["Content-Length"] = totalContentLength.Value.ToString(UsCulture); - } - if (isHeadRequest) { using (stream) @@ -614,11 +600,6 @@ namespace Emby.Server.Implementations.HttpServer } } - /// - /// The us culture - /// - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// /// Adds the caching responseHeaders. /// @@ -627,23 +608,23 @@ namespace Emby.Server.Implementations.HttpServer { if (noCache) { - responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate"; - responseHeaders["pragma"] = "no-cache, no-store, must-revalidate"; + responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate"; + responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate"; return; } if (cacheDuration.HasValue) { - responseHeaders["Cache-Control"] = "public, max-age=" + cacheDuration.Value.TotalSeconds; + responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds; } else { - responseHeaders["Cache-Control"] = "public"; + responseHeaders[HeaderNames.CacheControl] = "public"; } if (lastModifiedDate.HasValue) { - responseHeaders["Last-Modified"] = lastModifiedDate.ToString(); + responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString(); } } @@ -656,7 +637,7 @@ namespace Emby.Server.Implementations.HttpServer { if (lastDateModified.HasValue) { - responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); + responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); } } @@ -714,9 +695,4 @@ namespace Emby.Server.Implementations.HttpServer } } } - - public interface IBrotliCompressor - { - byte[] Compress(byte[] content); - } } diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs index 8350913610..005656d2c1 100644 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer { @@ -28,21 +27,11 @@ namespace Emby.Server.Implementations.HttpServer /// The web socket handler. Action WebSocketConnected { get; set; } - /// - /// Gets or sets the web socket connecting. - /// - /// The web socket connecting. - Action WebSocketConnecting { get; set; } - - /// - /// Starts this instance. - /// - /// The URL prefixes. - void Start(IEnumerable urlPrefixes); - /// /// Stops this instance. /// Task Stop(); + + Task ProcessWebSocketRequest(HttpContext ctx); } } diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 891a76ec2a..449159834a 100644 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { @@ -66,8 +67,8 @@ namespace Emby.Server.Implementations.HttpServer this._logger = logger; ContentType = contentType; - Headers["Content-Type"] = contentType; - Headers["Accept-Ranges"] = "bytes"; + Headers[HeaderNames.ContentType] = contentType; + Headers[HeaderNames.AcceptRanges] = "bytes"; StatusCode = HttpStatusCode.PartialContent; SetRangeValues(contentLength); @@ -95,9 +96,7 @@ namespace Emby.Server.Implementations.HttpServer RangeStart = requestedRange.Key; RangeLength = 1 + RangeEnd - RangeStart; - // Content-Length is the length of what we're serving, not the original content - Headers["Content-Length"] = RangeLength.ToString(UsCulture); - Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); + Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; if (RangeStart > 0 && SourceStream.CanSeek) { diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index da2bf983a0..a53d9bf0b9 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Text; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { @@ -25,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer public void FilterResponse(IRequest req, IResponse res, object dto) { // Try to prevent compatibility view - res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); + res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); res.AddHeader("Access-Control-Allow-Origin", "*"); @@ -44,20 +45,19 @@ namespace Emby.Server.Implementations.HttpServer if (dto is IHasHeaders hasHeaders) { - if (!hasHeaders.Headers.ContainsKey("Server")) + if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server)) { - hasHeaders.Headers["Server"] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; + hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; } // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy - if (hasHeaders.Headers.TryGetValue("Content-Length", out string contentLength) + if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength) && !string.IsNullOrEmpty(contentLength)) { var length = long.Parse(contentLength, UsCulture); if (length > 0) { - res.SetContentLength(length); res.SendChunked = false; } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index cab41e65b9..276312a300 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Services; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer.Security { @@ -176,7 +177,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (string.IsNullOrEmpty(auth)) { - auth = httpReq.Headers["Authorization"]; + auth = httpReq.Headers[HeaderNames.Authorization]; } return GetAuthorization(auth); diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index cb2e3580b2..66a13e20df 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { @@ -52,12 +53,7 @@ namespace Emby.Server.Implementations.HttpServer SourceStream = source; - Headers["Content-Type"] = contentType; - - if (source.CanSeek) - { - Headers["Content-Length"] = source.Length.ToString(UsCulture); - } + Headers[HeaderNames.ContentType] = contentType; } /// @@ -65,8 +61,7 @@ namespace Emby.Server.Implementations.HttpServer /// /// The source. /// Type of the content. - /// The logger. - public StreamWriter(byte[] source, string contentType, int contentLength) + public StreamWriter(byte[] source, string contentType) { if (string.IsNullOrEmpty(contentType)) { @@ -75,9 +70,7 @@ namespace Emby.Server.Implementations.HttpServer SourceBytes = source; - Headers["Content-Type"] = contentType; - - Headers["Content-Length"] = contentLength.ToString(UsCulture); + Headers[HeaderNames.ContentType] = contentType; } public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index e9d0bac74d..54a16040f7 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using UtfUnknown; @@ -67,7 +68,7 @@ namespace Emby.Server.Implementations.HttpServer /// Gets or sets the query string. /// /// The query string. - public QueryParamCollection QueryString { get; set; } + public IQueryCollection QueryString { get; set; } /// /// Initializes a new instance of the class. @@ -101,12 +102,6 @@ namespace Emby.Server.Implementations.HttpServer _socket = socket; _socket.OnReceiveBytes = OnReceiveInternal; - var memorySocket = socket as IMemoryWebSocket; - if (memorySocket != null) - { - memorySocket.OnReceiveMemoryBytes = OnReceiveInternal; - } - RemoteEndPoint = remoteEndPoint; _logger = logger; @@ -142,34 +137,6 @@ namespace Emby.Server.Implementations.HttpServer } } - /// - /// Called when [receive]. - /// - /// The memory block. - /// The length of the memory block. - private void OnReceiveInternal(Memory memory, int length) - { - LastActivityDate = DateTime.UtcNow; - - if (OnReceive == null) - { - return; - } - - var bytes = memory.Slice(0, length).ToArray(); - - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; - - if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)) - { - OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length)); - } - else - { - OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length)); - } - } - private void OnReceiveInternal(string message) { LastActivityDate = DateTime.UtcNow; @@ -193,7 +160,7 @@ namespace Emby.Server.Implementations.HttpServer var info = new WebSocketMessageInfo { MessageType = stub.MessageType, - Data = stub.Data == null ? null : stub.Data.ToString(), + Data = stub.Data?.ToString(), Connection = this }; diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index a64dfb607b..421592fada 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -7,7 +7,6 @@ using System.Text; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO @@ -711,20 +710,20 @@ namespace Emby.Server.Implementations.IO return GetFiles(path, null, false, recursive); } - public virtual IEnumerable GetFiles(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable GetFiles(string path, IReadOnlyList extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method - if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1) + if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1) { return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption)); } var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption); - if (extensions != null && extensions.Length > 0) + if (extensions != null && extensions.Count > 0) { files = files.Where(i => { diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 109c21f18d..46f209b4b4 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -20,6 +20,9 @@ namespace Emby.Server.Implementations.Images public abstract class BaseDynamicImageProvider : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider, IHasOrder where T : BaseItem { + protected virtual IReadOnlyCollection SupportedImages { get; } + = new ImageType[] { ImageType.Primary }; + protected IFileSystem FileSystem { get; private set; } protected IProviderManager ProviderManager { get; private set; } protected IApplicationPaths ApplicationPaths { get; private set; } @@ -33,18 +36,7 @@ namespace Emby.Server.Implementations.Images ImageProcessor = imageProcessor; } - protected virtual bool Supports(BaseItem item) - { - return true; - } - - public virtual ImageType[] GetSupportedImages(BaseItem item) - { - return new ImageType[] - { - ImageType.Primary - }; - } + protected virtual bool Supports(BaseItem _) => true; public async Task FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) { @@ -54,15 +46,14 @@ namespace Emby.Server.Implementations.Images } var updateType = ItemUpdateType.None; - var supportedImages = GetSupportedImages(item); - if (supportedImages.Contains(ImageType.Primary)) + if (SupportedImages.Contains(ImageType.Primary)) { var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false); updateType = updateType | primaryResult; } - if (supportedImages.Contains(ImageType.Thumb)) + if (SupportedImages.Contains(ImageType.Thumb)) { var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false); updateType = updateType | thumbResult; @@ -94,7 +85,7 @@ namespace Emby.Server.Implementations.Images } protected async Task FetchToFileInternal(BaseItem item, - List itemsWithImages, + IReadOnlyList itemsWithImages, ImageType imageType, CancellationToken cancellationToken) { @@ -119,9 +110,9 @@ namespace Emby.Server.Implementations.Images return ItemUpdateType.ImageUpdate; } - protected abstract List GetItemsWithImages(BaseItem item); + protected abstract IReadOnlyList GetItemsWithImages(BaseItem item); - protected string CreateThumbCollage(BaseItem primaryItem, List items, string outputPath) + protected string CreateThumbCollage(BaseItem primaryItem, IEnumerable items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 640, 360); } @@ -132,38 +123,38 @@ namespace Emby.Server.Implementations.Images .Select(i => { var image = i.GetImageInfo(ImageType.Primary, 0); - if (image != null && image.IsLocalFile) { return image.Path; } + image = i.GetImageInfo(ImageType.Thumb, 0); - if (image != null && image.IsLocalFile) { return image.Path; } + return null; }) .Where(i => !string.IsNullOrEmpty(i)); } - protected string CreatePosterCollage(BaseItem primaryItem, List items, string outputPath) + protected string CreatePosterCollage(BaseItem primaryItem, IEnumerable items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 400, 600); } - protected string CreateSquareCollage(BaseItem primaryItem, List items, string outputPath) + protected string CreateSquareCollage(BaseItem primaryItem, IEnumerable items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 600, 600); } - protected string CreateThumbCollage(BaseItem primaryItem, List items, string outputPath, int width, int height) + protected string CreateThumbCollage(BaseItem primaryItem, IEnumerable items, string outputPath, int width, int height) { return CreateCollage(primaryItem, items, outputPath, width, height); } - private string CreateCollage(BaseItem primaryItem, List items, string outputPath, int width, int height) + private string CreateCollage(BaseItem primaryItem, IEnumerable items, string outputPath, int width, int height) { Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); @@ -192,7 +183,7 @@ namespace Emby.Server.Implementations.Images public string Name => "Dynamic Image Provider"; protected virtual string CreateImage(BaseItem item, - List itemsWithImages, + IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) @@ -211,18 +202,15 @@ namespace Emby.Server.Implementations.Images if (imageType == ImageType.Primary) { - if (item is UserView) - { - return CreateSquareCollage(item, itemsWithImages, outputPath); - } - if (item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum) + if (item is UserView || item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum) { return CreateSquareCollage(item, itemsWithImages, outputPath); } + return CreatePosterCollage(item, itemsWithImages, outputPath); } - throw new ArgumentException("Unexpected image type"); + throw new ArgumentException("Unexpected image type", nameof(imageType)); } protected virtual int MaxImageAgeDays => 7; @@ -234,13 +222,11 @@ namespace Emby.Server.Implementations.Images return false; } - var supportedImages = GetSupportedImages(item); - - if (supportedImages.Contains(ImageType.Primary) && HasChanged(item, ImageType.Primary)) + if (SupportedImages.Contains(ImageType.Primary) && HasChanged(item, ImageType.Primary)) { return true; } - if (supportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb)) + if (SupportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb)) { return true; } @@ -285,7 +271,7 @@ namespace Emby.Server.Implementations.Images public int Order => 0; - protected string CreateSingleImage(List itemsWithImages, string outputPathWithoutExtension, ImageType imageType) + protected string CreateSingleImage(IEnumerable itemsWithImages, string outputPathWithoutExtension, ImageType imageType) { var image = itemsWithImages .Where(i => i.HasImage(imageType) && i.GetImageInfo(imageType, 0).IsLocalFile && Path.HasExtension(i.GetImagePath(imageType))) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 4013ac0c80..3ec1f81d31 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Text; using System.Threading.Tasks; using MediaBrowser.Controller.Authentication; @@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library public string Name => "Default"; public bool IsEnabled => true; - + + // This is dumb and an artifact of the backwards way auth providers were designed. + // This version of authenticate was never meant to be called, but needs to be here for interface compat + // Only the providers that don't provide local user support use this public Task Authenticate(string username, string password) { throw new NotImplementedException(); } - + + // This is the verson that we need to use for local users. Because reasons. public Task Authenticate(string username, string password, User resolvedUser) { + bool success = false; if (resolvedUser == null) { throw new Exception("Invalid username or password"); } - var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); + // As long as jellyfin supports passwordless users, we need this little block here to accomodate + if (IsPasswordEmpty(resolvedUser, password)) + { + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + ConvertPasswordFormat(resolvedUser); + byte[] passwordbytes = Encoding.UTF8.GetBytes(password); + + PasswordHash readyHash = new PasswordHash(resolvedUser.Password); + byte[] calculatedHash; + string calculatedHashString; + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)) + { + if (string.IsNullOrEmpty(readyHash.Salt)) + { + calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); + calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); + } + else + { + calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); + calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); + } + + if (calculatedHashString == readyHash.Hash) + { + success = true; + // throw new Exception("Invalid username or password"); + } + } + else + { + throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}")); + } + + // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); if (!success) { @@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library }); } + // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change + // but at least they are in the new format. + private void ConvertPasswordFormat(User user) + { + if (string.IsNullOrEmpty(user.Password)) + { + return; + } + + if (!user.Password.Contains("$")) + { + string hash = user.Password; + user.Password = string.Format("$SHA1${0}", hash); + } + + if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) + { + string hash = user.EasyPassword; + user.EasyPassword = string.Format("$SHA1${0}", hash); + } + } + public Task HasPassword(User user) { var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); return Task.FromResult(hasConfiguredPassword); } - private bool IsPasswordEmpty(User user, string passwordHash) + private bool IsPasswordEmpty(User user, string password) { - return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); + return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password)); } public Task ChangePassword(User user, string newPassword) { - string newPasswordHash = null; - - if (newPassword != null) + ConvertPasswordFormat(user); + // This is needed to support changing a no password user to a password user + if (string.IsNullOrEmpty(user.Password)) { - newPasswordHash = GetHashedString(user, newPassword); + PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); + newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes); + newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; + newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash); + user.Password = newPasswordHash.ToString(); + return Task.CompletedTask; } - if (string.IsNullOrWhiteSpace(newPasswordHash)) + PasswordHash passwordHash = new PasswordHash(user.Password); + if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) { - throw new ArgumentNullException(nameof(newPasswordHash)); + passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); + passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); + passwordHash.Id = _cryptographyProvider.DefaultHashMethod; + passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); + } + else if (newPassword != null) + { + passwordHash.Hash = GetHashedString(user, newPassword); } - user.Password = newPasswordHash; + if (string.IsNullOrWhiteSpace(passwordHash.Hash)) + { + throw new ArgumentNullException(nameof(passwordHash.Hash)); + } + + user.Password = passwordHash.ToString(); return Task.CompletedTask; } public string GetPasswordHash(User user) { - return string.IsNullOrEmpty(user.Password) - ? GetEmptyHashedString(user) - : user.Password; + return user.Password; } - public string GetEmptyHashedString(User user) + public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) { - return GetHashedString(user, string.Empty); + passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); } /// @@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library /// public string GetHashedString(User user, string str) { - var salt = user.Salt; - if (salt != null) + PasswordHash passwordHash; + if (string.IsNullOrEmpty(user.Password)) { - // return BCrypt.HashPassword(str, salt); + passwordHash = new PasswordHash(_cryptographyProvider); + } + else + { + ConvertPasswordFormat(user); + passwordHash = new PasswordHash(user.Password); } - // legacy - return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty); + if (passwordHash.SaltBytes != null) + { + // the password is modern format with PBKDF and we should take advantage of that + passwordHash.HashBytes = Encoding.UTF8.GetBytes(str); + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); + } + else + { + // the password has no salt and should be called with the older method for safety + return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); + } } } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 3c2272b566..6591d54c56 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -278,6 +278,7 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException(nameof(item)); } + if (item is IItemByName) { if (!(item is MusicArtist)) @@ -285,18 +286,7 @@ namespace Emby.Server.Implementations.Library return; } } - - else if (item.IsFolder) - { - //if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder)) - //{ - // if (item.SourceType != SourceType.Library) - // { - // return; - // } - //} - } - else + else if (!item.IsFolder) { if (!(item is Video) && !(item is LiveTvChannel)) { @@ -371,19 +361,20 @@ namespace Emby.Server.Implementations.Library foreach (var metadataPath in GetMetadataPaths(item, children)) { - _logger.LogDebug("Deleting path {0}", metadataPath); + if (!Directory.Exists(metadataPath)) + { + continue; + } + + _logger.LogDebug("Deleting path {MetadataPath}", metadataPath); try { Directory.Delete(metadataPath, true); - } - catch (IOException) - { - } catch (Exception ex) { - _logger.LogError(ex, "Error deleting {metadataPath}", metadataPath); + _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath); } } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index dfef8e997c..efb1ef4a50 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Events; @@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library } } - public bool IsValidUsername(string username) + public static bool IsValidUsername(string username) { - // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) - foreach (var currentChar in username) - { - if (!IsValidUsernameCharacter(currentChar)) - { - return false; - } - } - return true; + //This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.) + return Regex.IsMatch(username, "^[\\w-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) { - return !char.Equals(i, '<') && !char.Equals(i, '>'); + return IsValidUsername(i.ToString()); } public string MakeValidUsername(string username) @@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library private string GetLocalPasswordHash(User user) { return string.IsNullOrEmpty(user.EasyPassword) - ? _defaultAuthenticationProvider.GetEmptyHashedString(user) + ? null : user.EasyPassword; } - private bool IsPasswordEmpty(User user, string passwordHash) - { - return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase); - } - /// /// Loads the users from the repository /// @@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; - var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user)); + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); - var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? + bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; - var dto = new UserDto + UserDto dto = new UserDto { Id = user.Id, Name = user.Name, @@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library dto.EnableAutoLogin = true; } - var image = user.GetImageInfo(ImageType.Primary, 0); + ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0); if (image != null) { @@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library if (!IsValidUsername(name)) { - throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); } if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index fceb82ba19..58b3b6a69f 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -33,7 +33,6 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IProviderManager _providerManager; private readonly IMediaEncoder _mediaEncoder; private readonly IProcessFactory _processFactory; - private readonly IAssemblyInfo _assemblyInfo; private IMediaSourceManager _mediaSourceManager; public static EmbyTV Current; @@ -74,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public EmbyTV(IServerApplicationHost appHost, IStreamHelper streamHelper, IMediaSourceManager mediaSourceManager, - IAssemblyInfo assemblyInfo, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, @@ -101,12 +98,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _processFactory = processFactory; _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; - _assemblyInfo = assemblyInfo; _mediaSourceManager = mediaSourceManager; _streamHelper = streamHelper; - _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); - _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger); + _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json")); + _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"), _logger); _timerProvider.TimerFired += _timerProvider_TimerFired; _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index a2ac60b319..9c45ee36a2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -32,32 +31,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (_items == null) { + if (!File.Exists(_dataPath)) + { + return new List(); + } + Logger.LogInformation("Loading live tv data from {0}", _dataPath); _items = GetItemsFromFile(_dataPath); } + return _items.ToList(); } } private List GetItemsFromFile(string path) { - var jsonFile = path + ".json"; - - if (!File.Exists(jsonFile)) - { - return new List(); - } - try { - return _jsonSerializer.DeserializeFromFile>(jsonFile) ?? new List(); - } - catch (IOException) - { + return _jsonSerializer.DeserializeFromFile>(path); } catch (Exception ex) { - Logger.LogError(ex, "Error deserializing {jsonFile}", jsonFile); + Logger.LogError(ex, "Error deserializing {Path}", path); } return new List(); @@ -70,12 +65,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException(nameof(newList)); } - var file = _dataPath + ".json"; - Directory.CreateDirectory(Path.GetDirectoryName(file)); + Directory.CreateDirectory(Path.GetDirectoryName(_dataPath)); lock (_fileDataLock) { - _jsonSerializer.SerializeToFile(newList, file); + _jsonSerializer.SerializeToFile(newList, _dataPath); _items = newList; } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 0bbffb824b..4137760d07 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -17,6 +17,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.LiveTv.Listings { @@ -638,7 +639,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings #if NETSTANDARD2_0 if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - options.RequestHeaders["Accept-Encoding"] = "deflate"; + options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate"; } #endif @@ -676,7 +677,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings #if NETSTANDARD2_0 if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - options.RequestHeaders["Accept-Encoding"] = "deflate"; + options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate"; } #endif diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index fdaaf0bae7..588dcb843b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -19,6 +19,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.LiveTv.TunerHosts { @@ -145,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (protocol == MediaProtocol.Http) { // Use user-defined user-agent. If there isn't one, make it look like a browser. - httpHeaders["User-Agent"] = string.IsNullOrWhiteSpace(info.UserAgent) ? + httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.85 Safari/537.36" : info.UserAgent; } diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index c01bb0c501..dc73ba6b34 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -1,97 +1,97 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", - "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Continue Watching", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", + "Albums": "Álbumes", + "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", + "Application": "Aplicación", + "Artists": "Artistas", + "AuthenticationSucceededWithUserName": "{0} autenticado correctamente", + "Books": "Libros", + "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", + "Channels": "Canales", + "ChapterNameValue": "Capítulo {0}", + "Collections": "Colecciones", + "DeviceOfflineWithName": "{0} se ha desconectado", + "DeviceOnlineWithName": "{0} está conectado", + "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}", + "Favorites": "Favoritos", + "Folders": "Carpetas", + "Genres": "Géneros", + "HeaderAlbumArtists": "Artistas de álbumes", + "HeaderCameraUploads": "Subidas de cámara", + "HeaderContinueWatching": "Continuar viendo", + "HeaderFavoriteAlbums": "Álbumes favoritos", + "HeaderFavoriteArtists": "Artistas favoritos", + "HeaderFavoriteEpisodes": "Episodios favoritos", + "HeaderFavoriteShows": "Programas favoritos", + "HeaderFavoriteSongs": "Canciones favoritas", + "HeaderLiveTV": "TV en vivo", + "HeaderNextUp": "Continuar Viendo", + "HeaderRecordingGroups": "Grupos de grabación", + "HomeVideos": "Videos caseros", + "Inherit": "Heredar", + "ItemAddedWithName": "{0} se ha añadido a la biblioteca", + "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca", + "LabelIpAddressValue": "Dirección IP: {0}", + "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}", + "Latest": "Últimos", + "MessageApplicationUpdated": "El servidor Jellyfin fue actualizado", + "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Fue actualizada la sección {0} de la configuración del servidor", + "MessageServerConfigurationUpdated": "Fue actualizada la configuración del servidor", + "MixedContent": "Contenido mixto", + "Movies": "Películas", + "Music": "Música", + "MusicVideos": "Videos musicales", + "NameInstallFailed": "{0} error de instalación", + "NameSeasonNumber": "Temporada {0}", + "NameSeasonUnknown": "Temporada desconocida", + "NewVersionIsAvailable": "Disponible una nueva versión de Jellyfin para descargar.", + "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", + "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", + "NotificationOptionAudioPlayback": "Se inició la reproducción de audio", + "NotificationOptionAudioPlaybackStopped": "Se detuvo la reproducción de audio", + "NotificationOptionCameraImageUploaded": "Imagen de la cámara cargada", + "NotificationOptionInstallationFailed": "Error de instalación", + "NotificationOptionNewLibraryContent": "Nuevo contenido añadido", + "NotificationOptionPluginError": "Error en plugin", + "NotificationOptionPluginInstalled": "Plugin instalado", + "NotificationOptionPluginUninstalled": "Plugin desinstalado", + "NotificationOptionPluginUpdateInstalled": "Actualización del complemento instalada", + "NotificationOptionServerRestartRequired": "Se requiere reinicio del servidor", + "NotificationOptionTaskFailed": "Error de tarea programada", + "NotificationOptionUserLockedOut": "Usuario bloqueado", + "NotificationOptionVideoPlayback": "Se inició la reproducción de video", + "NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida", + "Photos": "Fotos", + "Playlists": "Listas de reproducción", "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "PluginInstalledWithName": "{0} fue instalado", + "PluginUninstalledWithName": "{0} fue desinstalado", + "PluginUpdatedWithName": "{0} fue actualizado", + "ProviderValue": "Proveedor: {0}", + "ScheduledTaskFailedWithName": "{0} falló", + "ScheduledTaskStartedWithName": "{0} iniciada", + "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", "Shows": "Series", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "Songs": "Canciones", + "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", - "System": "System", - "TvShows": "TV Shows", - "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}", + "SubtitlesDownloadedForItem": "Descargar subtítulos para {0}", + "Sync": "Sincronizar", + "System": "Sistema", + "TvShows": "Series de TV", + "User": "Usuario", + "UserCreatedWithName": "El usuario {0} ha sido creado", + "UserDeletedWithName": "El usuario {0} ha sido borrado", + "UserDownloadingItemWithValues": "{0} está descargando {1}", + "UserLockedOutWithName": "El usuario {0} ha sido bloqueado", + "UserOfflineFromDevice": "{0} se ha desconectado de {1}", + "UserOnlineFromDevice": "{0} está en línea desde {1}", + "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}", + "UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}", + "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}", + "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}", + "ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia", + "ValueSpecialEpisodeName": "Especial - {0}", + "VersionNumber": "Versión {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 7202be9f56..4b4db39a8b 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -1,97 +1,97 @@ { "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", + "AppDeviceValues": "Application : {0}, Appareil : {1}", "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", + "Artists": "Artistes", + "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès", + "Books": "Livres", + "CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}", + "Channels": "Chaînes", + "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", + "DeviceOfflineWithName": "{0} s'est déconnecté", + "DeviceOnlineWithName": "{0} est connecté", + "FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}", + "Favorites": "Favoris", + "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", + "HeaderAlbumArtists": "Artistes de l'album", + "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", + "HeaderFavoriteAlbums": "Albums favoris", + "HeaderFavoriteArtists": "Artistes favoris", + "HeaderFavoriteEpisodes": "Épisodes favoris", + "HeaderFavoriteShows": "Séries favorites", + "HeaderFavoriteSongs": "Chansons favorites", + "HeaderLiveTV": "TV en direct", "HeaderNextUp": "À Suivre", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", + "HeaderRecordingGroups": "Groupes d'enregistrements", + "HomeVideos": "Vidéos personnelles", + "Inherit": "Hériter", + "ItemAddedWithName": "{0} a été ajouté à la médiathèque", + "ItemRemovedWithName": "{0} a été supprimé de la médiathèque", + "LabelIpAddressValue": "Adresse IP : {0}", + "LabelRunningTimeValue": "Durée : {0}", + "Latest": "Derniers", + "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour", + "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour", + "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour", + "MixedContent": "Contenu mixte", + "Movies": "Films", + "Music": "Musique", + "MusicVideos": "Vidéos musicales", + "NameInstallFailed": "{0} échec d'installation", + "NameSeasonNumber": "Saison {0}", + "NameSeasonUnknown": "Saison Inconnue", + "NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.", + "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible", + "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée", + "NotificationOptionAudioPlayback": "Lecture audio démarrée", + "NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée", + "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée", + "NotificationOptionInstallationFailed": "Échec d'installation", + "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté", + "NotificationOptionPluginError": "Erreur d'extension", + "NotificationOptionPluginInstalled": "Extension installée", + "NotificationOptionPluginUninstalled": "Extension désinstallée", + "NotificationOptionPluginUpdateInstalled": "Mise à jour d'extension installée", + "NotificationOptionServerRestartRequired": "Un redémarrage du serveur est requis", + "NotificationOptionTaskFailed": "Échec de tâche planifiée", + "NotificationOptionUserLockedOut": "Utilisateur verrouillé", + "NotificationOptionVideoPlayback": "Lecture vidéo démarrée", + "NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée", "Photos": "Photos", - "Playlists": "Playlists", - "Plugin": "Plugin", - "PluginInstalledWithName": "{0} was installed", - "PluginUninstalledWithName": "{0} was uninstalled", - "PluginUpdatedWithName": "{0} was updated", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} failed", - "ScheduledTaskStartedWithName": "{0} started", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Series", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "Playlists": "Listes de lecture", + "Plugin": "Extension", + "PluginInstalledWithName": "{0} a été installé", + "PluginUninstalledWithName": "{0} a été désinstallé", + "PluginUpdatedWithName": "{0} a été mis à jour", + "ProviderValue": "Fournisseur : {0}", + "ScheduledTaskFailedWithName": "{0} a échoué", + "ScheduledTaskStartedWithName": "{0} a commencé", + "ServerNameNeedsToBeRestarted": "{0} doit être redémarré", + "Shows": "Émissions", + "Songs": "Chansons", + "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", - "System": "System", - "TvShows": "TV Shows", - "User": "User", - "UserCreatedWithName": "User {0} has been created", - "UserDeletedWithName": "User {0} has been deleted", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", + "SubtitlesDownloadedForItem": "Les sous-titres de {0} ont été téléchargés", + "Sync": "Synchroniser", + "System": "Système", + "TvShows": "Séries Télé", + "User": "Utilisateur", + "UserCreatedWithName": "L'utilisateur {0} a été créé", + "UserDeletedWithName": "L'utilisateur {0} a été supprimé", + "UserDownloadingItemWithValues": "{0} est en train de télécharger {1}", + "UserLockedOutWithName": "L'utilisateur {0} a été verrouillé", + "UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}", + "UserOnlineFromDevice": "{0} s'est connecté depuis {1}", + "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié", + "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", + "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}", + "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", + "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", "VersionNumber": "Version {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 52afb4e492..e434b7605b 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -44,7 +44,7 @@ "NameInstallFailed": "{0} échec d'installation", "NameSeasonNumber": "Saison {0}", "NameSeasonUnknown": "Saison Inconnue", - "NewVersionIsAvailable": "Une nouvelle version d'Jellyfin Serveur est disponible au téléchargement.", + "NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.", "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible", "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée", "NotificationOptionAudioPlayback": "Lecture audio démarrée", @@ -89,7 +89,7 @@ "UserOnlineFromDevice": "{0} s'est connecté depuis {1}", "UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié", "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", - "UserStartedPlayingItemWithValues": "{0} est entrain de lire {1} sur {2}", + "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}", "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie", "ValueSpecialEpisodeName": "Spécial - {0}", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index fff1d1f0ec..0ed998c4bd 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -1,8 +1,8 @@ { - "Albums": "Albums", + "Albums": "אלבומים", "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", + "Application": "אפליקציה", + "Artists": "אמנים", "AuthenticationSucceededWithUserName": "{0} successfully authenticated", "Books": "ספרים", "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index a5f1e8f94d..357883cd37 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -34,17 +34,17 @@ "LabelRunningTimeValue": "Durata: {0}", "Latest": "Più recenti", "MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata", "MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata", "MixedContent": "Contenuto misto", "Movies": "Film", "Music": "Musica", "MusicVideos": "Video musicali", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "{0} installazione fallita", "NameSeasonNumber": "Stagione {0}", "NameSeasonUnknown": "Stagione sconosciuto", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.", "NotificationOptionApplicationUpdateAvailable": "Aggiornamento dell'applicazione disponibile", "NotificationOptionApplicationUpdateInstalled": "Aggiornamento dell'applicazione installato", "NotificationOptionAudioPlayback": "La riproduzione audio è iniziata", @@ -70,12 +70,12 @@ "ProviderValue": "Provider: {0}", "ScheduledTaskFailedWithName": "{0} fallito", "ScheduledTaskStartedWithName": "{0} avviati", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato", "Shows": "Programmi", "Songs": "Canzoni", "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", "SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}", "SubtitlesDownloadedForItem": "Sottotitoli scaricati per {0}", "Sync": "Sincronizza", "System": "Sistema", @@ -91,7 +91,7 @@ "UserPolicyUpdatedWithName": "La politica dell'utente è stata aggiornata per {0}", "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1}", "UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale", "ValueSpecialEpisodeName": "Speciale - {0}", "VersionNumber": "Versione {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 658d168e9f..23841f37d6 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -3,15 +3,15 @@ "AppDeviceValues": "Qoldanba: {0}, Qurylǵy: {1}", "Application": "Qoldanba", "Artists": "Oryndaýshylar", - "AuthenticationSucceededWithUserName": "{0} túpnusqalyǵyn rastalýy sátti", + "AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy", "Books": "Kitaptar", - "CameraImageUploadedFrom": "Jańa sýret {0} kamerasynan júktep alyndy", + "CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy", "Channels": "Arnalar", "ChapterNameValue": "{0}-sahna", "Collections": "Jıyntyqtar", "DeviceOfflineWithName": "{0} ajyratylǵan", "DeviceOnlineWithName": "{0} qosylǵan", - "FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz", + "FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz aıaqtaldy", "Favorites": "Tańdaýlylar", "Folders": "Qaltalar", "Genres": "Janrlar", @@ -28,13 +28,13 @@ "HeaderRecordingGroups": "Jazba toptary", "HomeVideos": "Úılik beıneler", "Inherit": "Muraǵa ıelený", - "ItemAddedWithName": "{0} tasyǵyshhanaǵa ústelindi", + "ItemAddedWithName": "{0} tasyǵyshhanaǵa ústeldi", "ItemRemovedWithName": "{0} tasyǵyshhanadan alastaldy", "LabelIpAddressValue": "IP-mekenjaıy: {0}", "LabelRunningTimeValue": "Oınatý ýaqyty: {0}", "Latest": "Eń keıingi", "MessageApplicationUpdated": "Jellyfin Serveri jańartyldy", - "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} deńgeıge jańartyldy", + "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy", "MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy", "MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy", "MixedContent": "Aralas mazmun", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index aaedf08505..dbc9c4c4b8 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -5,7 +5,7 @@ "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Books": "Livros", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "Uma nova imagem da câmera foi submetida de {0}", "Channels": "Canais", "ChapterNameValue": "Capítulo {0}", "Collections": "Coletâneas", @@ -30,21 +30,21 @@ "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", - "LabelIpAddressValue": "Endereço ip: {0}", + "LabelIpAddressValue": "Endereço IP: {0}", "LabelRunningTimeValue": "Tempo de execução: {0}", "Latest": "Recente", "MessageApplicationUpdated": "O servidor Jellyfin foi atualizado", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "O Servidor Jellyfin foi atualizado para {0}", "MessageNamedServerConfigurationUpdatedWithValue": "A seção {0} da configuração do servidor foi atualizada", "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada", "MixedContent": "Conteúdo misto", "Movies": "Filmes", "Music": "Música", "MusicVideos": "Vídeos musicais", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "A instalação de {0} falhou", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada Desconhecida", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.", "NotificationOptionApplicationUpdateAvailable": "Atualização de aplicativo disponível", "NotificationOptionApplicationUpdateInstalled": "Atualização de aplicativo instalada", "NotificationOptionAudioPlayback": "Reprodução de áudio iniciada", @@ -70,12 +70,12 @@ "ProviderValue": "Provedor: {0}", "ScheduledTaskFailedWithName": "{0} falhou", "ScheduledTaskStartedWithName": "{0} iniciada", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "O servidor {0} precisa ser reiniciado", "Shows": "Séries", "Songs": "Músicas", "StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor tente novamente em breve.", "SubtitleDownloadFailureForItem": "Download de legendas falhou para {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Houve um problema ao baixar as legendas de {0} para {1}", "SubtitlesDownloadedForItem": "Legendas baixadas para {0}", "Sync": "Sincronizar", "System": "Sistema", @@ -91,7 +91,7 @@ "UserPolicyUpdatedWithName": "A política de usuário foi atualizada para {0}", "UserStartedPlayingItemWithValues": "{0} iniciou a reprodução de {1}", "UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "ValueHasBeenAddedToLibrary": "{0} foi adicionado a sua biblioteca", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versão {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index e850257d4a..b50ff4706e 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -1,62 +1,62 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", + "Albums": "Albumi", + "AppDeviceValues": "Aplikacija: {0}, Naprava: {1}", + "Application": "Aplikacija", + "Artists": "Izvajalci", + "AuthenticationSucceededWithUserName": "{0} preverjanje uspešno", + "Books": "Knjige", + "CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}", + "Channels": "Kanali", + "ChapterNameValue": "Poglavje {0}", + "Collections": "Zbirke", "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", - "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Continue Watching", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "DeviceOnlineWithName": "{0} je povezan", + "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", + "Favorites": "Priljubljeni", + "Folders": "Mape", + "Genres": "Zvrsti", + "HeaderAlbumArtists": "Izvajalci albuma", + "HeaderCameraUploads": "Posnetki kamere", + "HeaderContinueWatching": "Nadaljuj gledanje", + "HeaderFavoriteAlbums": "Priljubljeni albumi", + "HeaderFavoriteArtists": "Priljubljeni izvajalci", + "HeaderFavoriteEpisodes": "Priljubljene epizode", + "HeaderFavoriteShows": "Priljubljene serije", + "HeaderFavoriteSongs": "Priljubljene pesmi", + "HeaderLiveTV": "TV v živo", + "HeaderNextUp": "Sledi", + "HeaderRecordingGroups": "Zbirke posnetkov", + "HomeVideos": "Domači posnetki", + "Inherit": "Podeduj", + "ItemAddedWithName": "{0} je dodan v knjižnico", + "ItemRemovedWithName": "{0} je bil odstranjen iz knjižnice", + "LabelIpAddressValue": "IP naslov: {0}", + "LabelRunningTimeValue": "Čas trajanja: {0}", + "Latest": "Najnovejše", + "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen", + "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionServerRestartRequired": "Server restart required", + "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", + "MixedContent": "Razne vsebine", + "Movies": "Filmi", + "Music": "Glasba", + "MusicVideos": "Glasbeni posnetki", + "NameInstallFailed": "{0} namestitev neuspešna", + "NameSeasonNumber": "Sezona {0}", + "NameSeasonUnknown": "Season neznana", + "NewVersionIsAvailable": "Nova razničica Jellyfin strežnika je na voljo za prenos.", + "NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo", + "NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena", + "NotificationOptionAudioPlayback": "Predvajanje zvoka začeto", + "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno", + "NotificationOptionCameraImageUploaded": "Posnetek kamere naložen", + "NotificationOptionInstallationFailed": "Napaka pri nameščanju", + "NotificationOptionNewLibraryContent": "Nove vsebine dodane", + "NotificationOptionPluginError": "Napaka dodatka", + "NotificationOptionPluginInstalled": "Dodatek nameščen", + "NotificationOptionPluginUninstalled": "Dodatek odstranjen", + "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena", + "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika", "NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionVideoPlayback": "Video playback started", diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 495f82db6d..9e00eba62f 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -1,12 +1,12 @@ { - "Albums": "Albums", - "AppDeviceValues": "App: {0}, Device: {1}", - "Application": "Application", - "Artists": "Artists", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "Books": "Books", + "Albums": "Albümler", + "AppDeviceValues": "Uygulama: {0}, Aygıt: {1}", + "Application": "Uygulama", + "Artists": "Sanatçılar", + "AuthenticationSucceededWithUserName": "{0} başarı ile giriş yaptı", + "Books": "Kitaplar", "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", - "Channels": "Channels", + "Channels": "Kanallar", "ChapterNameValue": "Chapter {0}", "Collections": "Collections", "DeviceOfflineWithName": "{0} has disconnected", @@ -17,8 +17,8 @@ "Genres": "Genres", "HeaderAlbumArtists": "Album Artists", "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Continue Watching", - "HeaderFavoriteAlbums": "Favorite Albums", + "HeaderContinueWatching": "İzlemeye Devam Et", + "HeaderFavoriteAlbums": "Favori Albümler", "HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteShows": "Favori Showlar", @@ -30,21 +30,21 @@ "Inherit": "Inherit", "ItemAddedWithName": "{0} was added to the library", "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "LabelRunningTimeValue": "Running time: {0}", + "LabelIpAddressValue": "Ip adresi: {0}", + "LabelRunningTimeValue": "Çalışma süresi: {0}", "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", + "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi", "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MixedContent": "Mixed content", "Movies": "Movies", - "Music": "Music", - "MusicVideos": "Music videos", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "Music": "Müzik", + "MusicVideos": "Müzik videoları", + "NameInstallFailed": "{0} kurulum başarısız", + "NameSeasonNumber": "Sezon {0}", + "NameSeasonUnknown": "Bilinmeyen Sezon", + "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir versiyonu indirmek için hazır.", "NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionAudioPlayback": "Audio playback started", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 8910a6bce8..6f7d362d3b 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -34,14 +34,14 @@ "LabelRunningTimeValue": "运行时间:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 服务器已更新", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "Jellyfin Server 的版本已更新为 {0}", "MessageNamedServerConfigurationUpdatedWithValue": "服务器配置 {0} 部分已更新", "MessageServerConfigurationUpdated": "服务器配置已更新", "MixedContent": "混合内容", "Movies": "电影", "Music": "音乐", "MusicVideos": "音乐视频", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "{0} 安装失败", "NameSeasonNumber": "季 {0}", "NameSeasonUnknown": "未知季", "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", @@ -70,7 +70,7 @@ "ProviderValue": "提供商:{0}", "ScheduledTaskFailedWithName": "{0} 已失败", "ScheduledTaskStartedWithName": "{0} 已开始", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "{0} 需要重新启动", "Shows": "节目", "Songs": "歌曲", "StartupEmbyServerIsLoading": "Jellyfin 服务器加载中。请稍后再试。", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 31217730bf..762649b716 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization { const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings."; - Directory.CreateDirectory(LocalizationPath); - - var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName); - // Extract from the assembly foreach (var resource in _assembly.GetManifestResourceNames()) { @@ -74,100 +70,41 @@ namespace Emby.Server.Implementations.Localization continue; } - string filename = "ratings-" + resource.Substring(ratingsResource.Length); + string countryCode = resource.Substring(ratingsResource.Length, 2); + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (existingFiles.Contains(filename)) + using (var str = _assembly.GetManifestResourceStream(resource)) + using (var reader = new StreamReader(str)) { - continue; - } - - using (var stream = _assembly.GetManifestResourceStream(resource)) - { - string target = Path.Combine(LocalizationPath, filename); - _logger.LogInformation("Extracting ratings to {0}", target); - - using (var fs = _fileSystem.GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + string line; + while ((line = await reader.ReadLineAsync()) != null) { - await stream.CopyToAsync(fs); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] parts = line.Split(','); + if (parts.Length == 2 + && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value)) + { + dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value }); + } +#if DEBUG + else + { + _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); + } +#endif } } - } - foreach (var file in GetRatingsFiles(LocalizationPath)) - { - await LoadRatings(file); + _allParentalRatings[countryCode] = dict; } - LoadAdditionalRatings(); - await LoadCultures(); } - private void LoadAdditionalRatings() - { - LoadRatings("au", new[] - { - new ParentalRating("AU-G", 1), - new ParentalRating("AU-PG", 5), - new ParentalRating("AU-M", 6), - new ParentalRating("AU-MA15+", 7), - new ParentalRating("AU-M15+", 8), - new ParentalRating("AU-R18+", 9), - new ParentalRating("AU-X18+", 10), - new ParentalRating("AU-RC", 11) - }); - - LoadRatings("be", new[] - { - new ParentalRating("BE-AL", 1), - new ParentalRating("BE-MG6", 2), - new ParentalRating("BE-6", 3), - new ParentalRating("BE-9", 5), - new ParentalRating("BE-12", 6), - new ParentalRating("BE-16", 8) - }); - - LoadRatings("de", new[] - { - new ParentalRating("DE-0", 1), - new ParentalRating("FSK-0", 1), - new ParentalRating("DE-6", 5), - new ParentalRating("FSK-6", 5), - new ParentalRating("DE-12", 7), - new ParentalRating("FSK-12", 7), - new ParentalRating("DE-16", 8), - new ParentalRating("FSK-16", 8), - new ParentalRating("DE-18", 9), - new ParentalRating("FSK-18", 9) - }); - - LoadRatings("ru", new[] - { - new ParentalRating("RU-0+", 1), - new ParentalRating("RU-6+", 3), - new ParentalRating("RU-12+", 7), - new ParentalRating("RU-16+", 9), - new ParentalRating("RU-18+", 10) - }); - } - - private void LoadRatings(string country, ParentalRating[] ratings) - { - _allParentalRatings[country] = ratings.ToDictionary(i => i.Name); - } - - private IEnumerable GetRatingsFiles(string directory) - => _fileSystem.GetFilePaths(directory, false) - .Where(i => string.Equals(Path.GetExtension(i), ".csv", StringComparison.OrdinalIgnoreCase)) - .Where(i => Path.GetFileName(i).StartsWith("ratings-", StringComparison.OrdinalIgnoreCase)); - - /// - /// Gets the localization path. - /// - /// The localization path. - public string LocalizationPath - => Path.Combine(_configurationManager.ApplicationPaths.ProgramDataPath, "localization"); - public string NormalizeFormKD(string text) => text.Normalize(NormalizationForm.FormKD); @@ -288,47 +225,6 @@ namespace Emby.Server.Implementations.Localization return value; } - /// - /// Loads the ratings. - /// - /// The file. - /// Dictionary{System.StringParentalRating}. - private async Task LoadRatings(string file) - { - Dictionary dict - = new Dictionary(StringComparer.OrdinalIgnoreCase); - - using (var str = File.OpenRead(file)) - using (var reader = new StreamReader(str)) - { - string line; - while ((line = await reader.ReadLineAsync()) != null) - { - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value)) - { - dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value })); - } -#if DEBUG - else - { - _logger.LogWarning("Misformed line in {Path}", file); - } -#endif - } - } - - var countryCode = Path.GetFileNameWithoutExtension(file).Split('-')[1]; - - _allParentalRatings[countryCode] = dict; - } - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; /// diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv new file mode 100644 index 0000000000..940375e268 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/au.csv @@ -0,0 +1,8 @@ +AU-G,1 +AU-PG,5 +AU-M,6 +AU-MA15+,7 +AU-M15+,8 +AU-R18+,9 +AU-X18+,10 +AU-RC,11 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv new file mode 100644 index 0000000000..d3937caf78 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/be.csv @@ -0,0 +1,6 @@ +BE-AL,1 +BE-MG6,2 +BE-6,3 +BE-9,5 +BE-12,6 +BE-16,8 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv new file mode 100644 index 0000000000..f944a140d0 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/de.csv @@ -0,0 +1,10 @@ +DE-0,1 +FSK-0,1 +DE-6,5 +FSK-6,5 +DE-12,7 +FSK-12,7 +DE-16,8 +FSK-16,8 +DE-18,9 +FSK-18,9 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv new file mode 100644 index 0000000000..1bc94affd6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ru.csv @@ -0,0 +1,5 @@ +RU-0+,1 +RU-6+,3 +RU-12+,7 +RU-16+,9 +RU-18+,10 diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index e68046f6d7..52d07d784d 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -202,6 +202,10 @@ namespace Emby.Server.Implementations.MediaEncoder private static List GetSavedChapterImages(Video video, IDirectoryService directoryService) { var path = GetChapterImagesPath(video); + if (!Directory.Exists(path)) + { + return new List(); + } try { diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs new file mode 100644 index 0000000000..268bf4042d --- /dev/null +++ b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager; + +namespace Emby.Server.Implementations.Middleware +{ + public class WebSocketMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly WebSocketManager _webSocketManager; + + public WebSocketMiddleware(RequestDelegate next, ILogger logger, WebSocketManager webSocketManager) + { + _next = next; + _logger = logger; + _webSocketManager = webSocketManager; + } + + public async Task Invoke(HttpContext httpContext) + { + _logger.LogInformation("Handling request: " + httpContext.Request.Path); + + if (httpContext.WebSockets.IsWebSocketRequest) + { + var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); + if (webSocketContext != null) + { + await _webSocketManager.OnWebSocketConnected(webSocketContext); + } + } + else + { + await _next.Invoke(httpContext); + } + } + } +} diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs index 4671de07c5..4d160aa66f 100644 --- a/Emby.Server.Implementations/Net/IWebSocket.cs +++ b/Emby.Server.Implementations/Net/IWebSocket.cs @@ -45,9 +45,4 @@ namespace Emby.Server.Implementations.Net /// Task. Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken); } - - public interface IMemoryWebSocket - { - Action, int> OnReceiveMemoryBytes { get; set; } - } } diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs index 3ab8e854a0..e3047d3926 100644 --- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs +++ b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs @@ -1,5 +1,7 @@ using System; +using System.Net.WebSockets; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.Net { @@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Net /// Gets or sets the query string. /// /// The query string. - public QueryParamCollection QueryString { get; set; } + public IQueryCollection QueryString { get; set; } /// /// Gets or sets the web socket. /// diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs index 8a7c1492d4..cad66a80fd 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Playlists { } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var playlist = (Playlist)item; @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { return _libraryManager.GetItemList(new InternalItemsQuery { @@ -89,7 +89,6 @@ namespace Emby.Server.Implementations.Playlists Recursive = true, ImageTypes = new[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) - }); } } @@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.Playlists _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { return _libraryManager.GetItemList(new InternalItemsQuery { @@ -116,11 +115,5 @@ namespace Emby.Server.Implementations.Playlists DtoOptions = new DtoOptions(false) }); } - - //protected override Task CreateImage(IHasMetadata item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) - //{ - // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); - //} } - } diff --git a/Emby.Server.Implementations/Reflection/AssemblyInfo.cs b/Emby.Server.Implementations/Reflection/AssemblyInfo.cs deleted file mode 100644 index 9d16fe43f6..0000000000 --- a/Emby.Server.Implementations/Reflection/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using MediaBrowser.Model.Reflection; - -namespace Emby.Server.Implementations.Reflection -{ - public class AssemblyInfo : IAssemblyInfo - { - public Stream GetManifestResourceStream(Type type, string resource) - { - return type.Assembly.GetManifestResourceStream(resource); - } - - public string[] GetManifestResourceNames(Type type) - { - return type.Assembly.GetManifestResourceNames(); - } - - public Assembly[] GetCurrentAssemblies() - { - return AppDomain.CurrentDomain.GetAssemblies(); - } - } -} diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs index 296da2f7a0..b6758486ce 100644 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ b/Emby.Server.Implementations/Services/HttpResult.cs @@ -43,14 +43,9 @@ namespace Emby.Server.Implementations.Services { var contentLength = bytesResponse.Length; - if (response != null) - { - response.SetContentLength(contentLength); - } - if (contentLength > 0) { - await responseStream.WriteAsync(bytesResponse, 0, contentLength).ConfigureAwait(false); + await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); } return; } diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs index dc99753477..0301ff335f 100644 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ b/Emby.Server.Implementations/Services/ResponseHelper.cs @@ -20,8 +20,6 @@ namespace Emby.Server.Implementations.Services { response.StatusCode = (int)HttpStatusCode.NoContent; } - - response.SetContentLength(0); return Task.CompletedTask; } @@ -55,7 +53,6 @@ namespace Emby.Server.Implementations.Services { if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) { - response.SetContentLength(long.Parse(responseHeaders.Value)); continue; } @@ -104,7 +101,6 @@ namespace Emby.Server.Implementations.Services if (bytes != null) { response.ContentType = "application/octet-stream"; - response.SetContentLength(bytes.Length); if (bytes.Length > 0) { @@ -117,7 +113,6 @@ namespace Emby.Server.Implementations.Services if (responseText != null) { bytes = Encoding.UTF8.GetBytes(responseText); - response.SetContentLength(bytes.Length); if (bytes.Length > 0) { return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); @@ -149,8 +144,6 @@ namespace Emby.Server.Implementations.Services var contentLength = ms.Length; - response.SetContentLength(contentLength); - if (contentLength > 0) { await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index 79f5c59e65..38952628d8 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Services foreach (var requestFilter in actionContext.RequestFilters) { requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.IsClosed) + if (request.Response.OriginalResponse.HasStarted) { Task.FromResult(null); } diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 7e836e22c3..3c8adfc983 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -154,7 +154,7 @@ namespace Emby.Server.Implementations.Services { if (name == null) continue; //thank you ASP.NET - var values = request.QueryString.GetValues(name); + var values = request.QueryString[name]; if (values.Count == 1) { map[name] = values[0]; diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 24903f5e8c..a551433ed9 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Events; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Session } } - private SessionInfo GetSession(QueryParamCollection queryString, string remoteEndpoint) + private SessionInfo GetSession(IQueryCollection queryString, string remoteEndpoint) { if (queryString == null) { diff --git a/Jellyfin.Server/SocketSharp/HttpFile.cs b/Emby.Server.Implementations/SocketSharp/HttpFile.cs similarity index 87% rename from Jellyfin.Server/SocketSharp/HttpFile.cs rename to Emby.Server.Implementations/SocketSharp/HttpFile.cs index 448b666b63..120ac50d9c 100644 --- a/Jellyfin.Server/SocketSharp/HttpFile.cs +++ b/Emby.Server.Implementations/SocketSharp/HttpFile.cs @@ -1,7 +1,7 @@ using System.IO; using MediaBrowser.Model.Services; -namespace Jellyfin.Server.SocketSharp +namespace Emby.Server.Implementations.SocketSharp { public class HttpFile : IHttpFile { diff --git a/Jellyfin.Server/SocketSharp/HttpPostedFile.cs b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs similarity index 100% rename from Jellyfin.Server/SocketSharp/HttpPostedFile.cs rename to Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs index f38ed848ee..95b7912fbb 100644 --- a/Jellyfin.Server/SocketSharp/HttpPostedFile.cs +++ b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs @@ -63,6 +63,28 @@ public sealed class HttpPostedFile : IDisposable _position = offset; } + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => _end - _offset; + + public override long Position + { + get => _position - _offset; + set + { + if (value > Length) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _position = Seek(value, SeekOrigin.Begin); + } + } + public override void Flush() { } @@ -178,27 +200,5 @@ public sealed class HttpPostedFile : IDisposable { throw new NotSupportedException(); } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => _end - _offset; - - public override long Position - { - get => _position - _offset; - set - { - if (value > Length) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _position = Seek(value, SeekOrigin.Begin); - } - } } } diff --git a/Jellyfin.Server/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs similarity index 90% rename from Jellyfin.Server/SocketSharp/RequestMono.cs rename to Emby.Server.Implementations/SocketSharp/RequestMono.cs index 8396ad600d..373f6d7580 100644 --- a/Jellyfin.Server/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/SocketSharp/RequestMono.cs @@ -6,14 +6,16 @@ using System.Net; using System.Text; using System.Threading.Tasks; using MediaBrowser.Model.Services; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; -namespace Jellyfin.Server.SocketSharp +namespace Emby.Server.Implementations.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { internal static string GetParameter(ReadOnlySpan header, string attr) { - int ap = header.IndexOf(attr, StringComparison.Ordinal); + int ap = header.IndexOf(attr.AsSpan(), StringComparison.Ordinal); if (ap == -1) { return null; @@ -43,7 +45,7 @@ namespace Jellyfin.Server.SocketSharp private async Task LoadMultiPart(WebROCollection form) { - string boundary = GetParameter(ContentType, "; boundary="); + string boundary = GetParameter(ContentType.AsSpan(), "; boundary="); if (boundary == null) { return; @@ -77,7 +79,7 @@ namespace Jellyfin.Server.SocketSharp byte[] copy = new byte[e.Length]; input.Position = e.Start; - input.Read(copy, 0, (int)e.Length); + await input.ReadAsync(copy, 0, (int)e.Length).ConfigureAwait(false); form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); } @@ -96,23 +98,15 @@ namespace Jellyfin.Server.SocketSharp var form = new WebROCollection(); files = new Dictionary(); - if (IsContentType("multipart/form-data", true)) + if (IsContentType("multipart/form-data")) { await LoadMultiPart(form).ConfigureAwait(false); } - else if (IsContentType("application/x-www-form-urlencoded", true)) + else if (IsContentType("application/x-www-form-urlencoded")) { await LoadWwwForm(form).ConfigureAwait(false); } -#if NET_4_0 - if (validateRequestNewMode && !checked_form) { - // Setting this before calling the validator prevents - // possible endless recursion - checked_form = true; - ValidateNameValueCollection("Form", query_string_nvc, RequestValidationSource.Form); - } else -#endif if (validate_form && !checked_form) { checked_form = true; @@ -122,15 +116,11 @@ namespace Jellyfin.Server.SocketSharp return form; } - public string Accept => string.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"]; + public string Accept => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Accept]) ? null : request.Headers[HeaderNames.Accept].ToString(); - public string Authorization => string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"]; + public string Authorization => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]) ? null : request.Headers[HeaderNames.Authorization].ToString(); - protected bool validate_cookies { get; set; } - protected bool validate_query_string { get; set; } protected bool validate_form { get; set; } - protected bool checked_cookies { get; set; } - protected bool checked_query_string { get; set; } protected bool checked_form { get; set; } private static void ThrowValidationException(string name, string key, string value) @@ -210,26 +200,14 @@ namespace Jellyfin.Server.SocketSharp return false; } - public void ValidateInput() + private bool IsContentType(string ct) { - validate_cookies = true; - validate_query_string = true; - validate_form = true; - } - - private bool IsContentType(string ct, bool starts_with) - { - if (ct == null || ContentType == null) + if (ContentType == null) { return false; } - if (starts_with) - { - return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); - } - - return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); + return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); } private async Task LoadWwwForm(WebROCollection form) @@ -396,14 +374,14 @@ namespace Jellyfin.Server.SocketSharp var elem = new Element(); ReadOnlySpan header; - while ((header = ReadHeaders()) != null) + while ((header = ReadHeaders().AsSpan()) != null) { - if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase)) + if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase)) { elem.Name = GetContentDispositionAttribute(header, "name"); elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); } - else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase)) + else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase)) { elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString(); elem.Encoding = GetEncoding(elem.ContentType); @@ -455,7 +433,7 @@ namespace Jellyfin.Server.SocketSharp private static string GetContentDispositionAttribute(ReadOnlySpan l, string name) { - int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); + int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal); if (idx < 0) { return null; @@ -478,7 +456,7 @@ namespace Jellyfin.Server.SocketSharp private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan l, string name) { - int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal); + int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal); if (idx < 0) { return null; diff --git a/Jellyfin.Server/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs similarity index 56% rename from Jellyfin.Server/SocketSharp/SharpWebSocket.cs rename to Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs index 9b0951857f..62b16ed8c8 100644 --- a/Jellyfin.Server/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs @@ -1,11 +1,12 @@ using System; using System.Net.WebSockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Net; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.SocketSharp +namespace Emby.Server.Implementations.SocketSharp { public class SharpWebSocket : IWebSocket { @@ -20,67 +21,22 @@ namespace Jellyfin.Server.SocketSharp /// Gets or sets the web socket. /// /// The web socket. - private SocketHttpListener.WebSocket WebSocket { get; set; } + private readonly WebSocket _webSocket; - private TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed = false; + private bool _disposed; - public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger) + public SharpWebSocket(WebSocket socket, ILogger logger) { - if (socket == null) - { - throw new ArgumentNullException(nameof(socket)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - _logger = logger; - WebSocket = socket; - - socket.OnMessage += OnSocketMessage; - socket.OnClose += OnSocketClose; - socket.OnError += OnSocketError; - } - - public Task ConnectAsServerAsync() - => WebSocket.ConnectAsServer(); - - public Task StartReceive() - { - return _taskCompletionSource.Task; - } - - private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e) - { - _logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty); - - // Closed?.Invoke(this, EventArgs.Empty); - } - - private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e) - { - _taskCompletionSource.TrySetResult(true); - - Closed?.Invoke(this, EventArgs.Empty); - } - - private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e) - { - if (OnReceiveBytes != null) - { - OnReceiveBytes(e.RawData); - } + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _webSocket = socket ?? throw new ArgumentNullException(nameof(socket)); } /// /// Gets or sets the state. /// /// The state. - public WebSocketState State => WebSocket.ReadyState; + public WebSocketState State => _webSocket.State; /// /// Sends the async. @@ -91,7 +47,7 @@ namespace Jellyfin.Server.SocketSharp /// Task. public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) { - return WebSocket.SendAsync(bytes); + return _webSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken); } /// @@ -103,7 +59,7 @@ namespace Jellyfin.Server.SocketSharp /// Task. public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) { - return WebSocket.SendAsync(text); + return _webSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken); } /// @@ -128,13 +84,13 @@ namespace Jellyfin.Server.SocketSharp if (dispose) { - WebSocket.OnMessage -= OnSocketMessage; - WebSocket.OnClose -= OnSocketClose; - WebSocket.OnError -= OnSocketError; - _cancellationTokenSource.Cancel(); - - WebSocket.CloseAsync().GetAwaiter().GetResult(); + if (_webSocket.State == WebSocketState.Open) + { + _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client", + CancellationToken.None); + } + Closed?.Invoke(this, EventArgs.Empty); } _disposed = true; diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs new file mode 100644 index 0000000000..dd313b3363 --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.Net; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Emby.Server.Implementations.SocketSharp +{ + public class WebSocketSharpListener : IHttpListener + { + private readonly ILogger _logger; + + private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); + private CancellationToken _disposeCancellationToken; + + public WebSocketSharpListener( + ILogger logger) + { + _logger = logger; + + _disposeCancellationToken = _disposeCancellationTokenSource.Token; + } + + public Func ErrorHandler { get; set; } + public Func RequestHandler { get; set; } + + public Action WebSocketConnected { get; set; } + + private static void LogRequest(ILogger logger, HttpRequest request) + { + var url = request.GetDisplayUrl(); + + logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString()); + } + + public async Task ProcessWebSocketRequest(HttpContext ctx) + { + try + { + LogRequest(_logger, ctx.Request); + var endpoint = ctx.Connection.RemoteIpAddress.ToString(); + var url = ctx.Request.GetDisplayUrl(); + + var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); + var socket = new SharpWebSocket(webSocketContext, _logger); + + WebSocketConnected(new WebSocketConnectEventArgs + { + Url = url, + QueryString = ctx.Request.Query, + WebSocket = socket, + Endpoint = endpoint + }); + + WebSocketReceiveResult result; + var message = new List(); + + do + { + var buffer = WebSocket.CreateServerBuffer(4096); + result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken); + message.AddRange(buffer.Array.Take(result.Count)); + + if (result.EndOfMessage) + { + socket.OnReceiveBytes(message.ToArray()); + message.Clear(); + } + } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close); + + + if (webSocketContext.State == WebSocketState.Open) + { + await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + result.CloseStatusDescription, _disposeCancellationToken); + } + + socket.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "AcceptWebSocketAsync error"); + if (!ctx.Response.HasStarted) + { + ctx.Response.StatusCode = 500; + } + } + } + + public Task Stop() + { + _disposeCancellationTokenSource.Cancel(); + return Task.CompletedTask; + } + + /// + /// Releases the unmanaged resources and disposes of the managed resources used. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool _disposed; + + /// + /// Releases the unmanaged resources and disposes of the managed resources used. + /// + /// Whether or not the managed resources should be disposed + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + Stop().GetAwaiter().GetResult(); + } + + _disposed = true; + } + } +} diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs similarity index 73% rename from Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs rename to Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 069f47f9ab..e0a0ee2861 100644 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -2,59 +2,54 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Net; using System.Text; -using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; -using SocketHttpListener.Net; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; using IResponse = MediaBrowser.Model.Services.IResponse; -namespace Jellyfin.Server.SocketSharp +namespace Emby.Server.Implementations.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { - private readonly HttpListenerRequest request; - private readonly IHttpResponse response; + private readonly HttpRequest request; - public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, ILogger logger) + public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger) { this.OperationName = operationName; - this.request = httpContext.Request; - this.response = new WebSocketSharpResponse(logger, httpContext.Response, this); + this.request = httpContext; + this.Response = new WebSocketSharpResponse(logger, response); // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]); } - public HttpListenerRequest HttpRequest => request; + public HttpRequest HttpRequest => request; - public object OriginalRequest => request; - - public IResponse Response => response; - - public IHttpResponse HttpResponse => response; + public IResponse Response { get; } public string OperationName { get; set; } public object Dto { get; set; } - public string RawUrl => request.RawUrl; + public string RawUrl => request.GetEncodedPathAndQuery(); - public string AbsoluteUri => request.Url.AbsoluteUri.TrimEnd('/'); - - public string UserHostAddress => request.UserHostAddress; + public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); public string XForwardedFor - => string.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"]; + => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString(); public int? XForwardedPort - => string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture); + => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture); - public string XForwardedProtocol => string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"]; + public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString(); - public string XRealIp => string.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"]; + public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString(); private string remoteIp; public string RemoteIp @@ -66,19 +61,19 @@ namespace Jellyfin.Server.SocketSharp return remoteIp; } - var temp = CheckBadChars(XForwardedFor); + var temp = CheckBadChars(XForwardedFor.AsSpan()); if (temp.Length != 0) { return remoteIp = temp.ToString(); } - temp = CheckBadChars(XRealIp); + temp = CheckBadChars(XRealIp.AsSpan()); if (temp.Length != 0) { return remoteIp = NormalizeIp(temp).ToString(); } - return remoteIp = NormalizeIp(request.RemoteEndPoint?.Address.ToString()).ToString(); + return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString(); } } @@ -156,26 +151,13 @@ namespace Jellyfin.Server.SocketSharp return name; } - internal static bool ContainsNonAsciiChars(string token) - { - for (int i = 0; i < token.Length; ++i) - { - if ((token[i] < 0x20) || (token[i] > 0x7e)) - { - return true; - } - } - - return false; - } - private ReadOnlySpan NormalizeIp(ReadOnlySpan ip) { if (ip.Length != 0 && !ip.IsWhiteSpace()) { // Handle ipv4 mapped to ipv6 const string srch = "::ffff:"; - var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase); + var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase); if (index == 0) { ip = ip.Slice(srch.Length); @@ -185,9 +167,7 @@ namespace Jellyfin.Server.SocketSharp return ip; } - public bool IsSecureConnection => request.IsSecureConnection || XForwardedProtocol == "https"; - - public string[] AcceptTypes => request.AcceptTypes; + public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); private Dictionary items; public Dictionary Items => items ?? (items = new Dictionary()); @@ -197,13 +177,13 @@ namespace Jellyfin.Server.SocketSharp { get => responseContentType - ?? (responseContentType = GetResponseContentType(this)); + ?? (responseContentType = GetResponseContentType(HttpRequest)); set => this.responseContentType = value; } public const string FormUrlEncoded = "application/x-www-form-urlencoded"; public const string MultiPartFormData = "multipart/form-data"; - public static string GetResponseContentType(IRequest httpReq) + public static string GetResponseContentType(HttpRequest httpReq) { var specifiedContentType = GetQueryStringContentType(httpReq); if (!string.IsNullOrEmpty(specifiedContentType)) @@ -213,7 +193,7 @@ namespace Jellyfin.Server.SocketSharp const string serverDefaultContentType = "application/json"; - var acceptContentTypes = httpReq.AcceptTypes; + var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); string defaultContentType = null; if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) { @@ -261,7 +241,7 @@ namespace Jellyfin.Server.SocketSharp public const string Soap11 = "text/xml; charset=utf-8"; - public static bool HasAnyOfContentTypes(IRequest request, params string[] contentTypes) + public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes) { if (contentTypes == null || request.ContentType == null) { @@ -279,18 +259,18 @@ namespace Jellyfin.Server.SocketSharp return false; } - public static bool IsContentType(IRequest request, string contentType) + public static bool IsContentType(HttpRequest request, string contentType) { return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); } - private static string GetQueryStringContentType(IRequest httpReq) + private static string GetQueryStringContentType(HttpRequest httpReq) { - ReadOnlySpan format = httpReq.QueryString["format"]; + ReadOnlySpan format = httpReq.Query["format"].ToString().AsSpan(); if (format == null) { const int formatMaxLength = 4; - ReadOnlySpan pi = httpReq.PathInfo; + ReadOnlySpan pi = httpReq.Path.ToString().AsSpan(); if (pi == null || pi.Length <= formatMaxLength) { return null; @@ -309,11 +289,11 @@ namespace Jellyfin.Server.SocketSharp } format = LeftPart(format, '.'); - if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) + if (format.Contains("json".AsSpan(), StringComparison.OrdinalIgnoreCase)) { return "application/json"; } - else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase)) + else if (format.Contains("xml".AsSpan(), StringComparison.OrdinalIgnoreCase)) { return "application/xml"; } @@ -343,10 +323,10 @@ namespace Jellyfin.Server.SocketSharp { var mode = HandlerFactoryPath; - var pos = request.RawUrl.IndexOf('?', StringComparison.Ordinal); + var pos = RawUrl.IndexOf("?", StringComparison.Ordinal); if (pos != -1) { - var path = request.RawUrl.Substring(0, pos); + var path = RawUrl.Substring(0, pos); this.pathInfo = GetPathInfo( path, mode, @@ -354,10 +334,10 @@ namespace Jellyfin.Server.SocketSharp } else { - this.pathInfo = request.RawUrl; + this.pathInfo = RawUrl; } - this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo); + this.pathInfo = WebUtility.UrlDecode(pathInfo); this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString(); } @@ -421,59 +401,55 @@ namespace Jellyfin.Server.SocketSharp return null; } - var path = sbPathInfo.ToString(); - return path.Length > 1 ? path.TrimEnd('/') : "/"; + return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/"; } - private Dictionary cookies; - public IDictionary Cookies - { - get - { - if (cookies == null) - { - cookies = new Dictionary(); - foreach (var cookie in this.request.Cookies) - { - var httpCookie = (System.Net.Cookie)cookie; - cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain); - } - } + public string UserAgent => request.Headers[HeaderNames.UserAgent]; - return cookies; - } - } + public IHeaderDictionary Headers => request.Headers; - public string UserAgent => request.UserAgent; + public IQueryCollection QueryString => request.Query; - public QueryParamCollection Headers => request.Headers; - - private QueryParamCollection queryString; - public QueryParamCollection QueryString => queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query)); - - public bool IsLocal => request.IsLocal; + public bool IsLocal => string.Equals(request.HttpContext.Connection.LocalIpAddress.ToString(), request.HttpContext.Connection.RemoteIpAddress.ToString()); private string httpMethod; public string HttpMethod => httpMethod - ?? (httpMethod = request.HttpMethod); + ?? (httpMethod = request.Method); public string Verb => HttpMethod; public string ContentType => request.ContentType; - private Encoding contentEncoding; - public Encoding ContentEncoding + private Encoding ContentEncoding { - get => contentEncoding ?? request.ContentEncoding; - set => contentEncoding = value; + get + { + // TODO is this necessary? + if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP")) + { + string postDataCharset = Headers["x-up-devcap-post-charset"]; + if (!string.IsNullOrEmpty(postDataCharset)) + { + try + { + return Encoding.GetEncoding(postDataCharset); + } + catch (ArgumentException) + { + } + } + } + + return request.GetTypedHeaders().ContentType.Encoding ?? Encoding.UTF8; + } } - public Uri UrlReferrer => request.UrlReferrer; + public Uri UrlReferrer => request.GetTypedHeaders().Referer; public static Encoding GetEncoding(string contentTypeHeader) { - var param = GetParameter(contentTypeHeader, "charset="); + var param = GetParameter(contentTypeHeader.AsSpan(), "charset="); if (param == null) { return null; @@ -489,9 +465,9 @@ namespace Jellyfin.Server.SocketSharp } } - public Stream InputStream => request.InputStream; + public Stream InputStream => request.Body; - public long ContentLength => request.ContentLength64; + public long ContentLength => request.ContentLength ?? 0; private IHttpFile[] httpFiles; public IHttpFile[] Files @@ -530,13 +506,13 @@ namespace Jellyfin.Server.SocketSharp if (handlerPath != null) { var trimmed = pathInfo.AsSpan().TrimStart('/'); - if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase)) + if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase)) { - return trimmed.Slice(handlerPath.Length).ToString(); + return trimmed.Slice(handlerPath.Length).ToString().AsSpan(); } } - return pathInfo; + return pathInfo.AsSpan(); } } } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs new file mode 100644 index 0000000000..0f67eaa622 --- /dev/null +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using IRequest = MediaBrowser.Model.Services.IRequest; + +namespace Emby.Server.Implementations.SocketSharp +{ + public class WebSocketSharpResponse : IResponse + { + private readonly ILogger _logger; + + public WebSocketSharpResponse(ILogger logger, HttpResponse response) + { + _logger = logger; + OriginalResponse = response; + } + + public HttpResponse OriginalResponse { get; } + + public int StatusCode + { + get => OriginalResponse.StatusCode; + set => OriginalResponse.StatusCode = value; + } + + public string StatusDescription { get; set; } + + public string ContentType + { + get => OriginalResponse.ContentType; + set => OriginalResponse.ContentType = value; + } + + public void AddHeader(string name, string value) + { + if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + ContentType = value; + return; + } + + OriginalResponse.Headers.Add(name, value); + } + + public void Redirect(string url) + { + OriginalResponse.Redirect(url); + } + + public Stream OutputStream => OriginalResponse.Body; + + public bool SendChunked { get; set; } + + const int StreamCopyToBufferSize = 81920; + public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken) + { + var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + //if (count <= 0) + //{ + // allowAsync = true; + //} + + var fileOpenOptions = FileOpenOptions.SequentialScan; + + if (allowAsync) + { + fileOpenOptions |= FileOpenOptions.Asynchronous; + } + + // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + + using (var fs = fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) + { + if (offset > 0) + { + fs.Position = offset; + } + + if (count > 0) + { + await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false); + } + else + { + await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } + } + } +} diff --git a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index ce6c2cd87d..a3f3f6cb4d 100644 --- a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Emby.Server.Implementations.Images; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; @@ -20,7 +19,7 @@ namespace Emby.Server.Implementations.UserViews { } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var view = (CollectionFolder)item; var viewType = view.CollectionType; @@ -56,7 +55,7 @@ namespace Emby.Server.Implementations.UserViews includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" }; } - var recursive = !new[] { CollectionType.Playlists }.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase); return view.GetItemList(new InternalItemsQuery { @@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.UserViews }, IncludeItemTypes = includeItemTypes - }).ToList(); + }); } protected override bool Supports(BaseItem item) @@ -79,7 +78,7 @@ namespace Emby.Server.Implementations.UserViews return item is CollectionFolder; } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); diff --git a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs index 4ec68e550d..f485204436 100644 --- a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.UserViews _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { var view = (UserView)item; @@ -46,8 +46,7 @@ namespace Emby.Server.Implementations.UserViews var items = result.Select(i => { - var episode = i as Episode; - if (episode != null) + if (i is Episode episode) { var series = episode.Series; if (series != null) @@ -58,8 +57,7 @@ namespace Emby.Server.Implementations.UserViews return episode; } - var season = i as Season; - if (season != null) + if (i is Season season) { var series = season.Series; if (series != null) @@ -70,8 +68,7 @@ namespace Emby.Server.Implementations.UserViews return season; } - var audio = i as Audio; - if (audio != null) + if (i is Audio audio) { var album = audio.AlbumEntity; if (album != null && album.HasImage(ImageType.Primary)) @@ -122,7 +119,7 @@ namespace Emby.Server.Implementations.UserViews return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { if (itemsWithImages.Count == 0) { diff --git a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs index c810004ab2..4655cd928a 100644 --- a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs @@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.UserViews _libraryManager = libraryManager; } - protected override List GetItemsWithImages(BaseItem item) + protected override IReadOnlyList GetItemsWithImages(BaseItem item) { return _libraryManager.GetItemList(new InternalItemsQuery { @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.UserViews }); } - protected override string CreateImage(BaseItem item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs new file mode 100644 index 0000000000..eb18774408 --- /dev/null +++ b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using MediaBrowser.Model.Net; + +namespace Emby.Server.Implementations.WebSockets +{ + public interface IWebSocketHandler + { + Task ProcessMessage(WebSocketMessage message, TaskCompletionSource taskCompletionSource); + } +} diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs new file mode 100644 index 0000000000..04c73ecea7 --- /dev/null +++ b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Logging; +using UtfUnknown; + +namespace Emby.Server.Implementations.WebSockets +{ + public class WebSocketManager + { + private readonly IWebSocketHandler[] _webSocketHandlers; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private const int BufferSize = 4096; + + public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger logger) + { + _webSocketHandlers = webSocketHandlers; + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + public async Task OnWebSocketConnected(WebSocket webSocket) + { + var taskCompletionSource = new TaskCompletionSource(); + var cancellationToken = new CancellationTokenSource().Token; + WebSocketReceiveResult result; + var message = new List(); + + // Keep listening for incoming messages, otherwise the socket closes automatically + do + { + var buffer = WebSocket.CreateServerBuffer(BufferSize); + result = await webSocket.ReceiveAsync(buffer, cancellationToken); + message.AddRange(buffer.Array.Take(result.Count)); + + if (result.EndOfMessage) + { + await ProcessMessage(message.ToArray(), taskCompletionSource); + message.Clear(); + } + } while (!taskCompletionSource.Task.IsCompleted && + webSocket.State == WebSocketState.Open && + result.MessageType != WebSocketMessageType.Close); + + if (webSocket.State == WebSocketState.Open) + { + await webSocket.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + result.CloseStatusDescription, cancellationToken); + } + } + + private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource taskCompletionSource) + { + var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName; + var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase) + ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length) + : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length); + + // All messages are expected to be valid JSON objects + if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message); + return; + } + + try + { + var info = _jsonSerializer.DeserializeFromString>(message); + + _logger.LogDebug("Websocket message received: {0}", info.MessageType); + + var tasks = _webSocketHandlers.Select(handler => Task.Run(() => + { + try + { + handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}", + handler.GetType().Name, info.MessageType ?? string.Empty); + } + })); + + await Task.WhenAll(tasks); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing web socket message"); + } + } + } +} diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index dfdf398710..7d404ce644 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -77,21 +77,18 @@ namespace Jellyfin.Drawing.Skia { canvas.Clear(SKColors.Black); + // number of images used in the thumbnail + var iCount = 3; + // determine sizes for each image that will composited into the final image - var iSlice = Convert.ToInt32(width * 0.23475); - int iTrans = Convert.ToInt32(height * .25); - int iHeight = Convert.ToInt32(height * .70); - var horizontalImagePadding = Convert.ToInt32(width * 0.0125); - var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + var iSlice = Convert.ToInt32(width / iCount); + int iHeight = Convert.ToInt32(height * 1.00); int imageIndex = 0; - - for (int i = 0; i < 4; i++) + for (int i = 0; i < iCount; i++) { - using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) { imageIndex = newIndex; - if (currentBitmap == null) { continue; @@ -108,44 +105,7 @@ namespace Jellyfin.Drawing.Skia using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight))) { // draw image onto canvas - canvas.DrawImage(subset ?? image, (horizontalImagePadding * (i + 1)) + (iSlice * i), verticalSpacing); - - if (subset == null) - { - continue; - } - // create reflection of image below the drawn image - using (var croppedBitmap = SKBitmap.FromImage(subset)) - using (var reflectionBitmap = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType)) - { - // resize to half height - currentBitmap.ScalePixels(reflectionBitmap, SKFilterQuality.High); - - using (var flippedBitmap = new SKBitmap(reflectionBitmap.Width, reflectionBitmap.Height, reflectionBitmap.ColorType, reflectionBitmap.AlphaType)) - using (var flippedCanvas = new SKCanvas(flippedBitmap)) - { - // flip image vertically - var matrix = SKMatrix.MakeScale(1, -1); - matrix.SetScaleTranslate(1, -1, 0, flippedBitmap.Height); - flippedCanvas.SetMatrix(matrix); - flippedCanvas.DrawBitmap(reflectionBitmap, 0, 0); - flippedCanvas.ResetMatrix(); - - // create gradient to make image appear as a reflection - var remainingHeight = height - (iHeight + (2 * verticalSpacing)); - flippedCanvas.ClipRect(SKRect.Create(reflectionBitmap.Width, remainingHeight)); - using (var gradient = new SKPaint()) - { - gradient.IsAntialias = true; - gradient.BlendMode = SKBlendMode.SrcOver; - gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, remainingHeight), new[] { new SKColor(0, 0, 0, 128), new SKColor(0, 0, 0, 208), new SKColor(0, 0, 0, 240), new SKColor(0, 0, 0, 255) }, null, SKShaderTileMode.Clamp); - flippedCanvas.DrawPaint(gradient); - } - - // finally draw reflection onto canvas - canvas.DrawBitmap(flippedBitmap, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + (2 * verticalSpacing)); - } - } + canvas.DrawImage(subset ?? image, iSlice * i, 0); } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 84d78d3fb6..17259c7370 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Reflection; using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; -using Jellyfin.Server.SocketSharp; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.Extensions.Configuration; @@ -35,8 +34,6 @@ namespace Jellyfin.Server public override bool CanSelfRestart => StartupOptions.RestartPath != null; - protected override bool SupportsDualModeSockets => true; - protected override void RestartInternal() => Program.Restart(); protected override IEnumerable GetAssembliesWithPartsInternal() @@ -45,17 +42,5 @@ namespace Jellyfin.Server } protected override void ShutdownInternal() => Program.Shutdown(); - - protected override IHttpListener CreateHttpListener() - => new WebSocketSharpListener( - Logger, - Certificate, - StreamHelper, - NetworkManager, - SocketFactory, - CryptographyProvider, - SupportsDualModeSockets, - FileSystemManager, - EnvironmentInfo); } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bd670df527..9346a2d254 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -12,7 +12,8 @@ latest - SA1600;CS1591 + SA1600;SA1601;CS1591 + true @@ -23,10 +24,6 @@ - - true - - diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 41ee73a565..fa4b7b8e52 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -20,6 +20,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -124,7 +125,7 @@ namespace Jellyfin.Server SQLitePCL.Batteries_V2.Init(); // Allow all https requests - ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; } ); + ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, appPaths); @@ -144,8 +145,6 @@ namespace Jellyfin.Server await appHost.RunStartupTasks().ConfigureAwait(false); - // TODO: read input for a stop command - try { // Block main thread until shutdown @@ -175,7 +174,7 @@ namespace Jellyfin.Server { // dataDir // IF --datadir - // ELSE IF $JELLYFIN_DATA_PATH + // ELSE IF $JELLYFIN_DATA_DIR // ELSE IF windows, use <%APPDATA%>/jellyfin // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin // ELSE use $HOME/.local/share/jellyfin @@ -183,7 +182,7 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(dataDir)) { - dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH"); + dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_DIR"); if (string.IsNullOrEmpty(dataDir)) { @@ -192,8 +191,6 @@ namespace Jellyfin.Server } } - Directory.CreateDirectory(dataDir); - // configDir // IF --configdir // ELSE IF $JELLYFIN_CONFIG_DIR @@ -286,6 +283,7 @@ namespace Jellyfin.Server // Ensure the main folders exist before we continue try { + Directory.CreateDirectory(dataDir); Directory.CreateDirectory(logDir); Directory.CreateDirectory(configDir); Directory.CreateDirectory(cacheDir); diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs deleted file mode 100644 index 693c2328c6..0000000000 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using Emby.Server.Implementations.Net; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; -using SocketHttpListener.Net; - -namespace Jellyfin.Server.SocketSharp -{ - public class WebSocketSharpListener : IHttpListener - { - private HttpListener _listener; - - private readonly ILogger _logger; - private readonly X509Certificate _certificate; - private readonly IStreamHelper _streamHelper; - private readonly INetworkManager _networkManager; - private readonly ISocketFactory _socketFactory; - private readonly ICryptoProvider _cryptoProvider; - private readonly IFileSystem _fileSystem; - private readonly bool _enableDualMode; - private readonly IEnvironmentInfo _environment; - - private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - private CancellationToken _disposeCancellationToken; - - public WebSocketSharpListener( - ILogger logger, - X509Certificate certificate, - IStreamHelper streamHelper, - INetworkManager networkManager, - ISocketFactory socketFactory, - ICryptoProvider cryptoProvider, - bool enableDualMode, - IFileSystem fileSystem, - IEnvironmentInfo environment) - { - _logger = logger; - _certificate = certificate; - _streamHelper = streamHelper; - _networkManager = networkManager; - _socketFactory = socketFactory; - _cryptoProvider = cryptoProvider; - _enableDualMode = enableDualMode; - _fileSystem = fileSystem; - _environment = environment; - - _disposeCancellationToken = _disposeCancellationTokenSource.Token; - } - - public Func ErrorHandler { get; set; } - public Func RequestHandler { get; set; } - - public Action WebSocketConnecting { get; set; } - - public Action WebSocketConnected { get; set; } - - public void Start(IEnumerable urlPrefixes) - { - if (_listener == null) - { - _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _streamHelper, _fileSystem, _environment); - } - - _listener.EnableDualMode = _enableDualMode; - - if (_certificate != null) - { - _listener.LoadCert(_certificate); - } - - _logger.LogInformation("Adding HttpListener prefixes {Prefixes}", urlPrefixes); - _listener.Prefixes.AddRange(urlPrefixes); - - _listener.OnContext = async c => await InitTask(c, _disposeCancellationToken).ConfigureAwait(false); - - _listener.Start(); - } - - private static void LogRequest(ILogger logger, HttpListenerRequest request) - { - var url = request.Url.ToString(); - - logger.LogInformation( - "{0} {1}. UserAgent: {2}", - request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, - url, - request.UserAgent ?? string.Empty); - } - - private Task InitTask(HttpListenerContext context, CancellationToken cancellationToken) - { - IHttpRequest httpReq = null; - var request = context.Request; - - try - { - if (request.IsWebSocketRequest) - { - LogRequest(_logger, request); - - return ProcessWebSocketRequest(context); - } - - httpReq = GetRequest(context); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing request"); - - httpReq = httpReq ?? GetRequest(context); - return ErrorHandler(ex, httpReq, true, true); - } - - var uri = request.Url; - - return RequestHandler(httpReq, uri.OriginalString, uri.Host, uri.LocalPath, cancellationToken); - } - - private async Task ProcessWebSocketRequest(HttpListenerContext ctx) - { - try - { - var endpoint = ctx.Request.RemoteEndPoint.ToString(); - var url = ctx.Request.RawUrl; - - var queryString = ctx.Request.QueryString; - - var connectingArgs = new WebSocketConnectingEventArgs - { - Url = url, - QueryString = queryString, - Endpoint = endpoint - }; - - WebSocketConnecting?.Invoke(connectingArgs); - - if (connectingArgs.AllowConnection) - { - _logger.LogDebug("Web socket connection allowed"); - - var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false); - - if (WebSocketConnected != null) - { - var socket = new SharpWebSocket(webSocketContext.WebSocket, _logger); - await socket.ConnectAsServerAsync().ConfigureAwait(false); - - WebSocketConnected(new WebSocketConnectEventArgs - { - Url = url, - QueryString = queryString, - WebSocket = socket, - Endpoint = endpoint - }); - - await socket.StartReceive().ConfigureAwait(false); - } - } - else - { - _logger.LogWarning("Web socket connection not allowed"); - TryClose(ctx, 401); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "AcceptWebSocketAsync error"); - TryClose(ctx, 500); - } - } - - private void TryClose(HttpListenerContext ctx, int statusCode) - { - try - { - ctx.Response.StatusCode = statusCode; - ctx.Response.Close(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing web socket response"); - } - } - - private IHttpRequest GetRequest(HttpListenerContext httpContext) - { - var urlSegments = httpContext.Request.Url.Segments; - - var operationName = urlSegments[urlSegments.Length - 1]; - - var req = new WebSocketSharpRequest(httpContext, operationName, _logger); - - return req; - } - - public Task Stop() - { - _disposeCancellationTokenSource.Cancel(); - _listener?.Close(); - - return Task.CompletedTask; - } - - /// - /// Releases the unmanaged resources and disposes of the managed resources used. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool _disposed; - - /// - /// Releases the unmanaged resources and disposes of the managed resources used. - /// - /// Whether or not the managed resources should be disposed - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - Stop().GetAwaiter().GetResult(); - } - - _disposed = true; - } - } -} diff --git a/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs b/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs deleted file mode 100644 index cf5aee5d40..0000000000 --- a/Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; -using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; -using IRequest = MediaBrowser.Model.Services.IRequest; - -namespace Jellyfin.Server.SocketSharp -{ - public class WebSocketSharpResponse : IHttpResponse - { - private readonly ILogger _logger; - - private readonly HttpListenerResponse _response; - - public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request) - { - _logger = logger; - this._response = response; - Items = new Dictionary(); - Request = request; - } - - public IRequest Request { get; private set; } - - public Dictionary Items { get; private set; } - - public object OriginalResponse => _response; - - public int StatusCode - { - get => this._response.StatusCode; - set => this._response.StatusCode = value; - } - - public string StatusDescription - { - get => this._response.StatusDescription; - set => this._response.StatusDescription = value; - } - - public string ContentType - { - get => _response.ContentType; - set => _response.ContentType = value; - } - - public QueryParamCollection Headers => _response.Headers; - - private static string AsHeaderValue(Cookie cookie) - { - DateTime defaultExpires = DateTime.MinValue; - - var path = cookie.Expires == defaultExpires - ? "/" - : cookie.Path ?? "/"; - - var sb = new StringBuilder(); - - sb.Append($"{cookie.Name}={cookie.Value};path={path}"); - - if (cookie.Expires != defaultExpires) - { - sb.Append($";expires={cookie.Expires:R}"); - } - - if (!string.IsNullOrEmpty(cookie.Domain)) - { - sb.Append($";domain={cookie.Domain}"); - } - - if (cookie.Secure) - { - sb.Append(";Secure"); - } - - if (cookie.HttpOnly) - { - sb.Append(";HttpOnly"); - } - - return sb.ToString(); - } - - public void AddHeader(string name, string value) - { - if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) - { - ContentType = value; - return; - } - - _response.AddHeader(name, value); - } - - public string GetHeader(string name) - { - return _response.Headers[name]; - } - - public void Redirect(string url) - { - _response.Redirect(url); - } - - public Stream OutputStream => _response.OutputStream; - - public void Close() - { - if (!this.IsClosed) - { - this.IsClosed = true; - - try - { - var response = this._response; - - var outputStream = response.OutputStream; - - // This is needed with compression - outputStream.Flush(); - outputStream.Dispose(); - - response.Close(); - } - catch (SocketException) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in HttpListenerResponseWrapper"); - } - } - } - - public bool IsClosed - { - get; - private set; - } - - public void SetContentLength(long contentLength) - { - // you can happily set the Content-Length header in Asp.Net - // but HttpListener will complain if you do - you have to set ContentLength64 on the response. - // workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header - _response.ContentLength64 = contentLength; - } - - public void SetCookie(Cookie cookie) - { - var cookieStr = AsHeaderValue(cookie); - _response.Headers.Add("Set-Cookie", cookieStr); - } - - public bool SendChunked - { - get => _response.SendChunked; - set => _response.SendChunked = value; - } - - public bool KeepAlive { get; set; } - - public void ClearCookies() - { - } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); - } - } -} diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 5d3f7b171b..c8cdb984db 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -20,10 +20,10 @@ namespace Jellyfin.Server [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")] public string LogDir { get; set; } - [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")] + [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")] public string FFmpegPath { get; set; } - [Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")] + [Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")] public string FFprobePath { get; set; } [Option("service", Required = false, HelpText = "Run as headless service.")] diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index ceff6b02e5..700cbb9439 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -172,6 +172,11 @@ namespace MediaBrowser.Api { var path = _config.ApplicationPaths.GetTranscodingTempPath(); + if (!Directory.Exists(path)) + { + return; + } + foreach (var file in _fileSystem.GetFilePaths(path, true)) { _fileSystem.DeleteFile(file); diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 61db7b8d47..10bbc9e5d8 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Images { @@ -311,35 +312,35 @@ namespace MediaBrowser.Api.Images private ImageInfo GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) { + int? width = null; + int? height = null; + long length = 0; + try { - int? width = null; - int? height = null; - long length = 0; - - try + if (info.IsLocalFile) { - if (info.IsLocalFile) + var fileInfo = _fileSystem.GetFileInfo(info.Path); + length = fileInfo.Length; + + ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true); + width = size.Width; + height = size.Height; + + if (width <= 0 || height <= 0) { - var fileInfo = _fileSystem.GetFileInfo(info.Path); - length = fileInfo.Length; - - ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true); - width = size.Width; - height = size.Height; - - if (width <= 0 || height <= 0) - { - width = null; - height = null; - } - + width = null; + height = null; } } - catch - { + } + catch (Exception ex) + { + Logger.LogError(ex, "Error getting image information for {Item}", item.Name); + } - } + try + { return new ImageInfo { Path = info.Path, @@ -353,7 +354,7 @@ namespace MediaBrowser.Api.Images } catch (Exception ex) { - Logger.LogError(ex, "Error getting image information for {path}", info.Path); + Logger.LogError(ex, "Error getting image information for {Path}", info.Path); return null; } @@ -518,16 +519,16 @@ namespace MediaBrowser.Api.Images request.AddPlayedIndicator = true; } } + if (request.PercentPlayed.HasValue) { request.UnplayedCount = null; } - if (request.UnplayedCount.HasValue) + + if (request.UnplayedCount.HasValue + && request.UnplayedCount.Value <= 0) { - if (request.UnplayedCount.Value <= 0) - { - request.UnplayedCount = null; - } + request.UnplayedCount = null; } if (item == null) @@ -541,7 +542,6 @@ namespace MediaBrowser.Api.Images } var imageInfo = GetImageInfo(request, item); - if (imageInfo == null) { var displayText = item == null ? itemId.ToString() : item.Name; @@ -549,7 +549,6 @@ namespace MediaBrowser.Api.Images } IImageEnhancer[] supportedImageEnhancers; - if (_imageProcessor.ImageEnhancers.Length > 0) { if (item == null) @@ -564,13 +563,15 @@ namespace MediaBrowser.Api.Images supportedImageEnhancers = Array.Empty(); } - var cropwhitespace = request.Type == ImageType.Logo || - request.Type == ImageType.Art; - + bool cropwhitespace; if (request.CropWhitespace.HasValue) { cropwhitespace = request.CropWhitespace.Value; } + else + { + cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art; + } var outputFormats = GetOutputFormats(request); @@ -634,7 +635,7 @@ namespace MediaBrowser.Api.Images var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); - headers["Vary"] = "Accept"; + headers[HeaderNames.Vary] = HeaderNames.Accept; return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { @@ -652,12 +653,10 @@ namespace MediaBrowser.Api.Images private ImageFormat[] GetOutputFormats(ImageRequest request) { - if (!string.IsNullOrWhiteSpace(request.Format)) + if (!string.IsNullOrWhiteSpace(request.Format) + && Enum.TryParse(request.Format, true, out ImageFormat format)) { - if (Enum.TryParse(request.Format, true, out ImageFormat format)) - { - return new ImageFormat[] { format }; - } + return new ImageFormat[] { format }; } return GetClientSupportedFormats(); @@ -665,8 +664,19 @@ namespace MediaBrowser.Api.Images private ImageFormat[] GetClientSupportedFormats() { - //logger.LogDebug("Request types: {0}", string.Join(",", Request.AcceptTypes ?? Array.Empty())); - var supportedFormats = (Request.AcceptTypes ?? Array.Empty()).Select(i => i.Split(';')[0]).ToArray(); + var supportedFormats = Request.AcceptTypes ?? Array.Empty(); + if (supportedFormats.Length > 0) + { + for (int i = 0; i < supportedFormats.Length; i++) + { + int index = supportedFormats[i].IndexOf(';'); + if (index != -1) + { + supportedFormats[i] = supportedFormats[i].Substring(0, index); + } + } + } + var acceptParam = Request.QueryString["accept"]; var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false); @@ -699,7 +709,7 @@ namespace MediaBrowser.Api.Images return formats.ToArray(); } - private bool SupportsFormat(string[] requestAcceptTypes, string acceptParam, string format, bool acceptAll) + private bool SupportsFormat(IEnumerable requestAcceptTypes, string acceptParam, string format, bool acceptAll) { var mimeType = "image/" + format; diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index d44b07256d..8eefbdf2ca 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -29,6 +29,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Library { @@ -827,7 +828,7 @@ namespace MediaBrowser.Api.Library var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty); if (!string.IsNullOrWhiteSpace(filename)) { - headers["Content-Disposition"] = "attachment; filename=\"" + filename + "\""; + headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\""; } return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 8fdd726b7b..88ed734569 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -24,6 +24,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.LiveTv { @@ -750,9 +751,10 @@ namespace MediaBrowser.Api.LiveTv throw new FileNotFoundException(); } - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - outputHeaders["Content-Type"] = Model.Net.MimeTypes.GetMimeType(path); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType(path) + }; return new ProgressiveFileCopier(_fileSystem, _streamHelper, path, outputHeaders, Logger, _environment) { @@ -772,9 +774,10 @@ namespace MediaBrowser.Api.LiveTv var directStreamProvider = liveStreamInfo; - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - outputHeaders["Content-Type"] = Model.Net.MimeTypes.GetMimeType("file." + request.Container); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file." + request.Container) + }; return new ProgressiveFileCopier(directStreamProvider, _streamHelper, outputHeaders, Logger, _environment) { diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index a6be071b84..ae259a4f59 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -609,12 +609,12 @@ namespace MediaBrowser.Api.Playback { foreach (var param in Request.QueryString) { - if (char.IsLower(param.Name[0])) + if (char.IsLower(param.Key[0])) { // This was probably not parsed initially and should be a StreamOptions // TODO: This should be incorporated either in the lower framework for parsing requests // or the generated URL should correctly serialize it - request.StreamOptions[param.Name] = param.Value; + request.StreamOptions[param.Key] = param.Value; } } } @@ -867,7 +867,7 @@ namespace MediaBrowser.Api.Playback private void ApplyDeviceProfileSettings(StreamState state) { - var headers = Request.Headers.ToDictionary(); + var headers = Request.Headers; if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId)) { diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 6a98c5e8a6..f43f26b2f3 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Api.Playback.Progressive { @@ -154,7 +155,7 @@ namespace MediaBrowser.Api.Playback.Progressive var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); // TODO: Don't hardcode this - outputHeaders["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType("file.ts"); + outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts"); return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None) { @@ -196,9 +197,11 @@ namespace MediaBrowser.Api.Playback.Progressive { if (state.MediaSource.IsInfiniteStream) { - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = contentType + }; - outputHeaders["Content-Type"] = contentType; return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None) { @@ -298,16 +301,16 @@ namespace MediaBrowser.Api.Playback.Progressive if (trySupportSeek) { - if (!string.IsNullOrWhiteSpace(Request.QueryString["Range"])) + if (!string.IsNullOrWhiteSpace(Request.QueryString[HeaderNames.Range])) { - options.RequestHeaders["Range"] = Request.QueryString["Range"]; + options.RequestHeaders[HeaderNames.Range] = Request.QueryString[HeaderNames.Range]; } } var response = await HttpClient.GetResponse(options).ConfigureAwait(false); if (trySupportSeek) { - foreach (var name in new[] { "Content-Range", "Accept-Ranges" }) + foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges }) { var val = response.Headers[name]; if (!string.IsNullOrWhiteSpace(val)) @@ -318,13 +321,7 @@ namespace MediaBrowser.Api.Playback.Progressive } else { - responseHeaders["Accept-Ranges"] = "none"; - } - - // Seeing cases of -1 here - if (response.ContentLength.HasValue && response.ContentLength.Value >= 0) - { - responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture); + responseHeaders[HeaderNames.AcceptRanges] = "none"; } if (isHeadRequest) @@ -337,7 +334,7 @@ namespace MediaBrowser.Api.Playback.Progressive var result = new StaticRemoteStreamWriter(response); - result.Headers["Content-Type"] = response.ContentType; + result.Headers[HeaderNames.ContentType] = response.ContentType; // Add the response headers to the result object foreach (var header in responseHeaders) @@ -361,41 +358,15 @@ namespace MediaBrowser.Api.Playback.Progressive // Use the command line args with a dummy playlist path var outputPath = state.OutputFilePath; - responseHeaders["Accept-Ranges"] = "none"; + responseHeaders[HeaderNames.AcceptRanges] = "none"; var contentType = state.GetMimeType(outputPath); // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response - // What we really want to do is hunt that down and remove that - var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null; - - if (contentLength.HasValue) - { - responseHeaders["Content-Length"] = contentLength.Value.ToString(UsCulture); - } - // Headers only if (isHeadRequest) { - var streamResult = ResultFactory.GetResult(null, new byte[] { }, contentType, responseHeaders); - - var hasHeaders = streamResult as IHasHeaders; - if (hasHeaders != null) - { - if (contentLength.HasValue) - { - hasHeaders.Headers["Content-Length"] = contentLength.Value.ToString(CultureInfo.InvariantCulture); - } - else - { - if (hasHeaders.Headers.ContainsKey("Content-Length")) - { - hasHeaders.Headers.Remove("Content-Length"); - } - } - } - - return streamResult; + return ResultFactory.GetResult(null, Array.Empty(), contentType, responseHeaders); } var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath); @@ -414,9 +385,11 @@ namespace MediaBrowser.Api.Playback.Progressive state.Dispose(); } - var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HeaderNames.ContentType] = contentType + }; - outputHeaders["Content-Type"] = contentType; // Add the response headers to the result object foreach (var item in responseHeaders) @@ -431,22 +404,5 @@ namespace MediaBrowser.Api.Playback.Progressive transcodingLock.Release(); } } - - /// - /// Gets the length of the estimated content. - /// - /// The state. - /// System.Nullable{System.Int64}. - private long? GetEstimatedContentLength(StreamState state) - { - var totalBitrate = state.TotalOutputBitrate ?? 0; - - if (totalBitrate > 0 && state.RunTimeTicks.HasValue) - { - return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8); - } - - return null; - } } } diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs index b0c4b29c17..d24a187430 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs @@ -58,9 +58,8 @@ namespace MediaBrowser.Api.ScheduledTasks /// /// Gets the data to send. /// - /// The state. /// Task{IEnumerable{TaskInfo}}. - protected override Task> GetDataToSend(WebSocketListenerState state, CancellationToken cancellationToken) + protected override Task> GetDataToSend() { return Task.FromResult(TaskManager.ScheduledTasks .OrderBy(i => i.Name) diff --git a/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs b/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs index beb2fb11d0..b79e9f84b1 100644 --- a/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs +++ b/MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs @@ -79,9 +79,8 @@ namespace MediaBrowser.Api.Session /// /// Gets the data to send. /// - /// The state. /// Task{SystemInfo}. - protected override Task> GetDataToSend(WebSocketListenerState state, CancellationToken cancellationToken) + protected override Task> GetDataToSend() { return Task.FromResult(_sessionManager.Sessions); } diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs index 43f3c5a223..a036619b81 100644 --- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs +++ b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs @@ -38,9 +38,8 @@ namespace MediaBrowser.Api.System /// /// Gets the data to send. /// - /// The state. /// Task{SystemInfo}. - protected override Task> GetDataToSend(WebSocketListenerState state, CancellationToken CancellationToken) + protected override Task> GetDataToSend() { return Task.FromResult(new List()); } diff --git a/MediaBrowser.Common/Extensions/CollectionExtensions.cs b/MediaBrowser.Common/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000000..f7c0e3cf04 --- /dev/null +++ b/MediaBrowser.Common/Extensions/CollectionExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Common.Extensions +{ + // The MS CollectionExtensions are only available in netcoreapp + public static class CollectionExtensions + { + public static TValue GetValueOrDefault (this IReadOnlyDictionary dictionary, TKey key) + { + dictionary.TryGetValue(key, out var ret); + return ret; + } + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 715f4fccd3..05b48a2a12 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -1,4 +1,4 @@ - + Jellyfin Contributors @@ -13,6 +13,7 @@ + diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index dadac5e03d..bea178517b 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using Microsoft.Net.Http.Headers; namespace MediaBrowser.Common.Net { @@ -24,8 +25,8 @@ namespace MediaBrowser.Common.Net /// The accept header. public string AcceptHeader { - get => GetHeaderValue("Accept"); - set => RequestHeaders["Accept"] = value; + get => GetHeaderValue(HeaderNames.Accept); + set => RequestHeaders[HeaderNames.Accept] = value; } /// /// Gets or sets the cancellation token. @@ -45,8 +46,8 @@ namespace MediaBrowser.Common.Net /// The user agent. public string UserAgent { - get => GetHeaderValue("User-Agent"); - set => RequestHeaders["User-Agent"] = value; + get => GetHeaderValue(HeaderNames.UserAgent); + set => RequestHeaders[HeaderNames.UserAgent] = value; } /// diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs index a6ee7c5050..41a7686a37 100644 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dlna; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Dlna { @@ -17,7 +18,7 @@ namespace MediaBrowser.Controller.Dlna /// /// The headers. /// DeviceProfile. - DeviceProfile GetProfile(IDictionary headers); + DeviceProfile GetProfile(IHeaderDictionary headers); /// /// Gets the default profile. @@ -64,7 +65,7 @@ namespace MediaBrowser.Controller.Dlna /// The server uu identifier. /// The server address. /// System.String. - string GetServerDescriptionXml(IDictionary headers, string serverUuId, string serverAddress); + string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress); /// /// Gets the icon. diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 43fee79a10..e20641c99a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -36,10 +36,26 @@ namespace MediaBrowser.Controller.Entities /// public abstract class BaseItem : IHasProviderIds, IHasLookupInfo { - protected static MetadataFields[] EmptyMetadataFieldsArray = Array.Empty(); - protected static MediaUrl[] EmptyMediaUrlArray = Array.Empty(); - protected static ItemImageInfo[] EmptyItemImageInfoArray = Array.Empty(); - public static readonly LinkedChild[] EmptyLinkedChildArray = Array.Empty(); + /// + /// The supported image extensions + /// + public static readonly string[] SupportedImageExtensions + = new [] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + + private static readonly List _supportedExtensions = new List(SupportedImageExtensions) + { + ".nfo", + ".xml", + ".srt", + ".vtt", + ".sub", + ".idx", + ".txt", + ".edl", + ".bif", + ".smi", + ".ttml" + }; protected BaseItem() { @@ -49,8 +65,8 @@ namespace MediaBrowser.Controller.Entities Genres = Array.Empty(); Studios = Array.Empty(); ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - LockedFields = EmptyMetadataFieldsArray; - ImageInfos = EmptyItemImageInfoArray; + LockedFields = Array.Empty(); + ImageInfos = Array.Empty(); ProductionLocations = Array.Empty(); RemoteTrailers = Array.Empty(); ExtraIds = Array.Empty(); @@ -59,11 +75,6 @@ namespace MediaBrowser.Controller.Entities public static readonly char[] SlugReplaceChars = { '?', '/', '&' }; public static char SlugChar = '-'; - /// - /// The supported image extensions - /// - public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; - /// /// The trailer folder name /// @@ -2452,10 +2463,8 @@ namespace MediaBrowser.Controller.Entities } var filename = System.IO.Path.GetFileNameWithoutExtension(Path); - var extensions = new List { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" }; - extensions.AddRange(SupportedImageExtensions); - return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), extensions.ToArray(), false, false) + return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path), _supportedExtensions, false, false) .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase)) .ToList(); } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e49ff20baa..c056bc0b4e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Controller.Entities public Folder() { - LinkedChildren = EmptyLinkedChildArray; + LinkedChildren = Array.Empty(); } [IgnoreDataMember] diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index ff6b133982..8484938642 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -25,22 +25,10 @@ namespace MediaBrowser.Controller.Entities public DateTime DateModified { get; set; } public int Width { get; set; } + public int Height { get; set; } [IgnoreDataMember] - public bool IsLocalFile - { - get - { - if (Path != null) - { - if (Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - return true; - } - } + public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 124a943ef8..a532b5ee9e 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Entities.Movies { public BoxSet() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty(); LocalTrailerIds = Array.Empty(); RemoteTrailerIds = Array.Empty(); diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 232d116241..20c5b35219 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -21,10 +21,10 @@ namespace MediaBrowser.Controller.Entities.Movies public Movie() { - SpecialFeatureIds = new Guid[] { }; - RemoteTrailers = EmptyMediaUrlArray; - LocalTrailerIds = new Guid[] { }; - RemoteTrailerIds = new Guid[] { }; + SpecialFeatureIds = Array.Empty(); + RemoteTrailers = Array.Empty(); + LocalTrailerIds = Array.Empty(); + RemoteTrailerIds = Array.Empty(); } public Guid[] LocalTrailerIds { get; set; } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 072b1d89af..fb29c07b0d 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Entities.TV { public Episode() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty(); LocalTrailerIds = Array.Empty(); RemoteTrailerIds = Array.Empty(); } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 570e9389e9..eae834f6f0 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -7,7 +7,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; @@ -22,7 +21,7 @@ namespace MediaBrowser.Controller.Entities.TV { public Series() { - RemoteTrailers = EmptyMediaUrlArray; + RemoteTrailers = Array.Empty(); LocalTrailerIds = Array.Empty(); RemoteTrailerIds = Array.Empty(); AirDays = Array.Empty(); diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 31cd429756..8379dcc090 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Entities AdditionalParts = Array.Empty(); LocalAlternateVersions = Array.Empty(); SubtitleFiles = Array.Empty(); - LinkedAlternateVersions = EmptyLinkedChildArray; + LinkedAlternateVersions = Array.Empty(); } public override bool CanDownload() diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 057e439104..d032a849e7 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.System; namespace MediaBrowser.Controller.MediaEncoding { @@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// public interface IMediaEncoder : ITranscoderSupport { - string EncoderLocationType { get; } + FFmpegLocation EncoderLocation { get; } /// /// Gets the encoder path. @@ -73,7 +74,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// The input files. /// The protocol. /// System.String. - string GetInputArgument(string[] inputFiles, MediaProtocol protocol); + string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol); /// /// Gets the time parameter. @@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// System.String. string EscapeSubtitleFilterPath(string path); - void Init(); + void SetFFmpegPath(); void UpdateEncoderPath(string path, string pathType); bool SupportsEncoder(string encoder); diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 4242a00e21..8444125462 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -22,8 +22,8 @@ namespace MediaBrowser.Controller.Net /// /// The _active connections /// - protected readonly List> ActiveConnections = - new List>(); + protected readonly List> ActiveConnections = + new List>(); /// /// Gets the name. @@ -34,9 +34,8 @@ namespace MediaBrowser.Controller.Net /// /// Gets the data to send. /// - /// The state. /// Task{`1}. - protected abstract Task GetDataToSend(TStateType state, CancellationToken cancellationToken); + protected abstract Task GetDataToSend(); /// /// The logger @@ -80,13 +79,6 @@ namespace MediaBrowser.Controller.Net protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - protected virtual bool SendOnTimer => false; - - protected virtual void ParseMessageParams(string[] values) - { - - } - /// /// Starts sending messages over a web socket /// @@ -98,19 +90,10 @@ namespace MediaBrowser.Controller.Net var dueTimeMs = long.Parse(vals[0], UsCulture); var periodMs = long.Parse(vals[1], UsCulture); - if (vals.Length > 2) - { - ParseMessageParams(vals.Skip(2).ToArray()); - } - var cancellationTokenSource = new CancellationTokenSource(); Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); - var timer = SendOnTimer ? - new Timer(TimerCallback, message.Connection, Timeout.Infinite, Timeout.Infinite) : - null; - var state = new TStateType { IntervalMs = periodMs, @@ -119,47 +102,13 @@ namespace MediaBrowser.Controller.Net lock (ActiveConnections) { - ActiveConnections.Add(new Tuple(message.Connection, cancellationTokenSource, timer, state)); + ActiveConnections.Add(new Tuple(message.Connection, cancellationTokenSource, state)); } - - if (timer != null) - { - timer.Change(TimeSpan.FromMilliseconds(dueTimeMs), TimeSpan.FromMilliseconds(periodMs)); - } - } - - /// - /// Timers the callback. - /// - /// The state. - private void TimerCallback(object state) - { - var connection = (IWebSocketConnection)state; - - Tuple tuple; - - lock (ActiveConnections) - { - tuple = ActiveConnections.FirstOrDefault(c => c.Item1 == connection); - } - - if (tuple == null) - { - return; - } - - if (connection.State != WebSocketState.Open || tuple.Item2.IsCancellationRequested) - { - DisposeConnection(tuple); - return; - } - - SendData(tuple); } protected void SendData(bool force) { - Tuple[] tuples; + Tuple[] tuples; lock (ActiveConnections) { @@ -168,7 +117,7 @@ namespace MediaBrowser.Controller.Net { if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested) { - var state = c.Item4; + var state = c.Item3; if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs) { @@ -187,17 +136,17 @@ namespace MediaBrowser.Controller.Net } } - private async void SendData(Tuple tuple) + private async void SendData(Tuple tuple) { var connection = tuple.Item1; try { - var state = tuple.Item4; + var state = tuple.Item3; var cancellationToken = tuple.Item2.Token; - var data = await GetDataToSend(state, cancellationToken).ConfigureAwait(false); + var data = await GetDataToSend().ConfigureAwait(false); if (data != null) { @@ -246,23 +195,12 @@ namespace MediaBrowser.Controller.Net /// Disposes the connection. /// /// The connection. - private void DisposeConnection(Tuple connection) + private void DisposeConnection(Tuple connection) { Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); - var timer = connection.Item3; - - if (timer != null) - { - try - { - timer.Dispose(); - } - catch (ObjectDisposedException) - { - //TODO Investigate and properly fix. - } - } + // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really... + // connection.Item1.Dispose(); try { diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index f413030076..46933c0465 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Model.Events; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { @@ -29,11 +32,30 @@ namespace MediaBrowser.Controller.Net /// /// Inits this instance. /// - void Init(IEnumerable services, IEnumerable listener); + void Init(IEnumerable services, IEnumerable listener, IEnumerable urlPrefixes); /// /// If set, all requests will respond with this message /// string GlobalResponse { get; set; } + + /// + /// Sends the http context to the socket listener + /// + /// + /// + Task ProcessWebSocketRequest(HttpContext ctx); + + /// + /// The HTTP request handler + /// + /// + /// + /// + /// + /// + /// + Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, + CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index a09b2f7a22..566897b31f 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Net; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { @@ -35,7 +36,7 @@ namespace MediaBrowser.Controller.Net /// Gets or sets the query string. /// /// The query string. - QueryParamCollection QueryString { get; set; } + IQueryCollection QueryString { get; set; } /// /// Gets or sets the receive action. diff --git a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs deleted file mode 100644 index f26b764bb3..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Class WebSocketConnectEventArgs - /// - public class WebSocketConnectingEventArgs : EventArgs - { - /// - /// Gets or sets the URL. - /// - /// The URL. - public string Url { get; set; } - /// - /// Gets or sets the endpoint. - /// - /// The endpoint. - public string Endpoint { get; set; } - /// - /// Gets or sets the query string. - /// - /// The query string. - public QueryParamCollection QueryString { get; set; } - /// - /// Gets or sets a value indicating whether [allow connection]. - /// - /// true if [allow connection]; otherwise, false. - public bool AllowConnection { get; set; } - - public WebSocketConnectingEventArgs() - { - QueryString = new QueryParamCollection(); - AllowConnection = true; - } - } - -} diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index c0706ceeb0..25a8ad5966 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -5,6 +5,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Images { @@ -12,11 +13,16 @@ namespace MediaBrowser.LocalMetadata.Images { private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; - public InternalMetadataFolderImageProvider(IServerConfigurationManager config, IFileSystem fileSystem) + public InternalMetadataFolderImageProvider( + IServerConfigurationManager config, + IFileSystem fileSystem, + ILogger logger) { _config = config; _fileSystem = fileSystem; + _logger = logger; } public string Name => "Internal Images"; @@ -53,12 +59,18 @@ namespace MediaBrowser.LocalMetadata.Images { var path = item.GetInternalMetadataPath(); + if (!Directory.Exists(path)) + { + return new List(); + } + try { return new LocalImageProvider(_fileSystem).GetImages(item, path, false, directoryService); } - catch (IOException) + catch (IOException ex) { + _logger.LogError(ex, "Error while getting images for {Library}", item.Name); return new List(); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f725d2c019..3eed891cb3 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _processFactory = processFactory; } - public (IEnumerable decoders, IEnumerable encoders) Validate(string encoderPath) + public (IEnumerable decoders, IEnumerable encoders) GetAvailableCoders(string encoderPath) { _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath); @@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.IsNullOrWhiteSpace(output)) { + if (logOutput) + { + _logger.LogError("FFmpeg validation: The process returned no result"); + } return false; } @@ -55,6 +59,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) { + if (logOutput) + { + _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); + } return false; } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 44e62446b2..d4aede572b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -6,11 +6,11 @@ namespace MediaBrowser.MediaEncoding.Encoder { public static class EncodingUtils { - public static string GetInputArgument(List inputFiles, MediaProtocol protocol) + public static string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol) { if (protocol != MediaProtocol.File) { - var url = inputFiles.First(); + var url = inputFiles[0]; return string.Format("\"{0}\"", url); } @@ -29,7 +29,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // If there's more than one we'll need to use the concat command if (inputFiles.Count > 1) { - var files = string.Join("|", inputFiles.Select(NormalizePath).ToArray()); + var files = string.Join("|", inputFiles.Select(NormalizePath)); return string.Format("concat:\"{0}\"", files); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 7f29c06b4c..94beda3dbb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -3,17 +3,14 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Session; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Diagnostics; @@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder @@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder public class MediaEncoder : IMediaEncoder, IDisposable { /// - /// The _logger + /// Gets the encoder path. /// + /// The encoder path. + public string EncoderPath => FFmpegPath; + + /// + /// The location of the discovered FFmpeg tool. + /// + public FFmpegLocation EncoderLocation { get; private set; } + private readonly ILogger _logger; - - /// - /// Gets the json serializer. - /// - /// The json serializer. private readonly IJsonSerializer _jsonSerializer; - - /// - /// The _thumbnail resource pool - /// - private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); - - public string FFMpegPath { get; private set; } - - public string FFProbePath { get; private set; } - + private string FFmpegPath; + private string FFprobePath; protected readonly IServerConfigurationManager ConfigurationManager; protected readonly IFileSystem FileSystem; - protected readonly ILiveTvManager LiveTvManager; - protected readonly IIsoManager IsoManager; - protected readonly ILibraryManager LibraryManager; - protected readonly IChannelManager ChannelManager; - protected readonly ISessionManager SessionManager; protected readonly Func SubtitleEncoder; protected readonly Func MediaSourceManager; - private readonly IHttpClient _httpClient; - private readonly IZipClient _zipClient; private readonly IProcessFactory _processFactory; - - private readonly List _runningProcesses = new List(); - private readonly bool _hasExternalEncoder; - private readonly string _originalFFMpegPath; - private readonly string _originalFFProbePath; private readonly int DefaultImageExtractionTimeoutMs; + private readonly string StartupOptionFFmpegPath; + private readonly string StartupOptionFFprobePath; + + private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); + private readonly List _runningProcesses = new List(); public MediaEncoder( ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, - string ffMpegPath, - string ffProbePath, - bool hasExternalEncoder, + string startupOptionsFFmpegPath, + string startupOptionsFFprobePath, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - ILiveTvManager liveTvManager, - IIsoManager isoManager, - ILibraryManager libraryManager, - IChannelManager channelManager, - ISessionManager sessionManager, Func subtitleEncoder, Func mediaSourceManager, - IHttpClient httpClient, - IZipClient zipClient, IProcessFactory processFactory, int defaultImageExtractionTimeoutMs) { _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); _jsonSerializer = jsonSerializer; + StartupOptionFFmpegPath = startupOptionsFFmpegPath; + StartupOptionFFprobePath = startupOptionsFFprobePath; ConfigurationManager = configurationManager; FileSystem = fileSystem; - LiveTvManager = liveTvManager; - IsoManager = isoManager; - LibraryManager = libraryManager; - ChannelManager = channelManager; - SessionManager = sessionManager; SubtitleEncoder = subtitleEncoder; - MediaSourceManager = mediaSourceManager; - _httpClient = httpClient; - _zipClient = zipClient; _processFactory = processFactory; DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; - FFProbePath = ffProbePath; - FFMpegPath = ffMpegPath; - _originalFFProbePath = ffProbePath; - _originalFFMpegPath = ffMpegPath; - _hasExternalEncoder = hasExternalEncoder; } - public string EncoderLocationType + /// + /// Run at startup or if the user removes a Custom path from transcode page. + /// Sets global variables FFmpegPath. + /// Precedence is: Config > CLI > $PATH + /// + public void SetFFmpegPath() { - get + // ToDo - Finalise removal of the --ffprobe switch + if (!string.IsNullOrEmpty(StartupOptionFFprobePath)) { - if (_hasExternalEncoder) - { - return "External"; - } - - if (string.IsNullOrWhiteSpace(FFMpegPath)) - { - return null; - } - - if (IsSystemInstalledPath(FFMpegPath)) - { - return "System"; - } - - return "Custom"; - } - } - - private bool IsSystemInstalledPath(string path) - { - if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1) - { - return true; + _logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release"); } - return false; - } - - public void Init() - { - InitPaths(); - - if (!string.IsNullOrWhiteSpace(FFMpegPath)) + // 1) Custom path stored in config/encoding xml file under tag takes precedence + if (!ValidatePath(ConfigurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) { - var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath); + // 2) Check if the --ffmpeg CLI switch has been given + if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + { + // 3) Search system $PATH environment variable for valid FFmpeg + if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) + { + EncoderLocation = FFmpegLocation.NotFound; + FFmpegPath = null; + } + } + } + + // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty; + ConfigurationManager.SaveConfiguration("encoding", config); + + // Only if mpeg path is set, try and set path to probe + if (FFmpegPath != null) + { + // Determine a probe path from the mpeg path + FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); + + // Interrogate to understand what coders are supported + var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath); SetAvailableDecoders(result.decoders); SetAvailableEncoders(result.encoders); } + + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty); } - private void InitPaths() - { - ConfigureEncoderPaths(); - - if (_hasExternalEncoder) - { - LogPaths(); - return; - } - - // If the path was passed in, save it into config now. - var encodingOptions = GetEncodingOptions(); - var appPath = encodingOptions.EncoderAppPath; - - var valueToSave = FFMpegPath; - - if (!string.IsNullOrWhiteSpace(valueToSave)) - { - // if using system variable, don't save this. - if (IsSystemInstalledPath(valueToSave) || _hasExternalEncoder) - { - valueToSave = null; - } - } - - if (!string.Equals(valueToSave, appPath, StringComparison.Ordinal)) - { - encodingOptions.EncoderAppPath = valueToSave; - ConfigurationManager.SaveConfiguration("encoding", encodingOptions); - } - } - + /// + /// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use. + /// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here. + /// + /// + /// public void UpdateEncoderPath(string path, string pathType) { - if (_hasExternalEncoder) - { - return; - } + string newPath; _logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty); - Tuple newPaths; - - if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase)) - { - path = "ffmpeg"; - - newPaths = TestForInstalledVersions(); - } - else if (string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - if (!File.Exists(path) && !Directory.Exists(path)) - { - throw new ResourceNotFoundException(); - } - newPaths = GetEncoderPaths(path); - } - else + if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Unexpected pathType value"); } - - if (string.IsNullOrWhiteSpace(newPaths.Item1)) + else if (string.IsNullOrWhiteSpace(path)) { - throw new ResourceNotFoundException("ffmpeg not found"); + // User had cleared the custom path in UI + newPath = string.Empty; } - if (string.IsNullOrWhiteSpace(newPaths.Item2)) + else if (File.Exists(path)) { - throw new ResourceNotFoundException("ffprobe not found"); + newPath = path; + } + else if (Directory.Exists(path)) + { + // Given path is directory, so resolve down to filename + newPath = GetEncoderPathFromDirectory(path, "ffmpeg"); + } + else + { + throw new ResourceNotFoundException(); } - path = newPaths.Item1; - - if (!ValidateVersion(path, true)) - { - throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required."); - } - - var config = GetEncodingOptions(); - config.EncoderAppPath = path; + // Write the new ffmpeg path to the xml as + // This ensures its not lost on next startup + var config = ConfigurationManager.GetConfiguration("encoding"); + config.EncoderAppPath = newPath; ConfigurationManager.SaveConfiguration("encoding", config); - Init(); + // Trigger SetFFmpegPath so we validate the new path and setup probe path + SetFFmpegPath(); } - private bool ValidateVersion(string path, bool logOutput) + /// + /// Validates the supplied FQPN to ensure it is a ffmpeg utility. + /// If checks pass, global variable FFmpegPath and EncoderLocation are updated. + /// + /// FQPN to test + /// Location (External, Custom, System) of tool + /// + private bool ValidatePath(string path, FFmpegLocation location) { - return new EncoderValidator(_logger, _processFactory).ValidateVersion(path, logOutput); - } + bool rc = false; - private void ConfigureEncoderPaths() - { - if (_hasExternalEncoder) + if (!string.IsNullOrEmpty(path)) { - return; - } - - var appPath = GetEncodingOptions().EncoderAppPath; - - if (string.IsNullOrWhiteSpace(appPath)) - { - appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg"); - } - - var newPaths = GetEncoderPaths(appPath); - if (string.IsNullOrWhiteSpace(newPaths.Item1) || string.IsNullOrWhiteSpace(newPaths.Item2) || IsSystemInstalledPath(appPath)) - { - newPaths = TestForInstalledVersions(); - } - - if (!string.IsNullOrWhiteSpace(newPaths.Item1) && !string.IsNullOrWhiteSpace(newPaths.Item2)) - { - FFMpegPath = newPaths.Item1; - FFProbePath = newPaths.Item2; - } - - LogPaths(); - } - - private Tuple GetEncoderPaths(string configuredPath) - { - var appPath = configuredPath; - - if (!string.IsNullOrWhiteSpace(appPath)) - { - if (Directory.Exists(appPath)) + if (File.Exists(path)) { - return GetPathsFromDirectory(appPath); + rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true); + + if (!rc) + { + _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path); + } + + // ToDo - Enable the ffmpeg validator. At the moment any version can be used. + rc = true; + + FFmpegPath = path; + EncoderLocation = location; } - - if (File.Exists(appPath)) + else { - return new Tuple(appPath, GetProbePathFromEncoderPath(appPath)); + _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path); } } - return new Tuple(null, null); + return rc; } - private Tuple TestForInstalledVersions() + private string GetEncoderPathFromDirectory(string path, string filename) { - string encoderPath = null; - string probePath = null; - - if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true)) + try { - encoderPath = _originalFFMpegPath; - probePath = _originalFFProbePath; + var files = FileSystem.GetFilePaths(path); + + var excludeExtensions = new[] { ".c" }; + + return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase) + && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); } - - if (string.IsNullOrWhiteSpace(encoderPath)) + catch (Exception) { - if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false)) + // Trap all exceptions, like DirNotExists, and return null + return null; + } + } + + /// + /// Search the system $PATH environment variable looking for given filename. + /// + /// + /// + private string ExistsOnSystemPath(string filename) + { + var values = Environment.GetEnvironmentVariable("PATH"); + + foreach (var path in values.Split(Path.PathSeparator)) + { + var candidatePath = GetEncoderPathFromDirectory(path, filename); + + if (!string.IsNullOrEmpty(candidatePath)) { - encoderPath = "ffmpeg"; - probePath = "ffprobe"; + return candidatePath; } } - - return new Tuple(encoderPath, probePath); - } - - private Tuple GetPathsFromDirectory(string path) - { - // Since we can't predict the file extension, first try directly within the folder - // If that doesn't pan out, then do a recursive search - var files = FileSystem.GetFilePaths(path); - - var excludeExtensions = new[] { ".c" }; - - var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - - if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath)) - { - files = FileSystem.GetFilePaths(path, true); - - ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); - - if (!string.IsNullOrWhiteSpace(ffmpegPath)) - { - ffprobePath = GetProbePathFromEncoderPath(ffmpegPath); - } - } - - return new Tuple(ffmpegPath, ffprobePath); - } - - private string GetProbePathFromEncoderPath(string appPath) - { - return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath)) - .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); - } - - private void LogPaths() - { - _logger.LogInformation("FFMpeg: {0}", FFMpegPath ?? "not found"); - _logger.LogInformation("FFProbe: {0}", FFProbePath ?? "not found"); - } - - private EncodingOptions GetEncodingOptions() - { - return ConfigurationManager.GetConfiguration("encoding"); + return null; } private List _encoders = new List(); @@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return true; } - /// - /// Gets the encoder path. - /// - /// The encoder path. - public string EncoderPath => FFMpegPath; - /// /// Gets the media info. /// @@ -459,10 +334,8 @@ namespace MediaBrowser.MediaEncoding.Encoder /// The protocol. /// System.String. /// Unrecognized InputType - public string GetInputArgument(string[] inputFiles, MediaProtocol protocol) - { - return EncodingUtils.GetInputArgument(inputFiles.ToList(), protocol); - } + public string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol) + => EncodingUtils.GetInputArgument(inputFiles, protocol); /// /// Gets the media info internal. @@ -479,8 +352,9 @@ namespace MediaBrowser.MediaEncoding.Encoder CancellationToken cancellationToken) { var args = extractChapters - ? "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_chapters -show_format" - : "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format"; + ? "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_chapters -show_format" + : "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format"; + args = string.Format(args, probeSizeArgument, inputPath).Trim(); var process = _processFactory.Create(new ProcessOptions { @@ -489,8 +363,10 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, - FileName = FFProbePath, - Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(), + + FileName = FFprobePath, + Arguments = args, + IsHidden = true, ErrorDialog = false, @@ -508,36 +384,14 @@ namespace MediaBrowser.MediaEncoding.Encoder using (var processWrapper = new ProcessWrapper(process, this, _logger)) { + _logger.LogDebug("Starting ffprobe with args {Args}", args); StartProcess(processWrapper); + InternalMediaInfoResult result; try { - //process.BeginErrorReadLine(); - - var result = await _jsonSerializer.DeserializeFromStreamAsync(process.StandardOutput.BaseStream).ConfigureAwait(false); - - if (result == null || (result.streams == null && result.format == null)) - { - throw new Exception("ffprobe failed - streams and format are both null."); - } - - if (result.streams != null) - { - // Normalize aspect ratio if invalid - foreach (var stream in result.streams) - { - if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) - { - stream.display_aspect_ratio = string.Empty; - } - if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) - { - stream.sample_aspect_ratio = string.Empty; - } - } - } - - return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + result = await _jsonSerializer.DeserializeFromStreamAsync( + process.StandardOutput.BaseStream).ConfigureAwait(false); } catch { @@ -545,6 +399,30 @@ namespace MediaBrowser.MediaEncoding.Encoder throw; } + + if (result == null || (result.streams == null && result.format == null)) + { + throw new Exception("ffprobe failed - streams and format are both null."); + } + + if (result.streams != null) + { + // Normalize aspect ratio if invalid + foreach (var stream in result.streams) + { + if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + { + stream.display_aspect_ratio = string.Empty; + } + + if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + { + stream.sample_aspect_ratio = string.Empty; + } + } + } + + return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } } @@ -691,7 +569,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFMpegPath, + FileName = FFmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -814,7 +692,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFMpegPath, + FileName = FFmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 8584bd3ddb..285ff4ba58 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -8,7 +8,14 @@ namespace MediaBrowser.Model.Configuration public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } public string HardwareAccelerationType { get; set; } + /// + /// FFmpeg path as set by the user via the UI + /// public string EncoderAppPath { get; set; } + /// + /// The current FFmpeg path being used by the system and displayed on the transcode page + /// + public string EncoderAppPathDisplay { get; set; } public string VaapiDevice { get; set; } public int H264Crf { get; set; } public string H264Preset { get; set; } diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index b027d2ad0b..5988112c2e 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Collections.Generic; namespace MediaBrowser.Model.Cryptography { @@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography byte[] ComputeMD5(Stream str); byte[] ComputeMD5(byte[] bytes); byte[] ComputeSHA1(byte[] bytes); + IEnumerable GetSupportedHashMethods(); + byte[] ComputeHash(string HashMethod, byte[] bytes); + byte[] ComputeHashWithDefaultMethod(byte[] bytes); + byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt); + byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); + byte[] ComputeHash(PasswordHash hash); + byte[] GenerateSalt(); + string DefaultHashMethod { get; } } } diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs new file mode 100644 index 0000000000..a9d0f67446 --- /dev/null +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Model.Cryptography +{ + public class PasswordHash + { + // Defined from this hash storage spec + // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md + // $[$=(,=)*][$[$]] + // with one slight amendment to ease the transition, we're writing out the bytes in hex + // rather than making them a BASE64 string with stripped padding + + private string _id; + + private Dictionary _parameters = new Dictionary(); + + private string _salt; + + private byte[] _saltBytes; + + private string _hash; + + private byte[] _hashBytes; + + public string Id { get => _id; set => _id = value; } + + public Dictionary Parameters { get => _parameters; set => _parameters = value; } + + public string Salt { get => _salt; set => _salt = value; } + + public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; } + + public string Hash { get => _hash; set => _hash = value; } + + public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; } + + public PasswordHash(string storageString) + { + string[] splitted = storageString.Split('$'); + _id = splitted[1]; + if (splitted[2].Contains("=")) + { + foreach (string paramset in (splitted[2].Split(','))) + { + if (!string.IsNullOrEmpty(paramset)) + { + string[] fields = paramset.Split('='); + if (fields.Length == 2) + { + _parameters.Add(fields[0], fields[1]); + } + else + { + throw new Exception($"Malformed parameter in password hash string {paramset}"); + } + } + } + if (splitted.Length == 5) + { + _salt = splitted[3]; + _saltBytes = ConvertFromByteString(_salt); + _hash = splitted[4]; + _hashBytes = ConvertFromByteString(_hash); + } + else + { + _salt = string.Empty; + _hash = splitted[3]; + _hashBytes = ConvertFromByteString(_hash); + } + } + else + { + if (splitted.Length == 4) + { + _salt = splitted[2]; + _saltBytes = ConvertFromByteString(_salt); + _hash = splitted[3]; + _hashBytes = ConvertFromByteString(_hash); + } + else + { + _salt = string.Empty; + _hash = splitted[2]; + _hashBytes = ConvertFromByteString(_hash); + } + + } + + } + + public PasswordHash(ICryptoProvider cryptoProvider) + { + _id = cryptoProvider.DefaultHashMethod; + _saltBytes = cryptoProvider.GenerateSalt(); + _salt = ConvertToByteString(SaltBytes); + } + + public static byte[] ConvertFromByteString(string byteString) + { + byte[] bytes = new byte[byteString.Length / 2]; + for (int i = 0; i < byteString.Length; i += 2) + { + // TODO: NetStandard2.1 switch this to use a span instead of a substring. + bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16); + } + + return bytes; + } + + public static string ConvertToByteString(byte[] bytes) + { + return BitConverter.ToString(bytes).Replace("-", ""); + } + + private string SerializeParameters() + { + string returnString = string.Empty; + foreach (var KVP in _parameters) + { + returnString += $",{KVP.Key}={KVP.Value}"; + } + + if ((!string.IsNullOrEmpty(returnString)) && returnString[0] == ',') + { + returnString = returnString.Remove(0, 1); + } + + return returnString; + } + + public override string ToString() + { + string outString = "$" + _id; + string paramstring = SerializeParameters(); + if (!string.IsNullOrEmpty(paramstring)) + { + outString += $"${paramstring}"; + } + + if (!string.IsNullOrEmpty(_salt)) + { + outString += $"${_salt}"; + } + + outString += $"${_hash}"; + return outString; + } + } + +} diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index e0771245fd..ca99b28ca4 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; namespace MediaBrowser.Model.IO { @@ -177,7 +176,7 @@ namespace MediaBrowser.Model.IO /// IEnumerable GetFiles(string path, bool recursive = false); - IEnumerable GetFiles(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive); + IEnumerable GetFiles(string path, IReadOnlyList extensions, bool enableCaseSensitiveExtensions, bool recursive); /// /// Gets the file system entries. diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index f17fd7159d..3de2cca2d2 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -13,6 +13,7 @@ + diff --git a/MediaBrowser.Model/Reflection/IAssemblyInfo.cs b/MediaBrowser.Model/Reflection/IAssemblyInfo.cs deleted file mode 100644 index 5c4536c1c1..0000000000 --- a/MediaBrowser.Model/Reflection/IAssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.IO; -using System.Reflection; - -namespace MediaBrowser.Model.Reflection -{ - public interface IAssemblyInfo - { - Stream GetManifestResourceStream(Type type, string resource); - string[] GetManifestResourceNames(Type type); - - Assembly[] GetCurrentAssemblies(); - } -} diff --git a/MediaBrowser.Model/Services/HttpUtility.cs b/MediaBrowser.Model/Services/HttpUtility.cs deleted file mode 100644 index be180334c6..0000000000 --- a/MediaBrowser.Model/Services/HttpUtility.cs +++ /dev/null @@ -1,691 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; - -namespace MediaBrowser.Model.Services -{ - public static class MyHttpUtility - { - // Must be sorted - static readonly long[] entities = new long[] { - (long)'A' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'A' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'A' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24, - (long)'A' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24, - (long)'A' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'A' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'B' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'C' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'C' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'D' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16, - (long)'D' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'E' << 56 | (long)'T' << 48 | (long)'H' << 40, - (long)'E' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'E' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'E' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'E' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'E' << 56 | (long)'t' << 48 | (long)'a' << 40, - (long)'E' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'G' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'I' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'I' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'I' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'I' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'I' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'K' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24, - (long)'L' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16, - (long)'M' << 56 | (long)'u' << 48, - (long)'N' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'N' << 56 | (long)'u' << 48, - (long)'O' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'O' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'O' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24, - (long)'O' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'O' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16, - (long)'O' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'O' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'P' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'P' << 56 | (long)'i' << 48, - (long)'P' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24, - (long)'P' << 56 | (long)'s' << 48 | (long)'i' << 40, - (long)'R' << 56 | (long)'h' << 48 | (long)'o' << 40, - (long)'S' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16, - (long)'S' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'T' << 56 | (long)'H' << 48 | (long)'O' << 40 | (long)'R' << 32 | (long)'N' << 24, - (long)'T' << 56 | (long)'a' << 48 | (long)'u' << 40, - (long)'T' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'U' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'U' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'U' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'U' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'U' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'X' << 56 | (long)'i' << 48, - (long)'Y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'Y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'Z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'a' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'a' << 56 | (long)'c' << 48 | (long)'u' << 40 | (long)'t' << 32 | (long)'e' << 24, - (long)'a' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'a' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'l' << 48 | (long)'e' << 40 | (long)'f' << 32 | (long)'s' << 24 | (long)'y' << 16 | (long)'m' << 8, - (long)'a' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24, - (long)'a' << 56 | (long)'m' << 48 | (long)'p' << 40, - (long)'a' << 56 | (long)'n' << 48 | (long)'d' << 40, - (long)'a' << 56 | (long)'n' << 48 | (long)'g' << 40, - (long)'a' << 56 | (long)'p' << 48 | (long)'o' << 40 | (long)'s' << 32, - (long)'a' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24, - (long)'a' << 56 | (long)'s' << 48 | (long)'y' << 40 | (long)'m' << 32 | (long)'p' << 24, - (long)'a' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'a' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'b' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'b' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'b' << 56 | (long)'r' << 48 | (long)'v' << 40 | (long)'b' << 32 | (long)'a' << 24 | (long)'r' << 16, - (long)'b' << 56 | (long)'u' << 48 | (long)'l' << 40 | (long)'l' << 32, - (long)'c' << 56 | (long)'a' << 48 | (long)'p' << 40, - (long)'c' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'c' << 56 | (long)'e' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'c' << 56 | (long)'e' << 48 | (long)'n' << 40 | (long)'t' << 32, - (long)'c' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'c' << 56 | (long)'i' << 48 | (long)'r' << 40 | (long)'c' << 32, - (long)'c' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'b' << 32 | (long)'s' << 24, - (long)'c' << 56 | (long)'o' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'c' << 56 | (long)'o' << 48 | (long)'p' << 40 | (long)'y' << 32, - (long)'c' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'r' << 24, - (long)'c' << 56 | (long)'u' << 48 | (long)'p' << 40, - (long)'c' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'n' << 16, - (long)'d' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'d' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16, - (long)'d' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'d' << 56 | (long)'e' << 48 | (long)'g' << 40, - (long)'d' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'d' << 56 | (long)'i' << 48 | (long)'a' << 40 | (long)'m' << 32 | (long)'s' << 24, - (long)'d' << 56 | (long)'i' << 48 | (long)'v' << 40 | (long)'i' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'e' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'e' << 56 | (long)'m' << 48 | (long)'p' << 40 | (long)'t' << 32 | (long)'y' << 24, - (long)'e' << 56 | (long)'m' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'e' << 56 | (long)'n' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'e' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'e' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'i' << 32 | (long)'v' << 24, - (long)'e' << 56 | (long)'t' << 48 | (long)'a' << 40, - (long)'e' << 56 | (long)'t' << 48 | (long)'h' << 40, - (long)'e' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'e' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'o' << 32, - (long)'e' << 56 | (long)'x' << 48 | (long)'i' << 40 | (long)'s' << 32 | (long)'t' << 24, - (long)'f' << 56 | (long)'n' << 48 | (long)'o' << 40 | (long)'f' << 32, - (long)'f' << 56 | (long)'o' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'l' << 24 | (long)'l' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'2' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'4' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'3' << 24 | (long)'4' << 16, - (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'l' << 24, - (long)'g' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'g' << 56 | (long)'e' << 48, - (long)'g' << 56 | (long)'t' << 48, - (long)'h' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'h' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'h' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'t' << 24 | (long)'s' << 16, - (long)'h' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'l' << 32 | (long)'i' << 24 | (long)'p' << 16, - (long)'i' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'i' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'i' << 56 | (long)'e' << 48 | (long)'x' << 40 | (long)'c' << 32 | (long)'l' << 24, - (long)'i' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'i' << 56 | (long)'m' << 48 | (long)'a' << 40 | (long)'g' << 32 | (long)'e' << 24, - (long)'i' << 56 | (long)'n' << 48 | (long)'f' << 40 | (long)'i' << 32 | (long)'n' << 24, - (long)'i' << 56 | (long)'n' << 48 | (long)'t' << 40, - (long)'i' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'i' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'e' << 32 | (long)'s' << 24 | (long)'t' << 16, - (long)'i' << 56 | (long)'s' << 48 | (long)'i' << 40 | (long)'n' << 32, - (long)'i' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'k' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24, - (long)'l' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'l' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16, - (long)'l' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'l' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'l' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'l' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'e' << 48, - (long)'l' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16, - (long)'l' << 56 | (long)'o' << 48 | (long)'w' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'t' << 16, - (long)'l' << 56 | (long)'o' << 48 | (long)'z' << 40, - (long)'l' << 56 | (long)'r' << 48 | (long)'m' << 40, - (long)'l' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16, - (long)'l' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'l' << 56 | (long)'t' << 48, - (long)'m' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'r' << 32, - (long)'m' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24, - (long)'m' << 56 | (long)'i' << 48 | (long)'c' << 40 | (long)'r' << 32 | (long)'o' << 24, - (long)'m' << 56 | (long)'i' << 48 | (long)'d' << 40 | (long)'d' << 32 | (long)'o' << 24 | (long)'t' << 16, - (long)'m' << 56 | (long)'i' << 48 | (long)'n' << 40 | (long)'u' << 32 | (long)'s' << 24, - (long)'m' << 56 | (long)'u' << 48, - (long)'n' << 56 | (long)'a' << 48 | (long)'b' << 40 | (long)'l' << 32 | (long)'a' << 24, - (long)'n' << 56 | (long)'b' << 48 | (long)'s' << 40 | (long)'p' << 32, - (long)'n' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24, - (long)'n' << 56 | (long)'e' << 48, - (long)'n' << 56 | (long)'i' << 48, - (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40, - (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'i' << 32 | (long)'n' << 24, - (long)'n' << 56 | (long)'s' << 48 | (long)'u' << 40 | (long)'b' << 32, - (long)'n' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'n' << 56 | (long)'u' << 48, - (long)'o' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'o' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'o' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'l' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'e' << 24, - (long)'o' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24, - (long)'o' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'o' << 56 | (long)'p' << 48 | (long)'l' << 40 | (long)'u' << 32 | (long)'s' << 24, - (long)'o' << 56 | (long)'r' << 48, - (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'f' << 32, - (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'m' << 32, - (long)'o' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16, - (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16, - (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24 | (long)'s' << 16, - (long)'o' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'a' << 32, - (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'t' << 32, - (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'m' << 32 | (long)'i' << 24 | (long)'l' << 16, - (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'p' << 32, - (long)'p' << 56 | (long)'h' << 48 | (long)'i' << 40, - (long)'p' << 56 | (long)'i' << 48, - (long)'p' << 56 | (long)'i' << 48 | (long)'v' << 40, - (long)'p' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'s' << 32 | (long)'m' << 24 | (long)'n' << 16, - (long)'p' << 56 | (long)'o' << 48 | (long)'u' << 40 | (long)'n' << 32 | (long)'d' << 24, - (long)'p' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24, - (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'d' << 32, - (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'p' << 32, - (long)'p' << 56 | (long)'s' << 48 | (long)'i' << 40, - (long)'q' << 56 | (long)'u' << 48 | (long)'o' << 40 | (long)'t' << 32, - (long)'r' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'r' << 56 | (long)'a' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'c' << 24, - (long)'r' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32, - (long)'r' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'r' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'r' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24, - (long)'r' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'r' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'l' << 32, - (long)'r' << 56 | (long)'e' << 48 | (long)'g' << 40, - (long)'r' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16, - (long)'r' << 56 | (long)'h' << 48 | (long)'o' << 40, - (long)'r' << 56 | (long)'l' << 48 | (long)'m' << 40, - (long)'r' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16, - (long)'r' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'s' << 56 | (long)'b' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24, - (long)'s' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16, - (long)'s' << 56 | (long)'d' << 48 | (long)'o' << 40 | (long)'t' << 32, - (long)'s' << 56 | (long)'e' << 48 | (long)'c' << 40 | (long)'t' << 32, - (long)'s' << 56 | (long)'h' << 48 | (long)'y' << 40, - (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24, - (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24 | (long)'f' << 16, - (long)'s' << 56 | (long)'i' << 48 | (long)'m' << 40, - (long)'s' << 56 | (long)'p' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24 | (long)'s' << 16, - (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40 | (long)'e' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'m' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'1' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'2' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'3' << 32, - (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'e' << 32, - (long)'s' << 56 | (long)'z' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, - (long)'t' << 56 | (long)'a' << 48 | (long)'u' << 40, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'4' << 16, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24, - (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24 | (long)'s' << 16 | (long)'y' << 8 | (long)'m' << 0, - (long)'t' << 56 | (long)'h' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'s' << 24 | (long)'p' << 16, - (long)'t' << 56 | (long)'h' << 48 | (long)'o' << 40 | (long)'r' << 32 | (long)'n' << 24, - (long)'t' << 56 | (long)'i' << 48 | (long)'l' << 40 | (long)'d' << 32 | (long)'e' << 24, - (long)'t' << 56 | (long)'i' << 48 | (long)'m' << 40 | (long)'e' << 32 | (long)'s' << 24, - (long)'t' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24, - (long)'u' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'u' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'u' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32, - (long)'u' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24, - (long)'u' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16, - (long)'u' << 56 | (long)'m' << 48 | (long)'l' << 40, - (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'h' << 24, - (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8, - (long)'u' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'w' << 56 | (long)'e' << 48 | (long)'i' << 40 | (long)'e' << 32 | (long)'r' << 24 | (long)'p' << 16, - (long)'x' << 56 | (long)'i' << 48, - (long)'y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16, - (long)'y' << 56 | (long)'e' << 48 | (long)'n' << 40, - (long)'y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32, - (long)'z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32, - (long)'z' << 56 | (long)'w' << 48 | (long)'j' << 40, - (long)'z' << 56 | (long)'w' << 48 | (long)'n' << 40 | (long)'j' << 32 - }; - - static readonly char[] entities_values = new char[] { - '\u00C6', '\u00C1', '\u00C2', '\u00C0', '\u0391', '\u00C5', '\u00C3', '\u00C4', '\u0392', '\u00C7', '\u03A7', - '\u2021', '\u0394', '\u00D0', '\u00C9', '\u00CA', '\u00C8', '\u0395', '\u0397', '\u00CB', '\u0393', '\u00CD', - '\u00CE', '\u00CC', '\u0399', '\u00CF', '\u039A', '\u039B', '\u039C', '\u00D1', '\u039D', '\u0152', '\u00D3', - '\u00D4', '\u00D2', '\u03A9', '\u039F', '\u00D8', '\u00D5', '\u00D6', '\u03A6', '\u03A0', '\u2033', '\u03A8', - '\u03A1', '\u0160', '\u03A3', '\u00DE', '\u03A4', '\u0398', '\u00DA', '\u00DB', '\u00D9', '\u03A5', '\u00DC', - '\u039E', '\u00DD', '\u0178', '\u0396', '\u00E1', '\u00E2', '\u00B4', '\u00E6', '\u00E0', '\u2135', '\u03B1', - '\u0026', '\u2227', '\u2220', '\u0027', '\u00E5', '\u2248', '\u00E3', '\u00E4', '\u201E', '\u03B2', '\u00A6', - '\u2022', '\u2229', '\u00E7', '\u00B8', '\u00A2', '\u03C7', '\u02C6', '\u2663', '\u2245', '\u00A9', '\u21B5', - '\u222A', '\u00A4', '\u21D3', '\u2020', '\u2193', '\u00B0', '\u03B4', '\u2666', '\u00F7', '\u00E9', '\u00EA', - '\u00E8', '\u2205', '\u2003', '\u2002', '\u03B5', '\u2261', '\u03B7', '\u00F0', '\u00EB', '\u20AC', '\u2203', - '\u0192', '\u2200', '\u00BD', '\u00BC', '\u00BE', '\u2044', '\u03B3', '\u2265', '\u003E', '\u21D4', '\u2194', - '\u2665', '\u2026', '\u00ED', '\u00EE', '\u00A1', '\u00EC', '\u2111', '\u221E', '\u222B', '\u03B9', '\u00BF', - '\u2208', '\u00EF', '\u03BA', '\u21D0', '\u03BB', '\u2329', '\u00AB', '\u2190', '\u2308', '\u201C', '\u2264', - '\u230A', '\u2217', '\u25CA', '\u200E', '\u2039', '\u2018', '\u003C', '\u00AF', '\u2014', '\u00B5', '\u00B7', - '\u2212', '\u03BC', '\u2207', '\u00A0', '\u2013', '\u2260', '\u220B', '\u00AC', '\u2209', '\u2284', '\u00F1', - '\u03BD', '\u00F3', '\u00F4', '\u0153', '\u00F2', '\u203E', '\u03C9', '\u03BF', '\u2295', '\u2228', '\u00AA', - '\u00BA', '\u00F8', '\u00F5', '\u2297', '\u00F6', '\u00B6', '\u2202', '\u2030', '\u22A5', '\u03C6', '\u03C0', - '\u03D6', '\u00B1', '\u00A3', '\u2032', '\u220F', '\u221D', '\u03C8', '\u0022', '\u21D2', '\u221A', '\u232A', - '\u00BB', '\u2192', '\u2309', '\u201D', '\u211C', '\u00AE', '\u230B', '\u03C1', '\u200F', '\u203A', '\u2019', - '\u201A', '\u0161', '\u22C5', '\u00A7', '\u00AD', '\u03C3', '\u03C2', '\u223C', '\u2660', '\u2282', '\u2286', - '\u2211', '\u2283', '\u00B9', '\u00B2', '\u00B3', '\u2287', '\u00DF', '\u03C4', '\u2234', '\u03B8', '\u03D1', - '\u2009', '\u00FE', '\u02DC', '\u00D7', '\u2122', '\u21D1', '\u00FA', '\u2191', '\u00FB', '\u00F9', '\u00A8', - '\u03D2', '\u03C5', '\u00FC', '\u2118', '\u03BE', '\u00FD', '\u00A5', '\u00FF', '\u03B6', '\u200D', '\u200C' - }; - - #region Methods - - static void WriteCharBytes(IList buf, char ch, Encoding e) - { - if (ch > 255) - { - foreach (byte b in e.GetBytes(new char[] { ch })) - buf.Add(b); - } - else - buf.Add((byte)ch); - } - - public static string UrlDecode(string s, Encoding e) - { - if (null == s) - return null; - - if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1) - return s; - - if (e == null) - e = Encoding.UTF8; - - long len = s.Length; - var bytes = new List(); - int xchar; - char ch; - - for (int i = 0; i < len; i++) - { - ch = s[i]; - if (ch == '%' && i + 2 < len && s[i + 1] != '%') - { - if (s[i + 1] == 'u' && i + 5 < len) - { - // unicode hex sequence - xchar = GetChar(s, i + 2, 4); - if (xchar != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 5; - } - else - WriteCharBytes(bytes, '%', e); - } - else if ((xchar = GetChar(s, i + 1, 2)) != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 2; - } - else - { - WriteCharBytes(bytes, '%', e); - } - continue; - } - - if (ch == '+') - WriteCharBytes(bytes, ' ', e); - else - WriteCharBytes(bytes, ch, e); - } - - byte[] buf = bytes.ToArray(); - bytes = null; - return e.GetString(buf, 0, buf.Length); - - } - - static int GetInt(byte b) - { - char c = (char)b; - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; - } - - static int GetChar(string str, int offset, int length) - { - int val = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - char c = str[i]; - if (c > 127) - return -1; - - int current = GetInt((byte)c); - if (current == -1) - return -1; - val = (val << 4) + current; - } - - return val; - } - - static bool TryConvertKeyToEntity(string key, out char value) - { - var token = CalculateKeyValue(key); - if (token == 0) - { - value = '\0'; - return false; - } - - var idx = Array.BinarySearch(entities, token); - if (idx < 0) - { - value = '\0'; - return false; - } - - value = entities_values[idx]; - return true; - } - - static long CalculateKeyValue(string s) - { - if (s.Length > 8) - return 0; - - long key = 0; - for (int i = 0; i < s.Length; ++i) - { - long ch = s[i]; - if (ch > 'z' || ch < '0') - return 0; - - key |= ch << ((7 - i) * 8); - } - - return key; - } - - /// - /// Decodes an HTML-encoded string and returns the decoded string. - /// - /// The HTML string to decode. - /// The decoded text. - public static string HtmlDecode(string s) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - - if (s.IndexOf('&') == -1) - return s; - - var entity = new StringBuilder(); - var output = new StringBuilder(); - int len = s.Length; - // 0 -> nothing, - // 1 -> right after '&' - // 2 -> between '&' and ';' but no '#' - // 3 -> '#' found after '&' and getting numbers - int state = 0; - int number = 0; - int digit_start = 0; - bool hex_number = false; - - for (int i = 0; i < len; i++) - { - char c = s[i]; - if (state == 0) - { - if (c == '&') - { - entity.Append(c); - state = 1; - } - else - { - output.Append(c); - } - continue; - } - - if (c == '&') - { - state = 1; - if (digit_start > 0) - { - entity.Append(s, digit_start, i - digit_start); - digit_start = 0; - } - - output.Append(entity.ToString()); - entity.Length = 0; - entity.Append('&'); - continue; - } - - switch (state) - { - case 1: - if (c == ';') - { - state = 0; - output.Append(entity.ToString()); - output.Append(c); - entity.Length = 0; - break; - } - - number = 0; - hex_number = false; - if (c != '#') - { - state = 2; - } - else - { - state = 3; - } - entity.Append(c); - - break; - case 2: - entity.Append(c); - if (c == ';') - { - string key = entity.ToString(); - state = 0; - entity.Length = 0; - - if (key.Length > 1) - { - var skey = key.Substring(1, key.Length - 2); - if (TryConvertKeyToEntity(skey, out c)) - { - output.Append(c); - break; - } - } - - output.Append(key); - } - - break; - case 3: - if (c == ';') - { - if (number < 0x10000) - { - output.Append((char)number); - } - else - { - output.Append((char)(0xd800 + ((number - 0x10000) >> 10))); - output.Append((char)(0xdc00 + ((number - 0x10000) & 0x3ff))); - } - state = 0; - entity.Length = 0; - digit_start = 0; - break; - } - - if (c == 'x' || c == 'X' && !hex_number) - { - digit_start = i; - hex_number = true; - break; - } - - if (char.IsDigit(c)) - { - if (digit_start == 0) - digit_start = i; - - number = number * (hex_number ? 16 : 10) + ((int)c - '0'); - break; - } - - if (hex_number) - { - if (c >= 'a' && c <= 'f') - { - number = number * 16 + 10 + ((int)c - 'a'); - break; - } - if (c >= 'A' && c <= 'F') - { - number = number * 16 + 10 + ((int)c - 'A'); - break; - } - } - - state = 2; - if (digit_start > 0) - { - entity.Append(s, digit_start, i - digit_start); - digit_start = 0; - } - - entity.Append(c); - break; - } - } - - if (entity.Length > 0) - { - output.Append(entity); - } - else if (digit_start > 0) - { - output.Append(s, digit_start, s.Length - digit_start); - } - return output.ToString(); - } - - public static QueryParamCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static QueryParamCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException(nameof(query)); - if (encoding == null) - throw new ArgumentNullException(nameof(encoding)); - if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new QueryParamCollection(); - if (query[0] == '?') - query = query.Substring(1); - - var result = new QueryParamCollection(); - ParseQueryString(query, encoding, result); - return result; - } - - internal static void ParseQueryString(string query, Encoding encoding, QueryParamCollection result) - { - if (query.Length == 0) - return; - - string decoded = HtmlDecode(query); - int decodedLength = decoded.Length; - int namePos = 0; - bool first = true; - while (namePos <= decodedLength) - { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) - { - if (valuePos == -1 && decoded[q] == '=') - { - valuePos = q + 1; - } - else if (decoded[q] == '&') - { - valueEnd = q; - break; - } - } - - if (first) - { - first = false; - if (decoded[namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) - { - name = null; - valuePos = namePos; - } - else - { - name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); - } - if (valueEnd < 0) - { - namePos = -1; - valueEnd = decoded.Length; - } - else - { - namePos = valueEnd + 1; - } - value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); - - result.Add(name, value); - if (namePos == -1) - break; - } - } - #endregion // Methods - } -} diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs index 579f80c968..50c6076f30 100644 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ b/MediaBrowser.Model/Services/IHttpRequest.cs @@ -2,11 +2,6 @@ namespace MediaBrowser.Model.Services { public interface IHttpRequest : IRequest { - /// - /// The HttpResponse - /// - IHttpResponse HttpResponse { get; } - /// /// The HTTP Verb /// diff --git a/MediaBrowser.Model/Services/IHttpResponse.cs b/MediaBrowser.Model/Services/IHttpResponse.cs deleted file mode 100644 index a8b79f3949..0000000000 --- a/MediaBrowser.Model/Services/IHttpResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Net; - -namespace MediaBrowser.Model.Services -{ - public interface IHttpResponse : IResponse - { - //ICookies Cookies { get; } - - /// - /// Adds a new Set-Cookie instruction to Response - /// - /// - void SetCookie(Cookie cookie); - - /// - /// Removes all pending Set-Cookie instructions - /// - void ClearCookies(); - } -} diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index ac9b981b98..4f6ddb476e 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -1,20 +1,15 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Model.Services { public interface IRequest { - /// - /// The underlying ASP.NET or HttpListener HttpRequest - /// - object OriginalRequest { get; } - IResponse Response { get; } /// @@ -41,8 +36,6 @@ namespace MediaBrowser.Model.Services string UserAgent { get; } - IDictionary Cookies { get; } - /// /// The expected Response ContentType for this request /// @@ -53,9 +46,9 @@ namespace MediaBrowser.Model.Services /// Dictionary Items { get; } - QueryParamCollection Headers { get; } + IHeaderDictionary Headers { get; } - QueryParamCollection QueryString { get; } + IQueryCollection QueryString { get; } Task GetFormData(); @@ -63,11 +56,6 @@ namespace MediaBrowser.Model.Services string AbsoluteUri { get; } - /// - /// The Remote Ip as reported by Request.UserHostAddress - /// - string UserHostAddress { get; } - /// /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress /// @@ -78,11 +66,6 @@ namespace MediaBrowser.Model.Services /// string Authorization { get; } - /// - /// e.g. is https or not - /// - bool IsSecureConnection { get; } - string[] AcceptTypes { get; } string PathInfo { get; } @@ -118,7 +101,7 @@ namespace MediaBrowser.Model.Services public interface IResponse { - IRequest Request { get; } + HttpResponse OriginalResponse { get; } int StatusCode { get; set; } @@ -128,31 +111,11 @@ namespace MediaBrowser.Model.Services void AddHeader(string name, string value); - string GetHeader(string name); - void Redirect(string url); Stream OutputStream { get; } - /// - /// Signal that this response has been handled and no more processing should be done. - /// When used in a request or response filter, no more filters or processing is done on this request. - /// - void Close(); - - /// - /// Gets a value indicating whether this instance is closed. - /// - bool IsClosed { get; } - - void SetContentLength(long contentLength); - - //Add Metadata to Response - Dictionary Items { get; } - - QueryParamCollection Headers { get; } - - Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken); + Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken); bool SendChunked { get; set; } } diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs index 4297b97c66..7708db00a8 100644 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ b/MediaBrowser.Model/Services/QueryParamCollection.cs @@ -5,19 +5,11 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Services { + // Remove this garbage class, it's just a bastard copy of NameValueCollection public class QueryParamCollection : List { public QueryParamCollection() { - - } - - public QueryParamCollection(IDictionary headers) - { - foreach (var pair in headers) - { - Add(pair.Key, pair.Value); - } } private static StringComparison GetStringComparison() @@ -30,30 +22,15 @@ namespace MediaBrowser.Model.Services return StringComparer.OrdinalIgnoreCase; } - public string GetKey(int index) - { - return this[index].Name; - } - - public string Get(int index) - { - return this[index].Value; - } - - public virtual string[] GetValues(int index) - { - return new[] { Get(index) }; - } - /// /// Adds a new query parameter. /// - public virtual void Add(string key, string value) + public void Add(string key, string value) { Add(new NameValuePair(key, value)); } - public virtual void Set(string key, string value) + private void Set(string key, string value) { if (string.IsNullOrEmpty(value)) { @@ -81,17 +58,7 @@ namespace MediaBrowser.Model.Services Add(key, value); } - /// - /// Removes all parameters of the given name. - /// - /// The number of parameters that were removed - /// is null. - public virtual int Remove(string name) - { - return RemoveAll(p => p.Name == name); - } - - public string Get(string name) + private string Get(string name) { var stringComparison = GetStringComparison(); @@ -106,7 +73,7 @@ namespace MediaBrowser.Model.Services return null; } - public virtual List GetItems(string name) + private List GetItems(string name) { var stringComparison = GetStringComparison(); @@ -140,20 +107,6 @@ namespace MediaBrowser.Model.Services return list; } - public Dictionary ToDictionary() - { - var stringComparer = GetStringComparer(); - - var headers = new Dictionary(stringComparer); - - foreach (var pair in this) - { - headers[pair.Name] = pair.Value; - } - - return headers; - } - public IEnumerable Keys { get diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 581a1069cd..6482f2c840 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates; namespace MediaBrowser.Model.System { + /// + /// Enum describing the location of the FFmpeg tool. + /// + public enum FFmpegLocation + { + /// No path to FFmpeg found. + NotFound, + /// Path supplied via command line using switch --ffmpeg. + SetByArgument, + /// User has supplied path via Transcoding UI page. + Custom, + /// FFmpeg tool found on system $PATH. + System + }; + /// /// Class SystemInfo /// @@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System /// true if this instance has update available; otherwise, false. public bool HasUpdateAvailable { get; set; } - public string EncoderLocationType { get; set; } + public FFmpegLocation EncoderLocation { get; set; } public Architecture SystemArchitecture { get; set; } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 52a52efdc0..cfbb85ea6b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -11,6 +11,7 @@ + diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index cd026b39b8..8195591e17 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILocalizationManager _localization; private readonly IFileSystem _fileSystem; - private string[] SubtitleExtensions = new[] + private static readonly HashSet SubtitleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".srt", ".ssa", @@ -49,9 +49,16 @@ namespace MediaBrowser.Providers.MediaInfo startIndex += streams.Count; + string folder = video.GetInternalMetadataPath(); + + if (!Directory.Exists(folder)) + { + return streams; + } + try { - AddExternalSubtitleStreams(streams, video.GetInternalMetadataPath(), video.Path, startIndex, directoryService, clearCache); + AddExternalSubtitleStreams(streams, folder, video.Path, startIndex, directoryService, clearCache); } catch (IOException) { @@ -105,7 +112,7 @@ namespace MediaBrowser.Providers.MediaInfo { var extension = Path.GetExtension(fullName); - if (!SubtitleExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!SubtitleExtensions.Contains(extension)) { continue; } diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 335f2c5395..3eb32fe16b 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -15,6 +15,8 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Xml; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Music @@ -28,14 +30,22 @@ namespace MediaBrowser.Providers.Music private readonly ILogger _logger; private readonly IJsonSerializer _json; - public static string MusicBrainzBaseUrl = "https://www.musicbrainz.org"; + public readonly string MusicBrainzBaseUrl; - public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger, IJsonSerializer json) + public MusicBrainzAlbumProvider( + IHttpClient httpClient, + IApplicationHost appHost, + ILogger logger, + IJsonSerializer json, + IConfiguration configuration) { _httpClient = httpClient; _appHost = appHost; _logger = logger; _json = json; + + MusicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"]; + Current = this; } diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 531978e1dd..aa0208495b 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -13,7 +13,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Reflection; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; @@ -117,20 +116,26 @@ namespace MediaBrowser.WebDashboard.Api private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; private readonly IJsonSerializer _jsonSerializer; - private readonly IAssemblyInfo _assemblyInfo; private IResourceFileManager _resourceFileManager; /// /// Initializes a new instance of the class. /// - public DashboardService(IServerApplicationHost appHost, IResourceFileManager resourceFileManager, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, IAssemblyInfo assemblyInfo, ILogger logger, IHttpResultFactory resultFactory) + public DashboardService( + IServerApplicationHost appHost, + IResourceFileManager resourceFileManager, + IServerConfigurationManager serverConfigurationManager, + IFileSystem fileSystem, + ILocalizationManager localization, + IJsonSerializer jsonSerializer, + ILogger logger, + IHttpResultFactory resultFactory) { _appHost = appHost; _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; _localization = localization; _jsonSerializer = jsonSerializer; - _assemblyInfo = assemblyInfo; _logger = logger; _resultFactory = resultFactory; _resourceFileManager = resourceFileManager; @@ -187,7 +192,7 @@ namespace MediaBrowser.WebDashboard.Api if (altPage != null) { plugin = altPage.Item2; - stream = _assemblyInfo.GetManifestResourceStream(plugin.GetType(), altPage.Item1.EmbeddedResourcePath); + stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html"); diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 62ae58d732..a3c0569bad 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 @@ -36,8 +37,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{CB7F2326-6497-4A3D-BA03-48513B17A7BE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SocketHttpListener", "SocketHttpListener\SocketHttpListener.csproj", "{1D74413B-E7CF-455B-B021-F52BDF881542}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby.Notifications\Emby.Notifications.csproj", "{2E030C33-6923-4530-9E54-FA29FA6AD1A9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" @@ -56,7 +55,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution SharedVersion.cs = SharedVersion.cs EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -132,10 +131,6 @@ Global {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.Build.0 = Release|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D74413B-E7CF-455B-B021-F52BDF881542}.Release|Any CPU.Build.0 = Release|Any CPU {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -185,7 +180,7 @@ Global GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 $0.StandardHeader = $1 - $1.Text = @### This header should be used to start new files.\n### It provides an explicit per-file license reference that should be present on all new files.\n### To use this header, delete these lines and the following empty line, modify to\n### the proper full path, and then add new code following the header and a single empty line.\n\n// ${FileName}\n// Part of the Jellyfin project (https://jellyfin.media)\n//\n// All copyright belongs to the Jellyfin contributors; a full list can\n// be found in the file CONTRIBUTORS.md\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, version 2.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see .\n + $1.IncludeInNewFiles = False $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection diff --git a/README.md b/README.md index d869c89789..1f635bdd25 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ GPL 2.0 License Current Release Translations -Build Status +Azure DevOps builds Docker Pull Count
Donate diff --git a/SocketHttpListener/ByteOrder.cs b/SocketHttpListener/ByteOrder.cs deleted file mode 100644 index c04150c74b..0000000000 --- a/SocketHttpListener/ByteOrder.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values that indicate whether the byte order is a Little-endian or Big-endian. - /// - public enum ByteOrder : byte - { - /// - /// Indicates a Little-endian. - /// - Little, - /// - /// Indicates a Big-endian. - /// - Big - } -} diff --git a/SocketHttpListener/CloseEventArgs.cs b/SocketHttpListener/CloseEventArgs.cs deleted file mode 100644 index c6460fd230..0000000000 --- a/SocketHttpListener/CloseEventArgs.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Text; - -namespace SocketHttpListener -{ - /// - /// Contains the event data associated with a event. - /// - /// - /// A event occurs when the WebSocket connection has been closed. - /// If you would like to get the reason for the close, you should access the or - /// property. - /// - public class CloseEventArgs : EventArgs - { - #region Private Fields - - private bool _clean; - private ushort _code; - private string _reason; - - #endregion - - #region Internal Constructors - - internal CloseEventArgs(PayloadData payload) - { - var data = payload.ApplicationData; - var len = data.Length; - _code = len > 1 - ? data.SubArray(0, 2).ToUInt16(ByteOrder.Big) - : (ushort)CloseStatusCode.NoStatusCode; - - _reason = len > 2 - ? GetUtf8String(data.SubArray(2, len - 2)) - : string.Empty; - } - - private static string GetUtf8String(byte[] bytes) - { - return Encoding.UTF8.GetString(bytes, 0, bytes.Length); - } - - #endregion - - #region Public Properties - - /// - /// Gets the status code for the close. - /// - /// - /// A that represents the status code for the close if any. - /// - public ushort Code => _code; - - /// - /// Gets the reason for the close. - /// - /// - /// A that represents the reason for the close if any. - /// - public string Reason => _reason; - - /// - /// Gets a value indicating whether the WebSocket connection has been closed cleanly. - /// - /// - /// true if the WebSocket connection has been closed cleanly; otherwise, false. - /// - public bool WasClean - { - get => _clean; - - internal set => _clean = value; - } - - #endregion - } -} diff --git a/SocketHttpListener/CloseStatusCode.cs b/SocketHttpListener/CloseStatusCode.cs deleted file mode 100644 index 428595bb09..0000000000 --- a/SocketHttpListener/CloseStatusCode.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values of the status code for the WebSocket connection close. - /// - /// - /// - /// The values of the status code are defined in - /// Section 7.4 - /// of RFC 6455. - /// - /// - /// "Reserved value" must not be set as a status code in a close control frame - /// by an endpoint. It's designated for use in applications expecting a status - /// code to indicate that the connection was closed due to the system grounds. - /// - /// - public enum CloseStatusCode : ushort - { - /// - /// Equivalent to close status 1000. - /// Indicates a normal close. - /// - Normal = 1000, - /// - /// Equivalent to close status 1001. - /// Indicates that an endpoint is going away. - /// - Away = 1001, - /// - /// Equivalent to close status 1002. - /// Indicates that an endpoint is terminating the connection due to a protocol error. - /// - ProtocolError = 1002, - /// - /// Equivalent to close status 1003. - /// Indicates that an endpoint is terminating the connection because it has received - /// an unacceptable type message. - /// - IncorrectData = 1003, - /// - /// Equivalent to close status 1004. - /// Still undefined. Reserved value. - /// - Undefined = 1004, - /// - /// Equivalent to close status 1005. - /// Indicates that no status code was actually present. Reserved value. - /// - NoStatusCode = 1005, - /// - /// Equivalent to close status 1006. - /// Indicates that the connection was closed abnormally. Reserved value. - /// - Abnormal = 1006, - /// - /// Equivalent to close status 1007. - /// Indicates that an endpoint is terminating the connection because it has received - /// a message that contains a data that isn't consistent with the type of the message. - /// - InconsistentData = 1007, - /// - /// Equivalent to close status 1008. - /// Indicates that an endpoint is terminating the connection because it has received - /// a message that violates its policy. - /// - PolicyViolation = 1008, - /// - /// Equivalent to close status 1009. - /// Indicates that an endpoint is terminating the connection because it has received - /// a message that is too big to process. - /// - TooBig = 1009, - /// - /// Equivalent to close status 1010. - /// Indicates that the client is terminating the connection because it has expected - /// the server to negotiate one or more extension, but the server didn't return them - /// in the handshake response. - /// - IgnoreExtension = 1010, - /// - /// Equivalent to close status 1011. - /// Indicates that the server is terminating the connection because it has encountered - /// an unexpected condition that prevented it from fulfilling the request. - /// - ServerError = 1011, - /// - /// Equivalent to close status 1015. - /// Indicates that the connection was closed due to a failure to perform a TLS handshake. - /// Reserved value. - /// - TlsHandshakeFailure = 1015 - } -} diff --git a/SocketHttpListener/CompressionMethod.cs b/SocketHttpListener/CompressionMethod.cs deleted file mode 100644 index d6bcd63d89..0000000000 --- a/SocketHttpListener/CompressionMethod.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values of the compression method used to compress the message on the WebSocket - /// connection. - /// - /// - /// The values of the compression method are defined in - /// Compression - /// Extensions for WebSocket. - /// - public enum CompressionMethod : byte - { - /// - /// Indicates non compression. - /// - None, - /// - /// Indicates using DEFLATE. - /// - Deflate - } -} diff --git a/SocketHttpListener/ErrorEventArgs.cs b/SocketHttpListener/ErrorEventArgs.cs deleted file mode 100644 index 9502d2a156..0000000000 --- a/SocketHttpListener/ErrorEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace SocketHttpListener -{ - /// - /// Contains the event data associated with a event. - /// - /// - /// A event occurs when the gets an error. - /// If you would like to get the error message, you should access the - /// property. - /// - public class ErrorEventArgs : EventArgs - { - #region Private Fields - - private string _message; - - #endregion - - #region Internal Constructors - - internal ErrorEventArgs(string message) - { - _message = message; - } - - #endregion - - #region Public Properties - - /// - /// Gets the error message. - /// - /// - /// A that represents the error message. - /// - public string Message => _message; - - #endregion - } -} diff --git a/SocketHttpListener/Ext.cs b/SocketHttpListener/Ext.cs deleted file mode 100644 index 2b3c67071c..0000000000 --- a/SocketHttpListener/Ext.cs +++ /dev/null @@ -1,947 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; -using WebSocketState = System.Net.WebSockets.WebSocketState; - -namespace SocketHttpListener -{ - /// - /// Provides a set of static methods for the websocket-sharp. - /// - public static class Ext - { - #region Private Const Fields - - private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; - - #endregion - - #region Private Methods - - private static MemoryStream compress(this Stream stream) - { - var output = new MemoryStream(); - if (stream.Length == 0) - return output; - - stream.Position = 0; - using (var ds = new DeflateStream(output, CompressionMode.Compress, true)) - { - stream.CopyTo(ds); - //ds.Close(); // "BFINAL" set to 1. - output.Position = 0; - - return output; - } - } - - private static byte[] decompress(this byte[] value) - { - if (value.Length == 0) - return value; - - using (var input = new MemoryStream(value)) - { - return input.decompressToArray(); - } - } - - private static MemoryStream decompress(this Stream stream) - { - var output = new MemoryStream(); - if (stream.Length == 0) - return output; - - stream.Position = 0; - using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true)) - { - ds.CopyTo(output, true); - return output; - } - } - - private static byte[] decompressToArray(this Stream stream) - { - using (var decomp = stream.decompress()) - { - return decomp.ToArray(); - } - } - - private static async Task ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length) - { - var len = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false); - if (len < 1) - return buffer.SubArray(0, offset); - - var tmp = 0; - while (len < length) - { - tmp = await stream.ReadAsync(buffer, offset + len, length - len).ConfigureAwait(false); - if (tmp < 1) - { - break; - } - - len += tmp; - } - - return len < length - ? buffer.SubArray(0, offset + len) - : buffer; - } - - private static async Task ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length, Stream dest) - { - var bytes = await stream.ReadBytesAsync(buffer, offset, length).ConfigureAwait(false); - var len = bytes.Length; - dest.Write(bytes, 0, len); - - return len == offset + length; - } - - #endregion - - #region Internal Methods - - internal static async Task AppendAsync(this ushort code, string reason) - { - using (var buffer = new MemoryStream()) - { - var tmp = code.ToByteArrayInternally(ByteOrder.Big); - await buffer.WriteAsync(tmp, 0, 2).ConfigureAwait(false); - if (reason != null && reason.Length > 0) - { - tmp = Encoding.UTF8.GetBytes(reason); - await buffer.WriteAsync(tmp, 0, tmp.Length).ConfigureAwait(false); - } - - return buffer.ToArray(); - } - } - - internal static string CheckIfClosable(this WebSocketState state) - { - return state == WebSocketState.CloseSent - ? "While closing the WebSocket connection." - : state == WebSocketState.Closed - ? "The WebSocket connection has already been closed." - : null; - } - - internal static string CheckIfOpen(this WebSocketState state) - { - return state == WebSocketState.Connecting - ? "A WebSocket connection isn't established." - : state == WebSocketState.CloseSent - ? "While closing the WebSocket connection." - : state == WebSocketState.Closed - ? "The WebSocket connection has already been closed." - : null; - } - - internal static string CheckIfValidControlData(this byte[] data, string paramName) - { - return data.Length > 125 - ? string.Format("'{0}' length must be less.", paramName) - : null; - } - - internal static Stream Compress(this Stream stream, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? stream.compress() - : stream; - } - - internal static bool Contains(this IEnumerable source, Func condition) - { - foreach (T elm in source) - if (condition(elm)) - return true; - - return false; - } - - internal static void CopyTo(this Stream src, Stream dest, bool setDefaultPosition) - { - var readLen = 0; - var bufferLen = 256; - var buffer = new byte[bufferLen]; - while ((readLen = src.Read(buffer, 0, bufferLen)) > 0) - { - dest.Write(buffer, 0, readLen); - } - - if (setDefaultPosition) - dest.Position = 0; - } - - internal static byte[] Decompress(this byte[] value, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? value.decompress() - : value; - } - - internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? stream.decompressToArray() - : stream.ToByteArray(); - } - - /// - /// Determines whether the specified equals the specified , - /// and invokes the specified Action<int> delegate at the same time. - /// - /// - /// true if equals ; - /// otherwise, false. - /// - /// - /// An to compare. - /// - /// - /// A to compare. - /// - /// - /// An Action<int> delegate that references the method(s) called at - /// the same time as comparing. An parameter to pass to - /// the method(s) is . - /// - /// - /// isn't between 0 and 255. - /// - internal static bool EqualsWith(this int value, char c, Action action) - { - if (value < 0 || value > 255) - throw new ArgumentOutOfRangeException(nameof(value)); - - action(value); - return value == c - 0; - } - - internal static string GetMessage(this CloseStatusCode code) - { - return code == CloseStatusCode.ProtocolError - ? "A WebSocket protocol error has occurred." - : code == CloseStatusCode.IncorrectData - ? "An incorrect data has been received." - : code == CloseStatusCode.Abnormal - ? "An exception has occurred." - : code == CloseStatusCode.InconsistentData - ? "An inconsistent data has been received." - : code == CloseStatusCode.PolicyViolation - ? "A policy violation has occurred." - : code == CloseStatusCode.TooBig - ? "A too big data has been received." - : code == CloseStatusCode.IgnoreExtension - ? "WebSocket client did not receive expected extension(s)." - : code == CloseStatusCode.ServerError - ? "WebSocket server got an internal error." - : code == CloseStatusCode.TlsHandshakeFailure - ? "An error has occurred while handshaking." - : string.Empty; - } - - internal static string GetNameInternal(this string nameAndValue, string separator) - { - var i = nameAndValue.IndexOf(separator); - return i > 0 - ? nameAndValue.Substring(0, i).Trim() - : null; - } - - internal static string GetValueInternal(this string nameAndValue, string separator) - { - var i = nameAndValue.IndexOf(separator); - return i >= 0 && i < nameAndValue.Length - 1 - ? nameAndValue.Substring(i + 1).Trim() - : null; - } - - internal static bool IsCompressionExtension(this string value, CompressionMethod method) - { - return value.StartsWith(method.ToExtensionString()); - } - - internal static bool IsPortNumber(this int value) - { - return value > 0 && value < 65536; - } - - internal static bool IsReserved(this ushort code) - { - return code == (ushort)CloseStatusCode.Undefined || - code == (ushort)CloseStatusCode.NoStatusCode || - code == (ushort)CloseStatusCode.Abnormal || - code == (ushort)CloseStatusCode.TlsHandshakeFailure; - } - - internal static bool IsReserved(this CloseStatusCode code) - { - return code == CloseStatusCode.Undefined || - code == CloseStatusCode.NoStatusCode || - code == CloseStatusCode.Abnormal || - code == CloseStatusCode.TlsHandshakeFailure; - } - - internal static bool IsText(this string value) - { - var len = value.Length; - for (var i = 0; i < len; i++) - { - char c = value[i]; - if (c < 0x20 && !"\r\n\t".Contains(c)) - return false; - - if (c == 0x7f) - return false; - - if (c == '\n' && ++i < len) - { - c = value[i]; - if (!" \t".Contains(c)) - return false; - } - } - - return true; - } - - internal static bool IsToken(this string value) - { - foreach (char c in value) - if (c < 0x20 || c >= 0x7f || _tspecials.Contains(c)) - return false; - - return true; - } - - internal static string Quote(this string value) - { - return value.IsToken() - ? value - : string.Format("\"{0}\"", value.Replace("\"", "\\\"")); - } - - internal static Task ReadBytesAsync(this Stream stream, int length) - => stream.ReadBytesAsync(new byte[length], 0, length); - - internal static async Task ReadBytesAsync(this Stream stream, long length, int bufferLength) - { - using (var result = new MemoryStream()) - { - var count = length / bufferLength; - var rem = (int)(length % bufferLength); - - var buffer = new byte[bufferLength]; - var end = false; - for (long i = 0; i < count; i++) - { - if (!await stream.ReadBytesAsync(buffer, 0, bufferLength, result).ConfigureAwait(false)) - { - end = true; - break; - } - } - - if (!end && rem > 0) - { - await stream.ReadBytesAsync(new byte[rem], 0, rem, result).ConfigureAwait(false); - } - - return result.ToArray(); - } - } - - internal static string RemovePrefix(this string value, params string[] prefixes) - { - var i = 0; - foreach (var prefix in prefixes) - { - if (value.StartsWith(prefix)) - { - i = prefix.Length; - break; - } - } - - return i > 0 - ? value.Substring(i) - : value; - } - - internal static T[] Reverse(this T[] array) - { - var len = array.Length; - T[] reverse = new T[len]; - - var end = len - 1; - for (var i = 0; i <= end; i++) - reverse[i] = array[end - i]; - - return reverse; - } - - internal static IEnumerable SplitHeaderValue( - this string value, params char[] separator) - { - var len = value.Length; - var separators = new string(separator); - - var buffer = new StringBuilder(32); - var quoted = false; - var escaped = false; - - char c; - for (var i = 0; i < len; i++) - { - c = value[i]; - if (c == '"') - { - if (escaped) - escaped = !escaped; - else - quoted = !quoted; - } - else if (c == '\\') - { - if (i < len - 1 && value[i + 1] == '"') - escaped = true; - } - else if (separators.Contains(c)) - { - if (!quoted) - { - yield return buffer.ToString(); - buffer.Length = 0; - - continue; - } - } - else - { - } - - buffer.Append(c); - } - - if (buffer.Length > 0) - yield return buffer.ToString(); - } - - internal static byte[] ToByteArray(this Stream stream) - { - using (var output = new MemoryStream()) - { - stream.Position = 0; - stream.CopyTo(output); - - return output.ToArray(); - } - } - - internal static byte[] ToByteArrayInternally(this ushort value, ByteOrder order) - { - var bytes = BitConverter.GetBytes(value); - if (!order.IsHostOrder()) - Array.Reverse(bytes); - - return bytes; - } - - internal static byte[] ToByteArrayInternally(this ulong value, ByteOrder order) - { - var bytes = BitConverter.GetBytes(value); - if (!order.IsHostOrder()) - Array.Reverse(bytes); - - return bytes; - } - - internal static string ToExtensionString( - this CompressionMethod method, params string[] parameters) - { - if (method == CompressionMethod.None) - return string.Empty; - - var m = string.Format("permessage-{0}", method.ToString().ToLowerInvariant()); - if (parameters == null || parameters.Length == 0) - return m; - - return string.Format("{0}; {1}", m, parameters.ToString("; ")); - } - - internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder) - { - src.ToHostOrder(srcOrder); - return BitConverter.ToUInt16(src, 0); - } - - internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder) - { - src.ToHostOrder(srcOrder); - return BitConverter.ToUInt64(src, 0); - } - - internal static string TrimEndSlash(this string value) - { - value = value.TrimEnd('/'); - return value.Length > 0 - ? value - : "/"; - } - - internal static string Unquote(this string value) - { - var start = value.IndexOf('\"'); - var end = value.LastIndexOf('\"'); - if (start < end) - value = value.Substring(start + 1, end - start - 1).Replace("\\\"", "\""); - - return value.Trim(); - } - - internal static void WriteBytes(this Stream stream, byte[] value) - { - using (var src = new MemoryStream(value)) - { - src.CopyTo(stream); - } - } - - #endregion - - #region Public Methods - - /// - /// Determines whether the specified contains any of characters - /// in the specified array of . - /// - /// - /// true if contains any of ; - /// otherwise, false. - /// - /// - /// A to test. - /// - /// - /// An array of that contains characters to find. - /// - public static bool Contains(this string value, params char[] chars) - { - return chars == null || chars.Length == 0 - ? true - : value == null || value.Length == 0 - ? false - : value.IndexOfAny(chars) != -1; - } - - /// - /// Determines whether the specified contains the entry - /// with the specified . - /// - /// - /// true if contains the entry - /// with ; otherwise, false. - /// - /// - /// A to test. - /// - /// - /// A that represents the key of the entry to find. - /// - public static bool Contains(this QueryParamCollection collection, string name) - { - return collection == null || collection.Count == 0 - ? false - : collection[name] != null; - } - - /// - /// Determines whether the specified contains the entry - /// with the specified both and . - /// - /// - /// true if contains the entry - /// with both and ; - /// otherwise, false. - /// - /// - /// A to test. - /// - /// - /// A that represents the key of the entry to find. - /// - /// - /// A that represents the value of the entry to find. - /// - public static bool Contains(this QueryParamCollection collection, string name, string value) - { - if (collection == null || collection.Count == 0) - return false; - - var values = collection[name]; - if (values == null) - return false; - - foreach (var v in values.Split(',')) - if (v.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) - return true; - - return false; - } - - /// - /// Emits the specified EventHandler<TEventArgs> delegate - /// if it isn't . - /// - /// - /// An EventHandler<TEventArgs> to emit. - /// - /// - /// An from which emits this . - /// - /// - /// A TEventArgs that represents the event data. - /// - /// - /// The type of the event data generated by the event. - /// - public static void Emit( - this EventHandler eventHandler, object sender, TEventArgs e) - where TEventArgs : EventArgs - { - if (eventHandler != null) - eventHandler(sender, e); - } - - /// - /// Gets the description of the specified HTTP status . - /// - /// - /// A that represents the description of the HTTP status code. - /// - /// - /// One of enum values, indicates the HTTP status codes. - /// - public static string GetDescription(this HttpStatusCode code) - { - return ((int)code).GetStatusDescription(); - } - - /// - /// Gets the description of the specified HTTP status . - /// - /// - /// A that represents the description of the HTTP status code. - /// - /// - /// An that represents the HTTP status code. - /// - public static string GetStatusDescription(this int code) - { - switch (code) - { - case 100: return "Continue"; - case 101: return "Switching Protocols"; - case 102: return "Processing"; - case 200: return "OK"; - case 201: return "Created"; - case 202: return "Accepted"; - case 203: return "Non-Authoritative Information"; - case 204: return "No Content"; - case 205: return "Reset Content"; - case 206: return "Partial Content"; - case 207: return "Multi-Status"; - case 300: return "Multiple Choices"; - case 301: return "Moved Permanently"; - case 302: return "Found"; - case 303: return "See Other"; - case 304: return "Not Modified"; - case 305: return "Use Proxy"; - case 307: return "Temporary Redirect"; - case 400: return "Bad Request"; - case 401: return "Unauthorized"; - case 402: return "Payment Required"; - case 403: return "Forbidden"; - case 404: return "Not Found"; - case 405: return "Method Not Allowed"; - case 406: return "Not Acceptable"; - case 407: return "Proxy Authentication Required"; - case 408: return "Request Timeout"; - case 409: return "Conflict"; - case 410: return "Gone"; - case 411: return "Length Required"; - case 412: return "Precondition Failed"; - case 413: return "Request Entity Too Large"; - case 414: return "Request-Uri Too Long"; - case 415: return "Unsupported Media Type"; - case 416: return "Requested Range Not Satisfiable"; - case 417: return "Expectation Failed"; - case 422: return "Unprocessable Entity"; - case 423: return "Locked"; - case 424: return "Failed Dependency"; - case 500: return "Internal Server Error"; - case 501: return "Not Implemented"; - case 502: return "Bad Gateway"; - case 503: return "Service Unavailable"; - case 504: return "Gateway Timeout"; - case 505: return "Http Version Not Supported"; - case 507: return "Insufficient Storage"; - } - - return string.Empty; - } - - /// - /// Determines whether the specified is host - /// (this computer architecture) byte order. - /// - /// - /// true if is host byte order; - /// otherwise, false. - /// - /// - /// One of the enum values, to test. - /// - public static bool IsHostOrder(this ByteOrder order) - { - // true : !(true ^ true) or !(false ^ false) - // false: !(true ^ false) or !(false ^ true) - return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little)); - } - - /// - /// Determines whether the specified is a predefined scheme. - /// - /// - /// true if is a predefined scheme; otherwise, false. - /// - /// - /// A to test. - /// - public static bool IsPredefinedScheme(this string value) - { - if (value == null || value.Length < 2) - return false; - - var c = value[0]; - if (c == 'h') - return value == "http" || value == "https"; - - if (c == 'w') - return value == "ws" || value == "wss"; - - if (c == 'f') - return value == "file" || value == "ftp"; - - if (c == 'n') - { - c = value[1]; - return c == 'e' - ? value == "news" || value == "net.pipe" || value == "net.tcp" - : value == "nntp"; - } - - return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto"); - } - - /// - /// Determines whether the specified is a URI string. - /// - /// - /// true if may be a URI string; otherwise, false. - /// - /// - /// A to test. - /// - public static bool MaybeUri(this string value) - { - if (value == null || value.Length == 0) - return false; - - var i = value.IndexOf(':'); - if (i == -1) - return false; - - if (i >= 10) - return false; - - return value.Substring(0, i).IsPredefinedScheme(); - } - - /// - /// Retrieves a sub-array from the specified . - /// A sub-array starts at the specified element position. - /// - /// - /// An array of T that receives a sub-array, or an empty array of T if any problems - /// with the parameters. - /// - /// - /// An array of T that contains the data to retrieve a sub-array. - /// - /// - /// An that contains the zero-based starting position of a sub-array - /// in . - /// - /// - /// An that contains the number of elements to retrieve a sub-array. - /// - /// - /// The type of elements in the . - /// - public static T[] SubArray(this T[] array, int startIndex, int length) - { - if (array == null || array.Length == 0) - return new T[0]; - - if (startIndex < 0 || length <= 0) - return new T[0]; - - if (startIndex + length > array.Length) - return new T[0]; - - if (startIndex == 0 && array.Length == length) - return array; - - T[] subArray = new T[length]; - Array.Copy(array, startIndex, subArray, 0, length); - - return subArray; - } - - /// - /// Converts the order of the specified array of to the host byte order. - /// - /// - /// An array of converted from . - /// - /// - /// An array of to convert. - /// - /// - /// One of the enum values, indicates the byte order of - /// . - /// - /// - /// is . - /// - public static void ToHostOrder(this byte[] src, ByteOrder srcOrder) - { - if (src == null) - { - throw new ArgumentNullException(nameof(src)); - } - - if (src.Length > 1 && !srcOrder.IsHostOrder()) - { - Array.Reverse(src); - } - } - - /// - /// Converts the specified to a that - /// concatenates the each element of across the specified - /// . - /// - /// - /// A converted from , - /// or if is empty. - /// - /// - /// An array of T to convert. - /// - /// - /// A that represents the separator string. - /// - /// - /// The type of elements in . - /// - /// - /// is . - /// - public static string ToString(this T[] array, string separator) - { - if (array == null) - throw new ArgumentNullException(nameof(array)); - - var len = array.Length; - if (len == 0) - return string.Empty; - - if (separator == null) - separator = string.Empty; - - var buff = new StringBuilder(64); - (len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator)); - - buff.Append(array[len - 1].ToString()); - return buff.ToString(); - } - - /// - /// Executes the specified Action<int> delegate times. - /// - /// - /// An is the number of times to execute. - /// - /// - /// An Action<int> delegate that references the method(s) to execute. - /// An parameter to pass to the method(s) is the zero-based count of - /// iteration. - /// - public static void Times(this int n, Action action) - { - if (n > 0 && action != null) - for (int i = 0; i < n; i++) - action(i); - } - - /// - /// Converts the specified to a . - /// - /// - /// A converted from , or - /// if isn't successfully converted. - /// - /// - /// A to convert. - /// - public static Uri ToUri(this string uriString) - { - return Uri.TryCreate( - uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out var res) - ? res - : null; - } - - /// - /// URL-decodes the specified . - /// - /// - /// A that receives the decoded string, or the - /// if it's or empty. - /// - /// - /// A to decode. - /// - public static string UrlDecode(this string value) - { - return value == null || value.Length == 0 - ? value - : WebUtility.UrlDecode(value); - } - - #endregion - } -} diff --git a/SocketHttpListener/Fin.cs b/SocketHttpListener/Fin.cs deleted file mode 100644 index c8ffbf2b2f..0000000000 --- a/SocketHttpListener/Fin.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener -{ - internal enum Fin : byte - { - More = 0x0, - Final = 0x1 - } -} diff --git a/SocketHttpListener/HttpBase.cs b/SocketHttpListener/HttpBase.cs deleted file mode 100644 index c386b93744..0000000000 --- a/SocketHttpListener/HttpBase.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Text; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener -{ - internal abstract class HttpBase - { - #region Private Fields - - private QueryParamCollection _headers; - private Version _version; - - #endregion - - #region Protected Fields - - protected const string CrLf = "\r\n"; - - #endregion - - #region Protected Constructors - - protected HttpBase(Version version, QueryParamCollection headers) - { - _version = version; - _headers = headers; - } - - #endregion - - #region Public Properties - - public QueryParamCollection Headers => _headers; - - public Version ProtocolVersion => _version; - - #endregion - - #region Private Methods - - private static Encoding getEncoding(string contentType) - { - if (contentType == null || contentType.Length == 0) - return Encoding.UTF8; - - var i = contentType.IndexOf("charset=", StringComparison.Ordinal); - if (i == -1) - return Encoding.UTF8; - - var charset = contentType.Substring(i + 8); - i = charset.IndexOf(';'); - if (i != -1) - charset = charset.Substring(0, i).TrimEnd(); - - return Encoding.GetEncoding(charset.Trim('"')); - } - - #endregion - - #region Public Methods - - public byte[] ToByteArray() - { - return Encoding.UTF8.GetBytes(ToString()); - } - - #endregion - } -} diff --git a/SocketHttpListener/HttpResponse.cs b/SocketHttpListener/HttpResponse.cs deleted file mode 100644 index d5d9b4a1c8..0000000000 --- a/SocketHttpListener/HttpResponse.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Text; -using MediaBrowser.Model.Services; -using SocketHttpListener.Net; -using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; -using HttpVersion = SocketHttpListener.Net.HttpVersion; - -namespace SocketHttpListener -{ - internal class HttpResponse : HttpBase - { - #region Private Fields - - private string _code; - private string _reason; - - #endregion - - #region Private Constructors - - private HttpResponse(string code, string reason, Version version, QueryParamCollection headers) - : base(version, headers) - { - _code = code; - _reason = reason; - } - - #endregion - - #region Internal Constructors - - internal HttpResponse(HttpStatusCode code) - : this(code, code.GetDescription()) - { - } - - internal HttpResponse(HttpStatusCode code, string reason) - : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection()) - { - Headers["Server"] = "websocket-sharp/1.0"; - } - - #endregion - - #region Public Properties - - public CookieCollection Cookies => GetCookies(Headers, true); - - private static CookieCollection GetCookies(QueryParamCollection headers, bool response) - { - var name = response ? "Set-Cookie" : "Cookie"; - return headers == null || !headers.Contains(name) - ? new CookieCollection() - : CookieHelper.Parse(headers[name], response); - } - - public bool IsProxyAuthenticationRequired => _code == "407"; - - public bool IsUnauthorized => _code == "401"; - - public bool IsWebSocketResponse - { - get - { - var headers = Headers; - return ProtocolVersion > HttpVersion.Version10 && - _code == "101" && - headers.Contains("Upgrade", "websocket") && - headers.Contains("Connection", "Upgrade"); - } - } - - public string Reason => _reason; - - public string StatusCode => _code; - - #endregion - - #region Internal Methods - - internal static HttpResponse CreateCloseResponse(HttpStatusCode code) - { - var res = new HttpResponse(code); - res.Headers["Connection"] = "close"; - - return res; - } - - #endregion - - #region Public Methods - - public void SetCookies(CookieCollection cookies) - { - if (cookies == null || cookies.Count == 0) - return; - - var headers = Headers; - var sorted = cookies.OfType().OrderBy(i => i.Name).ToList(); - - foreach (var cookie in sorted) - headers.Add("Set-Cookie", cookie.ToString()); - } - - public override string ToString() - { - var output = new StringBuilder(64); - output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); - - var headers = Headers; - foreach (var key in headers.Keys) - output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf); - - output.Append(CrLf); - - return output.ToString(); - } - - #endregion - } -} diff --git a/SocketHttpListener/Mask.cs b/SocketHttpListener/Mask.cs deleted file mode 100644 index c56f5b08c0..0000000000 --- a/SocketHttpListener/Mask.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener -{ - internal enum Mask : byte - { - Unmask = 0x0, - Mask = 0x1 - } -} diff --git a/SocketHttpListener/MessageEventArgs.cs b/SocketHttpListener/MessageEventArgs.cs deleted file mode 100644 index 8e2151cb7a..0000000000 --- a/SocketHttpListener/MessageEventArgs.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Text; - -namespace SocketHttpListener -{ - /// - /// Contains the event data associated with a event. - /// - /// - /// A event occurs when the receives - /// a text or binary data frame. - /// If you want to get the received data, you access the or - /// property. - /// - public class MessageEventArgs : EventArgs - { - #region Private Fields - - private string _data; - private Opcode _opcode; - private byte[] _rawData; - - #endregion - - #region Internal Constructors - - internal MessageEventArgs(Opcode opcode, byte[] data) - { - _opcode = opcode; - _rawData = data; - _data = convertToString(opcode, data); - } - - internal MessageEventArgs(Opcode opcode, PayloadData payload) - { - _opcode = opcode; - _rawData = payload.ApplicationData; - _data = convertToString(opcode, _rawData); - } - - #endregion - - #region Public Properties - - /// - /// Gets the received data as a . - /// - /// - /// A that contains the received data. - /// - public string Data => _data; - - /// - /// Gets the received data as an array of . - /// - /// - /// An array of that contains the received data. - /// - public byte[] RawData => _rawData; - - /// - /// Gets the type of the received data. - /// - /// - /// One of the values, indicates the type of the received data. - /// - public Opcode Type => _opcode; - - #endregion - - #region Private Methods - - private static string convertToString(Opcode opcode, byte[] data) - { - return data.Length == 0 - ? string.Empty - : opcode == Opcode.Text - ? Encoding.UTF8.GetString(data, 0, data.Length) - : opcode.ToString(); - } - - #endregion - } -} diff --git a/SocketHttpListener/Net/AuthenticationSchemeSelector.cs b/SocketHttpListener/Net/AuthenticationSchemeSelector.cs deleted file mode 100644 index 77e6d0b8ab..0000000000 --- a/SocketHttpListener/Net/AuthenticationSchemeSelector.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Net; - -namespace SocketHttpListener.Net -{ - public delegate AuthenticationSchemes AuthenticationSchemeSelector(HttpListenerRequest httpRequest); -} diff --git a/SocketHttpListener/Net/AuthenticationTypes.cs b/SocketHttpListener/Net/AuthenticationTypes.cs deleted file mode 100644 index a3dbe9dfff..0000000000 --- a/SocketHttpListener/Net/AuthenticationTypes.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal class AuthenticationTypes - { - internal const string NTLM = "NTLM"; - internal const string Negotiate = "Negotiate"; - internal const string Basic = "Basic"; - } -} diff --git a/SocketHttpListener/Net/BoundaryType.cs b/SocketHttpListener/Net/BoundaryType.cs deleted file mode 100644 index da0b22910e..0000000000 --- a/SocketHttpListener/Net/BoundaryType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal enum BoundaryType - { - ContentLength = 0, // Content-Length: XXX - Chunked = 1, // Transfer-Encoding: chunked - Multipart = 3, - None = 4, - Invalid = 5, - } -} diff --git a/SocketHttpListener/Net/ChunkStream.cs b/SocketHttpListener/Net/ChunkStream.cs deleted file mode 100644 index 3836947d4c..0000000000 --- a/SocketHttpListener/Net/ChunkStream.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal sealed class ChunkStream - { - private enum State - { - None, - PartialSize, - Body, - BodyFinished, - Trailer - } - - private class Chunk - { - public byte[] Bytes; - public int Offset; - - public Chunk(byte[] chunk) - { - Bytes = chunk; - } - - public int Read(byte[] buffer, int offset, int size) - { - int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size; - Buffer.BlockCopy(Bytes, Offset, buffer, offset, nread); - Offset += nread; - return nread; - } - } - - internal WebHeaderCollection _headers; - private int _chunkSize; - private int _chunkRead; - private int _totalWritten; - private State _state; - private StringBuilder _saved; - private bool _sawCR; - private bool _gotit; - private int _trailerState; - private List _chunks; - - public ChunkStream(WebHeaderCollection headers) - { - _headers = headers; - _saved = new StringBuilder(); - _chunks = new List(); - _chunkSize = -1; - _totalWritten = 0; - } - - public void ResetBuffer() - { - _chunkSize = -1; - _chunkRead = 0; - _totalWritten = 0; - _chunks.Clear(); - } - - public int Read(byte[] buffer, int offset, int size) - { - return ReadFromChunks(buffer, offset, size); - } - - private int ReadFromChunks(byte[] buffer, int offset, int size) - { - int count = _chunks.Count; - int nread = 0; - - var chunksForRemoving = new List(count); - for (int i = 0; i < count; i++) - { - var chunk = _chunks[i]; - - if (chunk.Offset == chunk.Bytes.Length) - { - chunksForRemoving.Add(chunk); - continue; - } - - nread += chunk.Read(buffer, offset + nread, size - nread); - if (nread == size) - break; - } - - foreach (var chunk in chunksForRemoving) - _chunks.Remove(chunk); - - return nread; - } - - public void Write(byte[] buffer, int offset, int size) - { - // Note, the logic here only works when offset is 0 here. - // Otherwise, it would treat "size" as the end offset instead of an actual byte count from offset. - - if (offset < size) - InternalWrite(buffer, ref offset, size); - } - - private void InternalWrite(byte[] buffer, ref int offset, int size) - { - if (_state == State.None || _state == State.PartialSize) - { - _state = GetChunkSize(buffer, ref offset, size); - if (_state == State.PartialSize) - return; - - _saved.Length = 0; - _sawCR = false; - _gotit = false; - } - - if (_state == State.Body && offset < size) - { - _state = ReadBody(buffer, ref offset, size); - if (_state == State.Body) - return; - } - - if (_state == State.BodyFinished && offset < size) - { - _state = ReadCRLF(buffer, ref offset, size); - if (_state == State.BodyFinished) - return; - - _sawCR = false; - } - - if (_state == State.Trailer && offset < size) - { - _state = ReadTrailer(buffer, ref offset, size); - if (_state == State.Trailer) - return; - - _saved.Length = 0; - _sawCR = false; - _gotit = false; - } - - if (offset < size) - InternalWrite(buffer, ref offset, size); - } - - public bool WantMore => (_chunkRead != _chunkSize || _chunkSize != 0 || _state != State.None); - - public bool DataAvailable - { - get - { - int count = _chunks.Count; - for (int i = 0; i < count; i++) - { - var ch = _chunks[i]; - if (ch == null || ch.Bytes == null) - continue; - if (ch.Bytes.Length > 0 && ch.Offset < ch.Bytes.Length) - return (_state != State.Body); - } - return false; - } - } - - public int TotalDataSize => _totalWritten; - - public int ChunkLeft => _chunkSize - _chunkRead; - - private State ReadBody(byte[] buffer, ref int offset, int size) - { - if (_chunkSize == 0) - return State.BodyFinished; - - int diff = size - offset; - if (diff + _chunkRead > _chunkSize) - diff = _chunkSize - _chunkRead; - - byte[] chunk = new byte[diff]; - Buffer.BlockCopy(buffer, offset, chunk, 0, diff); - _chunks.Add(new Chunk(chunk)); - offset += diff; - _chunkRead += diff; - _totalWritten += diff; - - return (_chunkRead == _chunkSize) ? State.BodyFinished : State.Body; - } - - private State GetChunkSize(byte[] buffer, ref int offset, int size) - { - _chunkRead = 0; - _chunkSize = 0; - char c = '\0'; - while (offset < size) - { - c = (char)buffer[offset++]; - if (c == '\r') - { - if (_sawCR) - ThrowProtocolViolation("2 CR found"); - - _sawCR = true; - continue; - } - - if (_sawCR && c == '\n') - break; - - if (c == ' ') - _gotit = true; - - if (!_gotit) - _saved.Append(c); - - if (_saved.Length > 20) - ThrowProtocolViolation("chunk size too long."); - } - - if (!_sawCR || c != '\n') - { - if (offset < size) - ThrowProtocolViolation("Missing \\n"); - - try - { - if (_saved.Length > 0) - { - _chunkSize = int.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber); - } - } - catch (Exception) - { - ThrowProtocolViolation("Cannot parse chunk size."); - } - - return State.PartialSize; - } - - _chunkRead = 0; - try - { - _chunkSize = int.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber); - } - catch (Exception) - { - ThrowProtocolViolation("Cannot parse chunk size."); - } - - if (_chunkSize == 0) - { - _trailerState = 2; - return State.Trailer; - } - - return State.Body; - } - - private static string RemoveChunkExtension(string input) - { - int idx = input.IndexOf(';'); - if (idx == -1) - return input; - return input.Substring(0, idx); - } - - private State ReadCRLF(byte[] buffer, ref int offset, int size) - { - if (!_sawCR) - { - if ((char)buffer[offset++] != '\r') - ThrowProtocolViolation("Expecting \\r"); - - _sawCR = true; - if (offset == size) - return State.BodyFinished; - } - - if (_sawCR && (char)buffer[offset++] != '\n') - ThrowProtocolViolation("Expecting \\n"); - - return State.None; - } - - private State ReadTrailer(byte[] buffer, ref int offset, int size) - { - char c = '\0'; - - // short path - if (_trailerState == 2 && (char)buffer[offset] == '\r' && _saved.Length == 0) - { - offset++; - if (offset < size && (char)buffer[offset] == '\n') - { - offset++; - return State.None; - } - offset--; - } - - int st = _trailerState; - string stString = "\r\n\r"; - while (offset < size && st < 4) - { - c = (char)buffer[offset++]; - if ((st == 0 || st == 2) && c == '\r') - { - st++; - continue; - } - - if ((st == 1 || st == 3) && c == '\n') - { - st++; - continue; - } - - if (st > 0) - { - _saved.Append(stString.Substring(0, _saved.Length == 0 ? st - 2 : st)); - st = 0; - if (_saved.Length > 4196) - ThrowProtocolViolation("Error reading trailer (too long)."); - } - } - - if (st < 4) - { - _trailerState = st; - if (offset < size) - ThrowProtocolViolation("Error reading trailer."); - - return State.Trailer; - } - - var reader = new StringReader(_saved.ToString()); - string line; - while ((line = reader.ReadLine()) != null && line != "") - _headers.Add(line); - - return State.None; - } - - private static void ThrowProtocolViolation(string message) - { - var we = new WebException(message, null, WebExceptionStatus.ServerProtocolViolation, null); - throw we; - } - } -} diff --git a/SocketHttpListener/Net/ChunkedInputStream.cs b/SocketHttpListener/Net/ChunkedInputStream.cs deleted file mode 100644 index 7aa8529e38..0000000000 --- a/SocketHttpListener/Net/ChunkedInputStream.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.IO; -using System.Net; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal sealed class ChunkedInputStream : HttpRequestStream - { - private ChunkStream _decoder; - private readonly HttpListenerContext _context; - private bool _no_more_data; - - private class ReadBufferState - { - public byte[] Buffer; - public int Offset; - public int Count; - public int InitialCount; - public HttpStreamAsyncResult Ares; - public ReadBufferState(byte[] buffer, int offset, int count, HttpStreamAsyncResult ares) - { - Buffer = buffer; - Offset = offset; - Count = count; - InitialCount = count; - Ares = ares; - } - } - - public ChunkedInputStream(HttpListenerContext context, Stream stream, byte[] buffer, int offset, int length) - : base(stream, buffer, offset, length) - { - _context = context; - var coll = (WebHeaderCollection)context.Request.Headers; - _decoder = new ChunkStream(coll); - } - - public ChunkStream Decoder - { - get => _decoder; - set => _decoder = value; - } - - protected override int ReadCore(byte[] buffer, int offset, int count) - { - IAsyncResult ares = BeginReadCore(buffer, offset, count, null, null); - return EndRead(ares); - } - - protected override IAsyncResult BeginReadCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state) - { - var ares = new HttpStreamAsyncResult(this); - ares._callback = cback; - ares._state = state; - if (_no_more_data || size == 0 || _closed) - { - ares.Complete(); - return ares; - } - int nread = _decoder.Read(buffer, offset, size); - offset += nread; - size -= nread; - if (size == 0) - { - // got all we wanted, no need to bother the decoder yet - ares._count = nread; - ares.Complete(); - return ares; - } - if (!_decoder.WantMore) - { - _no_more_data = nread == 0; - ares._count = nread; - ares.Complete(); - return ares; - } - ares._buffer = new byte[8192]; - ares._offset = 0; - ares._count = 8192; - var rb = new ReadBufferState(buffer, offset, size, ares); - rb.InitialCount += nread; - base.BeginReadCore(ares._buffer, ares._offset, ares._count, OnRead, rb); - return ares; - } - - private void OnRead(IAsyncResult base_ares) - { - ReadBufferState rb = (ReadBufferState)base_ares.AsyncState; - var ares = rb.Ares; - try - { - int nread = base.EndRead(base_ares); - if (nread == 0) - { - _no_more_data = true; - ares._count = rb.InitialCount - rb.Count; - ares.Complete(); - return; - } - - _decoder.Write(ares._buffer, ares._offset, nread); - nread = _decoder.Read(rb.Buffer, rb.Offset, rb.Count); - rb.Offset += nread; - rb.Count -= nread; - if (rb.Count == 0 || !_decoder.WantMore) - { - _no_more_data = !_decoder.WantMore && nread == 0; - ares._count = rb.InitialCount - rb.Count; - ares.Complete(); - return; - } - ares._offset = 0; - ares._count = Math.Min(8192, _decoder.ChunkLeft + 6); - base.BeginReadCore(ares._buffer, ares._offset, ares._count, OnRead, rb); - } - catch (Exception e) - { - _context.Connection.SendError(e.Message, 400); - ares.Complete(e); - } - } - - public override int EndRead(IAsyncResult asyncResult) - { - if (asyncResult == null) - throw new ArgumentNullException(nameof(asyncResult)); - - var ares = asyncResult as HttpStreamAsyncResult; - if (ares == null || !ReferenceEquals(this, ares._parent)) - { - throw new ArgumentException("Invalid async result"); - } - if (ares._endCalled) - { - throw new InvalidOperationException("Invalid end call"); - } - ares._endCalled = true; - - if (!asyncResult.IsCompleted) - asyncResult.AsyncWaitHandle.WaitOne(); - - if (ares._error != null) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "Operation aborted"); - - return ares._count; - } - } -} diff --git a/SocketHttpListener/Net/CookieHelper.cs b/SocketHttpListener/Net/CookieHelper.cs deleted file mode 100644 index 3ad76ff238..0000000000 --- a/SocketHttpListener/Net/CookieHelper.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Text; - -namespace SocketHttpListener.Net -{ - public static class CookieHelper - { - internal static CookieCollection Parse(string value, bool response) - { - return response - ? parseResponse(value) - : null; - } - - private static string[] splitCookieHeaderValue(string value) - { - return new List(value.SplitHeaderValue(',', ';')).ToArray(); - } - - private static CookieCollection parseResponse(string value) - { - var cookies = new CookieCollection(); - - Cookie cookie = null; - var pairs = splitCookieHeaderValue(value); - for (int i = 0; i < pairs.Length; i++) - { - var pair = pairs[i].Trim(); - if (pair.Length == 0) - continue; - - if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Version = int.Parse(pair.GetValueInternal("=").Trim('"')); - } - else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase)) - { - var buffer = new StringBuilder(pair.GetValueInternal("="), 32); - if (i < pairs.Length - 1) - buffer.AppendFormat(", {0}", pairs[++i].Trim()); - - if (!DateTime.TryParseExact( - buffer.ToString(), - new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, - new CultureInfo("en-US"), - DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, - out var expires)) - expires = DateTime.Now; - - if (cookie != null && cookie.Expires == DateTime.MinValue) - cookie.Expires = expires.ToLocalTime(); - } - else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase)) - { - var max = int.Parse(pair.GetValueInternal("=").Trim('"')); - var expires = DateTime.Now.AddSeconds((double)max); - if (cookie != null) - cookie.Expires = expires; - } - else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Path = pair.GetValueInternal("="); - } - else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Domain = pair.GetValueInternal("="); - } - else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase)) - { - var port = pair.Equals("port", StringComparison.OrdinalIgnoreCase) - ? "\"\"" - : pair.GetValueInternal("="); - - if (cookie != null) - cookie.Port = port; - } - else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Comment = pair.GetValueInternal("=").UrlDecode(); - } - else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.CommentUri = pair.GetValueInternal("=").Trim('"').ToUri(); - } - else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Discard = true; - } - else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.Secure = true; - } - else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase)) - { - if (cookie != null) - cookie.HttpOnly = true; - } - else - { - if (cookie != null) - cookies.Add(cookie); - - string name; - string val = string.Empty; - - var pos = pair.IndexOf('='); - if (pos == -1) - { - name = pair; - } - else if (pos == pair.Length - 1) - { - name = pair.Substring(0, pos).TrimEnd(' '); - } - else - { - name = pair.Substring(0, pos).TrimEnd(' '); - val = pair.Substring(pos + 1).TrimStart(' '); - } - - cookie = new Cookie(name, val); - } - } - - if (cookie != null) - cookies.Add(cookie); - - return cookies; - } - } -} diff --git a/SocketHttpListener/Net/EntitySendFormat.cs b/SocketHttpListener/Net/EntitySendFormat.cs deleted file mode 100644 index 3ed981084d..0000000000 --- a/SocketHttpListener/Net/EntitySendFormat.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal enum EntitySendFormat - { - ContentLength = 0, // Content-Length: XXX - Chunked = 1, // Transfer-Encoding: chunked - } -} diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs deleted file mode 100644 index e89f4ed9b1..0000000000 --- a/SocketHttpListener/Net/HttpConnection.cs +++ /dev/null @@ -1,528 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; -namespace SocketHttpListener.Net -{ - sealed class HttpConnection - { - private static AsyncCallback s_onreadCallback = new AsyncCallback(OnRead); - const int BufferSize = 8192; - Socket _socket; - Stream _stream; - HttpEndPointListener _epl; - MemoryStream _memoryStream; - byte[] _buffer; - HttpListenerContext _context; - StringBuilder _currentLine; - ListenerPrefix _prefix; - HttpRequestStream _requestStream; - HttpResponseStream _responseStream; - bool _chunked; - int _reuses; - bool _contextBound; - bool secure; - IPEndPoint local_ep; - HttpListener _lastListener; - X509Certificate cert; - SslStream ssl_stream; - - private readonly ILogger _logger; - private readonly ICryptoProvider _cryptoProvider; - private readonly IStreamHelper _streamHelper; - private readonly IFileSystem _fileSystem; - private readonly IEnvironmentInfo _environment; - - public HttpConnection(ILogger logger, Socket socket, HttpEndPointListener epl, bool secure, - X509Certificate cert, ICryptoProvider cryptoProvider, IStreamHelper streamHelper, IFileSystem fileSystem, - IEnvironmentInfo environment) - { - _logger = logger; - this._socket = socket; - this._epl = epl; - this.secure = secure; - this.cert = cert; - _cryptoProvider = cryptoProvider; - _streamHelper = streamHelper; - _fileSystem = fileSystem; - _environment = environment; - - if (secure == false) - { - _stream = new SocketStream(_socket, false); - } - else - { - ssl_stream = new SslStream(new SocketStream(_socket, false), false, (t, c, ch, e) => - { - if (c == null) - { - return true; - } - - //var c2 = c as X509Certificate2; - //if (c2 == null) - //{ - // c2 = new X509Certificate2(c.GetRawCertData()); - //} - - //_clientCert = c2; - //_clientCertErrors = new int[] { (int)e }; - return true; - }); - - _stream = ssl_stream; - } - } - - public Stream Stream => _stream; - - public async Task Init() - { - if (ssl_stream != null) - { - var enableAsync = true; - if (enableAsync) - { - await ssl_stream.AuthenticateAsServerAsync(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false).ConfigureAwait(false); - } - else - { - ssl_stream.AuthenticateAsServer(cert, false, (SslProtocols)ServicePointManager.SecurityProtocol, false); - } - } - - InitInternal(); - } - - private void InitInternal() - { - _contextBound = false; - _requestStream = null; - _responseStream = null; - _prefix = null; - _chunked = false; - _memoryStream = new MemoryStream(); - _position = 0; - _inputState = InputState.RequestLine; - _lineState = LineState.None; - _context = new HttpListenerContext(this); - } - - public bool IsClosed => (_socket == null); - - public int Reuses => _reuses; - - public IPEndPoint LocalEndPoint - { - get - { - if (local_ep != null) - return local_ep; - - local_ep = (IPEndPoint)_socket.LocalEndPoint; - return local_ep; - } - } - - public IPEndPoint RemoteEndPoint => _socket.RemoteEndPoint as IPEndPoint; - - public bool IsSecure => secure; - - public ListenerPrefix Prefix - { - get => _prefix; - set => _prefix = value; - } - - private void OnTimeout(object unused) - { - //_logger.LogInformation("HttpConnection timer fired"); - CloseSocket(); - Unbind(); - } - - public void BeginReadRequest() - { - if (_buffer == null) - _buffer = new byte[BufferSize]; - try - { - _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); - } - catch - { - CloseSocket(); - Unbind(); - } - } - - public HttpRequestStream GetRequestStream(bool chunked, long contentlength) - { - if (_requestStream == null) - { - byte[] buffer = _memoryStream.GetBuffer(); - int length = (int)_memoryStream.Length; - _memoryStream = null; - if (chunked) - { - _chunked = true; - //_context.Response.SendChunked = true; - _requestStream = new ChunkedInputStream(_context, _stream, buffer, _position, length - _position); - } - else - { - _requestStream = new HttpRequestStream(_stream, buffer, _position, length - _position, contentlength); - } - } - return _requestStream; - } - - public HttpResponseStream GetResponseStream(bool isExpect100Continue = false) - { - // TODO: can we get this _stream before reading the input? - if (_responseStream == null) - { - var supportsDirectSocketAccess = !_context.Response.SendChunked && !isExpect100Continue && !secure; - - _responseStream = new HttpResponseStream(_stream, _context.Response, false, _streamHelper, _socket, supportsDirectSocketAccess, _environment, _fileSystem, _logger); - } - return _responseStream; - } - - private static void OnRead(IAsyncResult ares) - { - var cnc = (HttpConnection)ares.AsyncState; - cnc.OnReadInternal(ares); - } - - private void OnReadInternal(IAsyncResult ares) - { - int nread = -1; - try - { - nread = _stream.EndRead(ares); - _memoryStream.Write(_buffer, 0, nread); - if (_memoryStream.Length > 32768) - { - SendError("Bad Request", 400); - Close(true); - return; - } - } - catch - { - if (_memoryStream != null && _memoryStream.Length > 0) - SendError(); - if (_socket != null) - { - CloseSocket(); - Unbind(); - } - return; - } - - if (nread == 0) - { - CloseSocket(); - Unbind(); - return; - } - - if (ProcessInput(_memoryStream)) - { - if (!_context.HaveError) - _context.Request.FinishInitialization(); - - if (_context.HaveError) - { - SendError(); - Close(true); - return; - } - - if (!_epl.BindContext(_context)) - { - const int NotFoundErrorCode = 404; - SendError(HttpStatusDescription.Get(NotFoundErrorCode), NotFoundErrorCode); - Close(true); - return; - } - HttpListener listener = _epl.Listener; - if (_lastListener != listener) - { - RemoveConnection(); - listener.AddConnection(this); - _lastListener = listener; - } - - _contextBound = true; - listener.RegisterContext(_context); - return; - } - _stream.BeginRead(_buffer, 0, BufferSize, s_onreadCallback, this); - } - - private void RemoveConnection() - { - if (_lastListener == null) - _epl.RemoveConnection(this); - else - _lastListener.RemoveConnection(this); - } - - private enum InputState - { - RequestLine, - Headers - } - - private enum LineState - { - None, - CR, - LF - } - - InputState _inputState = InputState.RequestLine; - LineState _lineState = LineState.None; - int _position; - - // true -> done processing - // false -> need more input - private bool ProcessInput(MemoryStream ms) - { - byte[] buffer = ms.GetBuffer(); - int len = (int)ms.Length; - int used = 0; - string line; - - while (true) - { - if (_context.HaveError) - return true; - - if (_position >= len) - break; - - try - { - line = ReadLine(buffer, _position, len - _position, ref used); - _position += used; - } - catch - { - _context.ErrorMessage = "Bad request"; - _context.ErrorStatus = 400; - return true; - } - - if (line == null) - break; - - if (line == "") - { - if (_inputState == InputState.RequestLine) - continue; - _currentLine = null; - ms = null; - return true; - } - - if (_inputState == InputState.RequestLine) - { - _context.Request.SetRequestLine(line); - _inputState = InputState.Headers; - } - else - { - try - { - _context.Request.AddHeader(line); - } - catch (Exception e) - { - _context.ErrorMessage = e.Message; - _context.ErrorStatus = 400; - return true; - } - } - } - - if (used == len) - { - ms.SetLength(0); - _position = 0; - } - return false; - } - - private string ReadLine(byte[] buffer, int offset, int len, ref int used) - { - if (_currentLine == null) - _currentLine = new StringBuilder(128); - int last = offset + len; - used = 0; - for (int i = offset; i < last && _lineState != LineState.LF; i++) - { - used++; - byte b = buffer[i]; - if (b == 13) - { - _lineState = LineState.CR; - } - else if (b == 10) - { - _lineState = LineState.LF; - } - else - { - _currentLine.Append((char)b); - } - } - - string result = null; - if (_lineState == LineState.LF) - { - _lineState = LineState.None; - result = _currentLine.ToString(); - _currentLine.Length = 0; - } - - return result; - } - - public void SendError(string msg, int status) - { - try - { - HttpListenerResponse response = _context.Response; - response.StatusCode = status; - response.ContentType = "text/html"; - string description = HttpStatusDescription.Get(status); - string str; - if (msg != null) - str = string.Format("

{0} ({1})

", description, msg); - else - str = string.Format("

{0}

", description); - - byte[] error = Encoding.UTF8.GetBytes(str); - response.Close(error, false); - } - catch - { - // response was already closed - } - } - - public void SendError() - { - SendError(_context.ErrorMessage, _context.ErrorStatus); - } - - private void Unbind() - { - if (_contextBound) - { - _epl.UnbindContext(_context); - _contextBound = false; - } - } - - public void Close() - { - Close(false); - } - - private void CloseSocket() - { - if (_socket == null) - return; - - try - { - _socket.Close(); - } - catch { } - finally - { - _socket = null; - } - - RemoveConnection(); - } - - internal void Close(bool force) - { - if (_socket != null) - { - Stream st = GetResponseStream(); - if (st != null) - st.Close(); - - _responseStream = null; - } - - if (_socket != null) - { - force |= !_context.Request.KeepAlive; - if (!force) - force = (string.Equals(_context.Response.Headers["connection"], "close", StringComparison.OrdinalIgnoreCase)); - - if (!force && _context.Request.FlushInput()) - { - if (_chunked && _context.Response.ForceCloseChunked == false) - { - // Don't close. Keep working. - _reuses++; - Unbind(); - InitInternal(); - BeginReadRequest(); - return; - } - - _reuses++; - Unbind(); - InitInternal(); - BeginReadRequest(); - return; - } - - Socket s = _socket; - _socket = null; - try - { - if (s != null) - s.Shutdown(SocketShutdown.Both); - } - catch - { - } - finally - { - if (s != null) - { - try - { - s.Close(); - } - catch { } - } - } - Unbind(); - RemoveConnection(); - return; - } - } - } -} diff --git a/SocketHttpListener/Net/HttpEndPointListener.cs b/SocketHttpListener/Net/HttpEndPointListener.cs deleted file mode 100644 index 35d1af6486..0000000000 --- a/SocketHttpListener/Net/HttpEndPointListener.cs +++ /dev/null @@ -1,526 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - internal sealed class HttpEndPointListener - { - private HttpListener _listener; - private IPEndPoint _endpoint; - private Socket _socket; - private Dictionary _prefixes; - private List _unhandledPrefixes; // host = '*' - private List _allPrefixes; // host = '+' - private X509Certificate _cert; - private bool _secure; - private Dictionary _unregisteredConnections; - - private readonly ILogger _logger; - private bool _closed; - private bool _enableDualMode; - private readonly ICryptoProvider _cryptoProvider; - private readonly ISocketFactory _socketFactory; - private readonly IStreamHelper _streamHelper; - private readonly IFileSystem _fileSystem; - private readonly IEnvironmentInfo _environment; - - public HttpEndPointListener(HttpListener listener, IPAddress addr, int port, bool secure, X509Certificate cert, - ILogger logger, ICryptoProvider cryptoProvider, ISocketFactory socketFactory, IStreamHelper streamHelper, - IFileSystem fileSystem, IEnvironmentInfo environment) - { - this._listener = listener; - _logger = logger; - _cryptoProvider = cryptoProvider; - _socketFactory = socketFactory; - _streamHelper = streamHelper; - _fileSystem = fileSystem; - _environment = environment; - - this._secure = secure; - this._cert = cert; - - _enableDualMode = addr.Equals(IPAddress.IPv6Any); - _endpoint = new IPEndPoint(addr, port); - - _prefixes = new Dictionary(); - _unregisteredConnections = new Dictionary(); - - CreateSocket(); - } - - internal HttpListener Listener => _listener; - - private void CreateSocket() - { - try - { - _socket = CreateSocket(_endpoint.Address.AddressFamily, _enableDualMode); - } - catch (SocketCreateException ex) - { - if (_enableDualMode && _endpoint.Address.Equals(IPAddress.IPv6Any) && - (string.Equals(ex.ErrorCode, "AddressFamilyNotSupported", StringComparison.OrdinalIgnoreCase) || - // mono 4.8.1 and lower on bsd is throwing this - string.Equals(ex.ErrorCode, "ProtocolNotSupported", StringComparison.OrdinalIgnoreCase) || - // mono 5.2 on bsd is throwing this - string.Equals(ex.ErrorCode, "OperationNotSupported", StringComparison.OrdinalIgnoreCase))) - { - _endpoint = new IPEndPoint(IPAddress.Any, _endpoint.Port); - _enableDualMode = false; - _socket = CreateSocket(_endpoint.Address.AddressFamily, _enableDualMode); - } - else - { - throw; - } - } - - try - { - _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - catch (SocketException) - { - // This is not supported on all operating systems (qnap) - } - - _socket.Bind(_endpoint); - - // This is the number TcpListener uses. - _socket.Listen(2147483647); - - Accept(); - - _closed = false; - } - - private void Accept() - { - var acceptEventArg = new SocketAsyncEventArgs(); - acceptEventArg.UserToken = this; - acceptEventArg.Completed += new EventHandler(OnAccept); - - Accept(acceptEventArg); - } - - private static void TryCloseAndDispose(Socket socket) - { - try - { - using (socket) - { - socket.Close(); - } - } - catch - { - - } - } - - private static void TryClose(Socket socket) - { - try - { - socket.Close(); - } - catch - { - - } - } - - private void Accept(SocketAsyncEventArgs acceptEventArg) - { - // acceptSocket must be cleared since the context object is being reused - acceptEventArg.AcceptSocket = null; - - try - { - bool willRaiseEvent = _socket.AcceptAsync(acceptEventArg); - - if (!willRaiseEvent) - { - ProcessAccept(acceptEventArg); - } - } - catch (ObjectDisposedException) - { - // TODO Investigate or properly fix. - } - catch (Exception ex) - { - var epl = (HttpEndPointListener)acceptEventArg.UserToken; - - epl._logger.LogError(ex, "Error in socket.AcceptAsync"); - } - } - - // This method is the callback method associated with Socket.AcceptAsync - // operations and is invoked when an accept operation is complete - // - private static void OnAccept(object sender, SocketAsyncEventArgs e) - { - ProcessAccept(e); - } - - private static async void ProcessAccept(SocketAsyncEventArgs args) - { - var epl = (HttpEndPointListener)args.UserToken; - - if (epl._closed) - { - return; - } - - // http://msdn.microsoft.com/en-us/library/system.net.sockets.acceptSocket.acceptasync%28v=vs.110%29.aspx - // Under certain conditions ConnectionReset can occur - // Need to attept to re-accept - var socketError = args.SocketError; - var accepted = args.AcceptSocket; - - epl.Accept(args); - - if (socketError == SocketError.ConnectionReset) - { - epl._logger.LogError("SocketError.ConnectionReset reported. Attempting to re-accept."); - return; - } - - if (accepted == null) - { - return; - } - - if (epl._secure && epl._cert == null) - { - TryClose(accepted); - return; - } - - try - { - var remoteEndPointString = accepted.RemoteEndPoint == null ? string.Empty : accepted.RemoteEndPoint.ToString(); - var localEndPointString = accepted.LocalEndPoint == null ? string.Empty : accepted.LocalEndPoint.ToString(); - //_logger.LogInformation("HttpEndPointListener Accepting connection from {0} to {1} secure connection requested: {2}", remoteEndPointString, localEndPointString, _secure); - - var conn = new HttpConnection(epl._logger, accepted, epl, epl._secure, epl._cert, epl._cryptoProvider, epl._streamHelper, epl._fileSystem, epl._environment); - - await conn.Init().ConfigureAwait(false); - - //_logger.LogDebug("Adding unregistered connection to {0}. Id: {1}", accepted.RemoteEndPoint, connectionId); - lock (epl._unregisteredConnections) - { - epl._unregisteredConnections[conn] = conn; - } - conn.BeginReadRequest(); - } - catch (Exception ex) - { - epl._logger.LogError(ex, "Error in ProcessAccept"); - - TryClose(accepted); - epl.Accept(); - return; - } - } - - private Socket CreateSocket(AddressFamily addressFamily, bool dualMode) - { - try - { - var socket = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - - if (dualMode) - { - socket.DualMode = true; - } - - return socket; - } - catch (SocketException ex) - { - throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex); - } - catch (ArgumentException ex) - { - if (dualMode) - { - // Mono for BSD incorrectly throws ArgumentException instead of SocketException - throw new SocketCreateException("AddressFamilyNotSupported", ex); - } - else - { - throw; - } - } - } - - internal void RemoveConnection(HttpConnection conn) - { - lock (_unregisteredConnections) - { - _unregisteredConnections.Remove(conn); - } - } - - public bool BindContext(HttpListenerContext context) - { - var req = context.Request; - HttpListener listener = SearchListener(req.Url, out var prefix); - if (listener == null) - return false; - - context.Connection.Prefix = prefix; - return true; - } - - public void UnbindContext(HttpListenerContext context) - { - if (context == null || context.Request == null) - return; - - _listener.UnregisterContext(context); - } - - private HttpListener SearchListener(Uri uri, out ListenerPrefix prefix) - { - prefix = null; - if (uri == null) - return null; - - string host = uri.Host; - int port = uri.Port; - string path = WebUtility.UrlDecode(uri.AbsolutePath); - string pathSlash = path[path.Length - 1] == '/' ? path : path + "/"; - - HttpListener bestMatch = null; - int bestLength = -1; - - if (host != null && host != "") - { - Dictionary localPrefixes = _prefixes; - foreach (var p in localPrefixes.Keys) - { - string ppath = p.Path; - if (ppath.Length < bestLength) - continue; - - if (p.Host != host || p.Port != port) - continue; - - if (path.StartsWith(ppath) || pathSlash.StartsWith(ppath)) - { - bestLength = ppath.Length; - bestMatch = localPrefixes[p]; - prefix = p; - } - } - if (bestLength != -1) - return bestMatch; - } - - List list = _unhandledPrefixes; - bestMatch = MatchFromList(host, path, list, out prefix); - - if (path != pathSlash && bestMatch == null) - bestMatch = MatchFromList(host, pathSlash, list, out prefix); - - if (bestMatch != null) - return bestMatch; - - list = _allPrefixes; - bestMatch = MatchFromList(host, path, list, out prefix); - - if (path != pathSlash && bestMatch == null) - bestMatch = MatchFromList(host, pathSlash, list, out prefix); - - if (bestMatch != null) - return bestMatch; - - return null; - } - - private HttpListener MatchFromList(string host, string path, List list, out ListenerPrefix prefix) - { - prefix = null; - if (list == null) - return null; - - HttpListener bestMatch = null; - int bestLength = -1; - - foreach (ListenerPrefix p in list) - { - string ppath = p.Path; - if (ppath.Length < bestLength) - continue; - - if (path.StartsWith(ppath)) - { - bestLength = ppath.Length; - bestMatch = p._listener; - prefix = p; - } - } - - return bestMatch; - } - - private void AddSpecial(List list, ListenerPrefix prefix) - { - if (list == null) - return; - - foreach (ListenerPrefix p in list) - { - if (p.Path == prefix.Path) - throw new Exception("net_listener_already"); - } - list.Add(prefix); - } - - private bool RemoveSpecial(List list, ListenerPrefix prefix) - { - if (list == null) - return false; - - int c = list.Count; - for (int i = 0; i < c; i++) - { - ListenerPrefix p = list[i]; - if (p.Path == prefix.Path) - { - list.RemoveAt(i); - return true; - } - } - return false; - } - - private void CheckIfRemove() - { - if (_prefixes.Count > 0) - return; - - List list = _unhandledPrefixes; - if (list != null && list.Count > 0) - return; - - list = _allPrefixes; - if (list != null && list.Count > 0) - return; - - HttpEndPointManager.RemoveEndPoint(this, _endpoint); - } - - public void Close() - { - _closed = true; - _socket.Close(); - lock (_unregisteredConnections) - { - // Clone the list because RemoveConnection can be called from Close - var connections = new List(_unregisteredConnections.Keys); - - foreach (HttpConnection c in connections) - c.Close(true); - _unregisteredConnections.Clear(); - } - } - - public void AddPrefix(ListenerPrefix prefix, HttpListener listener) - { - List current; - List future; - if (prefix.Host == "*") - { - do - { - current = _unhandledPrefixes; - future = current != null ? new List(current) : new List(); - prefix._listener = listener; - AddSpecial(future, prefix); - } while (Interlocked.CompareExchange(ref _unhandledPrefixes, future, current) != current); - return; - } - - if (prefix.Host == "+") - { - do - { - current = _allPrefixes; - future = current != null ? new List(current) : new List(); - prefix._listener = listener; - AddSpecial(future, prefix); - } while (Interlocked.CompareExchange(ref _allPrefixes, future, current) != current); - return; - } - - Dictionary prefs, p2; - do - { - prefs = _prefixes; - if (prefs.ContainsKey(prefix)) - { - throw new Exception("net_listener_already"); - } - p2 = new Dictionary(prefs); - p2[prefix] = listener; - } while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs); - } - - public void RemovePrefix(ListenerPrefix prefix, HttpListener listener) - { - List current; - List future; - if (prefix.Host == "*") - { - do - { - current = _unhandledPrefixes; - future = current != null ? new List(current) : new List(); - if (!RemoveSpecial(future, prefix)) - break; // Prefix not found - } while (Interlocked.CompareExchange(ref _unhandledPrefixes, future, current) != current); - - CheckIfRemove(); - return; - } - - if (prefix.Host == "+") - { - do - { - current = _allPrefixes; - future = current != null ? new List(current) : new List(); - if (!RemoveSpecial(future, prefix)) - break; // Prefix not found - } while (Interlocked.CompareExchange(ref _allPrefixes, future, current) != current); - CheckIfRemove(); - return; - } - - Dictionary prefs, p2; - do - { - prefs = _prefixes; - if (!prefs.ContainsKey(prefix)) - break; - - p2 = new Dictionary(prefs); - p2.Remove(prefix); - } while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs); - CheckIfRemove(); - } - } -} diff --git a/SocketHttpListener/Net/HttpEndPointManager.cs b/SocketHttpListener/Net/HttpEndPointManager.cs deleted file mode 100644 index 27b4244a6c..0000000000 --- a/SocketHttpListener/Net/HttpEndPointManager.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - internal sealed class HttpEndPointManager - { - private static Dictionary> s_ipEndPoints = new Dictionary>(); - - private HttpEndPointManager() - { - } - - public static void AddListener(ILogger logger, HttpListener listener) - { - var added = new List(); - try - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - foreach (string prefix in listener.Prefixes) - { - AddPrefixInternal(logger, prefix, listener); - added.Add(prefix); - } - } - } - catch - { - foreach (string prefix in added) - { - RemovePrefix(logger, prefix, listener); - } - throw; - } - } - - public static void AddPrefix(ILogger logger, string prefix, HttpListener listener) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - AddPrefixInternal(logger, prefix, listener); - } - } - - private static void AddPrefixInternal(ILogger logger, string p, HttpListener listener) - { - int start = p.IndexOf(':') + 3; - int colon = p.IndexOf(':', start); - if (colon != -1) - { - // root can't be -1 here, since we've already checked for ending '/' in ListenerPrefix. - int root = p.IndexOf('/', colon, p.Length - colon); - string portString = p.Substring(colon + 1, root - colon - 1); - - if (!int.TryParse(portString, out var port) || port <= 0 || port >= 65536) - { - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_invalid_port"); - } - } - - var lp = new ListenerPrefix(p); - if (lp.Host != "*" && lp.Host != "+" && Uri.CheckHostName(lp.Host) == UriHostNameType.Unknown) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_listener_host"); - - if (lp.Path.IndexOf('%') != -1) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_invalid_path"); - - if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) - throw new HttpListenerException((int)HttpStatusCode.BadRequest, "net_invalid_path"); - - // listens on all the interfaces if host name cannot be parsed by IPAddress. - HttpEndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure); - epl.AddPrefix(lp, listener); - } - - private static IPAddress GetIpAnyAddress(HttpListener listener) - { - return listener.EnableDualMode ? IPAddress.IPv6Any : IPAddress.Any; - } - - private static HttpEndPointListener GetEPListener(ILogger logger, string host, int port, HttpListener listener, bool secure) - { - IPAddress addr; - if (host == "*" || host == "+") - { - addr = GetIpAnyAddress(listener); - } - else - { - const int NotSupportedErrorCode = 50; - try - { - addr = Dns.GetHostAddresses(host)[0]; - } - catch - { - // Throw same error code as windows, request is not supported. - throw new HttpListenerException(NotSupportedErrorCode, "net_listener_not_supported"); - } - - if (IPAddress.Any.Equals(addr)) - { - // Don't support listening to 0.0.0.0, match windows behavior. - throw new HttpListenerException(NotSupportedErrorCode, "net_listener_not_supported"); - } - } - - Dictionary p = null; - if (s_ipEndPoints.ContainsKey(addr)) - { - p = s_ipEndPoints[addr]; - } - else - { - p = new Dictionary(); - s_ipEndPoints[addr] = p; - } - - HttpEndPointListener epl = null; - if (p.ContainsKey(port)) - { - epl = p[port]; - } - else - { - try - { - epl = new HttpEndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.SocketFactory, listener.StreamHelper, listener.FileSystem, listener.EnvironmentInfo); - } - catch (SocketException ex) - { - throw new HttpListenerException(ex.ErrorCode, ex.Message); - } - p[port] = epl; - } - - return epl; - } - - public static void RemoveEndPoint(HttpEndPointListener epl, IPEndPoint ep) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - Dictionary p = null; - p = s_ipEndPoints[ep.Address]; - p.Remove(ep.Port); - if (p.Count == 0) - { - s_ipEndPoints.Remove(ep.Address); - } - epl.Close(); - } - } - - public static void RemoveListener(ILogger logger, HttpListener listener) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - foreach (string prefix in listener.Prefixes) - { - RemovePrefixInternal(logger, prefix, listener); - } - } - } - - public static void RemovePrefix(ILogger logger, string prefix, HttpListener listener) - { - lock ((s_ipEndPoints as ICollection).SyncRoot) - { - RemovePrefixInternal(logger, prefix, listener); - } - } - - private static void RemovePrefixInternal(ILogger logger, string prefix, HttpListener listener) - { - var lp = new ListenerPrefix(prefix); - if (lp.Path.IndexOf('%') != -1) - return; - - if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) - return; - - HttpEndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure); - epl.RemovePrefix(lp, listener); - } - } -} diff --git a/SocketHttpListener/Net/HttpKnownHeaderNames.cs b/SocketHttpListener/Net/HttpKnownHeaderNames.cs deleted file mode 100644 index dc6f2ce415..0000000000 --- a/SocketHttpListener/Net/HttpKnownHeaderNames.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal static partial class HttpKnownHeaderNames - { - // When adding a new constant, add it to HttpKnownHeaderNames.TryGetHeaderName.cs as well. - - public const string Accept = "Accept"; - public const string AcceptCharset = "Accept-Charset"; - public const string AcceptEncoding = "Accept-Encoding"; - public const string AcceptLanguage = "Accept-Language"; - public const string AcceptPatch = "Accept-Patch"; - public const string AcceptRanges = "Accept-Ranges"; - public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; - public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; - public const string AccessControlAllowMethods = "Access-Control-Allow-Methods"; - public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; - public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; - public const string AccessControlMaxAge = "Access-Control-Max-Age"; - public const string Age = "Age"; - public const string Allow = "Allow"; - public const string AltSvc = "Alt-Svc"; - public const string Authorization = "Authorization"; - public const string CacheControl = "Cache-Control"; - public const string Connection = "Connection"; - public const string ContentDisposition = "Content-Disposition"; - public const string ContentEncoding = "Content-Encoding"; - public const string ContentLanguage = "Content-Language"; - public const string ContentLength = "Content-Length"; - public const string ContentLocation = "Content-Location"; - public const string ContentMD5 = "Content-MD5"; - public const string ContentRange = "Content-Range"; - public const string ContentSecurityPolicy = "Content-Security-Policy"; - public const string ContentType = "Content-Type"; - public const string Cookie = "Cookie"; - public const string Cookie2 = "Cookie2"; - public const string Date = "Date"; - public const string ETag = "ETag"; - public const string Expect = "Expect"; - public const string Expires = "Expires"; - public const string From = "From"; - public const string Host = "Host"; - public const string IfMatch = "If-Match"; - public const string IfModifiedSince = "If-Modified-Since"; - public const string IfNoneMatch = "If-None-Match"; - public const string IfRange = "If-Range"; - public const string IfUnmodifiedSince = "If-Unmodified-Since"; - public const string KeepAlive = "Keep-Alive"; - public const string LastModified = "Last-Modified"; - public const string Link = "Link"; - public const string Location = "Location"; - public const string MaxForwards = "Max-Forwards"; - public const string Origin = "Origin"; - public const string P3P = "P3P"; - public const string Pragma = "Pragma"; - public const string ProxyAuthenticate = "Proxy-Authenticate"; - public const string ProxyAuthorization = "Proxy-Authorization"; - public const string ProxyConnection = "Proxy-Connection"; - public const string PublicKeyPins = "Public-Key-Pins"; - public const string Range = "Range"; - public const string Referer = "Referer"; // NB: The spelling-mistake "Referer" for "Referrer" must be matched. - public const string RetryAfter = "Retry-After"; - public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; - public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; - public const string SecWebSocketKey = "Sec-WebSocket-Key"; - public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; - public const string SecWebSocketVersion = "Sec-WebSocket-Version"; - public const string Server = "Server"; - public const string SetCookie = "Set-Cookie"; - public const string SetCookie2 = "Set-Cookie2"; - public const string StrictTransportSecurity = "Strict-Transport-Security"; - public const string TE = "TE"; - public const string TSV = "TSV"; - public const string Trailer = "Trailer"; - public const string TransferEncoding = "Transfer-Encoding"; - public const string Upgrade = "Upgrade"; - public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests"; - public const string UserAgent = "User-Agent"; - public const string Vary = "Vary"; - public const string Via = "Via"; - public const string WWWAuthenticate = "WWW-Authenticate"; - public const string Warning = "Warning"; - public const string XAspNetVersion = "X-AspNet-Version"; - public const string XContentDuration = "X-Content-Duration"; - public const string XContentTypeOptions = "X-Content-Type-Options"; - public const string XFrameOptions = "X-Frame-Options"; - public const string XMSEdgeRef = "X-MSEdge-Ref"; - public const string XPoweredBy = "X-Powered-By"; - public const string XRequestID = "X-Request-ID"; - public const string XUACompatible = "X-UA-Compatible"; - } -} diff --git a/SocketHttpListener/Net/HttpListener.cs b/SocketHttpListener/Net/HttpListener.cs deleted file mode 100644 index f17036a21a..0000000000 --- a/SocketHttpListener/Net/HttpListener.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Net; -using System.Security.Cryptography.X509Certificates; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - public sealed class HttpListener : IDisposable - { - internal ICryptoProvider CryptoProvider { get; private set; } - internal ISocketFactory SocketFactory { get; private set; } - internal IFileSystem FileSystem { get; private set; } - internal IStreamHelper StreamHelper { get; private set; } - internal IEnvironmentInfo EnvironmentInfo { get; private set; } - - public bool EnableDualMode { get; set; } - - private AuthenticationSchemes auth_schemes; - private HttpListenerPrefixCollection prefixes; - private AuthenticationSchemeSelector auth_selector; - private string realm; - private bool unsafe_ntlm_auth; - private bool listening; - private bool disposed; - - private Dictionary registry; - private Dictionary connections; - private ILogger _logger; - private X509Certificate _certificate; - - public Action OnContext { get; set; } - - public HttpListener( - ILogger logger, - ICryptoProvider cryptoProvider, - ISocketFactory socketFactory, - IStreamHelper streamHelper, - IFileSystem fileSystem, - IEnvironmentInfo environmentInfo) - { - _logger = logger; - CryptoProvider = cryptoProvider; - SocketFactory = socketFactory; - StreamHelper = streamHelper; - FileSystem = fileSystem; - EnvironmentInfo = environmentInfo; - - prefixes = new HttpListenerPrefixCollection(logger, this); - registry = new Dictionary(); - connections = new Dictionary(); - auth_schemes = AuthenticationSchemes.Anonymous; - } - - public HttpListener( - ILogger logger, - X509Certificate certificate, - ICryptoProvider cryptoProvider, - ISocketFactory socketFactory, - IStreamHelper streamHelper, - IFileSystem fileSystem, - IEnvironmentInfo environmentInfo) - : this(logger, cryptoProvider, socketFactory, streamHelper, fileSystem, environmentInfo) - { - _certificate = certificate; - } - - public void LoadCert(X509Certificate cert) - { - _certificate = cert; - } - - // TODO: Digest, NTLM and Negotiate require ControlPrincipal - public AuthenticationSchemes AuthenticationSchemes - { - get => auth_schemes; - set - { - CheckDisposed(); - auth_schemes = value; - } - } - - public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate - { - get => auth_selector; - set - { - CheckDisposed(); - auth_selector = value; - } - } - - public bool IsListening => listening; - - public static bool IsSupported => true; - - public HttpListenerPrefixCollection Prefixes - { - get - { - CheckDisposed(); - return prefixes; - } - } - - // TODO: use this - public string Realm - { - get => realm; - set - { - CheckDisposed(); - realm = value; - } - } - - public bool UnsafeConnectionNtlmAuthentication - { - get => unsafe_ntlm_auth; - set - { - CheckDisposed(); - unsafe_ntlm_auth = value; - } - } - - //internal IMonoSslStream CreateSslStream(Stream innerStream, bool ownsStream, MSI.MonoRemoteCertificateValidationCallback callback) - //{ - // lock (registry) - // { - // if (tlsProvider == null) - // tlsProvider = MonoTlsProviderFactory.GetProviderInternal(); - // if (tlsSettings == null) - // tlsSettings = MSI.MonoTlsSettings.CopyDefaultSettings(); - // if (tlsSettings.RemoteCertificateValidationCallback == null) - // tlsSettings.RemoteCertificateValidationCallback = callback; - // return tlsProvider.CreateSslStream(innerStream, ownsStream, tlsSettings); - // } - //} - - internal X509Certificate Certificate => _certificate; - - public void Abort() - { - if (disposed) - return; - - if (!listening) - { - return; - } - - Close(true); - } - - public void Close() - { - if (disposed) - return; - - if (!listening) - { - disposed = true; - return; - } - - Close(true); - disposed = true; - } - - void Close(bool force) - { - CheckDisposed(); - HttpEndPointManager.RemoveListener(_logger, this); - Cleanup(force); - } - - void Cleanup(bool close_existing) - { - lock (registry) - { - if (close_existing) - { - // Need to copy this since closing will call UnregisterContext - ICollection keys = registry.Keys; - var all = new HttpListenerContext[keys.Count]; - keys.CopyTo(all, 0); - registry.Clear(); - for (int i = all.Length - 1; i >= 0; i--) - all[i].Connection.Close(true); - } - - lock (connections) - { - ICollection keys = connections.Keys; - var conns = new HttpConnection[keys.Count]; - keys.CopyTo(conns, 0); - connections.Clear(); - for (int i = conns.Length - 1; i >= 0; i--) - conns[i].Close(true); - } - } - } - - internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerContext context) - { - if (AuthenticationSchemeSelectorDelegate != null) - return AuthenticationSchemeSelectorDelegate(context.Request); - else - return auth_schemes; - } - - public void Start() - { - CheckDisposed(); - if (listening) - return; - - HttpEndPointManager.AddListener(_logger, this); - listening = true; - } - - public void Stop() - { - CheckDisposed(); - listening = false; - Close(false); - } - - void IDisposable.Dispose() - { - if (disposed) - return; - - Close(true); //TODO: Should we force here or not? - disposed = true; - } - - internal void CheckDisposed() - { - if (disposed) - throw new ObjectDisposedException(GetType().Name); - } - - internal void RegisterContext(HttpListenerContext context) - { - if (OnContext != null && IsListening) - { - OnContext(context); - } - - lock (registry) - registry[context] = context; - } - - internal void UnregisterContext(HttpListenerContext context) - { - lock (registry) - registry.Remove(context); - } - - internal void AddConnection(HttpConnection cnc) - { - lock (connections) - { - connections[cnc] = cnc; - } - } - - internal void RemoveConnection(HttpConnection cnc) - { - lock (connections) - { - connections.Remove(cnc); - } - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerBasicIdentity.cs b/SocketHttpListener/Net/HttpListenerBasicIdentity.cs deleted file mode 100644 index 5f6ec44b96..0000000000 --- a/SocketHttpListener/Net/HttpListenerBasicIdentity.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Security.Principal; - -namespace SocketHttpListener.Net -{ - public class HttpListenerBasicIdentity : GenericIdentity - { - string password; - - public HttpListenerBasicIdentity(string username, string password) - : base(username, "Basic") - { - this.password = password; - } - - public virtual string Password => password; - } - - public class GenericIdentity : IIdentity - { - private string m_name; - private string m_type; - - public GenericIdentity(string name) - { - if (name == null) - throw new System.ArgumentNullException(nameof(name)); - - m_name = name; - m_type = ""; - } - - public GenericIdentity(string name, string type) - { - if (name == null) - throw new System.ArgumentNullException(nameof(name)); - if (type == null) - throw new System.ArgumentNullException(nameof(type)); - - m_name = name; - m_type = type; - } - - public virtual string Name => m_name; - - public virtual string AuthenticationType => m_type; - - public virtual bool IsAuthenticated => !m_name.Equals(""); - } -} diff --git a/SocketHttpListener/Net/HttpListenerContext.Managed.cs b/SocketHttpListener/Net/HttpListenerContext.Managed.cs deleted file mode 100644 index 79742bf37f..0000000000 --- a/SocketHttpListener/Net/HttpListenerContext.Managed.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.ComponentModel; -using System.Security.Principal; -using System.Text; -using System.Threading.Tasks; -using SocketHttpListener.Net.WebSockets; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerContext - { - private HttpConnection _connection; - - internal HttpListenerContext(HttpConnection connection) - { - _connection = connection; - _response = new HttpListenerResponse(this); - Request = new HttpListenerRequest(this); - ErrorStatus = 400; - } - - internal int ErrorStatus { get; set; } - - internal string ErrorMessage { get; set; } - - internal bool HaveError => ErrorMessage != null; - - internal HttpConnection Connection => _connection; - - internal void ParseAuthentication(System.Net.AuthenticationSchemes expectedSchemes) - { - if (expectedSchemes == System.Net.AuthenticationSchemes.Anonymous) - return; - - string header = Request.Headers["Authorization"]; - if (string.IsNullOrEmpty(header)) - return; - - if (IsBasicHeader(header)) - { - _user = ParseBasicAuthentication(header.Substring(AuthenticationTypes.Basic.Length + 1)); - } - } - - internal IPrincipal ParseBasicAuthentication(string authData) => - TryParseBasicAuth(authData, out HttpStatusCode errorCode, out string username, out string password) ? - new GenericPrincipal(new HttpListenerBasicIdentity(username, password), Array.Empty()) : - null; - - internal static bool IsBasicHeader(string header) => - header.Length >= 6 && - header[5] == ' ' && - string.Compare(header, 0, AuthenticationTypes.Basic, 0, 5, StringComparison.OrdinalIgnoreCase) == 0; - - internal static bool TryParseBasicAuth(string headerValue, out HttpStatusCode errorCode, out string username, out string password) - { - errorCode = HttpStatusCode.OK; - username = password = null; - try - { - if (string.IsNullOrWhiteSpace(headerValue)) - { - return false; - } - - string authString = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue)); - int colonPos = authString.IndexOf(':'); - if (colonPos < 0) - { - // username must be at least 1 char - errorCode = HttpStatusCode.BadRequest; - return false; - } - - username = authString.Substring(0, colonPos); - password = authString.Substring(colonPos + 1); - return true; - } - catch - { - errorCode = HttpStatusCode.InternalServerError; - return false; - } - } - - public Task AcceptWebSocketAsync(string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval) - { - return HttpWebSocket.AcceptWebSocketAsyncCore(this, subProtocol, receiveBufferSize, keepAliveInterval); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public Task AcceptWebSocketAsync(string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval, ArraySegment internalBuffer) - { - WebSocketValidate.ValidateArraySegment(internalBuffer, nameof(internalBuffer)); - HttpWebSocket.ValidateOptions(subProtocol, receiveBufferSize, HttpWebSocket.MinSendBufferSize, keepAliveInterval); - return HttpWebSocket.AcceptWebSocketAsyncCore(this, subProtocol, receiveBufferSize, keepAliveInterval, internalBuffer); - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerContext.cs b/SocketHttpListener/Net/HttpListenerContext.cs deleted file mode 100644 index d84e2d1aa0..0000000000 --- a/SocketHttpListener/Net/HttpListenerContext.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Net; -using System.Security.Principal; -using System.Threading.Tasks; -using SocketHttpListener.Net.WebSockets; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerContext - { - private HttpListenerResponse _response; - private IPrincipal _user; - - public HttpListenerRequest Request { get; } - - public IPrincipal User => _user; - - // This can be used to cache the results of HttpListener.AuthenticationSchemeSelectorDelegate. - internal AuthenticationSchemes AuthenticationSchemes { get; set; } - - public HttpListenerResponse Response => _response; - - public Task AcceptWebSocketAsync(string subProtocol) - { - return AcceptWebSocketAsync(subProtocol, HttpWebSocket.DefaultReceiveBufferSize, WebSocket.DefaultKeepAliveInterval); - } - - public Task AcceptWebSocketAsync(string subProtocol, TimeSpan keepAliveInterval) - { - return AcceptWebSocketAsync(subProtocol, HttpWebSocket.DefaultReceiveBufferSize, keepAliveInterval); - } - } - - public class GenericPrincipal : IPrincipal - { - private IIdentity m_identity; - private string[] m_roles; - - public GenericPrincipal(IIdentity identity, string[] roles) - { - if (identity == null) - throw new ArgumentNullException(nameof(identity)); - - m_identity = identity; - if (roles != null) - { - m_roles = new string[roles.Length]; - for (int i = 0; i < roles.Length; ++i) - { - m_roles[i] = roles[i]; - } - } - else - { - m_roles = null; - } - } - - public virtual IIdentity Identity => m_identity; - - public virtual bool IsInRole(string role) - { - if (role == null || m_roles == null) - return false; - - for (int i = 0; i < m_roles.Length; ++i) - { - if (m_roles[i] != null && string.Compare(m_roles[i], role, StringComparison.OrdinalIgnoreCase) == 0) - return true; - } - return false; - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerPrefixCollection.cs b/SocketHttpListener/Net/HttpListenerPrefixCollection.cs deleted file mode 100644 index 400a1adb61..0000000000 --- a/SocketHttpListener/Net/HttpListenerPrefixCollection.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable - { - private List _prefixes = new List(); - private HttpListener _listener; - - private ILogger _logger; - - internal HttpListenerPrefixCollection(ILogger logger, HttpListener listener) - { - _logger = logger; - _listener = listener; - } - - public int Count => _prefixes.Count; - - public bool IsReadOnly => false; - - public bool IsSynchronized => false; - - public void Add(string uriPrefix) - { - _listener.CheckDisposed(); - //ListenerPrefix.CheckUri(uriPrefix); - if (_prefixes.Contains(uriPrefix)) - { - return; - } - - _prefixes.Add(uriPrefix); - if (_listener.IsListening) - { - HttpEndPointManager.AddPrefix(_logger, uriPrefix, _listener); - } - } - - public void AddRange(IEnumerable uriPrefixes) - { - _listener.CheckDisposed(); - - foreach (var uriPrefix in uriPrefixes) - { - if (_prefixes.Contains(uriPrefix)) - { - continue; - } - - _prefixes.Add(uriPrefix); - if (_listener.IsListening) - { - HttpEndPointManager.AddPrefix(_logger, uriPrefix, _listener); - } - } - } - - public void Clear() - { - _listener.CheckDisposed(); - _prefixes.Clear(); - if (_listener.IsListening) - { - HttpEndPointManager.RemoveListener(_logger, _listener); - } - } - - public bool Contains(string uriPrefix) - { - _listener.CheckDisposed(); - return _prefixes.Contains(uriPrefix); - } - - public void CopyTo(string[] array, int offset) - { - _listener.CheckDisposed(); - _prefixes.CopyTo(array, offset); - } - - public void CopyTo(Array array, int offset) - { - _listener.CheckDisposed(); - ((ICollection)_prefixes).CopyTo(array, offset); - } - - public IEnumerator GetEnumerator() - { - return _prefixes.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _prefixes.GetEnumerator(); - } - - public bool Remove(string uriPrefix) - { - _listener.CheckDisposed(); - if (uriPrefix == null) - { - throw new ArgumentNullException(nameof(uriPrefix)); - } - - bool result = _prefixes.Remove(uriPrefix); - if (result && _listener.IsListening) - { - HttpEndPointManager.RemovePrefix(_logger, uriPrefix, _listener); - } - - return result; - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerRequest.Managed.cs b/SocketHttpListener/Net/HttpListenerRequest.Managed.cs deleted file mode 100644 index 3f9e32f089..0000000000 --- a/SocketHttpListener/Net/HttpListenerRequest.Managed.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System; -using System.IO; -using System.Text; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerRequest - { - private long _contentLength; - private bool _clSet; - private WebHeaderCollection _headers; - private string _method; - private Stream _inputStream; - private HttpListenerContext _context; - private bool _isChunked; - - private static byte[] s_100continue = Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); - - internal HttpListenerRequest(HttpListenerContext context) - { - _context = context; - _headers = new WebHeaderCollection(); - _version = HttpVersion.Version10; - } - - private static readonly char[] s_separators = new char[] { ' ' }; - - internal void SetRequestLine(string req) - { - string[] parts = req.Split(s_separators, 3); - if (parts.Length != 3) - { - _context.ErrorMessage = "Invalid request line (parts)."; - return; - } - - _method = parts[0]; - foreach (char c in _method) - { - int ic = (int)c; - - if ((ic >= 'A' && ic <= 'Z') || - (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && - c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && - c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && - c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) - continue; - - _context.ErrorMessage = "(Invalid verb)"; - return; - } - - _rawUrl = parts[1]; - if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/")) - { - _context.ErrorMessage = "Invalid request line (version)."; - return; - } - - try - { - _version = new Version(parts[2].Substring(5)); - } - catch - { - _context.ErrorMessage = "Invalid request line (version)."; - return; - } - - if (_version.Major < 1) - { - _context.ErrorMessage = "Invalid request line (version)."; - return; - } - if (_version.Major > 1) - { - _context.ErrorStatus = (int)HttpStatusCode.HttpVersionNotSupported; - _context.ErrorMessage = HttpStatusDescription.Get(HttpStatusCode.HttpVersionNotSupported); - return; - } - } - - private static bool MaybeUri(string s) - { - int p = s.IndexOf(':'); - if (p == -1) - return false; - - if (p >= 10) - return false; - - return IsPredefinedScheme(s.Substring(0, p)); - } - - private static bool IsPredefinedScheme(string scheme) - { - if (scheme == null || scheme.Length < 3) - return false; - - char c = scheme[0]; - if (c == 'h') - return (scheme == UriScheme.Http || scheme == UriScheme.Https); - if (c == 'f') - return (scheme == UriScheme.File || scheme == UriScheme.Ftp); - - if (c == 'n') - { - c = scheme[1]; - if (c == 'e') - return (scheme == UriScheme.News || scheme == UriScheme.NetPipe || scheme == UriScheme.NetTcp); - if (scheme == UriScheme.Nntp) - return true; - return false; - } - if ((c == 'g' && scheme == UriScheme.Gopher) || (c == 'm' && scheme == UriScheme.Mailto)) - return true; - - return false; - } - - internal void FinishInitialization() - { - string host = UserHostName; - if (_version > HttpVersion.Version10 && (host == null || host.Length == 0)) - { - _context.ErrorMessage = "Invalid host name"; - return; - } - - string path; - Uri raw_uri = null; - if (MaybeUri(_rawUrl.ToLowerInvariant()) && Uri.TryCreate(_rawUrl, UriKind.Absolute, out raw_uri)) - path = raw_uri.PathAndQuery; - else - path = _rawUrl; - - if ((host == null || host.Length == 0)) - host = UserHostAddress; - - if (raw_uri != null) - host = raw_uri.Host; - - int colon = host.IndexOf(']') == -1 ? host.IndexOf(':') : host.LastIndexOf(':'); - if (colon >= 0) - host = host.Substring(0, colon); - - string base_uri = string.Format("{0}://{1}:{2}", RequestScheme, host, LocalEndPoint.Port); - - if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out _requestUri)) - { - _context.ErrorMessage = System.Net.WebUtility.HtmlEncode("Invalid url: " + base_uri + path); - return; - } - - _requestUri = HttpListenerRequestUriBuilder.GetRequestUri(_rawUrl, _requestUri.Scheme, - _requestUri.Authority, _requestUri.LocalPath, _requestUri.Query); - - if (_version >= HttpVersion.Version11) - { - string t_encoding = Headers[HttpKnownHeaderNames.TransferEncoding]; - _isChunked = (t_encoding != null && string.Equals(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase)); - // 'identity' is not valid! - if (t_encoding != null && !_isChunked) - { - _context.Connection.SendError(null, 501); - return; - } - } - - if (!_isChunked && !_clSet) - { - if (string.Equals(_method, "POST", StringComparison.OrdinalIgnoreCase) || - string.Equals(_method, "PUT", StringComparison.OrdinalIgnoreCase)) - { - _context.Connection.SendError(null, 411); - return; - } - } - - if (string.Compare(Headers[HttpKnownHeaderNames.Expect], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) - { - HttpResponseStream output = _context.Connection.GetResponseStream(); - output.InternalWrite(s_100continue, 0, s_100continue.Length); - } - } - - internal static string Unquote(string str) - { - int start = str.IndexOf('\"'); - int end = str.LastIndexOf('\"'); - if (start >= 0 && end >= 0) - str = str.Substring(start + 1, end - 1); - return str.Trim(); - } - - internal void AddHeader(string header) - { - int colon = header.IndexOf(':'); - if (colon == -1 || colon == 0) - { - _context.ErrorMessage = HttpStatusDescription.Get(400); - _context.ErrorStatus = 400; - return; - } - - string name = header.Substring(0, colon).Trim(); - string val = header.Substring(colon + 1).Trim(); - if (name.Equals("content-length", StringComparison.OrdinalIgnoreCase)) - { - // To match Windows behavior: - // Content lengths >= 0 and <= long.MaxValue are accepted as is. - // Content lengths > long.MaxValue and <= ulong.MaxValue are treated as 0. - // Content lengths < 0 cause the requests to fail. - // Other input is a failure, too. - long parsedContentLength = - ulong.TryParse(val, out ulong parsedUlongContentLength) ? (parsedUlongContentLength <= long.MaxValue ? (long)parsedUlongContentLength : 0) : - long.Parse(val); - if (parsedContentLength < 0 || (_clSet && parsedContentLength != _contentLength)) - { - _context.ErrorMessage = "Invalid Content-Length."; - } - else - { - _contentLength = parsedContentLength; - _clSet = true; - } - } - else if (name.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase)) - { - if (Headers[HttpKnownHeaderNames.TransferEncoding] != null) - { - _context.ErrorStatus = (int)HttpStatusCode.NotImplemented; - _context.ErrorMessage = HttpStatusDescription.Get(HttpStatusCode.NotImplemented); - } - } - - if (_context.ErrorMessage == null) - { - _headers.Set(name, val); - } - } - - // returns true is the stream could be reused. - internal bool FlushInput() - { - if (!HasEntityBody) - return true; - - int length = 2048; - if (_contentLength > 0) - length = (int)Math.Min(_contentLength, (long)length); - - byte[] bytes = new byte[length]; - while (true) - { - try - { - IAsyncResult ares = InputStream.BeginRead(bytes, 0, length, null, null); - if (!ares.IsCompleted && !ares.AsyncWaitHandle.WaitOne(1000)) - return false; - if (InputStream.EndRead(ares) <= 0) - return true; - } - catch (ObjectDisposedException) - { - _inputStream = null; - return true; - } - catch - { - return false; - } - } - } - - public long ContentLength64 - { - get - { - if (_isChunked) - _contentLength = -1; - - return _contentLength; - } - } - - public bool HasEntityBody => (_contentLength > 0 || _isChunked); - - public QueryParamCollection Headers => _headers; - - public string HttpMethod => _method; - - public Stream InputStream - { - get - { - if (_inputStream == null) - { - if (_isChunked || _contentLength > 0) - _inputStream = _context.Connection.GetRequestStream(_isChunked, _contentLength); - else - _inputStream = Stream.Null; - } - - return _inputStream; - } - } - - public bool IsAuthenticated => false; - - public bool IsSecureConnection => _context.Connection.IsSecure; - - public System.Net.IPEndPoint LocalEndPoint => _context.Connection.LocalEndPoint; - - public System.Net.IPEndPoint RemoteEndPoint => _context.Connection.RemoteEndPoint; - - public Guid RequestTraceIdentifier { get; } = Guid.NewGuid(); - - public string ServiceName => null; - - private Uri RequestUri => _requestUri; - private bool SupportsWebSockets => true; - } -} diff --git a/SocketHttpListener/Net/HttpListenerRequest.cs b/SocketHttpListener/Net/HttpListenerRequest.cs deleted file mode 100644 index 667d58ea7b..0000000000 --- a/SocketHttpListener/Net/HttpListenerRequest.cs +++ /dev/null @@ -1,537 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Text; -using MediaBrowser.Model.Services; -using SocketHttpListener.Net.WebSockets; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerRequest - { - private CookieCollection _cookies; - private bool? _keepAlive; - private string _rawUrl; - private Uri _requestUri; - private Version _version; - - public string[] AcceptTypes => Helpers.ParseMultivalueHeader(Headers[HttpKnownHeaderNames.Accept]); - - public string[] UserLanguages => Helpers.ParseMultivalueHeader(Headers[HttpKnownHeaderNames.AcceptLanguage]); - - private static CookieCollection ParseCookies(Uri uri, string setCookieHeader) - { - var cookies = new CookieCollection(); - return cookies; - } - - public CookieCollection Cookies - { - get - { - if (_cookies == null) - { - string cookieString = Headers[HttpKnownHeaderNames.Cookie]; - if (!string.IsNullOrEmpty(cookieString)) - { - _cookies = ParseCookies(RequestUri, cookieString); - } - if (_cookies == null) - { - _cookies = new CookieCollection(); - } - } - return _cookies; - } - } - - public Encoding ContentEncoding - { - get - { - if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP")) - { - string postDataCharset = Headers["x-up-devcap-post-charset"]; - if (postDataCharset != null && postDataCharset.Length > 0) - { - try - { - return Encoding.GetEncoding(postDataCharset); - } - catch (ArgumentException) - { - } - } - } - if (HasEntityBody) - { - if (ContentType != null) - { - string charSet = Helpers.GetCharSetValueFromHeader(ContentType); - if (charSet != null) - { - try - { - return Encoding.GetEncoding(charSet); - } - catch (ArgumentException) - { - } - } - } - } - return Encoding.UTF8; - } - } - - public string ContentType => Headers[HttpKnownHeaderNames.ContentType]; - - public bool IsLocal => LocalEndPoint.Address.Equals(RemoteEndPoint.Address); - - public bool IsWebSocketRequest - { - get - { - if (!SupportsWebSockets) - { - return false; - } - - bool foundConnectionUpgradeHeader = false; - if (string.IsNullOrEmpty(Headers[HttpKnownHeaderNames.Connection]) || string.IsNullOrEmpty(Headers[HttpKnownHeaderNames.Upgrade])) - { - return false; - } - - foreach (string connection in Headers.GetValues(HttpKnownHeaderNames.Connection)) - { - if (string.Equals(connection, HttpKnownHeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase)) - { - foundConnectionUpgradeHeader = true; - break; - } - } - - if (!foundConnectionUpgradeHeader) - { - return false; - } - - foreach (string upgrade in Headers.GetValues(HttpKnownHeaderNames.Upgrade)) - { - if (string.Equals(upgrade, HttpWebSocket.WebSocketUpgradeToken, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - } - - public bool KeepAlive - { - get - { - if (!_keepAlive.HasValue) - { - string header = Headers[HttpKnownHeaderNames.ProxyConnection]; - if (string.IsNullOrEmpty(header)) - { - header = Headers[HttpKnownHeaderNames.Connection]; - } - if (string.IsNullOrEmpty(header)) - { - if (ProtocolVersion >= HttpVersion.Version11) - { - _keepAlive = true; - } - else - { - header = Headers[HttpKnownHeaderNames.KeepAlive]; - _keepAlive = !string.IsNullOrEmpty(header); - } - } - else - { - header = header.ToLowerInvariant(); - _keepAlive = - header.IndexOf("close", StringComparison.OrdinalIgnoreCase) < 0 || - header.IndexOf("keep-alive", StringComparison.OrdinalIgnoreCase) >= 0; - } - } - - return _keepAlive.Value; - } - } - - public QueryParamCollection QueryString - { - get - { - var queryString = new QueryParamCollection(); - Helpers.FillFromString(queryString, Url.Query, true, ContentEncoding); - return queryString; - } - } - - public string RawUrl => _rawUrl; - - private string RequestScheme => IsSecureConnection ? UriScheme.Https : UriScheme.Http; - - public string UserAgent => Headers[HttpKnownHeaderNames.UserAgent]; - - public string UserHostAddress => LocalEndPoint.ToString(); - - public string UserHostName => Headers[HttpKnownHeaderNames.Host]; - - public Uri UrlReferrer - { - get - { - string referrer = Headers[HttpKnownHeaderNames.Referer]; - if (referrer == null) - { - return null; - } - - bool success = Uri.TryCreate(referrer, UriKind.RelativeOrAbsolute, out var urlReferrer); - return success ? urlReferrer : null; - } - } - - public Uri Url => RequestUri; - - public Version ProtocolVersion => _version; - - private static class Helpers - { - // - // Get attribute off header value - // - internal static string GetCharSetValueFromHeader(string headerValue) - { - const string AttrName = "charset"; - - if (headerValue == null) - return null; - - int l = headerValue.Length; - int k = AttrName.Length; - - // find properly separated attribute name - int i = 1; // start searching from 1 - - while (i < l) - { - i = CultureInfo.InvariantCulture.CompareInfo.IndexOf(headerValue, AttrName, i, CompareOptions.IgnoreCase); - if (i < 0) - break; - if (i + k >= l) - break; - - char chPrev = headerValue[i - 1]; - char chNext = headerValue[i + k]; - if ((chPrev == ';' || chPrev == ',' || char.IsWhiteSpace(chPrev)) && (chNext == '=' || char.IsWhiteSpace(chNext))) - break; - - i += k; - } - - if (i < 0 || i >= l) - return null; - - // skip to '=' and the following whitespace - i += k; - while (i < l && char.IsWhiteSpace(headerValue[i])) - i++; - if (i >= l || headerValue[i] != '=') - return null; - i++; - while (i < l && char.IsWhiteSpace(headerValue[i])) - i++; - if (i >= l) - return null; - - // parse the value - string attrValue = null; - - int j; - - if (i < l && headerValue[i] == '"') - { - if (i == l - 1) - return null; - j = headerValue.IndexOf('"', i + 1); - if (j < 0 || j == i + 1) - return null; - - attrValue = headerValue.Substring(i + 1, j - i - 1).Trim(); - } - else - { - for (j = i; j < l; j++) - { - if (headerValue[j] == ';') - break; - } - - if (j == i) - return null; - - attrValue = headerValue.Substring(i, j - i).Trim(); - } - - return attrValue; - } - - internal static string[] ParseMultivalueHeader(string s) - { - if (s == null) - return null; - - int l = s.Length; - - // collect comma-separated values into list - - var values = new List(); - int i = 0; - - while (i < l) - { - // find next , - int ci = s.IndexOf(',', i); - if (ci < 0) - ci = l; - - // append corresponding server value - values.Add(s.Substring(i, ci - i)); - - // move to next - i = ci + 1; - - // skip leading space - if (i < l && s[i] == ' ') - i++; - } - - // return list as array of strings - - int n = values.Count; - string[] strings; - - // if n is 0 that means s was empty string - - if (n == 0) - { - strings = new string[1]; - strings[0] = string.Empty; - } - else - { - strings = new string[n]; - values.CopyTo(0, strings, 0, n); - } - return strings; - } - - - private static string UrlDecodeStringFromStringInternal(string s, Encoding e) - { - int count = s.Length; - var helper = new UrlDecoder(count, e); - - // go through the string's chars collapsing %XX and %uXXXX and - // appending each char as char, with exception of %XX constructs - // that are appended as bytes - - for (int pos = 0; pos < count; pos++) - { - char ch = s[pos]; - - if (ch == '+') - { - ch = ' '; - } - else if (ch == '%' && pos < count - 2) - { - if (s[pos + 1] == 'u' && pos < count - 5) - { - int h1 = HexToInt(s[pos + 2]); - int h2 = HexToInt(s[pos + 3]); - int h3 = HexToInt(s[pos + 4]); - int h4 = HexToInt(s[pos + 5]); - - if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) - { // valid 4 hex chars - ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4); - pos += 5; - - // only add as char - helper.AddChar(ch); - continue; - } - } - else - { - int h1 = HexToInt(s[pos + 1]); - int h2 = HexToInt(s[pos + 2]); - - if (h1 >= 0 && h2 >= 0) - { // valid 2 hex chars - byte b = (byte)((h1 << 4) | h2); - pos += 2; - - // don't add as char - helper.AddByte(b); - continue; - } - } - } - - if ((ch & 0xFF80) == 0) - helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode - else - helper.AddChar(ch); - } - - return helper.GetString(); - } - - private static int HexToInt(char h) - { - return (h >= '0' && h <= '9') ? h - '0' : - (h >= 'a' && h <= 'f') ? h - 'a' + 10 : - (h >= 'A' && h <= 'F') ? h - 'A' + 10 : - -1; - } - - private class UrlDecoder - { - private int _bufferSize; - - // Accumulate characters in a special array - private int _numChars; - private char[] _charBuffer; - - // Accumulate bytes for decoding into characters in a special array - private int _numBytes; - private byte[] _byteBuffer; - - // Encoding to convert chars to bytes - private Encoding _encoding; - - private void FlushBytes() - { - if (_numBytes > 0) - { - _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); - _numBytes = 0; - } - } - - internal UrlDecoder(int bufferSize, Encoding encoding) - { - _bufferSize = bufferSize; - _encoding = encoding; - - _charBuffer = new char[bufferSize]; - // byte buffer created on demand - } - - internal void AddChar(char ch) - { - if (_numBytes > 0) - FlushBytes(); - - _charBuffer[_numChars++] = ch; - } - - internal void AddByte(byte b) - { - { - if (_byteBuffer == null) - _byteBuffer = new byte[_bufferSize]; - - _byteBuffer[_numBytes++] = b; - } - } - - internal string GetString() - { - if (_numBytes > 0) - FlushBytes(); - - if (_numChars > 0) - return new string(_charBuffer, 0, _numChars); - else - return string.Empty; - } - } - - - internal static void FillFromString(QueryParamCollection nvc, string s, bool urlencoded, Encoding encoding) - { - int l = (s != null) ? s.Length : 0; - int i = (s.Length > 0 && s[0] == '?') ? 1 : 0; - - while (i < l) - { - // find next & while noting first = on the way (and if there are more) - - int si = i; - int ti = -1; - - while (i < l) - { - char ch = s[i]; - - if (ch == '=') - { - if (ti < 0) - ti = i; - } - else if (ch == '&') - { - break; - } - - i++; - } - - // extract the name / value pair - - string name = null; - string value = null; - - if (ti >= 0) - { - name = s.Substring(si, ti - si); - value = s.Substring(ti + 1, i - ti - 1); - } - else - { - value = s.Substring(si, i - si); - } - - // add name / value pair to the collection - - if (urlencoded) - nvc.Add( - name == null ? null : UrlDecodeStringFromStringInternal(name, encoding), - UrlDecodeStringFromStringInternal(value, encoding)); - else - nvc.Add(name, value); - - // trailing '&' - - if (i == l - 1 && s[i] == '&') - nvc.Add(null, ""); - - i++; - } - } - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerRequestUriBuilder.cs b/SocketHttpListener/Net/HttpListenerRequestUriBuilder.cs deleted file mode 100644 index 7b4b619e68..0000000000 --- a/SocketHttpListener/Net/HttpListenerRequestUriBuilder.cs +++ /dev/null @@ -1,443 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Text; - -namespace SocketHttpListener.Net -{ - // We don't use the cooked URL because http.sys unescapes all percent-encoded values. However, - // we also can't just use the raw Uri, since http.sys supports not only Utf-8, but also ANSI/DBCS and - // Unicode code points. System.Uri only supports Utf-8. - // The purpose of this class is to convert all ANSI, DBCS, and Unicode code points into percent encoded - // Utf-8 characters. - internal sealed class HttpListenerRequestUriBuilder - { - private static readonly Encoding s_utf8Encoding = new UTF8Encoding(false, true); - private static readonly Encoding s_ansiEncoding = Encoding.GetEncoding(0, new EncoderExceptionFallback(), new DecoderExceptionFallback()); - - private readonly string _rawUri; - private readonly string _cookedUriScheme; - private readonly string _cookedUriHost; - private readonly string _cookedUriPath; - private readonly string _cookedUriQuery; - - // This field is used to build the final request Uri string from the Uri parts passed to the ctor. - private StringBuilder _requestUriString; - - // The raw path is parsed by looping through all characters from left to right. 'rawOctets' - // is used to store consecutive percent encoded octets as actual byte values: e.g. for path /pa%C3%84th%2F/ - // rawOctets will be set to { 0xC3, 0x84 } when we reach character 't' and it will be { 0x2F } when - // we reach the final '/'. I.e. after a sequence of percent encoded octets ends, we use rawOctets as - // input to the encoding and percent encode the resulting string into UTF-8 octets. - // - // When parsing ANSI (Latin 1) encoded path '/pa%C4th/', %C4 will be added to rawOctets and when - // we reach 't', the content of rawOctets { 0xC4 } will be fed into the ANSI encoding. The resulting - // string 'Ä' will be percent encoded into UTF-8 octets and appended to requestUriString. The final - // path will be '/pa%C3%84th/', where '%C3%84' is the UTF-8 percent encoded character 'Ä'. - private List _rawOctets; - private string _rawPath; - - // Holds the final request Uri. - private Uri _requestUri; - - private HttpListenerRequestUriBuilder(string rawUri, string cookedUriScheme, string cookedUriHost, - string cookedUriPath, string cookedUriQuery) - { - _rawUri = rawUri; - _cookedUriScheme = cookedUriScheme; - _cookedUriHost = cookedUriHost; - _cookedUriPath = AddSlashToAsteriskOnlyPath(cookedUriPath); - _cookedUriQuery = cookedUriQuery ?? string.Empty; - } - - public static Uri GetRequestUri(string rawUri, string cookedUriScheme, string cookedUriHost, - string cookedUriPath, string cookedUriQuery) - { - var builder = new HttpListenerRequestUriBuilder(rawUri, - cookedUriScheme, cookedUriHost, cookedUriPath, cookedUriQuery); - - return builder.Build(); - } - - private Uri Build() - { - BuildRequestUriUsingRawPath(); - - if (_requestUri == null) - { - BuildRequestUriUsingCookedPath(); - } - - return _requestUri; - } - - private void BuildRequestUriUsingCookedPath() - { - bool isValid = Uri.TryCreate(_cookedUriScheme + Uri.SchemeDelimiter + _cookedUriHost + _cookedUriPath + - _cookedUriQuery, UriKind.Absolute, out _requestUri); - - // Creating a Uri from the cooked Uri should really always work: If not, we log at least. - if (!isValid) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_create_uri, _cookedUriScheme, _cookedUriHost, _cookedUriPath, _cookedUriQuery)); - } - } - - private void BuildRequestUriUsingRawPath() - { - bool isValid = false; - - // Initialize 'rawPath' only if really needed; i.e. if we build the request Uri from the raw Uri. - _rawPath = GetPath(_rawUri); - - // Try to check the raw path using first the primary encoding (according to http.sys settings); - // if it fails try the secondary encoding. - ParsingResult result = BuildRequestUriUsingRawPath(GetEncoding(EncodingType.Primary)); - if (result == ParsingResult.EncodingError) - { - Encoding secondaryEncoding = GetEncoding(EncodingType.Secondary); - result = BuildRequestUriUsingRawPath(secondaryEncoding); - } - isValid = (result == ParsingResult.Success) ? true : false; - - // Log that we weren't able to create a Uri from the raw string. - if (!isValid) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_create_uri, _cookedUriScheme, _cookedUriHost, _rawPath, _cookedUriQuery)); - } - } - - private static Encoding GetEncoding(EncodingType type) - { - Debug.Assert((type == EncodingType.Primary) || (type == EncodingType.Secondary), - "Unknown 'EncodingType' value: " + type.ToString()); - - if (type == EncodingType.Secondary) - { - return s_ansiEncoding; - } - else - { - return s_utf8Encoding; - } - } - - private ParsingResult BuildRequestUriUsingRawPath(Encoding encoding) - { - Debug.Assert(encoding != null, "'encoding' must be assigned."); - Debug.Assert(!string.IsNullOrEmpty(_rawPath), "'rawPath' must have at least one character."); - - _rawOctets = new List(); - _requestUriString = new StringBuilder(); - _requestUriString.Append(_cookedUriScheme); - _requestUriString.Append(Uri.SchemeDelimiter); - _requestUriString.Append(_cookedUriHost); - - ParsingResult result = ParseRawPath(encoding); - if (result == ParsingResult.Success) - { - _requestUriString.Append(_cookedUriQuery); - - Debug.Assert(_rawOctets.Count == 0, - "Still raw octets left. They must be added to the result path."); - - if (!Uri.TryCreate(_requestUriString.ToString(), UriKind.Absolute, out _requestUri)) - { - // If we can't create a Uri from the string, this is an invalid string and it doesn't make - // sense to try another encoding. - result = ParsingResult.InvalidString; - } - } - - if (result != ParsingResult.Success) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_raw_path, _rawPath, encoding.EncodingName)); - } - - return result; - } - - private ParsingResult ParseRawPath(Encoding encoding) - { - Debug.Assert(encoding != null, "'encoding' must be assigned."); - - int index = 0; - char current = '\0'; - while (index < _rawPath.Length) - { - current = _rawPath[index]; - if (current == '%') - { - // Assert is enough, since http.sys accepted the request string already. This should never happen. - Debug.Assert(index + 2 < _rawPath.Length, "Expected >=2 characters after '%' (e.g. %2F)"); - - index++; - current = _rawPath[index]; - if (current == 'u' || current == 'U') - { - // We found "%u" which means, we have a Unicode code point of the form "%uXXXX". - Debug.Assert(index + 4 < _rawPath.Length, "Expected >=4 characters after '%u' (e.g. %u0062)"); - - // Decode the content of rawOctets into percent encoded UTF-8 characters and append them - // to requestUriString. - if (!EmptyDecodeAndAppendRawOctetsList(encoding)) - { - return ParsingResult.EncodingError; - } - if (!AppendUnicodeCodePointValuePercentEncoded(_rawPath.Substring(index + 1, 4))) - { - return ParsingResult.InvalidString; - } - index += 5; - } - else - { - // We found '%', but not followed by 'u', i.e. we have a percent encoded octed: %XX - if (!AddPercentEncodedOctetToRawOctetsList(encoding, _rawPath.Substring(index, 2))) - { - return ParsingResult.InvalidString; - } - index += 2; - } - } - else - { - // We found a non-'%' character: decode the content of rawOctets into percent encoded - // UTF-8 characters and append it to the result. - if (!EmptyDecodeAndAppendRawOctetsList(encoding)) - { - return ParsingResult.EncodingError; - } - // Append the current character to the result. - _requestUriString.Append(current); - index++; - } - } - - // if the raw path ends with a sequence of percent encoded octets, make sure those get added to the - // result (requestUriString). - if (!EmptyDecodeAndAppendRawOctetsList(encoding)) - { - return ParsingResult.EncodingError; - } - - return ParsingResult.Success; - } - - private bool AppendUnicodeCodePointValuePercentEncoded(string codePoint) - { - // http.sys only supports %uXXXX (4 hex-digits), even though unicode code points could have up to - // 6 hex digits. Therefore we parse always 4 characters after %u and convert them to an int. - if (!int.TryParse(codePoint, NumberStyles.HexNumber, null, out var codePointValue)) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_percent_value, codePoint)); - return false; - } - - string unicodeString = null; - try - { - unicodeString = char.ConvertFromUtf32(codePointValue); - AppendOctetsPercentEncoded(_requestUriString, s_utf8Encoding.GetBytes(unicodeString)); - - return true; - } - catch (ArgumentOutOfRangeException) - { - //if (NetEventSource.IsEnabled) - // NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_percent_value, codePoint)); - } - catch (EncoderFallbackException) - { - // If utf8Encoding.GetBytes() fails - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_to_utf8, unicodeString, e.Message)); - } - - return false; - } - - private bool AddPercentEncodedOctetToRawOctetsList(Encoding encoding, string escapedCharacter) - { - if (!byte.TryParse(escapedCharacter, NumberStyles.HexNumber, null, out byte encodedValue)) - { - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_percent_value, escapedCharacter)); - return false; - } - - _rawOctets.Add(encodedValue); - - return true; - } - - private bool EmptyDecodeAndAppendRawOctetsList(Encoding encoding) - { - if (_rawOctets.Count == 0) - { - return true; - } - - string decodedString = null; - try - { - // If the encoding can get a string out of the byte array, this is a valid string in the - // 'encoding' encoding. - decodedString = encoding.GetString(_rawOctets.ToArray()); - - if (encoding == s_utf8Encoding) - { - AppendOctetsPercentEncoded(_requestUriString, _rawOctets.ToArray()); - } - else - { - AppendOctetsPercentEncoded(_requestUriString, s_utf8Encoding.GetBytes(decodedString)); - } - - _rawOctets.Clear(); - - return true; - } - catch (DecoderFallbackException) - { - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_bytes, GetOctetsAsString(_rawOctets), e.Message)); - } - catch (EncoderFallbackException) - { - // If utf8Encoding.GetBytes() fails - //if (NetEventSource.IsEnabled) NetEventSource.Error(this, SR.Format(SR.net_log_listener_cant_convert_to_utf8, decodedString, e.Message)); - } - - return false; - } - - private static void AppendOctetsPercentEncoded(StringBuilder target, IEnumerable octets) - { - foreach (byte octet in octets) - { - target.Append('%'); - target.Append(octet.ToString("X2", CultureInfo.InvariantCulture)); - } - } - - private static string GetOctetsAsString(IEnumerable octets) - { - var octetString = new StringBuilder(); - - bool first = true; - foreach (byte octet in octets) - { - if (first) - { - first = false; - } - else - { - octetString.Append(' '); - } - octetString.Append(octet.ToString("X2", CultureInfo.InvariantCulture)); - } - - return octetString.ToString(); - } - - private static string GetPath(string uriString) - { - Debug.Assert(uriString != null, "uriString must not be null"); - Debug.Assert(uriString.Length > 0, "uriString must not be empty"); - - int pathStartIndex = 0; - - // Perf. improvement: nearly all strings are relative Uris. So just look if the - // string starts with '/'. If so, we have a relative Uri and the path starts at position 0. - // (http.sys already trimmed leading whitespaces) - if (uriString[0] != '/') - { - // We can't check against cookedUriScheme, since http.sys allows for request http://myserver/ to - // use a request line 'GET https://myserver/' (note http vs. https). Therefore check if the - // Uri starts with either http:// or https://. - int authorityStartIndex = 0; - if (uriString.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) - { - authorityStartIndex = 7; - } - else if (uriString.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - authorityStartIndex = 8; - } - - if (authorityStartIndex > 0) - { - // we have an absolute Uri. Find out where the authority ends and the path begins. - // Note that Uris like "http://server?query=value/1/2" are invalid according to RFC2616 - // and http.sys behavior: If the Uri contains a query, there must be at least one '/' - // between the authority and the '?' character: It's safe to just look for the first - // '/' after the authority to determine the beginning of the path. - pathStartIndex = uriString.IndexOf('/', authorityStartIndex); - if (pathStartIndex == -1) - { - // e.g. for request lines like: 'GET http://myserver' (no final '/') - pathStartIndex = uriString.Length; - } - } - else - { - // RFC2616: Request-URI = "*" | absoluteURI | abs_path | authority - // 'authority' can only be used with CONNECT which is never received by HttpListener. - // I.e. if we don't have an absolute path (must start with '/') and we don't have - // an absolute Uri (must start with http:// or https://), then 'uriString' must be '*'. - Debug.Assert((uriString.Length == 1) && (uriString[0] == '*'), "Unknown request Uri string format", - "Request Uri string is not an absolute Uri, absolute path, or '*': {0}", uriString); - - // Should we ever get here, be consistent with 2.0/3.5 behavior: just add an initial - // slash to the string and treat it as a path: - uriString = "/" + uriString; - } - } - - // Find end of path: The path is terminated by - // - the first '?' character - // - the first '#' character: This is never the case here, since http.sys won't accept - // Uris containing fragments. Also, RFC2616 doesn't allow fragments in request Uris. - // - end of Uri string - int queryIndex = uriString.IndexOf('?'); - if (queryIndex == -1) - { - queryIndex = uriString.Length; - } - - // will always return a != null string. - return AddSlashToAsteriskOnlyPath(uriString.Substring(pathStartIndex, queryIndex - pathStartIndex)); - } - - private static string AddSlashToAsteriskOnlyPath(string path) - { - Debug.Assert(path != null, "'path' must not be null"); - - // If a request like "OPTIONS * HTTP/1.1" is sent to the listener, then the request Uri - // should be "http[s]://server[:port]/*" to be compatible with pre-4.0 behavior. - if ((path.Length == 1) && (path[0] == '*')) - { - return "/*"; - } - - return path; - } - - private enum ParsingResult - { - Success, - InvalidString, - EncodingError - } - - private enum EncodingType - { - Primary, - Secondary - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerResponse.Managed.cs b/SocketHttpListener/Net/HttpListenerResponse.Managed.cs deleted file mode 100644 index f595fce7cc..0000000000 --- a/SocketHttpListener/Net/HttpListenerResponse.Managed.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerResponse : IDisposable - { - private long _contentLength; - private Version _version = HttpVersion.Version11; - private int _statusCode = 200; - internal object _headersLock = new object(); - private bool _forceCloseChunked; - - internal HttpListenerResponse(HttpListenerContext context) - { - _httpContext = context; - } - - internal bool ForceCloseChunked => _forceCloseChunked; - - private void EnsureResponseStream() - { - if (_responseStream == null) - { - _responseStream = _httpContext.Connection.GetResponseStream(); - } - } - - public Version ProtocolVersion - { - get => _version; - set - { - CheckDisposed(); - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) - { - throw new ArgumentException("Wrong version"); - } - - _version = new Version(value.Major, value.Minor); // match Windows behavior, trimming to just Major.Minor - } - } - - public int StatusCode - { - get => _statusCode; - set - { - CheckDisposed(); - - if (value < 100 || value > 999) - throw new ProtocolViolationException("Invalid status"); - - _statusCode = value; - } - } - - private void Dispose() - { - Close(true); - } - - public void Close() - { - if (Disposed) - return; - - Close(false); - } - - public void Abort() - { - if (Disposed) - return; - - Close(true); - } - - private void Close(bool force) - { - Disposed = true; - _httpContext.Connection.Close(force); - } - - public void Close(byte[] responseEntity, bool willBlock) - { - CheckDisposed(); - - if (responseEntity == null) - { - throw new ArgumentNullException(nameof(responseEntity)); - } - - if (!SentHeaders && _boundaryType != BoundaryType.Chunked) - { - ContentLength64 = responseEntity.Length; - } - - if (willBlock) - { - try - { - OutputStream.Write(responseEntity, 0, responseEntity.Length); - } - finally - { - Close(false); - } - } - else - { - OutputStream.BeginWrite(responseEntity, 0, responseEntity.Length, iar => - { - var thisRef = (HttpListenerResponse)iar.AsyncState; - try - { - try - { - thisRef.OutputStream.EndWrite(iar); - } - finally - { - thisRef.Close(false); - } - } - catch (Exception) - { - // In case response was disposed during this time - } - }, this); - } - } - - public void CopyFrom(HttpListenerResponse templateResponse) - { - _webHeaders.Clear(); - //_webHeaders.Add(templateResponse._webHeaders); - _contentLength = templateResponse._contentLength; - _statusCode = templateResponse._statusCode; - _statusDescription = templateResponse._statusDescription; - _keepAlive = templateResponse._keepAlive; - _version = templateResponse._version; - } - - internal void SendHeaders(bool closing, MemoryStream ms, bool isWebSocketHandshake = false) - { - if (!isWebSocketHandshake) - { - if (_webHeaders["Server"] == null) - { - _webHeaders.Set("Server", "Microsoft-NetCore/2.0"); - } - - if (_webHeaders["Date"] == null) - { - _webHeaders.Set("Date", DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture)); - } - - if (_boundaryType == BoundaryType.None) - { - if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10) - { - _keepAlive = false; - } - else - { - _boundaryType = BoundaryType.Chunked; - } - - if (CanSendResponseBody(_httpContext.Response.StatusCode)) - { - _contentLength = -1; - } - else - { - _boundaryType = BoundaryType.ContentLength; - _contentLength = 0; - } - } - - if (_boundaryType != BoundaryType.Chunked) - { - if (_boundaryType != BoundaryType.ContentLength && closing) - { - _contentLength = CanSendResponseBody(_httpContext.Response.StatusCode) ? -1 : 0; - } - - if (_boundaryType == BoundaryType.ContentLength) - { - _webHeaders.Set("Content-Length", _contentLength.ToString("D", CultureInfo.InvariantCulture)); - } - } - - /* Apache forces closing the connection for these status codes: - * HttpStatusCode.BadRequest 400 - * HttpStatusCode.RequestTimeout 408 - * HttpStatusCode.LengthRequired 411 - * HttpStatusCode.RequestEntityTooLarge 413 - * HttpStatusCode.RequestUriTooLong 414 - * HttpStatusCode.InternalServerError 500 - * HttpStatusCode.ServiceUnavailable 503 - */ - bool conn_close = (_statusCode == (int)HttpStatusCode.BadRequest || _statusCode == (int)HttpStatusCode.RequestTimeout - || _statusCode == (int)HttpStatusCode.LengthRequired || _statusCode == (int)HttpStatusCode.RequestEntityTooLarge - || _statusCode == (int)HttpStatusCode.RequestUriTooLong || _statusCode == (int)HttpStatusCode.InternalServerError - || _statusCode == (int)HttpStatusCode.ServiceUnavailable); - - if (!conn_close) - { - conn_close = !_httpContext.Request.KeepAlive; - } - - // They sent both KeepAlive: true and Connection: close - if (!_keepAlive || conn_close) - { - _webHeaders.Set("Connection", "Close"); - conn_close = true; - } - - if (SendChunked) - { - _webHeaders.Set("Transfer-Encoding", "Chunked"); - } - - int reuses = _httpContext.Connection.Reuses; - if (reuses >= 100) - { - _forceCloseChunked = true; - if (!conn_close) - { - _webHeaders.Set("Connection", "Close"); - conn_close = true; - } - } - - if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10) - { - if (_keepAlive) - { - Headers["Keep-Alive"] = "true"; - } - - if (!conn_close) - { - _webHeaders.Set("Connection", "Keep-Alive"); - } - } - - ComputeCookies(); - } - - var encoding = Encoding.UTF8; - var writer = new StreamWriter(ms, encoding, 256); - writer.Write("HTTP/1.1 {0} ", _statusCode); // "1.1" matches Windows implementation, which ignores the response version - writer.Flush(); - byte[] statusDescriptionBytes = WebHeaderEncoding.GetBytes(StatusDescription); - ms.Write(statusDescriptionBytes, 0, statusDescriptionBytes.Length); - writer.Write("\r\n"); - - writer.Write(FormatHeaders(_webHeaders)); - writer.Flush(); - int preamble = encoding.GetPreamble().Length; - EnsureResponseStream(); - - /* Assumes that the ms was at position 0 */ - ms.Position = preamble; - SentHeaders = !isWebSocketHandshake; - } - - private static bool HeaderCanHaveEmptyValue(string name) => - !string.Equals(name, "Location", StringComparison.OrdinalIgnoreCase); - - private static string FormatHeaders(WebHeaderCollection headers) - { - var sb = new StringBuilder(); - - for (int i = 0; i < headers.Count; i++) - { - string key = headers.GetKey(i); - string[] values = headers.GetValues(i); - - int startingLength = sb.Length; - - sb.Append(key).Append(": "); - bool anyValues = false; - for (int j = 0; j < values.Length; j++) - { - string value = values[j]; - if (!string.IsNullOrWhiteSpace(value)) - { - if (anyValues) - { - sb.Append(", "); - } - sb.Append(value); - anyValues = true; - } - } - - if (anyValues || HeaderCanHaveEmptyValue(key)) - { - // Complete the header - sb.Append("\r\n"); - } - else - { - // Empty header; remove it. - sb.Length = startingLength; - } - } - - return sb.Append("\r\n").ToString(); - } - - private bool Disposed { get; set; } - internal bool SentHeaders { get; set; } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); - } - } -} diff --git a/SocketHttpListener/Net/HttpListenerResponse.cs b/SocketHttpListener/Net/HttpListenerResponse.cs deleted file mode 100644 index 66ef885643..0000000000 --- a/SocketHttpListener/Net/HttpListenerResponse.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Text; - -namespace SocketHttpListener.Net -{ - public sealed partial class HttpListenerResponse : IDisposable - { - private BoundaryType _boundaryType = BoundaryType.None; - private CookieCollection _cookies; - private HttpListenerContext _httpContext; - private bool _keepAlive = true; - private HttpResponseStream _responseStream; - private string _statusDescription; - private WebHeaderCollection _webHeaders = new WebHeaderCollection(); - - public WebHeaderCollection Headers => _webHeaders; - - public Encoding ContentEncoding { get; set; } - - public string ContentType - { - get => Headers["Content-Type"]; - set - { - CheckDisposed(); - if (string.IsNullOrEmpty(value)) - { - Headers.Remove("Content-Type"); - } - else - { - Headers.Set("Content-Type", value); - } - } - } - - private HttpListenerContext HttpListenerContext => _httpContext; - - private HttpListenerRequest HttpListenerRequest => HttpListenerContext.Request; - - internal EntitySendFormat EntitySendFormat - { - get => (EntitySendFormat)_boundaryType; - set - { - CheckDisposed(); - CheckSentHeaders(); - if (value == EntitySendFormat.Chunked && HttpListenerRequest.ProtocolVersion.Minor == 0) - { - throw new ProtocolViolationException("net_nochunkuploadonhttp10"); - } - _boundaryType = (BoundaryType)value; - if (value != EntitySendFormat.ContentLength) - { - _contentLength = -1; - } - } - } - - public bool SendChunked - { - get => EntitySendFormat == EntitySendFormat.Chunked; - set => EntitySendFormat = value ? EntitySendFormat.Chunked : EntitySendFormat.ContentLength; - } - - // We MUST NOT send message-body when we send responses with these Status codes - private static readonly int[] s_noResponseBody = { 100, 101, 204, 205, 304 }; - - private static bool CanSendResponseBody(int responseCode) - { - for (int i = 0; i < s_noResponseBody.Length; i++) - { - if (responseCode == s_noResponseBody[i]) - { - return false; - } - } - return true; - } - - public long ContentLength64 - { - get => _contentLength; - set - { - CheckDisposed(); - CheckSentHeaders(); - if (value >= 0) - { - _contentLength = value; - _boundaryType = BoundaryType.ContentLength; - } - else - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - } - } - - public CookieCollection Cookies - { - get => _cookies ?? (_cookies = new CookieCollection()); - set => _cookies = value; - } - - public bool KeepAlive - { - get => _keepAlive; - set - { - CheckDisposed(); - _keepAlive = value; - } - } - - public Stream OutputStream - { - get - { - CheckDisposed(); - EnsureResponseStream(); - return _responseStream; - } - } - - public string RedirectLocation - { - get => Headers["Location"]; - set - { - // note that this doesn't set the status code to a redirect one - CheckDisposed(); - if (string.IsNullOrEmpty(value)) - { - Headers.Remove("Location"); - } - else - { - Headers.Set("Location", value); - } - } - } - - public string StatusDescription - { - get - { - if (_statusDescription == null) - { - // if the user hasn't set this, generated on the fly, if possible. - // We know this one is safe, no need to verify it as in the setter. - _statusDescription = HttpStatusDescription.Get(StatusCode); - } - if (_statusDescription == null) - { - _statusDescription = string.Empty; - } - return _statusDescription; - } - set - { - CheckDisposed(); - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - // Need to verify the status description doesn't contain any control characters except HT. We mask off the high - // byte since that's how it's encoded. - for (int i = 0; i < value.Length; i++) - { - char c = (char)(0x000000ff & (uint)value[i]); - if ((c <= 31 && c != (byte)'\t') || c == 127) - { - throw new ArgumentException("net_WebHeaderInvalidControlChars"); - } - } - - _statusDescription = value; - } - } - - public void AddHeader(string name, string value) - { - Headers.Set(name, value); - } - - public void AppendHeader(string name, string value) - { - Headers.Add(name, value); - } - - public void AppendCookie(Cookie cookie) - { - if (cookie == null) - { - throw new ArgumentNullException(nameof(cookie)); - } - Cookies.Add(cookie); - } - - private void ComputeCookies() - { - if (_cookies != null) - { - // now go through the collection, and concatenate all the cookies in per-variant strings - //string setCookie2 = null, setCookie = null; - //for (int index = 0; index < _cookies.Count; index++) - //{ - // Cookie cookie = _cookies[index]; - // string cookieString = cookie.ToServerString(); - // if (cookieString == null || cookieString.Length == 0) - // { - // continue; - // } - - // if (cookie.IsRfc2965Variant()) - // { - // setCookie2 = setCookie2 == null ? cookieString : setCookie2 + ", " + cookieString; - // } - // else - // { - // setCookie = setCookie == null ? cookieString : setCookie + ", " + cookieString; - // } - //} - - //if (!string.IsNullOrEmpty(setCookie)) - //{ - // Headers.Set(HttpKnownHeaderNames.SetCookie, setCookie); - // if (string.IsNullOrEmpty(setCookie2)) - // { - // Headers.Remove(HttpKnownHeaderNames.SetCookie2); - // } - //} - - //if (!string.IsNullOrEmpty(setCookie2)) - //{ - // Headers.Set(HttpKnownHeaderNames.SetCookie2, setCookie2); - // if (string.IsNullOrEmpty(setCookie)) - // { - // Headers.Remove(HttpKnownHeaderNames.SetCookie); - // } - //} - } - } - - public void Redirect(string url) - { - Headers["Location"] = url; - StatusCode = (int)HttpStatusCode.Redirect; - StatusDescription = "Found"; - } - - public void SetCookie(Cookie cookie) - { - if (cookie == null) - { - throw new ArgumentNullException(nameof(cookie)); - } - - //Cookie newCookie = cookie.Clone(); - //int added = Cookies.InternalAdd(newCookie, true); - - //if (added != 1) - //{ - // // The Cookie already existed and couldn't be replaced. - // throw new ArgumentException("Cookie exists"); - //} - } - - void IDisposable.Dispose() - { - Dispose(); - } - - private void CheckDisposed() - { - if (Disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - } - - private void CheckSentHeaders() - { - if (SentHeaders) - { - throw new InvalidOperationException(); - } - } - } -} diff --git a/SocketHttpListener/Net/HttpRequestStream.Managed.cs b/SocketHttpListener/Net/HttpRequestStream.Managed.cs deleted file mode 100644 index 42fc4d97c2..0000000000 --- a/SocketHttpListener/Net/HttpRequestStream.Managed.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.IO; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal partial class HttpRequestStream : Stream - { - private byte[] _buffer; - private int _offset; - private int _length; - private long _remainingBody; - protected bool _closed; - private Stream _stream; - - internal HttpRequestStream(Stream stream, byte[] buffer, int offset, int length) - : this(stream, buffer, offset, length, -1) - { - } - - internal HttpRequestStream(Stream stream, byte[] buffer, int offset, int length, long contentlength) - { - _stream = stream; - _buffer = buffer; - _offset = offset; - _length = length; - _remainingBody = contentlength; - } - - // Returns 0 if we can keep reading from the base stream, - // > 0 if we read something from the buffer. - // -1 if we had a content length set and we finished reading that many bytes. - private int FillFromBuffer(byte[] buffer, int offset, int count) - { - if (_remainingBody == 0) - return -1; - - if (_length == 0) - return 0; - - int size = Math.Min(_length, count); - if (_remainingBody > 0) - size = (int)Math.Min(size, _remainingBody); - - if (_offset > _buffer.Length - size) - { - size = Math.Min(size, _buffer.Length - _offset); - } - if (size == 0) - return 0; - - Buffer.BlockCopy(_buffer, _offset, buffer, offset, size); - _offset += size; - _length -= size; - if (_remainingBody > 0) - _remainingBody -= size; - return size; - } - - protected virtual int ReadCore(byte[] buffer, int offset, int size) - { - // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0 - int nread = FillFromBuffer(buffer, offset, size); - if (nread == -1) - { // No more bytes available (Content-Length) - return 0; - } - else if (nread > 0) - { - return nread; - } - - if (_remainingBody > 0) - { - size = (int)Math.Min(_remainingBody, (long)size); - } - - nread = _stream.Read(buffer, offset, size); - - if (_remainingBody > 0) - { - if (nread == 0) - { - throw new Exception("Bad request"); - } - - //Debug.Assert(nread <= _remainingBody); - _remainingBody -= nread; - } - - return nread; - } - - protected virtual IAsyncResult BeginReadCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state) - { - if (size == 0 || _closed) - { - var ares = new HttpStreamAsyncResult(this); - ares._callback = cback; - ares._state = state; - ares.Complete(); - return ares; - } - - int nread = FillFromBuffer(buffer, offset, size); - if (nread > 0 || nread == -1) - { - var ares = new HttpStreamAsyncResult(this); - ares._buffer = buffer; - ares._offset = offset; - ares._count = size; - ares._callback = cback; - ares._state = state; - ares._synchRead = Math.Max(0, nread); - ares.Complete(); - return ares; - } - - // Avoid reading past the end of the request to allow - // for HTTP pipelining - if (_remainingBody >= 0 && size > _remainingBody) - { - size = (int)Math.Min(_remainingBody, (long)size); - } - - return _stream.BeginRead(buffer, offset, size, cback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - if (asyncResult == null) - throw new ArgumentNullException(nameof(asyncResult)); - - var r = asyncResult as HttpStreamAsyncResult; - if (r != null) - { - if (!ReferenceEquals(this, r._parent)) - { - throw new ArgumentException("Invalid async result"); - } - if (r._endCalled) - { - throw new InvalidOperationException("invalid end call"); - } - r._endCalled = true; - - if (!asyncResult.IsCompleted) - { - asyncResult.AsyncWaitHandle.WaitOne(); - } - - return r._synchRead; - } - - if (_closed) - return 0; - - int nread = 0; - try - { - nread = _stream.EndRead(asyncResult); - } - catch (IOException e) when (e.InnerException is ArgumentException || e.InnerException is InvalidOperationException) - { - throw e.InnerException; - } - - if (_remainingBody > 0) - { - if (nread == 0) - { - throw new Exception("Bad request"); - } - - _remainingBody -= nread; - } - - return nread; - } - } -} diff --git a/SocketHttpListener/Net/HttpRequestStream.cs b/SocketHttpListener/Net/HttpRequestStream.cs deleted file mode 100644 index 1c554df20b..0000000000 --- a/SocketHttpListener/Net/HttpRequestStream.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal partial class HttpRequestStream : Stream - { - public override bool CanSeek => false; - public override bool CanWrite => false; - public override bool CanRead => true; - - public override int Read(byte[] buffer, int offset, int size) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - if (size == 0 || _closed) - { - return 0; - } - - return ReadCore(buffer, offset, size); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - - return BeginReadCore(buffer, offset, size, callback, state); - } - - public override void Flush() { } - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - - set => throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return base.BeginWrite(buffer, offset, count, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - base.EndWrite(asyncResult); - } - - internal bool Closed => _closed; - - protected override void Dispose(bool disposing) - { - _closed = true; - base.Dispose(disposing); - } - } -} diff --git a/SocketHttpListener/Net/HttpResponseStream.Managed.cs b/SocketHttpListener/Net/HttpResponseStream.Managed.cs deleted file mode 100644 index 5d02a9c956..0000000000 --- a/SocketHttpListener/Net/HttpResponseStream.Managed.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; - -namespace SocketHttpListener.Net -{ - // Licensed to the .NET Foundation under one or more agreements. - // See the LICENSE file in the project root for more information. - // - // System.Net.ResponseStream - // - // Author: - // Gonzalo Paniagua Javier (gonzalo@novell.com) - // - // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - // - // Permission is hereby granted, free of charge, to any person obtaining - // a copy of this software and associated documentation files (the - // "Software"), to deal in the Software without restriction, including - // without limitation the rights to use, copy, modify, merge, publish, - // distribute, sublicense, and/or sell copies of the Software, and to - // permit persons to whom the Software is furnished to do so, subject to - // the following conditions: - // - // The above copyright notice and this permission notice shall be - // included in all copies or substantial portions of the Software. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - // - - internal partial class HttpResponseStream : Stream - { - private HttpListenerResponse _response; - private bool _ignore_errors; - private bool _trailer_sent; - private Stream _stream; - private readonly IStreamHelper _streamHelper; - private readonly Socket _socket; - private readonly bool _supportsDirectSocketAccess; - private readonly IEnvironmentInfo _environment; - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - - internal HttpResponseStream(Stream stream, HttpListenerResponse response, bool ignore_errors, IStreamHelper streamHelper, Socket socket, bool supportsDirectSocketAccess, IEnvironmentInfo environment, IFileSystem fileSystem, ILogger logger) - { - _response = response; - _ignore_errors = ignore_errors; - _streamHelper = streamHelper; - _socket = socket; - _supportsDirectSocketAccess = supportsDirectSocketAccess; - _environment = environment; - _fileSystem = fileSystem; - _logger = logger; - _stream = stream; - } - - private void DisposeCore() - { - byte[] bytes = null; - MemoryStream ms = GetHeaders(true); - bool chunked = _response.SendChunked; - if (_stream.CanWrite) - { - try - { - if (ms != null) - { - long start = ms.Position; - if (chunked && !_trailer_sent) - { - bytes = GetChunkSizeBytes(0, true); - ms.Position = ms.Length; - ms.Write(bytes, 0, bytes.Length); - } - InternalWrite(ms.GetBuffer(), (int)start, (int)(ms.Length - start)); - _trailer_sent = true; - } - else if (chunked && !_trailer_sent) - { - bytes = GetChunkSizeBytes(0, true); - InternalWrite(bytes, 0, bytes.Length); - _trailer_sent = true; - } - } - catch (HttpListenerException) - { - // Ignore error due to connection reset by peer - } - } - _response.Close(); - } - - internal async Task WriteWebSocketHandshakeHeadersAsync() - { - if (_closed) - throw new ObjectDisposedException(GetType().ToString()); - - if (_stream.CanWrite) - { - MemoryStream ms = GetHeaders(closing: false, isWebSocketHandshake: true); - bool chunked = _response.SendChunked; - - long start = ms.Position; - if (chunked) - { - byte[] bytes = GetChunkSizeBytes(0, true); - ms.Position = ms.Length; - ms.Write(bytes, 0, bytes.Length); - } - - await InternalWriteAsync(ms.GetBuffer(), (int)start, (int)(ms.Length - start)).ConfigureAwait(false); - await _stream.FlushAsync().ConfigureAwait(false); - } - } - - private MemoryStream GetHeaders(bool closing, bool isWebSocketHandshake = false) - { - //// SendHeaders works on shared headers - //lock (_response.headers_lock) - //{ - // if (_response.HeadersSent) - // return null; - // var ms = CreateNew(); - // _response.SendHeaders(closing, ms); - // return ms; - //} - - // SendHeaders works on shared headers - lock (_response._headersLock) - { - if (_response.SentHeaders) - { - return null; - } - - MemoryStream ms = new MemoryStream(); - _response.SendHeaders(closing, ms, isWebSocketHandshake); - return ms; - } - } - - private static byte[] s_crlf = new byte[] { 13, 10 }; - private static byte[] GetChunkSizeBytes(int size, bool final) - { - string str = string.Format("{0:x}\r\n{1}", size, final ? "\r\n" : ""); - return Encoding.ASCII.GetBytes(str); - } - - internal void InternalWrite(byte[] buffer, int offset, int count) - { - if (_ignore_errors) - { - try - { - _stream.Write(buffer, offset, count); - } - catch { } - } - else - { - _stream.Write(buffer, offset, count); - } - } - - internal Task InternalWriteAsync(byte[] buffer, int offset, int count) => - _ignore_errors ? InternalWriteIgnoreErrorsAsync(buffer, offset, count) : _stream.WriteAsync(buffer, offset, count); - - private async Task InternalWriteIgnoreErrorsAsync(byte[] buffer, int offset, int count) - { - try { await _stream.WriteAsync(buffer, offset, count).ConfigureAwait(false); } - catch { } - } - - private void WriteCore(byte[] buffer, int offset, int size) - { - if (size == 0) - return; - - byte[] bytes = null; - MemoryStream ms = GetHeaders(false); - bool chunked = _response.SendChunked; - if (ms != null) - { - long start = ms.Position; // After the possible preamble for the encoding - ms.Position = ms.Length; - if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - ms.Write(bytes, 0, bytes.Length); - } - - int new_count = Math.Min(size, 16384 - (int)ms.Position + (int)start); - ms.Write(buffer, offset, new_count); - size -= new_count; - offset += new_count; - InternalWrite(ms.GetBuffer(), (int)start, (int)(ms.Length - start)); - ms.SetLength(0); - ms.Capacity = 0; // 'dispose' the buffer in ms. - } - else if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - InternalWrite(bytes, 0, bytes.Length); - } - - if (size > 0) - InternalWrite(buffer, offset, size); - if (chunked) - InternalWrite(s_crlf, 0, 2); - } - - private IAsyncResult BeginWriteCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state) - { - if (_closed) - { - var ares = new HttpStreamAsyncResult(this); - ares._callback = cback; - ares._state = state; - ares.Complete(); - return ares; - } - - byte[] bytes = null; - MemoryStream ms = GetHeaders(false); - bool chunked = _response.SendChunked; - if (ms != null) - { - long start = ms.Position; - ms.Position = ms.Length; - if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - ms.Write(bytes, 0, bytes.Length); - } - ms.Write(buffer, offset, size); - buffer = ms.GetBuffer(); - offset = (int)start; - size = (int)(ms.Position - start); - } - else if (chunked) - { - bytes = GetChunkSizeBytes(size, false); - InternalWrite(bytes, 0, bytes.Length); - } - - return _stream.BeginWrite(buffer, offset, size, cback, state); - } - - private void EndWriteCore(IAsyncResult asyncResult) - { - if (_closed) - return; - - if (_ignore_errors) - { - try - { - _stream.EndWrite(asyncResult); - if (_response.SendChunked) - _stream.Write(s_crlf, 0, 2); - } - catch { } - } - else - { - _stream.EndWrite(asyncResult); - if (_response.SendChunked) - _stream.Write(s_crlf, 0, 2); - } - } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken); - } - - const int StreamCopyToBufferSize = 81920; - private async Task TransmitFileManaged(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - var allowAsync = _environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows; - - //if (count <= 0) - //{ - // allowAsync = true; - //} - - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsync) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - var targetStream = this; - - if (count > 0) - { - await _streamHelper.CopyToAsync(fs, targetStream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(targetStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} diff --git a/SocketHttpListener/Net/HttpResponseStream.cs b/SocketHttpListener/Net/HttpResponseStream.cs deleted file mode 100644 index 085c2ad0cb..0000000000 --- a/SocketHttpListener/Net/HttpResponseStream.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net -{ - internal sealed partial class HttpResponseStream : Stream - { - private bool _closed; - internal bool Closed => _closed; - - public override bool CanRead => false; - public override bool CanSeek => false; - public override bool CanWrite => true; - - public override void Flush() { } - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - - set => throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return base.BeginRead(buffer, offset, count, callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - return base.EndRead(asyncResult); - } - - public override void Write(byte[] buffer, int offset, int size) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - if (_closed) - { - return; - } - - WriteCore(buffer, offset, size); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if (size < 0 || size > buffer.Length - offset) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - - return BeginWriteCore(buffer, offset, size, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - if (asyncResult == null) - { - throw new ArgumentNullException(nameof(asyncResult)); - } - - EndWriteCore(asyncResult); - } - - protected override void Dispose(bool disposing) - { - try - { - if (disposing) - { - if (_closed) - { - return; - } - _closed = true; - DisposeCore(); - } - } - finally - { - base.Dispose(disposing); - } - } - } -} diff --git a/SocketHttpListener/Net/HttpStatusCode.cs b/SocketHttpListener/Net/HttpStatusCode.cs deleted file mode 100644 index d4bb61b8a7..0000000000 --- a/SocketHttpListener/Net/HttpStatusCode.cs +++ /dev/null @@ -1,321 +0,0 @@ -namespace SocketHttpListener.Net -{ - /// - /// Contains the values of the HTTP status codes. - /// - /// - /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in - /// RFC 2616 for HTTP 1.1. - /// - public enum HttpStatusCode - { - /// - /// Equivalent to status code 100. - /// Indicates that the client should continue with its request. - /// - Continue = 100, - /// - /// Equivalent to status code 101. - /// Indicates that the server is switching the HTTP version or protocol on the connection. - /// - SwitchingProtocols = 101, - /// - /// Equivalent to status code 200. - /// Indicates that the client's request has succeeded. - /// - OK = 200, - /// - /// Equivalent to status code 201. - /// Indicates that the client's request has been fulfilled and resulted in a new resource being - /// created. - /// - Created = 201, - /// - /// Equivalent to status code 202. - /// Indicates that the client's request has been accepted for processing, but the processing - /// hasn't been completed. - /// - Accepted = 202, - /// - /// Equivalent to status code 203. - /// Indicates that the returned metainformation is from a local or a third-party copy instead of - /// the origin server. - /// - NonAuthoritativeInformation = 203, - /// - /// Equivalent to status code 204. - /// Indicates that the server has fulfilled the client's request but doesn't need to return - /// an entity-body. - /// - NoContent = 204, - /// - /// Equivalent to status code 205. - /// Indicates that the server has fulfilled the client's request, and the user agent should - /// reset the document view which caused the request to be sent. - /// - ResetContent = 205, - /// - /// Equivalent to status code 206. - /// Indicates that the server has fulfilled the partial GET request for the resource. - /// - PartialContent = 206, - /// - /// - /// Equivalent to status code 300. - /// Indicates that the requested resource corresponds to any of multiple representations. - /// - /// - /// MultipleChoices is a synonym for Ambiguous. - /// - /// - MultipleChoices = 300, - /// - /// - /// Equivalent to status code 300. - /// Indicates that the requested resource corresponds to any of multiple representations. - /// - /// - /// Ambiguous is a synonym for MultipleChoices. - /// - /// - Ambiguous = 300, - /// - /// - /// Equivalent to status code 301. - /// Indicates that the requested resource has been assigned a new permanent URI and - /// any future references to this resource should use one of the returned URIs. - /// - /// - /// MovedPermanently is a synonym for Moved. - /// - /// - MovedPermanently = 301, - /// - /// - /// Equivalent to status code 301. - /// Indicates that the requested resource has been assigned a new permanent URI and - /// any future references to this resource should use one of the returned URIs. - /// - /// - /// Moved is a synonym for MovedPermanently. - /// - /// - Moved = 301, - /// - /// - /// Equivalent to status code 302. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// Found is a synonym for Redirect. - /// - /// - Found = 302, - /// - /// - /// Equivalent to status code 302. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// Redirect is a synonym for Found. - /// - /// - Redirect = 302, - /// - /// - /// Equivalent to status code 303. - /// Indicates that the response to the request can be found under a different URI and - /// should be retrieved using a GET method on that resource. - /// - /// - /// SeeOther is a synonym for RedirectMethod. - /// - /// - SeeOther = 303, - /// - /// - /// Equivalent to status code 303. - /// Indicates that the response to the request can be found under a different URI and - /// should be retrieved using a GET method on that resource. - /// - /// - /// RedirectMethod is a synonym for SeeOther. - /// - /// - RedirectMethod = 303, - /// - /// Equivalent to status code 304. - /// Indicates that the client has performed a conditional GET request and access is allowed, - /// but the document hasn't been modified. - /// - NotModified = 304, - /// - /// Equivalent to status code 305. - /// Indicates that the requested resource must be accessed through the proxy given by - /// the Location field. - /// - UseProxy = 305, - /// - /// Equivalent to status code 306. - /// This status code was used in a previous version of the specification, is no longer used, - /// and is reserved for future use. - /// - Unused = 306, - /// - /// - /// Equivalent to status code 307. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// TemporaryRedirect is a synonym for RedirectKeepVerb. - /// - /// - TemporaryRedirect = 307, - /// - /// - /// Equivalent to status code 307. - /// Indicates that the requested resource is located temporarily under a different URI. - /// - /// - /// RedirectKeepVerb is a synonym for TemporaryRedirect. - /// - /// - RedirectKeepVerb = 307, - /// - /// Equivalent to status code 400. - /// Indicates that the client's request couldn't be understood by the server due to - /// malformed syntax. - /// - BadRequest = 400, - /// - /// Equivalent to status code 401. - /// Indicates that the client's request requires user authentication. - /// - Unauthorized = 401, - /// - /// Equivalent to status code 402. - /// This status code is reserved for future use. - /// - PaymentRequired = 402, - /// - /// Equivalent to status code 403. - /// Indicates that the server understood the client's request but is refusing to fulfill it. - /// - Forbidden = 403, - /// - /// Equivalent to status code 404. - /// Indicates that the server hasn't found anything matching the request URI. - /// - NotFound = 404, - /// - /// Equivalent to status code 405. - /// Indicates that the method specified in the request line isn't allowed for the resource - /// identified by the request URI. - /// - MethodNotAllowed = 405, - /// - /// Equivalent to status code 406. - /// Indicates that the server doesn't have the appropriate resource to respond to the Accept - /// headers in the client's request. - /// - NotAcceptable = 406, - /// - /// Equivalent to status code 407. - /// Indicates that the client must first authenticate itself with the proxy. - /// - ProxyAuthenticationRequired = 407, - /// - /// Equivalent to status code 408. - /// Indicates that the client didn't produce a request within the time that the server was - /// prepared to wait. - /// - RequestTimeout = 408, - /// - /// Equivalent to status code 409. - /// Indicates that the client's request couldn't be completed due to a conflict on the server. - /// - Conflict = 409, - /// - /// Equivalent to status code 410. - /// Indicates that the requested resource is no longer available at the server and - /// no forwarding address is known. - /// - Gone = 410, - /// - /// Equivalent to status code 411. - /// Indicates that the server refuses to accept the client's request without a defined - /// Content-Length. - /// - LengthRequired = 411, - /// - /// Equivalent to status code 412. - /// Indicates that the precondition given in one or more of the request headers evaluated to - /// false when it was tested on the server. - /// - PreconditionFailed = 412, - /// - /// Equivalent to status code 413. - /// Indicates that the entity of the client's request is larger than the server is willing or - /// able to process. - /// - RequestEntityTooLarge = 413, - /// - /// Equivalent to status code 414. - /// Indicates that the request URI is longer than the server is willing to interpret. - /// - RequestUriTooLong = 414, - /// - /// Equivalent to status code 415. - /// Indicates that the entity of the client's request is in a format not supported by - /// the requested resource for the requested method. - /// - UnsupportedMediaType = 415, - /// - /// Equivalent to status code 416. - /// Indicates that none of the range specifier values in a Range request header overlap - /// the current extent of the selected resource. - /// - RequestedRangeNotSatisfiable = 416, - /// - /// Equivalent to status code 417. - /// Indicates that the expectation given in an Expect request header couldn't be met by - /// the server. - /// - ExpectationFailed = 417, - /// - /// Equivalent to status code 500. - /// Indicates that the server encountered an unexpected condition which prevented it from - /// fulfilling the client's request. - /// - InternalServerError = 500, - /// - /// Equivalent to status code 501. - /// Indicates that the server doesn't support the functionality required to fulfill the client's - /// request. - /// - NotImplemented = 501, - /// - /// Equivalent to status code 502. - /// Indicates that a gateway or proxy server received an invalid response from the upstream - /// server. - /// - BadGateway = 502, - /// - /// Equivalent to status code 503. - /// Indicates that the server is currently unable to handle the client's request due to - /// a temporary overloading or maintenance of the server. - /// - ServiceUnavailable = 503, - /// - /// Equivalent to status code 504. - /// Indicates that a gateway or proxy server didn't receive a timely response from the upstream - /// server or some other auxiliary server. - /// - GatewayTimeout = 504, - /// - /// Equivalent to status code 505. - /// Indicates that the server doesn't support the HTTP version used in the client's request. - /// - HttpVersionNotSupported = 505, - } -} diff --git a/SocketHttpListener/Net/HttpStatusDescription.cs b/SocketHttpListener/Net/HttpStatusDescription.cs deleted file mode 100644 index a4e42560b9..0000000000 --- a/SocketHttpListener/Net/HttpStatusDescription.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal static class HttpStatusDescription - { - internal static string Get(HttpStatusCode code) - { - return Get((int)code); - } - - internal static string Get(int code) - { - switch (code) - { - case 100: return "Continue"; - case 101: return "Switching Protocols"; - case 102: return "Processing"; - - case 200: return "OK"; - case 201: return "Created"; - case 202: return "Accepted"; - case 203: return "Non-Authoritative Information"; - case 204: return "No Content"; - case 205: return "Reset Content"; - case 206: return "Partial Content"; - case 207: return "Multi-Status"; - - case 300: return "Multiple Choices"; - case 301: return "Moved Permanently"; - case 302: return "Found"; - case 303: return "See Other"; - case 304: return "Not Modified"; - case 305: return "Use Proxy"; - case 307: return "Temporary Redirect"; - - case 400: return "Bad Request"; - case 401: return "Unauthorized"; - case 402: return "Payment Required"; - case 403: return "Forbidden"; - case 404: return "Not Found"; - case 405: return "Method Not Allowed"; - case 406: return "Not Acceptable"; - case 407: return "Proxy Authentication Required"; - case 408: return "Request Timeout"; - case 409: return "Conflict"; - case 410: return "Gone"; - case 411: return "Length Required"; - case 412: return "Precondition Failed"; - case 413: return "Request Entity Too Large"; - case 414: return "Request-Uri Too Long"; - case 415: return "Unsupported Media Type"; - case 416: return "Requested Range Not Satisfiable"; - case 417: return "Expectation Failed"; - case 422: return "Unprocessable Entity"; - case 423: return "Locked"; - case 424: return "Failed Dependency"; - case 426: return "Upgrade Required"; // RFC 2817 - - case 500: return "Internal Server Error"; - case 501: return "Not Implemented"; - case 502: return "Bad Gateway"; - case 503: return "Service Unavailable"; - case 504: return "Gateway Timeout"; - case 505: return "Http Version Not Supported"; - case 507: return "Insufficient Storage"; - } - return null; - } - } -} diff --git a/SocketHttpListener/Net/HttpStreamAsyncResult.cs b/SocketHttpListener/Net/HttpStreamAsyncResult.cs deleted file mode 100644 index 46944c624a..0000000000 --- a/SocketHttpListener/Net/HttpStreamAsyncResult.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net -{ - internal class HttpStreamAsyncResult : IAsyncResult - { - private object _locker = new object(); - private ManualResetEvent _handle; - private bool _completed; - - internal readonly object _parent; - internal byte[] _buffer; - internal int _offset; - internal int _count; - internal AsyncCallback _callback; - internal object _state; - internal int _synchRead; - internal Exception _error; - internal bool _endCalled; - - internal HttpStreamAsyncResult(object parent) - { - _parent = parent; - } - - public void Complete(Exception e) - { - _error = e; - Complete(); - } - - public void Complete() - { - lock (_locker) - { - if (_completed) - return; - - _completed = true; - if (_handle != null) - _handle.Set(); - - if (_callback != null) - Task.Run(() => _callback(this)); - } - } - - public object AsyncState => _state; - - public WaitHandle AsyncWaitHandle - { - get - { - lock (_locker) - { - if (_handle == null) - _handle = new ManualResetEvent(_completed); - } - - return _handle; - } - } - - public bool CompletedSynchronously => false; - - public bool IsCompleted - { - get - { - lock (_locker) - { - return _completed; - } - } - } - } -} diff --git a/SocketHttpListener/Net/HttpVersion.cs b/SocketHttpListener/Net/HttpVersion.cs deleted file mode 100644 index c0839b46d5..0000000000 --- a/SocketHttpListener/Net/HttpVersion.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace SocketHttpListener.Net -{ - // - // - public class HttpVersion - { - - public static readonly Version Version10 = new Version(1, 0); - public static readonly Version Version11 = new Version(1, 1); - - // pretty useless.. - public HttpVersion() { } - } -} diff --git a/SocketHttpListener/Net/ListenerPrefix.cs b/SocketHttpListener/Net/ListenerPrefix.cs deleted file mode 100644 index edfcb89043..0000000000 --- a/SocketHttpListener/Net/ListenerPrefix.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Net; - -namespace SocketHttpListener.Net -{ - internal sealed class ListenerPrefix - { - private string _original; - private string _host; - private ushort _port; - private string _path; - private bool _secure; - private IPAddress[] _addresses; - internal HttpListener _listener; - - public ListenerPrefix(string prefix) - { - _original = prefix; - Parse(prefix); - } - - public override string ToString() - { - return _original; - } - - public IPAddress[] Addresses - { - get => _addresses; - set => _addresses = value; - } - public bool Secure => _secure; - - public string Host => _host; - - public int Port => _port; - - public string Path => _path; - - // Equals and GetHashCode are required to detect duplicates in HttpListenerPrefixCollection. - public override bool Equals(object o) - { - var other = o as ListenerPrefix; - if (other == null) - return false; - - return (_original == other._original); - } - - public override int GetHashCode() - { - return _original.GetHashCode(); - } - - private void Parse(string uri) - { - ushort default_port = 80; - if (uri.StartsWith("https://")) - { - default_port = 443; - _secure = true; - } - - int length = uri.Length; - int start_host = uri.IndexOf(':') + 3; - if (start_host >= length) - throw new ArgumentException("net_listener_host"); - - int colon = uri.IndexOf(':', start_host, length - start_host); - int root; - if (colon > 0) - { - _host = uri.Substring(start_host, colon - start_host); - root = uri.IndexOf('/', colon, length - colon); - _port = (ushort)int.Parse(uri.Substring(colon + 1, root - colon - 1)); - _path = uri.Substring(root); - } - else - { - root = uri.IndexOf('/', start_host, length - start_host); - _host = uri.Substring(start_host, root - start_host); - _port = default_port; - _path = uri.Substring(root); - } - if (_path.Length != 1) - _path = _path.Substring(0, _path.Length - 1); - } - } -} diff --git a/SocketHttpListener/Net/UriScheme.cs b/SocketHttpListener/Net/UriScheme.cs deleted file mode 100644 index 33d1f09db6..0000000000 --- a/SocketHttpListener/Net/UriScheme.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace SocketHttpListener.Net -{ - internal static class UriScheme - { - public const string File = "file"; - public const string Ftp = "ftp"; - public const string Gopher = "gopher"; - public const string Http = "http"; - public const string Https = "https"; - public const string News = "news"; - public const string NetPipe = "net.pipe"; - public const string NetTcp = "net.tcp"; - public const string Nntp = "nntp"; - public const string Mailto = "mailto"; - public const string Ws = "ws"; - public const string Wss = "wss"; - - public const string SchemeDelimiter = "://"; - } -} diff --git a/SocketHttpListener/Net/WebHeaderCollection.cs b/SocketHttpListener/Net/WebHeaderCollection.cs deleted file mode 100644 index 34fca808be..0000000000 --- a/SocketHttpListener/Net/WebHeaderCollection.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net -{ - [ComVisible(true)] - public class WebHeaderCollection : QueryParamCollection - { - [Flags] - internal enum HeaderInfo - { - Request = 1, - Response = 1 << 1, - MultiValue = 1 << 10 - } - - static readonly bool[] allowed_chars = { - false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false, false, false, false, false, true, false, true, true, true, true, false, false, false, true, - true, false, true, true, false, true, true, true, true, true, true, true, true, true, true, false, - false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, - false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, - true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, - false, true, false - }; - - static readonly Dictionary headers; - - static WebHeaderCollection() - { - headers = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "Allow", HeaderInfo.MultiValue }, - { "Accept", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Accept-Charset", HeaderInfo.MultiValue }, - { "Accept-Encoding", HeaderInfo.MultiValue }, - { "Accept-Language", HeaderInfo.MultiValue }, - { "Accept-Ranges", HeaderInfo.MultiValue }, - { "Age", HeaderInfo.Response }, - { "Authorization", HeaderInfo.MultiValue }, - { "Cache-Control", HeaderInfo.MultiValue }, - { "Cookie", HeaderInfo.MultiValue }, - { "Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Content-Encoding", HeaderInfo.MultiValue }, - { "Content-Length", HeaderInfo.Request | HeaderInfo.Response }, - { "Content-Type", HeaderInfo.Request }, - { "Content-Language", HeaderInfo.MultiValue }, - { "Date", HeaderInfo.Request }, - { "Expect", HeaderInfo.Request | HeaderInfo.MultiValue}, - { "Host", HeaderInfo.Request }, - { "If-Match", HeaderInfo.MultiValue }, - { "If-Modified-Since", HeaderInfo.Request }, - { "If-None-Match", HeaderInfo.MultiValue }, - { "Keep-Alive", HeaderInfo.Response }, - { "Pragma", HeaderInfo.MultiValue }, - { "Proxy-Authenticate", HeaderInfo.MultiValue }, - { "Proxy-Authorization", HeaderInfo.MultiValue }, - { "Proxy-Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Range", HeaderInfo.Request | HeaderInfo.MultiValue }, - { "Referer", HeaderInfo.Request }, - { "Set-Cookie", HeaderInfo.MultiValue }, - { "Set-Cookie2", HeaderInfo.MultiValue }, - { "Server", HeaderInfo.Response }, - { "TE", HeaderInfo.MultiValue }, - { "Trailer", HeaderInfo.MultiValue }, - { "Transfer-Encoding", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo.MultiValue }, - { "Translate", HeaderInfo.Request | HeaderInfo.Response }, - { "Upgrade", HeaderInfo.MultiValue }, - { "User-Agent", HeaderInfo.Request }, - { "Vary", HeaderInfo.MultiValue }, - { "Via", HeaderInfo.MultiValue }, - { "Warning", HeaderInfo.MultiValue }, - { "WWW-Authenticate", HeaderInfo.Response | HeaderInfo. MultiValue }, - { "SecWebSocketAccept", HeaderInfo.Response }, - { "SecWebSocketExtensions", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, - { "SecWebSocketKey", HeaderInfo.Request }, - { "Sec-WebSocket-Protocol", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, - { "SecWebSocketVersion", HeaderInfo.Response | HeaderInfo. MultiValue } - }; - } - - // Methods - - public void Add(string header) - { - if (header == null) - throw new ArgumentNullException(nameof(header)); - int pos = header.IndexOf(':'); - if (pos == -1) - throw new ArgumentException("no colon found", nameof(header)); - - this.Add(header.Substring(0, pos), header.Substring(pos + 1)); - } - - public override void Add(string name, string value) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - this.AddWithoutValidate(name, value); - } - - protected void AddWithoutValidate(string headerName, string headerValue) - { - if (!IsHeaderName(headerName)) - throw new ArgumentException("invalid header name: " + headerName, nameof(headerName)); - if (headerValue == null) - headerValue = string.Empty; - else - headerValue = headerValue.Trim(); - if (!IsHeaderValue(headerValue)) - throw new ArgumentException("invalid header value: " + headerValue, nameof(headerValue)); - - AddValue(headerName, headerValue); - } - - internal void AddValue(string headerName, string headerValue) - { - base.Add(headerName, headerValue); - } - - internal List GetValues_internal(string header, bool split) - { - if (header == null) - throw new ArgumentNullException(nameof(header)); - - var values = base.GetValues(header); - if (values == null || values.Count == 0) - return null; - - if (split && IsMultiValue(header)) - { - List separated = null; - foreach (var value in values) - { - if (value.IndexOf(',') < 0) - { - if (separated != null) - separated.Add(value); - - continue; - } - - if (separated == null) - { - separated = new List(values.Count + 1); - foreach (var v in values) - { - if (v == value) - break; - - separated.Add(v); - } - } - - var slices = value.Split(','); - var slices_length = slices.Length; - if (value[value.Length - 1] == ',') - --slices_length; - - for (int i = 0; i < slices_length; ++i) - { - separated.Add(slices[i].Trim()); - } - } - - if (separated != null) - return separated; - } - - return values; - } - - public override List GetValues(string header) - { - return GetValues_internal(header, true); - } - - public override string[] GetValues(int index) - { - string[] values = base.GetValues(index); - - if (values == null || values.Length == 0) - { - return null; - } - - return values; - } - - public static bool IsRestricted(string headerName) - { - return IsRestricted(headerName, false); - } - - public static bool IsRestricted(string headerName, bool response) - { - if (headerName == null) - throw new ArgumentNullException(nameof(headerName)); - - if (headerName.Length == 0) - throw new ArgumentException("empty string", nameof(headerName)); - - if (!IsHeaderName(headerName)) - throw new ArgumentException("Invalid character in header"); - - if (!headers.TryGetValue(headerName, out HeaderInfo info)) - return false; - - var flag = response ? HeaderInfo.Response : HeaderInfo.Request; - return (info & flag) != 0; - } - - public override void Set(string name, string value) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - if (!IsHeaderName(name)) - throw new ArgumentException("invalid header name"); - if (value == null) - value = string.Empty; - else - value = value.Trim(); - if (!IsHeaderValue(value)) - throw new ArgumentException("invalid header value"); - - base.Set(name, value); - } - - internal string ToStringMultiValue() - { - var sb = new StringBuilder(); - - int count = base.Count; - for (int i = 0; i < count; i++) - { - string key = GetKey(i); - if (IsMultiValue(key)) - { - foreach (string v in GetValues(i)) - { - sb.Append(key) - .Append(": ") - .Append(v) - .Append("\r\n"); - } - } - else - { - sb.Append(key) - .Append(": ") - .Append(Get(i)) - .Append("\r\n"); - } - } - return sb.Append("\r\n").ToString(); - } - - public override string ToString() - { - var sb = new StringBuilder(); - - int count = base.Count; - for (int i = 0; i < count; i++) - sb.Append(GetKey(i)) - .Append(": ") - .Append(Get(i)) - .Append("\r\n"); - - return sb.Append("\r\n").ToString(); - } - - - // Internal Methods - - // With this we don't check for invalid characters in header. See bug #55994. - internal void SetInternal(string header) - { - int pos = header.IndexOf(':'); - if (pos == -1) - throw new ArgumentException("no colon found", nameof(header)); - - SetInternal(header.Substring(0, pos), header.Substring(pos + 1)); - } - - internal void SetInternal(string name, string value) - { - if (value == null) - value = string.Empty; - else - value = value.Trim(); - if (!IsHeaderValue(value)) - throw new ArgumentException("invalid header value"); - - if (IsMultiValue(name)) - { - base.Add(name, value); - } - else - { - base.Remove(name); - base.Set(name, value); - } - } - - internal static bool IsMultiValue(string headerName) - { - if (headerName == null) - return false; - - return headers.TryGetValue(headerName, out HeaderInfo info) && (info & HeaderInfo.MultiValue) != 0; - } - - internal static bool IsHeaderValue(string value) - { - // TEXT any 8 bit value except CTL's (0-31 and 127) - // but including \r\n space and \t - // after a newline at least one space or \t must follow - // certain header fields allow comments () - - int len = value.Length; - for (int i = 0; i < len; i++) - { - char c = value[i]; - if (c == 127) - return false; - if (c < 0x20 && (c != '\r' && c != '\n' && c != '\t')) - return false; - if (c == '\n' && ++i < len) - { - c = value[i]; - if (c != ' ' && c != '\t') - return false; - } - } - - return true; - } - - internal static bool IsHeaderName(string name) - { - if (name == null || name.Length == 0) - return false; - - int len = name.Length; - for (int i = 0; i < len; i++) - { - char c = name[i]; - if (c > 126 || !allowed_chars[c]) - return false; - } - - return true; - } - } -} diff --git a/SocketHttpListener/Net/WebHeaderEncoding.cs b/SocketHttpListener/Net/WebHeaderEncoding.cs deleted file mode 100644 index 96e0cc85d8..0000000000 --- a/SocketHttpListener/Net/WebHeaderEncoding.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text; - -namespace SocketHttpListener.Net -{ - // we use this static class as a helper class to encode/decode HTTP headers. - // what we need is a 1-1 correspondence between a char in the range U+0000-U+00FF - // and a byte in the range 0x00-0xFF (which is the range that can hit the network). - // The Latin-1 encoding (ISO-88591-1) (GetEncoding(28591)) works for byte[] to string, but is a little slow. - // It doesn't work for string -> byte[] because of best-fit-mapping problems. - internal static class WebHeaderEncoding - { - // We don't want '?' replacement characters, just fail. - private static readonly Encoding s_utf8Decoder = Encoding.GetEncoding("utf-8", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); - - internal static unsafe string GetString(byte[] bytes, int byteIndex, int byteCount) - { - fixed (byte* pBytes = bytes) - return GetString(pBytes + byteIndex, byteCount); - } - - internal static unsafe string GetString(byte* pBytes, int byteCount) - { - if (byteCount < 1) - return ""; - - string s = new string('\0', byteCount); - - fixed (char* pStr = s) - { - char* pString = pStr; - while (byteCount >= 8) - { - pString[0] = (char)pBytes[0]; - pString[1] = (char)pBytes[1]; - pString[2] = (char)pBytes[2]; - pString[3] = (char)pBytes[3]; - pString[4] = (char)pBytes[4]; - pString[5] = (char)pBytes[5]; - pString[6] = (char)pBytes[6]; - pString[7] = (char)pBytes[7]; - pString += 8; - pBytes += 8; - byteCount -= 8; - } - for (int i = 0; i < byteCount; i++) - { - pString[i] = (char)pBytes[i]; - } - } - - return s; - } - - internal static int GetByteCount(string myString) - { - return myString.Length; - } - internal static unsafe void GetBytes(string myString, int charIndex, int charCount, byte[] bytes, int byteIndex) - { - if (myString.Length == 0) - { - return; - } - fixed (byte* bufferPointer = bytes) - { - byte* newBufferPointer = bufferPointer + byteIndex; - int finalIndex = charIndex + charCount; - while (charIndex < finalIndex) - { - *newBufferPointer++ = (byte)myString[charIndex++]; - } - } - } - internal static byte[] GetBytes(string myString) - { - byte[] bytes = new byte[myString.Length]; - if (myString.Length != 0) - { - GetBytes(myString, 0, myString.Length, bytes, 0); - } - return bytes; - } - } -} diff --git a/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs b/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs deleted file mode 100644 index 5ed49ec47d..0000000000 --- a/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Security.Principal; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net.WebSockets -{ - public class HttpListenerWebSocketContext : WebSocketContext - { - private readonly Uri _requestUri; - private readonly QueryParamCollection _headers; - private readonly CookieCollection _cookieCollection; - private readonly IPrincipal _user; - private readonly bool _isAuthenticated; - private readonly bool _isLocal; - private readonly bool _isSecureConnection; - - private readonly string _origin; - private readonly IEnumerable _secWebSocketProtocols; - private readonly string _secWebSocketVersion; - private readonly string _secWebSocketKey; - - private readonly WebSocket _webSocket; - - internal HttpListenerWebSocketContext( - Uri requestUri, - QueryParamCollection headers, - CookieCollection cookieCollection, - IPrincipal user, - bool isAuthenticated, - bool isLocal, - bool isSecureConnection, - string origin, - IEnumerable secWebSocketProtocols, - string secWebSocketVersion, - string secWebSocketKey, - WebSocket webSocket) - { - _cookieCollection = new CookieCollection(); - _cookieCollection.Add(cookieCollection); - - //_headers = new NameValueCollection(headers); - _headers = headers; - _user = CopyPrincipal(user); - - _requestUri = requestUri; - _isAuthenticated = isAuthenticated; - _isLocal = isLocal; - _isSecureConnection = isSecureConnection; - _origin = origin; - _secWebSocketProtocols = secWebSocketProtocols; - _secWebSocketVersion = secWebSocketVersion; - _secWebSocketKey = secWebSocketKey; - _webSocket = webSocket; - } - - public override Uri RequestUri => _requestUri; - - public override QueryParamCollection Headers => _headers; - - public override string Origin => _origin; - - public override IEnumerable SecWebSocketProtocols => _secWebSocketProtocols; - - public override string SecWebSocketVersion => _secWebSocketVersion; - - public override string SecWebSocketKey => _secWebSocketKey; - - public override CookieCollection CookieCollection => _cookieCollection; - - public override IPrincipal User => _user; - - public override bool IsAuthenticated => _isAuthenticated; - - public override bool IsLocal => _isLocal; - - public override bool IsSecureConnection => _isSecureConnection; - - public override WebSocket WebSocket => _webSocket; - - private static IPrincipal CopyPrincipal(IPrincipal user) - { - if (user != null) - { - throw new NotImplementedException(); - } - - return null; - } - } -} diff --git a/SocketHttpListener/Net/WebSockets/HttpWebSocket.Managed.cs b/SocketHttpListener/Net/WebSockets/HttpWebSocket.Managed.cs deleted file mode 100644 index 1cfd2dc902..0000000000 --- a/SocketHttpListener/Net/WebSockets/HttpWebSocket.Managed.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace SocketHttpListener.Net.WebSockets -{ - internal static partial class HttpWebSocket - { - private const string SupportedVersion = "13"; - - internal static async Task AcceptWebSocketAsyncCore(HttpListenerContext context, - string subProtocol, - int receiveBufferSize, - TimeSpan keepAliveInterval, - ArraySegment? internalBuffer = null) - { - ValidateOptions(subProtocol, receiveBufferSize, MinSendBufferSize, keepAliveInterval); - - // get property will create a new response if one doesn't exist. - HttpListenerResponse response = context.Response; - HttpListenerRequest request = context.Request; - ValidateWebSocketHeaders(context); - - string secWebSocketVersion = request.Headers[HttpKnownHeaderNames.SecWebSocketVersion]; - - // Optional for non-browser client - string origin = request.Headers[HttpKnownHeaderNames.Origin]; - - string[] secWebSocketProtocols = null; - bool shouldSendSecWebSocketProtocolHeader = - ProcessWebSocketProtocolHeader( - request.Headers[HttpKnownHeaderNames.SecWebSocketProtocol], - subProtocol, - out var outgoingSecWebSocketProtocolString); - - if (shouldSendSecWebSocketProtocolHeader) - { - secWebSocketProtocols = new string[] { outgoingSecWebSocketProtocolString }; - response.Headers.Add(HttpKnownHeaderNames.SecWebSocketProtocol, outgoingSecWebSocketProtocolString); - } - - // negotiate the websocket key return value - string secWebSocketKey = request.Headers[HttpKnownHeaderNames.SecWebSocketKey]; - string secWebSocketAccept = HttpWebSocket.GetSecWebSocketAcceptString(secWebSocketKey); - - response.Headers.Add(HttpKnownHeaderNames.Connection, HttpKnownHeaderNames.Upgrade); - response.Headers.Add(HttpKnownHeaderNames.Upgrade, WebSocketUpgradeToken); - response.Headers.Add(HttpKnownHeaderNames.SecWebSocketAccept, secWebSocketAccept); - - response.StatusCode = (int)HttpStatusCode.SwitchingProtocols; // HTTP 101 - response.StatusDescription = HttpStatusDescription.Get(HttpStatusCode.SwitchingProtocols); - - var responseStream = response.OutputStream as HttpResponseStream; - - // Send websocket handshake headers - await responseStream.WriteWebSocketHandshakeHeadersAsync().ConfigureAwait(false); - - //WebSocket webSocket = WebSocket.CreateFromStream(context.Connection.ConnectedStream, isServer: true, subProtocol, keepAliveInterval); - var webSocket = new WebSocket(subProtocol); - - var webSocketContext = new HttpListenerWebSocketContext( - request.Url, - request.Headers, - request.Cookies, - context.User, - request.IsAuthenticated, - request.IsLocal, - request.IsSecureConnection, - origin, - secWebSocketProtocols != null ? secWebSocketProtocols : Array.Empty(), - secWebSocketVersion, - secWebSocketKey, - webSocket); - - webSocket.SetContext(webSocketContext, context.Connection.Close, context.Connection.Stream); - - return webSocketContext; - } - - private const bool WebSocketsSupported = true; - } -} diff --git a/SocketHttpListener/Net/WebSockets/HttpWebSocket.cs b/SocketHttpListener/Net/WebSockets/HttpWebSocket.cs deleted file mode 100644 index b346cc98ec..0000000000 --- a/SocketHttpListener/Net/WebSockets/HttpWebSocket.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using System.Text; -using System.Threading; - -namespace SocketHttpListener.Net.WebSockets -{ - internal static partial class HttpWebSocket - { - internal const string SecWebSocketKeyGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - internal const string WebSocketUpgradeToken = "websocket"; - internal const int DefaultReceiveBufferSize = 16 * 1024; - internal const int DefaultClientSendBufferSize = 16 * 1024; - - [SuppressMessage("Microsoft.Security", "CA5350", Justification = "SHA1 used only for hashing purposes, not for crypto.")] - internal static string GetSecWebSocketAcceptString(string secWebSocketKey) - { - string retVal; - - // SHA1 used only for hashing purposes, not for crypto. Check here for FIPS compat. - using (var sha1 = SHA1.Create()) - { - string acceptString = string.Concat(secWebSocketKey, HttpWebSocket.SecWebSocketKeyGuid); - byte[] toHash = Encoding.UTF8.GetBytes(acceptString); - retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); - } - - return retVal; - } - - // return value here signifies if a Sec-WebSocket-Protocol header should be returned by the server. - internal static bool ProcessWebSocketProtocolHeader(string clientSecWebSocketProtocol, - string subProtocol, - out string acceptProtocol) - { - acceptProtocol = string.Empty; - if (string.IsNullOrEmpty(clientSecWebSocketProtocol)) - { - // client hasn't specified any Sec-WebSocket-Protocol header - if (subProtocol != null) - { - // If the server specified _anything_ this isn't valid. - throw new WebSocketException("UnsupportedProtocol"); - } - // Treat empty and null from the server as the same thing here, server should not send headers. - return false; - } - - // here, we know the client specified something and it's non-empty. - - if (subProtocol == null) - { - // client specified some protocols, server specified 'null'. So server should send headers. - return true; - } - - // here, we know that the client has specified something, it's not empty - // and the server has specified exactly one protocol - - string[] requestProtocols = clientSecWebSocketProtocol.Split(new char[] { ',' }, - StringSplitOptions.RemoveEmptyEntries); - acceptProtocol = subProtocol; - - // client specified protocols, serverOptions has exactly 1 non-empty entry. Check that - // this exists in the list the client specified. - for (int i = 0; i < requestProtocols.Length; i++) - { - string currentRequestProtocol = requestProtocols[i].Trim(); - if (string.Equals(acceptProtocol, currentRequestProtocol, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - throw new WebSocketException("net_WebSockets_AcceptUnsupportedProtocol"); - } - - internal static void ValidateOptions(string subProtocol, int receiveBufferSize, int sendBufferSize, TimeSpan keepAliveInterval) - { - if (subProtocol != null) - { - WebSocketValidate.ValidateSubprotocol(subProtocol); - } - - if (receiveBufferSize < MinReceiveBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(receiveBufferSize), "The receiveBufferSize was too small."); - } - - if (sendBufferSize < MinSendBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(sendBufferSize), "The sendBufferSize was too small."); - } - - if (receiveBufferSize > MaxBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(receiveBufferSize), "The receiveBufferSize was too large."); - } - - if (sendBufferSize > MaxBufferSize) - { - throw new ArgumentOutOfRangeException(nameof(sendBufferSize), "The sendBufferSize was too large."); - } - - if (keepAliveInterval < Timeout.InfiniteTimeSpan) // -1 millisecond - { - throw new ArgumentOutOfRangeException(nameof(keepAliveInterval), "The keepAliveInterval was too small."); - } - } - - internal const int MinSendBufferSize = 16; - internal const int MinReceiveBufferSize = 256; - internal const int MaxBufferSize = 64 * 1024; - - private static void ValidateWebSocketHeaders(HttpListenerContext context) - { - if (!WebSocketsSupported) - { - throw new PlatformNotSupportedException("net_WebSockets_UnsupportedPlatform"); - } - - if (!context.Request.IsWebSocketRequest) - { - throw new WebSocketException("net_WebSockets_AcceptNotAWebSocket"); - } - - string secWebSocketVersion = context.Request.Headers[HttpKnownHeaderNames.SecWebSocketVersion]; - if (string.IsNullOrEmpty(secWebSocketVersion)) - { - throw new WebSocketException("net_WebSockets_AcceptHeaderNotFound"); - } - - if (!string.Equals(secWebSocketVersion, SupportedVersion, StringComparison.OrdinalIgnoreCase)) - { - throw new WebSocketException("net_WebSockets_AcceptUnsupportedWebSocketVersion"); - } - - string secWebSocketKey = context.Request.Headers[HttpKnownHeaderNames.SecWebSocketKey]; - bool isSecWebSocketKeyInvalid = string.IsNullOrWhiteSpace(secWebSocketKey); - if (!isSecWebSocketKeyInvalid) - { - try - { - // key must be 16 bytes then base64-encoded - isSecWebSocketKeyInvalid = Convert.FromBase64String(secWebSocketKey).Length != 16; - } - catch - { - isSecWebSocketKeyInvalid = true; - } - } - if (isSecWebSocketKeyInvalid) - { - throw new WebSocketException("net_WebSockets_AcceptHeaderNotFound"); - } - } - } -} diff --git a/SocketHttpListener/Net/WebSockets/WebSocketCloseStatus.cs b/SocketHttpListener/Net/WebSockets/WebSocketCloseStatus.cs deleted file mode 100644 index 5ac89cf480..0000000000 --- a/SocketHttpListener/Net/WebSockets/WebSocketCloseStatus.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace SocketHttpListener.Net.WebSockets -{ - public enum WebSocketCloseStatus - { - NormalClosure = 1000, - EndpointUnavailable = 1001, - ProtocolError = 1002, - InvalidMessageType = 1003, - Empty = 1005, - // AbnormalClosure = 1006, // 1006 is reserved and should never be used by user - InvalidPayloadData = 1007, - PolicyViolation = 1008, - MessageTooBig = 1009, - MandatoryExtension = 1010, - InternalServerError = 1011 - // TLSHandshakeFailed = 1015, // 1015 is reserved and should never be used by user - - // 0 - 999 Status codes in the range 0-999 are not used. - // 1000 - 1999 Status codes in the range 1000-1999 are reserved for definition by this protocol. - // 2000 - 2999 Status codes in the range 2000-2999 are reserved for use by extensions. - // 3000 - 3999 Status codes in the range 3000-3999 MAY be used by libraries and frameworks. The - // interpretation of these codes is undefined by this protocol. End applications MUST - // NOT use status codes in this range. - // 4000 - 4999 Status codes in the range 4000-4999 MAY be used by application code. The interpretation - // of these codes is undefined by this protocol. - } -} diff --git a/SocketHttpListener/Net/WebSockets/WebSocketContext.cs b/SocketHttpListener/Net/WebSockets/WebSocketContext.cs deleted file mode 100644 index 10ad864398..0000000000 --- a/SocketHttpListener/Net/WebSockets/WebSocketContext.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Security.Principal; -using MediaBrowser.Model.Services; - -namespace SocketHttpListener.Net.WebSockets -{ - public abstract class WebSocketContext - { - public abstract Uri RequestUri { get; } - public abstract QueryParamCollection Headers { get; } - public abstract string Origin { get; } - public abstract IEnumerable SecWebSocketProtocols { get; } - public abstract string SecWebSocketVersion { get; } - public abstract string SecWebSocketKey { get; } - public abstract CookieCollection CookieCollection { get; } - public abstract IPrincipal User { get; } - public abstract bool IsAuthenticated { get; } - public abstract bool IsLocal { get; } - public abstract bool IsSecureConnection { get; } - public abstract WebSocket WebSocket { get; } - } -} diff --git a/SocketHttpListener/Net/WebSockets/WebSocketValidate.cs b/SocketHttpListener/Net/WebSockets/WebSocketValidate.cs deleted file mode 100644 index 0469e3b6c5..0000000000 --- a/SocketHttpListener/Net/WebSockets/WebSocketValidate.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using WebSocketState = System.Net.WebSockets.WebSocketState; - -namespace SocketHttpListener.Net.WebSockets -{ - internal static partial class WebSocketValidate - { - internal const int MaxControlFramePayloadLength = 123; - private const int CloseStatusCodeAbort = 1006; - private const int CloseStatusCodeFailedTLSHandshake = 1015; - private const int InvalidCloseStatusCodesFrom = 0; - private const int InvalidCloseStatusCodesTo = 999; - private const string Separators = "()<>@,;:\\\"/[]?={} "; - - internal static void ThrowIfInvalidState(WebSocketState currentState, bool isDisposed, WebSocketState[] validStates) - { - string validStatesText = string.Empty; - - if (validStates != null && validStates.Length > 0) - { - foreach (WebSocketState validState in validStates) - { - if (currentState == validState) - { - // Ordering is important to maintain .NET 4.5 WebSocket implementation exception behavior. - if (isDisposed) - { - throw new ObjectDisposedException(nameof(WebSocket)); - } - - return; - } - } - - validStatesText = string.Join(", ", validStates); - } - - throw new WebSocketException("net_WebSockets_InvalidState"); - } - - internal static void ValidateSubprotocol(string subProtocol) - { - if (string.IsNullOrWhiteSpace(subProtocol)) - { - throw new ArgumentException("net_WebSockets_InvalidEmptySubProtocol"); - } - - string invalidChar = null; - int i = 0; - while (i < subProtocol.Length) - { - char ch = subProtocol[i]; - if (ch < 0x21 || ch > 0x7e) - { - invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch); - break; - } - - if (!char.IsLetterOrDigit(ch) && - Separators.IndexOf(ch) >= 0) - { - invalidChar = ch.ToString(); - break; - } - - i++; - } - - if (invalidChar != null) - { - throw new ArgumentException("net_WebSockets_InvalidCharInProtocolString"); - } - } - - internal static void ValidateCloseStatus(WebSocketCloseStatus closeStatus, string statusDescription) - { - if (closeStatus == WebSocketCloseStatus.Empty && !string.IsNullOrEmpty(statusDescription)) - { - throw new ArgumentException("net_WebSockets_ReasonNotNull"); - } - - int closeStatusCode = (int)closeStatus; - - if ((closeStatusCode >= InvalidCloseStatusCodesFrom && - closeStatusCode <= InvalidCloseStatusCodesTo) || - closeStatusCode == CloseStatusCodeAbort || - closeStatusCode == CloseStatusCodeFailedTLSHandshake) - { - // CloseStatus 1006 means Aborted - this will never appear on the wire and is reflected by calling WebSocket.Abort - throw new ArgumentException("net_WebSockets_InvalidCloseStatusCode"); - } - - int length = 0; - if (!string.IsNullOrEmpty(statusDescription)) - { - length = Encoding.UTF8.GetByteCount(statusDescription); - } - - if (length > MaxControlFramePayloadLength) - { - throw new ArgumentException("net_WebSockets_InvalidCloseStatusDescription"); - } - } - - internal static void ValidateArraySegment(ArraySegment arraySegment, string parameterName) - { - if (arraySegment.Array == null) - { - throw new ArgumentNullException(parameterName + "." + nameof(arraySegment.Array)); - } - if (arraySegment.Offset < 0 || arraySegment.Offset > arraySegment.Array.Length) - { - throw new ArgumentOutOfRangeException(parameterName + "." + nameof(arraySegment.Offset)); - } - if (arraySegment.Count < 0 || arraySegment.Count > (arraySegment.Array.Length - arraySegment.Offset)) - { - throw new ArgumentOutOfRangeException(parameterName + "." + nameof(arraySegment.Count)); - } - } - - internal static void ValidateBuffer(byte[] buffer, int offset, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0 || count > (buffer.Length - offset)) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - } - } -} diff --git a/SocketHttpListener/Opcode.cs b/SocketHttpListener/Opcode.cs deleted file mode 100644 index 69cf3f3728..0000000000 --- a/SocketHttpListener/Opcode.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace SocketHttpListener -{ - /// - /// Contains the values of the opcode that indicates the type of a WebSocket frame. - /// - /// - /// The values of the opcode are defined in - /// Section 5.2 of RFC 6455. - /// - public enum Opcode : byte - { - /// - /// Equivalent to numeric value 0. - /// Indicates a continuation frame. - /// - Cont = 0x0, - /// - /// Equivalent to numeric value 1. - /// Indicates a text frame. - /// - Text = 0x1, - /// - /// Equivalent to numeric value 2. - /// Indicates a binary frame. - /// - Binary = 0x2, - /// - /// Equivalent to numeric value 8. - /// Indicates a connection close frame. - /// - Close = 0x8, - /// - /// Equivalent to numeric value 9. - /// Indicates a ping frame. - /// - Ping = 0x9, - /// - /// Equivalent to numeric value 10. - /// Indicates a pong frame. - /// - Pong = 0xa - } -} diff --git a/SocketHttpListener/PayloadData.cs b/SocketHttpListener/PayloadData.cs deleted file mode 100644 index 6d15a6bcb1..0000000000 --- a/SocketHttpListener/PayloadData.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; - -namespace SocketHttpListener -{ - internal class PayloadData : IEnumerable - { - #region Private Fields - - private byte[] _applicationData; - private byte[] _extensionData; - private bool _masked; - - #endregion - - #region Public Const Fields - - public const ulong MaxLength = long.MaxValue; - - #endregion - - #region Public Constructors - - public PayloadData() - : this(new byte[0], new byte[0], false) - { - } - - public PayloadData(byte[] applicationData) - : this(new byte[0], applicationData, false) - { - } - - public PayloadData(string applicationData) - : this(new byte[0], Encoding.UTF8.GetBytes(applicationData), false) - { - } - - public PayloadData(byte[] applicationData, bool masked) - : this(new byte[0], applicationData, masked) - { - } - - public PayloadData(byte[] extensionData, byte[] applicationData, bool masked) - { - _extensionData = extensionData; - _applicationData = applicationData; - _masked = masked; - } - - #endregion - - #region Internal Properties - - internal bool ContainsReservedCloseStatusCode => - _applicationData.Length > 1 && - _applicationData.SubArray(0, 2).ToUInt16(ByteOrder.Big).IsReserved(); - - #endregion - - #region Public Properties - - public byte[] ApplicationData => _applicationData; - - public byte[] ExtensionData => _extensionData; - - public bool IsMasked => _masked; - - public ulong Length => (ulong)(_extensionData.Length + _applicationData.Length); - - #endregion - - #region Private Methods - - private static void mask(byte[] src, byte[] key) - { - for (long i = 0; i < src.Length; i++) - src[i] = (byte)(src[i] ^ key[i % 4]); - } - - #endregion - - #region Public Methods - - public IEnumerator GetEnumerator() - { - foreach (byte b in _extensionData) - yield return b; - - foreach (byte b in _applicationData) - yield return b; - } - - public void Mask(byte[] maskingKey) - { - if (_extensionData.Length > 0) - mask(_extensionData, maskingKey); - - if (_applicationData.Length > 0) - mask(_applicationData, maskingKey); - - _masked = !_masked; - } - - public byte[] ToByteArray() - { - return _extensionData.Length > 0 - ? new List(this).ToArray() - : _applicationData; - } - - public override string ToString() - { - return BitConverter.ToString(ToByteArray()); - } - - #endregion - - #region Explicitly Implemented Interface Members - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - } -} diff --git a/SocketHttpListener/Properties/AssemblyInfo.cs b/SocketHttpListener/Properties/AssemblyInfo.cs deleted file mode 100644 index a69bd176f7..0000000000 --- a/SocketHttpListener/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SocketHttpListener")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin: The Free Software Media System")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/SocketHttpListener/Rsv.cs b/SocketHttpListener/Rsv.cs deleted file mode 100644 index 87283791ec..0000000000 --- a/SocketHttpListener/Rsv.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SocketHttpListener -{ - internal enum Rsv : byte - { - Off = 0x0, - On = 0x1 - } -} diff --git a/SocketHttpListener/SocketHttpListener.csproj b/SocketHttpListener/SocketHttpListener.csproj deleted file mode 100644 index e700540a91..0000000000 --- a/SocketHttpListener/SocketHttpListener.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - netstandard2.0 - true - false - - - diff --git a/SocketHttpListener/SocketStream.cs b/SocketHttpListener/SocketStream.cs deleted file mode 100644 index f51fde97ef..0000000000 --- a/SocketHttpListener/SocketStream.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.IO; -using System.Net.Sockets; - -namespace SocketHttpListener -{ - public class SocketStream : Stream - { - private readonly Socket _socket; - - public SocketStream(Socket socket, bool ownsSocket) - { - _socket = socket; - } - - public override void Flush() - { - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _socket.Send(buffer, offset, count, SocketFlags.None); - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return _socket.BeginSend(buffer, offset, count, SocketFlags.None, callback, state); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - _socket.EndSend(asyncResult); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _socket.Receive(buffer, offset, count, SocketFlags.None); - } - - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return _socket.BeginReceive(buffer, offset, count, SocketFlags.None, callback, state); - } - - public override int EndRead(IAsyncResult asyncResult) - { - return _socket.EndReceive(asyncResult); - } - } -} diff --git a/SocketHttpListener/WebSocket.cs b/SocketHttpListener/WebSocket.cs deleted file mode 100644 index 0dcb6a64bc..0000000000 --- a/SocketHttpListener/WebSocket.cs +++ /dev/null @@ -1,778 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using SocketHttpListener.Net.WebSockets; -using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; -using WebSocketState = System.Net.WebSockets.WebSocketState; - -namespace SocketHttpListener -{ - /// - /// Implements the WebSocket interface. - /// - /// - /// The WebSocket class provides a set of methods and properties for two-way communication using - /// the WebSocket protocol (RFC 6455). - /// - public class WebSocket : IDisposable - { - #region Private Fields - - private Action _closeContext; - private CompressionMethod _compression; - private WebSocketContext _context; - private CookieCollection _cookies; - private AutoResetEvent _exitReceiving; - private object _forConn; - private readonly SemaphoreSlim _forEvent = new SemaphoreSlim(1, 1); - private object _forMessageEventQueue; - private readonly SemaphoreSlim _forSend = new SemaphoreSlim(1, 1); - private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private Queue _messageEventQueue; - private string _protocol; - private volatile WebSocketState _readyState; - private AutoResetEvent _receivePong; - private bool _secure; - private Stream _stream; - private const string _version = "13"; - - #endregion - - #region Internal Fields - - internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14. - - #endregion - - #region Internal Constructors - - // As server - internal WebSocket(string protocol) - { - _protocol = protocol; - } - - public void SetContext(HttpListenerWebSocketContext context, Action closeContextFn, Stream stream) - { - _context = context; - - _closeContext = closeContextFn; - _secure = context.IsSecureConnection; - _stream = stream; - - init(); - } - - // In the .NET Framework, this pulls the value from a P/Invoke. Here we just hardcode it to a reasonable default. - public static TimeSpan DefaultKeepAliveInterval => TimeSpan.FromSeconds(30); - - #endregion - - /// - /// Gets the state of the WebSocket connection. - /// - /// - /// One of the enum values, indicates the state of the WebSocket - /// connection. The default value is . - /// - public WebSocketState ReadyState => _readyState; - - #region Public Events - - /// - /// Occurs when the WebSocket connection has been closed. - /// - public event EventHandler OnClose; - - /// - /// Occurs when the gets an error. - /// - public event EventHandler OnError; - - /// - /// Occurs when the receives a message. - /// - public event EventHandler OnMessage; - - /// - /// Occurs when the WebSocket connection has been established. - /// - public event EventHandler OnOpen; - - #endregion - - #region Private Methods - - private async Task CloseAsync(CloseStatusCode code, string reason, bool wait) - { - await CloseAsync(new PayloadData( - await ((ushort)code).AppendAsync(reason).ConfigureAwait(false)), - !code.IsReserved(), - wait).ConfigureAwait(false); - } - - private async Task CloseAsync(PayloadData payload, bool send, bool wait) - { - lock (_forConn) - { - if (_readyState == WebSocketState.CloseSent || _readyState == WebSocketState.Closed) - { - return; - } - - _readyState = WebSocketState.CloseSent; - } - - var e = new CloseEventArgs(payload) - { - WasClean = await CloseHandshakeAsync( - send ? WebSocketFrame.CreateCloseFrame(Mask.Unmask, payload).ToByteArray() : null, - wait ? 1000 : 0).ConfigureAwait(false) - }; - - _readyState = WebSocketState.Closed; - try - { - OnClose.Emit(this, e); - } - catch (Exception ex) - { - error("An exception has occurred while OnClose.", ex); - } - } - - private async Task CloseHandshakeAsync(byte[] frameAsBytes, int millisecondsTimeout) - { - var sent = frameAsBytes != null && await WriteBytesAsync(frameAsBytes).ConfigureAwait(false); - var received = - millisecondsTimeout == 0 || - (sent && _exitReceiving != null && _exitReceiving.WaitOne(millisecondsTimeout)); - - closeServerResources(); - - if (_receivePong != null) - { - _receivePong.Dispose(); - _receivePong = null; - } - - if (_exitReceiving != null) - { - _exitReceiving.Dispose(); - _exitReceiving = null; - } - - var result = sent && received; - - return result; - } - - // As server - private void closeServerResources() - { - if (_closeContext == null) - return; - - try - { - _closeContext(); - } - catch (SocketException) - { - // it could be unable to send the handshake response - } - - _closeContext = null; - _stream = null; - _context = null; - } - - private async Task ConcatenateFragmentsIntoAsync(Stream dest) - { - while (true) - { - var frame = await WebSocketFrame.ReadAsync(_stream, true).ConfigureAwait(false); - if (frame.IsFinal) - { - /* FINAL */ - - // CONT - if (frame.IsContinuation) - { - dest.WriteBytes(frame.PayloadData.ApplicationData); - break; - } - - // PING - if (frame.IsPing) - { - processPingFrame(frame); - continue; - } - - // PONG - if (frame.IsPong) - { - processPongFrame(frame); - continue; - } - - // CLOSE - if (frame.IsClose) - return await ProcessCloseFrameAsync(frame).ConfigureAwait(false); - } - else - { - /* MORE */ - - // CONT - if (frame.IsContinuation) - { - dest.WriteBytes(frame.PayloadData.ApplicationData); - continue; - } - } - - // ? - return await ProcessUnsupportedFrameAsync( - frame, - CloseStatusCode.IncorrectData, - "An incorrect data has been received while receiving fragmented data.").ConfigureAwait(false); - } - - return true; - } - - // As server - private HttpResponse createHandshakeCloseResponse(HttpStatusCode code) - { - var res = HttpResponse.CreateCloseResponse(code); - res.Headers["Sec-WebSocket-Version"] = _version; - - return res; - } - - private MessageEventArgs dequeueFromMessageEventQueue() - { - lock (_forMessageEventQueue) - return _messageEventQueue.Count > 0 - ? _messageEventQueue.Dequeue() - : null; - } - - private void enqueueToMessageEventQueue(MessageEventArgs e) - { - lock (_forMessageEventQueue) - _messageEventQueue.Enqueue(e); - } - - private void error(string message, Exception exception) - { - try - { - if (exception != null) - { - message += ". Exception.Message: " + exception.Message; - } - OnError.Emit(this, new ErrorEventArgs(message)); - } - catch (Exception) - { - } - } - - private void error(string message) - { - try - { - OnError.Emit(this, new ErrorEventArgs(message)); - } - catch (Exception) - { - } - } - - private void init() - { - _compression = CompressionMethod.None; - _cookies = new CookieCollection(); - _forConn = new object(); - _messageEventQueue = new Queue(); - _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; - _readyState = WebSocketState.Connecting; - } - - private async Task OpenAsync() - { - try - { - startReceiving(); - - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while opening.").ConfigureAwait(false); - } - - await _forEvent.WaitAsync().ConfigureAwait(false); - try - { - OnOpen?.Invoke(this, EventArgs.Empty); - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while OnOpen.").ConfigureAwait(false); - } - finally - { - _forEvent.Release(); - } - } - - private async Task ProcessCloseFrameAsync(WebSocketFrame frame) - { - var payload = frame.PayloadData; - await CloseAsync(payload, !payload.ContainsReservedCloseStatusCode, false).ConfigureAwait(false); - - return false; - } - - private bool processDataFrame(WebSocketFrame frame) - { - var e = frame.IsCompressed - ? new MessageEventArgs( - frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) - : new MessageEventArgs(frame.Opcode, frame.PayloadData); - - enqueueToMessageEventQueue(e); - return true; - } - - private async Task ProcessExceptionAsync(Exception exception, string message) - { - var code = CloseStatusCode.Abnormal; - var reason = message; - if (exception is WebSocketException) - { - var wsex = (WebSocketException)exception; - code = wsex.Code; - reason = wsex.Message; - } - - error(message ?? code.GetMessage(), exception); - if (_readyState == WebSocketState.Connecting) - { - await CloseAsync(HttpStatusCode.BadRequest).ConfigureAwait(false); - } - else - { - await CloseAsync(code, reason ?? code.GetMessage(), false).ConfigureAwait(false); - } - } - - private Task ProcessFragmentedFrameAsync(WebSocketFrame frame) - { - return frame.IsContinuation // Not first fragment - ? Task.FromResult(true) - : ProcessFragmentsAsync(frame); - } - - private async Task ProcessFragmentsAsync(WebSocketFrame first) - { - using (var buff = new MemoryStream()) - { - buff.WriteBytes(first.PayloadData.ApplicationData); - if (!await ConcatenateFragmentsIntoAsync(buff).ConfigureAwait(false)) - { - return false; - } - - byte[] data; - if (_compression != CompressionMethod.None) - { - data = buff.DecompressToArray(_compression); - } - else - { - data = buff.ToArray(); - } - - enqueueToMessageEventQueue(new MessageEventArgs(first.Opcode, data)); - return true; - } - } - - private bool processPingFrame(WebSocketFrame frame) - { - return true; - } - - private bool processPongFrame(WebSocketFrame frame) - { - _receivePong.Set(); - - return true; - } - - private async Task ProcessUnsupportedFrameAsync(WebSocketFrame frame, CloseStatusCode code, string reason) - { - await ProcessExceptionAsync(new WebSocketException(code, reason), null).ConfigureAwait(false); - - return false; - } - - private Task ProcessWebSocketFrameAsync(WebSocketFrame frame) - { - // TODO: @bond change to if/else chain - return frame.IsCompressed && _compression == CompressionMethod.None - ? ProcessUnsupportedFrameAsync( - frame, - CloseStatusCode.IncorrectData, - "A compressed data has been received without available decompression method.") - : frame.IsFragmented - ? ProcessFragmentedFrameAsync(frame) - : frame.IsData - ? Task.FromResult(processDataFrame(frame)) - : frame.IsPing - ? Task.FromResult(processPingFrame(frame)) - : frame.IsPong - ? Task.FromResult(processPongFrame(frame)) - : frame.IsClose - ? ProcessCloseFrameAsync(frame) - : ProcessUnsupportedFrameAsync(frame, CloseStatusCode.PolicyViolation, null); - } - - private async Task SendAsync(Opcode opcode, Stream stream) - { - await _forSend.WaitAsync().ConfigureAwait(false); - try - { - var src = stream; - var compressed = false; - var sent = false; - try - { - if (_compression != CompressionMethod.None) - { - stream = stream.Compress(_compression); - compressed = true; - } - - sent = await SendAsync(opcode, Mask.Unmask, stream, compressed).ConfigureAwait(false); - if (!sent) - error("Sending a data has been interrupted."); - } - catch (Exception ex) - { - error("An exception has occurred while sending a data.", ex); - } - finally - { - if (compressed) - stream.Dispose(); - - src.Dispose(); - } - - return sent; - } - finally - { - _forSend.Release(); - } - } - - private async Task SendAsync(Opcode opcode, Mask mask, Stream stream, bool compressed) - { - var len = stream.Length; - - /* Not fragmented */ - - if (len == 0) - return await SendAsync(Fin.Final, opcode, mask, new byte[0], compressed).ConfigureAwait(false); - - var quo = len / FragmentLength; - var rem = (int)(len % FragmentLength); - - byte[] buff = null; - if (quo == 0) - { - buff = new byte[rem]; - return await stream.ReadAsync(buff, 0, rem).ConfigureAwait(false) == rem && - await SendAsync(Fin.Final, opcode, mask, buff, compressed).ConfigureAwait(false); - } - - buff = new byte[FragmentLength]; - if (quo == 1 && rem == 0) - return await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) == FragmentLength && - await SendAsync(Fin.Final, opcode, mask, buff, compressed).ConfigureAwait(false); - - /* Send fragmented */ - - // Begin - if (await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) != FragmentLength || - !await SendAsync(Fin.More, opcode, mask, buff, compressed).ConfigureAwait(false)) - return false; - - var n = rem == 0 ? quo - 2 : quo - 1; - for (long i = 0; i < n; i++) - if (await stream.ReadAsync(buff, 0, FragmentLength).ConfigureAwait(false) != FragmentLength || - !await SendAsync(Fin.More, Opcode.Cont, mask, buff, compressed).ConfigureAwait(false)) - return false; - - // End - if (rem == 0) - rem = FragmentLength; - else - buff = new byte[rem]; - - return await stream.ReadAsync(buff, 0, rem).ConfigureAwait(false) == rem && - await SendAsync(Fin.Final, Opcode.Cont, mask, buff, compressed).ConfigureAwait(false); - } - - private Task SendAsync(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) - { - lock (_forConn) - { - if (_readyState != WebSocketState.Open) - { - return Task.FromResult(false); - } - - return WriteBytesAsync( - WebSocketFrame.CreateWebSocketFrame(fin, opcode, mask, data, compressed).ToByteArray()); - } - } - - // As server - private Task SendHttpResponseAsync(HttpResponse response) - => WriteBytesAsync(response.ToByteArray()); - - private void startReceiving() - { - if (_messageEventQueue.Count > 0) - { - _messageEventQueue.Clear(); - } - - _exitReceiving = new AutoResetEvent(false); - _receivePong = new AutoResetEvent(false); - - Action receive = null; - receive = async () => await WebSocketFrame.ReadAsync( - _stream, - true, - async frame => - { - if (await ProcessWebSocketFrameAsync(frame).ConfigureAwait(false) && _readyState != WebSocketState.Closed) - { - receive(); - - if (!frame.IsData) - { - return; - } - - await _forEvent.WaitAsync().ConfigureAwait(false); - - try - { - var e = dequeueFromMessageEventQueue(); - if (e != null && _readyState == WebSocketState.Open) - { - OnMessage.Emit(this, e); - } - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while OnMessage.").ConfigureAwait(false); - } - finally - { - _forEvent.Release(); - } - - } - else if (_exitReceiving != null) - { - _exitReceiving.Set(); - } - }, - async ex => await ProcessExceptionAsync(ex, "An exception has occurred while receiving a message.")).ConfigureAwait(false); - - receive(); - } - - private async Task WriteBytesAsync(byte[] data) - { - try - { - await _stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); - return true; - } - catch (Exception) - { - return false; - } - } - - #endregion - - #region Internal Methods - - // As server - internal async Task CloseAsync(HttpResponse response) - { - _readyState = WebSocketState.CloseSent; - await SendHttpResponseAsync(response).ConfigureAwait(false); - - closeServerResources(); - - _readyState = WebSocketState.Closed; - } - - // As server - internal Task CloseAsync(HttpStatusCode code) - => CloseAsync(createHandshakeCloseResponse(code)); - - // As server - public async Task ConnectAsServer() - { - try - { - _readyState = WebSocketState.Open; - await OpenAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - await ProcessExceptionAsync(ex, "An exception has occurred while connecting.").ConfigureAwait(false); - } - } - - #endregion - - #region Public Methods - - /// - /// Closes the WebSocket connection, and releases all associated resources. - /// - public Task CloseAsync() - { - var msg = _readyState.CheckIfClosable(); - if (msg != null) - { - error(msg); - - return Task.CompletedTask; - } - - var send = _readyState == WebSocketState.Open; - return CloseAsync(new PayloadData(), send, send); - } - - /// - /// Closes the WebSocket connection with the specified - /// and , and releases all associated resources. - /// - /// - /// This method emits a event if the size - /// of is greater than 123 bytes. - /// - /// - /// One of the enum values, represents the status code - /// indicating the reason for the close. - /// - /// - /// A that represents the reason for the close. - /// - public async Task CloseAsync(CloseStatusCode code, string reason) - { - byte[] data = null; - var msg = _readyState.CheckIfClosable() ?? - (data = await ((ushort)code).AppendAsync(reason).ConfigureAwait(false)).CheckIfValidControlData("reason"); - - if (msg != null) - { - error(msg); - - return; - } - - var send = _readyState == WebSocketState.Open && !code.IsReserved(); - await CloseAsync(new PayloadData(data), send, send).ConfigureAwait(false); - } - - /// - /// Sends a binary asynchronously using the WebSocket connection. - /// - /// - /// This method doesn't wait for the send to be complete. - /// - /// - /// An array of that represents the binary data to send. - /// - public Task SendAsync(byte[] data) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - var msg = _readyState.CheckIfOpen(); - if (msg != null) - { - throw new Exception(msg); - } - - return SendAsync(Opcode.Binary, new MemoryStream(data)); - } - - /// - /// Sends a text asynchronously using the WebSocket connection. - /// - /// - /// This method doesn't wait for the send to be complete. - /// - /// - /// A that represents the text data to send. - /// - public Task SendAsync(string data) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - var msg = _readyState.CheckIfOpen(); - if (msg != null) - { - throw new Exception(msg); - } - - return SendAsync(Opcode.Text, new MemoryStream(Encoding.UTF8.GetBytes(data))); - } - - #endregion - - #region Explicit Interface Implementation - - /// - /// Closes the WebSocket connection, and releases all associated resources. - /// - /// - /// This method closes the WebSocket connection with . - /// - void IDisposable.Dispose() - { - CloseAsync(CloseStatusCode.Away, null).GetAwaiter().GetResult(); - } - - #endregion - } -} diff --git a/SocketHttpListener/WebSocketException.cs b/SocketHttpListener/WebSocketException.cs deleted file mode 100644 index e86c46d0f5..0000000000 --- a/SocketHttpListener/WebSocketException.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; - -namespace SocketHttpListener -{ - /// - /// The exception that is thrown when a gets a fatal error. - /// - public class WebSocketException : Exception - { - #region Internal Constructors - - internal WebSocketException() - : this(CloseStatusCode.Abnormal, null, null) - { - } - - internal WebSocketException(string message) - : this(CloseStatusCode.Abnormal, message, null) - { - } - - internal WebSocketException(CloseStatusCode code) - : this(code, null, null) - { - } - - internal WebSocketException(string message, Exception innerException) - : this(CloseStatusCode.Abnormal, message, innerException) - { - } - - internal WebSocketException(CloseStatusCode code, string message) - : this(code, message, null) - { - } - - internal WebSocketException(CloseStatusCode code, string message, Exception innerException) - : base(message ?? code.GetMessage(), innerException) - { - Code = code; - } - - #endregion - - #region Public Properties - - /// - /// Gets the status code indicating the cause for the exception. - /// - /// - /// One of the enum values, represents the status code indicating - /// the cause for the exception. - /// - public CloseStatusCode Code - { - get; private set; - } - - #endregion - } -} diff --git a/SocketHttpListener/WebSocketFrame.cs b/SocketHttpListener/WebSocketFrame.cs deleted file mode 100644 index 8ec64026bd..0000000000 --- a/SocketHttpListener/WebSocketFrame.cs +++ /dev/null @@ -1,432 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace SocketHttpListener -{ - internal class WebSocketFrame : IEnumerable - { - #region Private Fields - - private byte[] _extPayloadLength; - private Fin _fin; - private Mask _mask; - private byte[] _maskingKey; - private Opcode _opcode; - private PayloadData _payloadData; - private byte _payloadLength; - private Rsv _rsv1; - private Rsv _rsv2; - private Rsv _rsv3; - - #endregion - - #region Internal Fields - - internal static readonly byte[] EmptyUnmaskPingData; - - #endregion - - #region Static Constructor - - static WebSocketFrame() - { - EmptyUnmaskPingData = CreatePingFrame(Mask.Unmask).ToByteArray(); - } - - #endregion - - #region Private Constructors - - private WebSocketFrame() - { - } - - #endregion - - #region Internal Constructors - - internal WebSocketFrame(Opcode opcode, PayloadData payload) - : this(Fin.Final, opcode, Mask.Mask, payload, false) - { - } - - internal WebSocketFrame(Opcode opcode, Mask mask, PayloadData payload) - : this(Fin.Final, opcode, mask, payload, false) - { - } - - internal WebSocketFrame(Fin fin, Opcode opcode, Mask mask, PayloadData payload) - : this(fin, opcode, mask, payload, false) - { - } - - internal WebSocketFrame( - Fin fin, Opcode opcode, Mask mask, PayloadData payload, bool compressed) - { - _fin = fin; - _rsv1 = isData(opcode) && compressed ? Rsv.On : Rsv.Off; - _rsv2 = Rsv.Off; - _rsv3 = Rsv.Off; - _opcode = opcode; - _mask = mask; - - var len = payload.Length; - if (len < 126) - { - _payloadLength = (byte)len; - _extPayloadLength = new byte[0]; - } - else if (len < 0x010000) - { - _payloadLength = (byte)126; - _extPayloadLength = ((ushort)len).ToByteArrayInternally(ByteOrder.Big); - } - else - { - _payloadLength = (byte)127; - _extPayloadLength = len.ToByteArrayInternally(ByteOrder.Big); - } - - if (mask == Mask.Mask) - { - _maskingKey = createMaskingKey(); - payload.Mask(_maskingKey); - } - else - { - _maskingKey = new byte[0]; - } - - _payloadData = payload; - } - - #endregion - - #region Public Properties - - public byte[] ExtendedPayloadLength => _extPayloadLength; - - public Fin Fin => _fin; - - public bool IsBinary => _opcode == Opcode.Binary; - - public bool IsClose => _opcode == Opcode.Close; - - public bool IsCompressed => _rsv1 == Rsv.On; - - public bool IsContinuation => _opcode == Opcode.Cont; - - public bool IsControl => _opcode == Opcode.Close || _opcode == Opcode.Ping || _opcode == Opcode.Pong; - - public bool IsData => _opcode == Opcode.Binary || _opcode == Opcode.Text; - - public bool IsFinal => _fin == Fin.Final; - - public bool IsFragmented => _fin == Fin.More || _opcode == Opcode.Cont; - - public bool IsMasked => _mask == Mask.Mask; - - public bool IsPerMessageCompressed => (_opcode == Opcode.Binary || _opcode == Opcode.Text) && _rsv1 == Rsv.On; - - public bool IsPing => _opcode == Opcode.Ping; - - public bool IsPong => _opcode == Opcode.Pong; - - public bool IsText => _opcode == Opcode.Text; - - public ulong Length => 2 + (ulong)(_extPayloadLength.Length + _maskingKey.Length) + _payloadData.Length; - - public Mask Mask => _mask; - - public byte[] MaskingKey => _maskingKey; - - public Opcode Opcode => _opcode; - - public PayloadData PayloadData => _payloadData; - - public byte PayloadLength => _payloadLength; - - public Rsv Rsv1 => _rsv1; - - public Rsv Rsv2 => _rsv2; - - public Rsv Rsv3 => _rsv3; - - #endregion - - #region Private Methods - - private byte[] createMaskingKey() - { - var key = new byte[4]; - var rand = new Random(); - rand.NextBytes(key); - - return key; - } - - private static bool isControl(Opcode opcode) - { - return opcode == Opcode.Close || opcode == Opcode.Ping || opcode == Opcode.Pong; - } - - private static bool isData(Opcode opcode) - { - return opcode == Opcode.Text || opcode == Opcode.Binary; - } - - private static async Task ReadAsync(byte[] header, Stream stream, bool unmask) - { - /* Header */ - - // FIN - var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; - // RSV1 - var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; - // RSV2 - var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; - // RSV3 - var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; - // Opcode - var opcode = (Opcode)(header[0] & 0x0f); - // MASK - var mask = (header[1] & 0x80) == 0x80 ? Mask.Mask : Mask.Unmask; - // Payload Length - var payloadLen = (byte)(header[1] & 0x7f); - - // Check if correct frame. - var incorrect = isControl(opcode) && fin == Fin.More - ? "A control frame is fragmented." - : !isData(opcode) && rsv1 == Rsv.On - ? "A non data frame is compressed." - : null; - - if (incorrect != null) - throw new WebSocketException(CloseStatusCode.IncorrectData, incorrect); - - // Check if consistent frame. - if (isControl(opcode) && payloadLen > 125) - throw new WebSocketException( - CloseStatusCode.InconsistentData, - "The length of payload data of a control frame is greater than 125 bytes."); - - var frame = new WebSocketFrame(); - frame._fin = fin; - frame._rsv1 = rsv1; - frame._rsv2 = rsv2; - frame._rsv3 = rsv3; - frame._opcode = opcode; - frame._mask = mask; - frame._payloadLength = payloadLen; - - /* Extended Payload Length */ - - var size = payloadLen < 126 - ? 0 - : payloadLen == 126 - ? 2 - : 8; - - var extPayloadLen = size > 0 ? await stream.ReadBytesAsync(size).ConfigureAwait(false) : Array.Empty(); - if (size > 0 && extPayloadLen.Length != size) - throw new WebSocketException( - "The 'Extended Payload Length' of a frame cannot be read from the data source."); - - frame._extPayloadLength = extPayloadLen; - - /* Masking Key */ - - var masked = mask == Mask.Mask; - var maskingKey = masked ? await stream.ReadBytesAsync(4).ConfigureAwait(false) : Array.Empty(); - if (masked && maskingKey.Length != 4) - throw new WebSocketException( - "The 'Masking Key' of a frame cannot be read from the data source."); - - frame._maskingKey = maskingKey; - - /* Payload Data */ - - ulong len = payloadLen < 126 - ? payloadLen - : payloadLen == 126 - ? extPayloadLen.ToUInt16(ByteOrder.Big) - : extPayloadLen.ToUInt64(ByteOrder.Big); - - byte[] data = null; - if (len > 0) - { - // Check if allowable payload data length. - if (payloadLen > 126 && len > PayloadData.MaxLength) - throw new WebSocketException( - CloseStatusCode.TooBig, - "The length of 'Payload Data' of a frame is greater than the allowable length."); - - data = payloadLen > 126 - ? await stream.ReadBytesAsync((long)len, 1024).ConfigureAwait(false) - : await stream.ReadBytesAsync((int)len).ConfigureAwait(false); - - //if (data.LongLength != (long)len) - // throw new WebSocketException( - // "The 'Payload Data' of a frame cannot be read from the data source."); - } - else - { - data = Array.Empty(); - } - - var payload = new PayloadData(data, masked); - if (masked && unmask) - { - payload.Mask(maskingKey); - frame._mask = Mask.Unmask; - frame._maskingKey = Array.Empty(); - } - - frame._payloadData = payload; - return frame; - } - - #endregion - - #region Internal Methods - - internal static WebSocketFrame CreateCloseFrame(Mask mask, byte[] data) - { - return new WebSocketFrame(Opcode.Close, mask, new PayloadData(data)); - } - - internal static WebSocketFrame CreateCloseFrame(Mask mask, PayloadData payload) - { - return new WebSocketFrame(Opcode.Close, mask, payload); - } - - internal static async Task CreateCloseFrameAsync(Mask mask, CloseStatusCode code, string reason) - { - return new WebSocketFrame( - Opcode.Close, mask, new PayloadData(await ((ushort)code).AppendAsync(reason).ConfigureAwait(false))); - } - - internal static WebSocketFrame CreatePingFrame(Mask mask) - { - return new WebSocketFrame(Opcode.Ping, mask, new PayloadData()); - } - - internal static WebSocketFrame CreatePingFrame(Mask mask, byte[] data) - { - return new WebSocketFrame(Opcode.Ping, mask, new PayloadData(data)); - } - - internal static WebSocketFrame CreatePongFrame(Mask mask, PayloadData payload) - { - return new WebSocketFrame(Opcode.Pong, mask, payload); - } - - internal static WebSocketFrame CreateWebSocketFrame( - Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) - { - return new WebSocketFrame(fin, opcode, mask, new PayloadData(data), compressed); - } - - internal static Task ReadAsync(Stream stream) - => ReadAsync(stream, true); - - internal static async Task ReadAsync(Stream stream, bool unmask) - { - var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); - if (header.Length != 2) - { - throw new WebSocketException( - "The header part of a frame cannot be read from the data source."); - } - - return await ReadAsync(header, stream, unmask).ConfigureAwait(false); - } - - internal static async Task ReadAsync( - Stream stream, bool unmask, Action completed, Action error) - { - try - { - var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); - if (header.Length != 2) - { - throw new WebSocketException( - "The header part of a frame cannot be read from the data source."); - } - - var frame = await ReadAsync(header, stream, unmask).ConfigureAwait(false); - completed?.Invoke(frame); - } - catch (Exception ex) - { - error.Invoke(ex); - } - } - - #endregion - - #region Public Methods - - public IEnumerator GetEnumerator() - { - foreach (var b in ToByteArray()) - yield return b; - } - - public void Print(bool dumped) - { - //Console.WriteLine(dumped ? dump(this) : print(this)); - } - - public byte[] ToByteArray() - { - using (var buff = new MemoryStream()) - { - var header = (int)_fin; - header = (header << 1) + (int)_rsv1; - header = (header << 1) + (int)_rsv2; - header = (header << 1) + (int)_rsv3; - header = (header << 4) + (int)_opcode; - header = (header << 1) + (int)_mask; - header = (header << 7) + (int)_payloadLength; - buff.Write(((ushort)header).ToByteArrayInternally(ByteOrder.Big), 0, 2); - - if (_payloadLength > 125) - buff.Write(_extPayloadLength, 0, _extPayloadLength.Length); - - if (_mask == Mask.Mask) - buff.Write(_maskingKey, 0, _maskingKey.Length); - - if (_payloadLength > 0) - { - var payload = _payloadData.ToByteArray(); - if (_payloadLength < 127) - buff.Write(payload, 0, payload.Length); - else - buff.WriteBytes(payload); - } - - return buff.ToArray(); - } - } - - public override string ToString() - { - return BitConverter.ToString(ToByteArray()); - } - - #endregion - - #region Explicitly Implemented Interface Members - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - } -} diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/deployment/debian-package-x64/pkg-src/conf/jellyfin index 58fe79332a..7db482d428 100644 --- a/deployment/debian-package-x64/pkg-src/conf/jellyfin +++ b/deployment/debian-package-x64/pkg-src/conf/jellyfin @@ -13,10 +13,10 @@ # # Program directories -JELLYFIN_DATA_DIRECTORY="/var/lib/jellyfin" -JELLYFIN_CONFIG_DIRECTORY="/etc/jellyfin" -JELLYFIN_LOG_DIRECTORY="/var/log/jellyfin" -JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin" +JELLYFIN_DATA_DIR="/var/lib/jellyfin" +JELLYFIN_CONFIG_DIR="/etc/jellyfin" +JELLYFIN_LOG_DIR="/var/log/jellyfin" +JELLYFIN_CACHE_DIR="/var/cache/jellyfin" # Restart script for in-app server control JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" @@ -38,4 +38,4 @@ JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe" # Application username JELLYFIN_USER="jellyfin" # Full application command -JELLYFIN_ARGS="--datadir=$JELLYFIN_DATA_DIRECTORY --configdir=$JELLYFIN_CONFIG_DIRECTORY --logdir=$JELLYFIN_LOG_DIRECTORY --cachedir=$JELLYFIN_CACHE_DIRECTORY $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_FFPROBE_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" +JELLYFIN_ARGS="$JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_FFPROBE_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.service b/deployment/debian-package-x64/pkg-src/jellyfin.service index 9c6c6667f1..b4da3a945b 100644 --- a/deployment/debian-package-x64/pkg-src/jellyfin.service +++ b/deployment/debian-package-x64/pkg-src/jellyfin.service @@ -6,7 +6,7 @@ After = network.target Type = simple EnvironmentFile = /etc/default/jellyfin User = jellyfin -ExecStart = /usr/bin/jellyfin --datadir=${JELLYFIN_DATA_DIRECTORY} --configdir=${JELLYFIN_CONFIG_DIRECTORY} --logdir=${JELLYFIN_LOG_DIRECTORY} --cachedir=${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart = /usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} Restart = on-failure TimeoutSec = 15 diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.env b/deployment/fedora-package-x64/pkg-src/jellyfin.env index abfa670e4a..143a317c43 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.env +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.env @@ -15,10 +15,10 @@ # # Program directories -JELLYFIN_DATA_DIRECTORY="/var/lib/jellyfin" -JELLYFIN_CONFIG_DIRECTORY="/etc/jellyfin" -JELLYFIN_LOG_DIRECTORY="/var/log/jellyfin" -JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin" +JELLYFIN_DATA_DIR="/var/lib/jellyfin" +JELLYFIN_CONFIG_DIR="/etc/jellyfin" +JELLYFIN_LOG_DIR="/var/log/jellyfin" +JELLYFIN_CACHE_DIR="/var/cache/jellyfin" # In-App service control JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh" diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.service b/deployment/fedora-package-x64/pkg-src/jellyfin.service index d58df9d949..1f83d3d386 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.service +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.service @@ -5,7 +5,7 @@ Description=Jellyfin is a free software media system that puts you in control of [Service] EnvironmentFile=/etc/sysconfig/jellyfin WorkingDirectory=/var/lib/jellyfin -ExecStart=/usr/bin/jellyfin --datadir=${JELLYFIN_DATA_DIRECTORY} --configdir=${JELLYFIN_CONFIG_DIRECTORY} --logdir=${JELLYFIN_LOG_DIRECTORY} --cachedir=${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart=/usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} TimeoutSec=15 Restart=on-failure User=jellyfin diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 4381349ca6..0a04b4c557 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -1,6 +1,11 @@ + + + + + diff --git a/new-file-header.txt b/new-file-header.txt deleted file mode 100644 index 4247a8e246..0000000000 --- a/new-file-header.txt +++ /dev/null @@ -1,22 +0,0 @@ -### This header should be used to start new files. -### It provides an explicit per-file license reference that should be present on all new files. -### To use this header, delete these lines and the following empty line, modify to -### the proper full path, and then add new code following the header and a single empty line. - -// -// Part of the Jellyfin project (https://jellyfin.media) -// -// All copyright belongs to the Jellyfin contributors; a full list can -// be found in the file CONTRIBUTORS.md -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, version 2. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see .