mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-04 03:27:21 -05:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/master' into 3.1.7
This commit is contained in:
		
						commit
						5ad81f7fe6
					
				@ -62,7 +62,6 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - task: DownloadPipelineArtifact@2
 | 
			
		||||
        displayName: 'Download Reference Assembly Build Artifact'
 | 
			
		||||
        enabled: false
 | 
			
		||||
        inputs:
 | 
			
		||||
          source: "specific"
 | 
			
		||||
          artifact: "$(NugetPackageName)"
 | 
			
		||||
@ -74,7 +73,6 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - task: CopyFiles@2
 | 
			
		||||
        displayName: 'Copy Reference Assembly Build Artifact'
 | 
			
		||||
        enabled: false
 | 
			
		||||
        inputs:
 | 
			
		||||
          sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
 | 
			
		||||
          contents: '**/*.dll'
 | 
			
		||||
@ -85,7 +83,6 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - task: DotNetCoreCLI@2
 | 
			
		||||
        displayName: 'Execute ABI Compatibility Check Tool'
 | 
			
		||||
        enabled: false
 | 
			
		||||
        inputs:
 | 
			
		||||
          command: custom
 | 
			
		||||
          custom: compat
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
  - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
 | 
			
		||||
    displayName: 'Run Dockerfile (stable)'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
 | 
			
		||||
  - task: PublishPipelineArtifact@1
 | 
			
		||||
    displayName: 'Publish Release'
 | 
			
		||||
@ -87,7 +87,7 @@ jobs:
 | 
			
		||||
  steps:
 | 
			
		||||
  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
 | 
			
		||||
    displayName: Set release version (stable)
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
 | 
			
		||||
  - task: Docker@2
 | 
			
		||||
    displayName: 'Push Unstable Image'
 | 
			
		||||
@ -104,7 +104,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
  - task: Docker@2
 | 
			
		||||
    displayName: 'Push Stable Image'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
    inputs:
 | 
			
		||||
      repository: 'jellyfin/jellyfin-server'
 | 
			
		||||
      command: buildAndPush
 | 
			
		||||
@ -116,8 +116,9 @@ jobs:
 | 
			
		||||
        $(JellyfinVersion)-$(BuildConfiguration)
 | 
			
		||||
 | 
			
		||||
- job: CollectArtifacts
 | 
			
		||||
  timeoutInMinutes: 10
 | 
			
		||||
  timeoutInMinutes: 20
 | 
			
		||||
  displayName: 'Collect Artifacts'
 | 
			
		||||
  continueOnError: true
 | 
			
		||||
  dependsOn:
 | 
			
		||||
  - BuildPackage
 | 
			
		||||
  - BuildDocker
 | 
			
		||||
@ -129,19 +130,21 @@ jobs:
 | 
			
		||||
  steps:
 | 
			
		||||
  - task: SSH@0
 | 
			
		||||
    displayName: 'Update Unstable Repository'
 | 
			
		||||
    continueOnError: true
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
 | 
			
		||||
    inputs:
 | 
			
		||||
      sshEndpoint: repository
 | 
			
		||||
      runOptions: 'commands'
 | 
			
		||||
      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
 | 
			
		||||
      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
 | 
			
		||||
 | 
			
		||||
  - task: SSH@0
 | 
			
		||||
    displayName: 'Update Stable Repository'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
 | 
			
		||||
    continueOnError: true
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
    inputs:
 | 
			
		||||
      sshEndpoint: repository
 | 
			
		||||
      runOptions: 'commands'
 | 
			
		||||
      commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
 | 
			
		||||
      commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
 | 
			
		||||
 | 
			
		||||
- job: PublishNuget
 | 
			
		||||
  displayName: 'Publish NuGet packages'
 | 
			
		||||
@ -155,7 +158,7 @@ jobs:
 | 
			
		||||
  steps:
 | 
			
		||||
  - task: DotNetCoreCLI@2
 | 
			
		||||
    displayName: 'Build Stable Nuget packages'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
    inputs:
 | 
			
		||||
      command: 'pack'
 | 
			
		||||
      packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
 | 
			
		||||
@ -172,7 +175,7 @@ jobs:
 | 
			
		||||
        MediaBrowser.Model/MediaBrowser.Model.csproj
 | 
			
		||||
        Emby.Naming/Emby.Naming.csproj
 | 
			
		||||
      custom: 'pack'
 | 
			
		||||
      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory)'
 | 
			
		||||
      arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
 | 
			
		||||
 | 
			
		||||
  - task: PublishBuildArtifacts@1
 | 
			
		||||
    displayName: 'Publish Nuget packages'
 | 
			
		||||
@ -180,10 +183,32 @@ jobs:
 | 
			
		||||
      pathToPublish: $(Build.ArtifactStagingDirectory)
 | 
			
		||||
      artifactName: Jellyfin Nuget Packages
 | 
			
		||||
 | 
			
		||||
  - task: NuGetAuthenticate@0
 | 
			
		||||
    displayName: 'Authenticate to stable Nuget feed'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
    inputs:
 | 
			
		||||
      nuGetServiceConnections: 'NugetOrg'
 | 
			
		||||
 | 
			
		||||
  - task: NuGetCommand@2
 | 
			
		||||
    displayName: 'Push Nuget packages to feed'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
 | 
			
		||||
    displayName: 'Push Nuget packages to stable feed'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 | 
			
		||||
    inputs:
 | 
			
		||||
      command: 'push'
 | 
			
		||||
      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
 | 
			
		||||
      includeNugetOrg: 'true'
 | 
			
		||||
      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
 | 
			
		||||
      nuGetFeedType: 'external'
 | 
			
		||||
      publishFeedCredentials: 'NugetOrg'
 | 
			
		||||
      allowPackageConflicts: true # This ignores an error if the version already exists
 | 
			
		||||
 | 
			
		||||
  - task: NuGetAuthenticate@0
 | 
			
		||||
    displayName: 'Authenticate to unstable Nuget feed'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
 | 
			
		||||
 | 
			
		||||
  - task: NuGetCommand@2
 | 
			
		||||
    displayName: 'Push Nuget packages to unstable feed'
 | 
			
		||||
    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
 | 
			
		||||
    inputs:
 | 
			
		||||
      command: 'push'
 | 
			
		||||
      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
 | 
			
		||||
      nuGetFeedType: 'internal'
 | 
			
		||||
      publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
 | 
			
		||||
      allowPackageConflicts: true # This ignores an error if the version already exists
 | 
			
		||||
 | 
			
		||||
@ -13,15 +13,21 @@ pr:
 | 
			
		||||
 | 
			
		||||
trigger:
 | 
			
		||||
  batch: true
 | 
			
		||||
  branches:
 | 
			
		||||
    include:
 | 
			
		||||
      - '*'
 | 
			
		||||
  tags:
 | 
			
		||||
    include:
 | 
			
		||||
      - 'v*'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
 | 
			
		||||
- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
 | 
			
		||||
  - template: azure-pipelines-main.yml
 | 
			
		||||
    parameters:
 | 
			
		||||
      LinuxImage: 'ubuntu-latest'
 | 
			
		||||
      RestoreBuildProjects: $(RestoreBuildProjects)
 | 
			
		||||
 | 
			
		||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
 | 
			
		||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
 | 
			
		||||
  - template: azure-pipelines-test.yml
 | 
			
		||||
    parameters:
 | 
			
		||||
      ImageNames:
 | 
			
		||||
@ -29,7 +35,7 @@ jobs:
 | 
			
		||||
        Windows: 'windows-latest'
 | 
			
		||||
        macOS: 'macos-latest'
 | 
			
		||||
 | 
			
		||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
 | 
			
		||||
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
 | 
			
		||||
  - template: azure-pipelines-abi.yml
 | 
			
		||||
    parameters:
 | 
			
		||||
      Packages:
 | 
			
		||||
@ -47,5 +53,5 @@ jobs:
 | 
			
		||||
          AssemblyFileName: MediaBrowser.Common.dll
 | 
			
		||||
      LinuxImage: 'ubuntu-latest'
 | 
			
		||||
 | 
			
		||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
 | 
			
		||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
 | 
			
		||||
  - template: azure-pipelines-package.yml
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
 - [bugfixin](https://github.com/bugfixin)
 | 
			
		||||
 - [chaosinnovator](https://github.com/chaosinnovator)
 | 
			
		||||
 - [ckcr4lyf](https://github.com/ckcr4lyf)
 | 
			
		||||
 - [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
 | 
			
		||||
 - [crankdoofus](https://github.com/crankdoofus)
 | 
			
		||||
 - [crobibero](https://github.com/crobibero)
 | 
			
		||||
 - [cromefire](https://github.com/cromefire)
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,15 @@
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 | 
			
		||||
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
 | 
			
		||||
    <IncludeSymbols>true</IncludeSymbols>
 | 
			
		||||
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
 | 
			
		||||
    <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
 | 
			
		||||
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
@ -28,6 +37,10 @@
 | 
			
		||||
    <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <!-- Code Analyzers-->
 | 
			
		||||
  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
 | 
			
		||||
    <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
 | 
			
		||||
 | 
			
		||||
@ -37,10 +37,10 @@ using Emby.Server.Implementations.LiveTv;
 | 
			
		||||
using Emby.Server.Implementations.Localization;
 | 
			
		||||
using Emby.Server.Implementations.Net;
 | 
			
		||||
using Emby.Server.Implementations.Playlists;
 | 
			
		||||
using Emby.Server.Implementations.QuickConnect;
 | 
			
		||||
using Emby.Server.Implementations.ScheduledTasks;
 | 
			
		||||
using Emby.Server.Implementations.Security;
 | 
			
		||||
using Emby.Server.Implementations.Serialization;
 | 
			
		||||
using Emby.Server.Implementations.Services;
 | 
			
		||||
using Emby.Server.Implementations.Session;
 | 
			
		||||
using Emby.Server.Implementations.SyncPlay;
 | 
			
		||||
using Emby.Server.Implementations.TV;
 | 
			
		||||
@ -71,6 +71,7 @@ using MediaBrowser.Controller.Persistence;
 | 
			
		||||
using MediaBrowser.Controller.Playlists;
 | 
			
		||||
using MediaBrowser.Controller.Plugins;
 | 
			
		||||
using MediaBrowser.Controller.Providers;
 | 
			
		||||
using MediaBrowser.Controller.QuickConnect;
 | 
			
		||||
using MediaBrowser.Controller.Resolvers;
 | 
			
		||||
using MediaBrowser.Controller.Security;
 | 
			
		||||
using MediaBrowser.Controller.Session;
 | 
			
		||||
@ -88,7 +89,6 @@ using MediaBrowser.Model.IO;
 | 
			
		||||
using MediaBrowser.Model.MediaInfo;
 | 
			
		||||
using MediaBrowser.Model.Net;
 | 
			
		||||
using MediaBrowser.Model.Serialization;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using MediaBrowser.Model.System;
 | 
			
		||||
using MediaBrowser.Model.Tasks;
 | 
			
		||||
using MediaBrowser.Providers.Chapters;
 | 
			
		||||
@ -96,11 +96,12 @@ using MediaBrowser.Providers.Manager;
 | 
			
		||||
using MediaBrowser.Providers.Plugins.TheTvdb;
 | 
			
		||||
using MediaBrowser.Providers.Subtitles;
 | 
			
		||||
using MediaBrowser.XbmcMetadata.Providers;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Prometheus.DotNetRuntime;
 | 
			
		||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 | 
			
		||||
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations
 | 
			
		||||
{
 | 
			
		||||
@ -121,14 +122,18 @@ namespace Emby.Server.Implementations
 | 
			
		||||
 | 
			
		||||
        private IMediaEncoder _mediaEncoder;
 | 
			
		||||
        private ISessionManager _sessionManager;
 | 
			
		||||
        private IHttpServer _httpServer;
 | 
			
		||||
        private IWebSocketManager _webSocketManager;
 | 
			
		||||
        private IHttpClient _httpClient;
 | 
			
		||||
 | 
			
		||||
        private string[] _urlPrefixes;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a value indicating whether this instance can self restart.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool CanSelfRestart => _startupOptions.RestartPath != null;
 | 
			
		||||
 | 
			
		||||
        public bool CoreStartupHasCompleted { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public virtual bool CanLaunchWebBrowser
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
@ -443,8 +448,7 @@ namespace Emby.Server.Implementations
 | 
			
		||||
            Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 | 
			
		||||
 | 
			
		||||
            Logger.LogInformation("Core startup complete");
 | 
			
		||||
            _httpServer.GlobalResponse = null;
 | 
			
		||||
 | 
			
		||||
            CoreStartupHasCompleted = true;
 | 
			
		||||
            stopWatch.Restart();
 | 
			
		||||
            await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
 | 
			
		||||
            Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 | 
			
		||||
@ -499,9 +503,6 @@ namespace Emby.Server.Implementations
 | 
			
		||||
            RegisterServices();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
 | 
			
		||||
            => _httpServer.RequestHandler(context);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Registers services/resources with the service collection that will be available via DI.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@ -541,8 +542,6 @@ namespace Emby.Server.Implementations
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<IZipClient, ZipClient>();
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<IServerApplicationHost>(this);
 | 
			
		||||
            ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
 | 
			
		||||
 | 
			
		||||
@ -578,8 +577,7 @@ namespace Emby.Server.Implementations
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<ServiceController>();
 | 
			
		||||
            ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
 | 
			
		||||
            ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 | 
			
		||||
 | 
			
		||||
@ -626,6 +624,7 @@ namespace Emby.Server.Implementations
 | 
			
		||||
            ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<IAuthService, AuthService>();
 | 
			
		||||
            ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
 | 
			
		||||
 | 
			
		||||
            ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
 | 
			
		||||
 | 
			
		||||
@ -651,7 +650,7 @@ namespace Emby.Server.Implementations
 | 
			
		||||
 | 
			
		||||
            _mediaEncoder = Resolve<IMediaEncoder>();
 | 
			
		||||
            _sessionManager = Resolve<ISessionManager>();
 | 
			
		||||
            _httpServer = Resolve<IHttpServer>();
 | 
			
		||||
            _webSocketManager = Resolve<IWebSocketManager>();
 | 
			
		||||
            _httpClient = Resolve<IHttpClient>();
 | 
			
		||||
 | 
			
		||||
            ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 | 
			
		||||
@ -753,7 +752,6 @@ namespace Emby.Server.Implementations
 | 
			
		||||
            CollectionFolder.XmlSerializer = _xmlSerializer;
 | 
			
		||||
            CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
 | 
			
		||||
            CollectionFolder.ApplicationHost = this;
 | 
			
		||||
            AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -773,7 +771,8 @@ namespace Emby.Server.Implementations
 | 
			
		||||
                        .Where(i => i != null)
 | 
			
		||||
                        .ToArray();
 | 
			
		||||
 | 
			
		||||
            _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
 | 
			
		||||
            _urlPrefixes = GetUrlPrefixes().ToArray();
 | 
			
		||||
            _webSocketManager.Init(GetExports<IWebSocketListener>());
 | 
			
		||||
 | 
			
		||||
            Resolve<ILibraryManager>().AddParts(
 | 
			
		||||
                GetExports<IResolverIgnoreRule>(),
 | 
			
		||||
@ -939,7 +938,7 @@ namespace Emby.Server.Implementations
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                requiresRestart = true;
 | 
			
		||||
            }
 | 
			
		||||
@ -1393,6 +1392,20 @@ namespace Emby.Server.Implementations
 | 
			
		||||
            _plugins = list.ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<Assembly> GetApiPluginAssemblies()
 | 
			
		||||
        {
 | 
			
		||||
            var assemblies = _allConcreteTypes
 | 
			
		||||
                .Where(i => typeof(ControllerBase).IsAssignableFrom(i))
 | 
			
		||||
                .Select(i => i.Assembly)
 | 
			
		||||
                .Distinct();
 | 
			
		||||
 | 
			
		||||
            foreach (var assembly in assemblies)
 | 
			
		||||
            {
 | 
			
		||||
                Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
 | 
			
		||||
                yield return assembly;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public virtual void LaunchUrl(string url)
 | 
			
		||||
        {
 | 
			
		||||
            if (!CanLaunchWebBrowser)
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ namespace Emby.Server.Implementations
 | 
			
		||||
        public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
 | 
			
		||||
        {
 | 
			
		||||
            { HostWebClientKey, bool.TrueString },
 | 
			
		||||
            { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
 | 
			
		||||
            { DefaultRedirectKey, "web/index.html" },
 | 
			
		||||
            { FfmpegProbeSizeKey, "1G" },
 | 
			
		||||
            { FfmpegAnalyzeDurationKey, "200M" },
 | 
			
		||||
            { PlaylistsAllowDuplicatesKey, bool.TrueString },
 | 
			
		||||
 | 
			
		||||
@ -1,250 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net;
 | 
			
		||||
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 Microsoft.Net.Http.Headers;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    public class FileWriter : IHttpResult
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
 | 
			
		||||
 | 
			
		||||
        private static readonly string[] _skipLogExtensions = {
 | 
			
		||||
            ".js",
 | 
			
		||||
            ".html",
 | 
			
		||||
            ".css"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        private readonly IStreamHelper _streamHelper;
 | 
			
		||||
        private readonly ILogger _logger;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The _options.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The _requested ranges.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private List<KeyValuePair<long, long?>> _requestedRanges;
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
            Path = path;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            RangeHeader = rangeHeader;
 | 
			
		||||
 | 
			
		||||
            Headers[HeaderNames.ContentType] = contentType;
 | 
			
		||||
 | 
			
		||||
            TotalContentLength = fileSystem.GetFileInfo(path).Length;
 | 
			
		||||
            Headers[HeaderNames.AcceptRanges] = "bytes";
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(rangeHeader))
 | 
			
		||||
            {
 | 
			
		||||
                Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
                StatusCode = HttpStatusCode.OK;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                StatusCode = HttpStatusCode.PartialContent;
 | 
			
		||||
                SetRangeValues();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            FileShare = FileShare.Read;
 | 
			
		||||
            Cookies = new List<Cookie>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string RangeHeader { get; set; }
 | 
			
		||||
 | 
			
		||||
        private bool IsHeadRequest { get; set; }
 | 
			
		||||
 | 
			
		||||
        private long RangeStart { get; set; }
 | 
			
		||||
 | 
			
		||||
        private long RangeEnd { get; set; }
 | 
			
		||||
 | 
			
		||||
        private long RangeLength { get; set; }
 | 
			
		||||
 | 
			
		||||
        public long TotalContentLength { get; set; }
 | 
			
		||||
 | 
			
		||||
        public Action OnComplete { get; set; }
 | 
			
		||||
 | 
			
		||||
        public Action OnError { get; set; }
 | 
			
		||||
 | 
			
		||||
        public List<Cookie> Cookies { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public FileShare FileShare { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the options.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The options.</value>
 | 
			
		||||
        public IDictionary<string, string> Headers => _options;
 | 
			
		||||
 | 
			
		||||
        public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the requested ranges.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The requested ranges.</value>
 | 
			
		||||
        protected List<KeyValuePair<long, long?>> RequestedRanges
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                if (_requestedRanges == null)
 | 
			
		||||
                {
 | 
			
		||||
                    _requestedRanges = new List<KeyValuePair<long, long?>>();
 | 
			
		||||
 | 
			
		||||
                    // Example: bytes=0-,32-63
 | 
			
		||||
                    var ranges = RangeHeader.Split('=')[1].Split(',');
 | 
			
		||||
 | 
			
		||||
                    foreach (var range in ranges)
 | 
			
		||||
                    {
 | 
			
		||||
                        var vals = range.Split('-');
 | 
			
		||||
 | 
			
		||||
                        long start = 0;
 | 
			
		||||
                        long? end = null;
 | 
			
		||||
 | 
			
		||||
                        if (!string.IsNullOrEmpty(vals[0]))
 | 
			
		||||
                        {
 | 
			
		||||
                            start = long.Parse(vals[0], UsCulture);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (!string.IsNullOrEmpty(vals[1]))
 | 
			
		||||
                        {
 | 
			
		||||
                            end = long.Parse(vals[1], UsCulture);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return _requestedRanges;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string ContentType { get; set; }
 | 
			
		||||
 | 
			
		||||
        public IRequest RequestContext { get; set; }
 | 
			
		||||
 | 
			
		||||
        public object Response { get; set; }
 | 
			
		||||
 | 
			
		||||
        public int Status { get; set; }
 | 
			
		||||
 | 
			
		||||
        public HttpStatusCode StatusCode
 | 
			
		||||
        {
 | 
			
		||||
            get => (HttpStatusCode)Status;
 | 
			
		||||
            set => Status = (int)value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Sets the range values.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private void SetRangeValues()
 | 
			
		||||
        {
 | 
			
		||||
            var requestedRange = RequestedRanges[0];
 | 
			
		||||
 | 
			
		||||
            // If the requested range is "0-", we can optimize by just doing a stream copy
 | 
			
		||||
            if (!requestedRange.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                RangeEnd = TotalContentLength - 1;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                RangeEnd = requestedRange.Value.Value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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(CultureInfo.InvariantCulture);
 | 
			
		||||
            Headers[HeaderNames.ContentLength] = lengthString;
 | 
			
		||||
            var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
 | 
			
		||||
            Headers[HeaderNames.ContentRange] = rangeString;
 | 
			
		||||
 | 
			
		||||
            _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // Headers only
 | 
			
		||||
                if (IsHeadRequest)
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var path = Path;
 | 
			
		||||
                var offset = RangeStart;
 | 
			
		||||
                var count = RangeLength;
 | 
			
		||||
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var extension = System.IO.Path.GetExtension(path);
 | 
			
		||||
 | 
			
		||||
                    if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        _logger.LogDebug("Transmit file {0}", path);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    offset = 0;
 | 
			
		||||
                    count = 0;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                OnComplete?.Invoke();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            var fileOptions = FileOptions.SequentialScan;
 | 
			
		||||
 | 
			
		||||
            // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
 | 
			
		||||
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 | 
			
		||||
            {
 | 
			
		||||
                fileOptions |= FileOptions.Asynchronous;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
 | 
			
		||||
            {
 | 
			
		||||
                if (offset > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    fs.Position = offset;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,766 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.Sockets;
 | 
			
		||||
using System.Net.WebSockets;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Emby.Server.Implementations.Services;
 | 
			
		||||
using Emby.Server.Implementations.SocketSharp;
 | 
			
		||||
using Jellyfin.Data.Events;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Common.Net;
 | 
			
		||||
using MediaBrowser.Controller;
 | 
			
		||||
using MediaBrowser.Controller.Authentication;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Model.Globalization;
 | 
			
		||||
using MediaBrowser.Model.Serialization;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Http.Extensions;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Microsoft.Extensions.Primitives;
 | 
			
		||||
using ServiceStack.Text.Jsv;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    public class HttpListenerHost : IHttpServer
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The key for a setting that specifies the default redirect path
 | 
			
		||||
        /// to use for requests where the URL base prefix is invalid or missing.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
 | 
			
		||||
 | 
			
		||||
        private readonly ILogger<HttpListenerHost> _logger;
 | 
			
		||||
        private readonly ILoggerFactory _loggerFactory;
 | 
			
		||||
        private readonly IServerConfigurationManager _config;
 | 
			
		||||
        private readonly INetworkManager _networkManager;
 | 
			
		||||
        private readonly IServerApplicationHost _appHost;
 | 
			
		||||
        private readonly IJsonSerializer _jsonSerializer;
 | 
			
		||||
        private readonly IXmlSerializer _xmlSerializer;
 | 
			
		||||
        private readonly Func<Type, Func<string, object>> _funcParseFn;
 | 
			
		||||
        private readonly string _defaultRedirectPath;
 | 
			
		||||
        private readonly string _baseUrlPrefix;
 | 
			
		||||
 | 
			
		||||
        private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
 | 
			
		||||
        private readonly IHostEnvironment _hostEnvironment;
 | 
			
		||||
 | 
			
		||||
        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
 | 
			
		||||
        private bool _disposed = false;
 | 
			
		||||
 | 
			
		||||
        public HttpListenerHost(
 | 
			
		||||
            IServerApplicationHost applicationHost,
 | 
			
		||||
            ILogger<HttpListenerHost> logger,
 | 
			
		||||
            IServerConfigurationManager config,
 | 
			
		||||
            IConfiguration configuration,
 | 
			
		||||
            INetworkManager networkManager,
 | 
			
		||||
            IJsonSerializer jsonSerializer,
 | 
			
		||||
            IXmlSerializer xmlSerializer,
 | 
			
		||||
            ILocalizationManager localizationManager,
 | 
			
		||||
            ServiceController serviceController,
 | 
			
		||||
            IHostEnvironment hostEnvironment,
 | 
			
		||||
            ILoggerFactory loggerFactory)
 | 
			
		||||
        {
 | 
			
		||||
            _appHost = applicationHost;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _config = config;
 | 
			
		||||
            _defaultRedirectPath = configuration[DefaultRedirectKey];
 | 
			
		||||
            _baseUrlPrefix = _config.Configuration.BaseUrl;
 | 
			
		||||
            _networkManager = networkManager;
 | 
			
		||||
            _jsonSerializer = jsonSerializer;
 | 
			
		||||
            _xmlSerializer = xmlSerializer;
 | 
			
		||||
            ServiceController = serviceController;
 | 
			
		||||
            _hostEnvironment = hostEnvironment;
 | 
			
		||||
            _loggerFactory = loggerFactory;
 | 
			
		||||
 | 
			
		||||
            _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
 | 
			
		||||
 | 
			
		||||
            Instance = this;
 | 
			
		||||
            ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
 | 
			
		||||
            GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
 | 
			
		||||
 | 
			
		||||
        public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
 | 
			
		||||
 | 
			
		||||
        public static HttpListenerHost Instance { get; protected set; }
 | 
			
		||||
 | 
			
		||||
        public string[] UrlPrefixes { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public string GlobalResponse { get; set; }
 | 
			
		||||
 | 
			
		||||
        public ServiceController ServiceController { get; }
 | 
			
		||||
 | 
			
		||||
        public object CreateInstance(Type type)
 | 
			
		||||
        {
 | 
			
		||||
            return _appHost.CreateInstance(type);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string NormalizeUrlPath(string path)
 | 
			
		||||
        {
 | 
			
		||||
            if (path.Length > 0 && path[0] == '/')
 | 
			
		||||
            {
 | 
			
		||||
                // If the path begins with a leading slash, just return it as-is
 | 
			
		||||
                return path;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // If the path does not begin with a leading slash, append one for consistency
 | 
			
		||||
                return "/" + path;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Applies the request filters. Returns whether or not the request has been handled
 | 
			
		||||
        /// and no more processing should be done.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
 | 
			
		||||
        {
 | 
			
		||||
            // Exec all RequestFilter attributes with Priority < 0
 | 
			
		||||
            var attributes = GetRequestFilterAttributes(requestDto.GetType());
 | 
			
		||||
 | 
			
		||||
            int count = attributes.Count;
 | 
			
		||||
            int i = 0;
 | 
			
		||||
            for (; i < count && attributes[i].Priority < 0; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var attribute = attributes[i];
 | 
			
		||||
                attribute.RequestFilter(req, res, requestDto);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Exec remaining RequestFilter attributes with Priority >= 0
 | 
			
		||||
            for (; i < count && attributes[i].Priority >= 0; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var attribute = attributes[i];
 | 
			
		||||
                attribute.RequestFilter(req, res, requestDto);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Type GetServiceTypeByRequest(Type requestType)
 | 
			
		||||
        {
 | 
			
		||||
            _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
 | 
			
		||||
            return serviceType;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void AddServiceInfo(Type serviceType, Type requestType)
 | 
			
		||||
        {
 | 
			
		||||
            _serviceOperationsMap[requestType] = serviceType;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
 | 
			
		||||
        {
 | 
			
		||||
            var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
 | 
			
		||||
 | 
			
		||||
            var serviceType = GetServiceTypeByRequest(requestDtoType);
 | 
			
		||||
            if (serviceType != null)
 | 
			
		||||
            {
 | 
			
		||||
                attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            attributes.Sort((x, y) => x.Priority - y.Priority);
 | 
			
		||||
 | 
			
		||||
            return attributes;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static Exception GetActualException(Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (ex is AggregateException agg)
 | 
			
		||||
            {
 | 
			
		||||
                var inner = agg.InnerException;
 | 
			
		||||
                if (inner != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return GetActualException(inner);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var inners = agg.InnerExceptions;
 | 
			
		||||
                    if (inners.Count > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        return GetActualException(inners[0]);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return ex;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private int GetStatusCode(Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            switch (ex)
 | 
			
		||||
            {
 | 
			
		||||
                case ArgumentException _: return 400;
 | 
			
		||||
                case AuthenticationException _: return 401;
 | 
			
		||||
                case SecurityException _: return 403;
 | 
			
		||||
                case DirectoryNotFoundException _:
 | 
			
		||||
                case FileNotFoundException _:
 | 
			
		||||
                case ResourceNotFoundException _: return 404;
 | 
			
		||||
                case MethodNotAllowedException _: return 405;
 | 
			
		||||
                default: return 500;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
 | 
			
		||||
        {
 | 
			
		||||
            if (ignoreStackTrace)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var httpRes = httpReq.Response;
 | 
			
		||||
 | 
			
		||||
            if (httpRes.HasStarted)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            httpRes.StatusCode = statusCode;
 | 
			
		||||
 | 
			
		||||
            var errContent = _hostEnvironment.IsDevelopment()
 | 
			
		||||
                    ? (NormalizeExceptionMessage(ex) ?? string.Empty)
 | 
			
		||||
                    : "Error processing request.";
 | 
			
		||||
            httpRes.ContentType = "text/plain";
 | 
			
		||||
            httpRes.ContentLength = errContent.Length;
 | 
			
		||||
            await httpRes.WriteAsync(errContent).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string NormalizeExceptionMessage(Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            // Do not expose the exception message for AuthenticationException
 | 
			
		||||
            if (ex is AuthenticationException)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Strip any information we don't want to reveal
 | 
			
		||||
            return ex.Message
 | 
			
		||||
                ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static string RemoveQueryStringByKey(string url, string key)
 | 
			
		||||
        {
 | 
			
		||||
            var uri = new Uri(url);
 | 
			
		||||
 | 
			
		||||
            // this gets all the query string key value pairs as a collection
 | 
			
		||||
            var newQueryString = QueryHelpers.ParseQuery(uri.Query);
 | 
			
		||||
 | 
			
		||||
            var originalCount = newQueryString.Count;
 | 
			
		||||
 | 
			
		||||
            if (originalCount == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return url;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // this removes the key if exists
 | 
			
		||||
            newQueryString.Remove(key);
 | 
			
		||||
 | 
			
		||||
            if (originalCount == newQueryString.Count)
 | 
			
		||||
            {
 | 
			
		||||
                return url;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // this gets the page path from root without QueryString
 | 
			
		||||
            string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
 | 
			
		||||
 | 
			
		||||
            return newQueryString.Count > 0
 | 
			
		||||
                ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
 | 
			
		||||
                : pagePathWithoutQueryString;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetUrlToLog(string url)
 | 
			
		||||
        {
 | 
			
		||||
            url = RemoveQueryStringByKey(url, "api_key");
 | 
			
		||||
 | 
			
		||||
            return url;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string NormalizeConfiguredLocalAddress(string address)
 | 
			
		||||
        {
 | 
			
		||||
            var add = address.AsSpan().Trim('/');
 | 
			
		||||
            int index = add.IndexOf('/');
 | 
			
		||||
            if (index != -1)
 | 
			
		||||
            {
 | 
			
		||||
                add = add.Slice(index + 1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return add.TrimStart('/').ToString();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool ValidateHost(string host)
 | 
			
		||||
        {
 | 
			
		||||
            var hosts = _config
 | 
			
		||||
                .Configuration
 | 
			
		||||
                .LocalNetworkAddresses
 | 
			
		||||
                .Select(NormalizeConfiguredLocalAddress)
 | 
			
		||||
                .ToList();
 | 
			
		||||
 | 
			
		||||
            if (hosts.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            host ??= string.Empty;
 | 
			
		||||
 | 
			
		||||
            if (_networkManager.IsInPrivateAddressSpace(host))
 | 
			
		||||
            {
 | 
			
		||||
                hosts.Add("localhost");
 | 
			
		||||
                hosts.Add("127.0.0.1");
 | 
			
		||||
 | 
			
		||||
                return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool ValidateRequest(string remoteIp, bool isLocal)
 | 
			
		||||
        {
 | 
			
		||||
            if (isLocal)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (_config.Configuration.EnableRemoteAccess)
 | 
			
		||||
            {
 | 
			
		||||
                var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
 | 
			
		||||
 | 
			
		||||
                if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
 | 
			
		||||
                {
 | 
			
		||||
                    if (_config.Configuration.IsRemoteIPFilterBlacklist)
 | 
			
		||||
                    {
 | 
			
		||||
                        return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (!_networkManager.IsInLocalNetwork(remoteIp))
 | 
			
		||||
                {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
 | 
			
		||||
        private bool ValidateSsl(string remoteIp, string urlString)
 | 
			
		||||
        {
 | 
			
		||||
            if (_config.Configuration.RequireHttps
 | 
			
		||||
                && _appHost.ListenWithHttps
 | 
			
		||||
                && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
 | 
			
		||||
                if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
 | 
			
		||||
                    || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!_networkManager.IsInLocalNetwork(remoteIp))
 | 
			
		||||
                {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public Task RequestHandler(HttpContext context)
 | 
			
		||||
        {
 | 
			
		||||
            if (context.WebSockets.IsWebSocketRequest)
 | 
			
		||||
            {
 | 
			
		||||
                return WebSocketRequestHandler(context);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var request = context.Request;
 | 
			
		||||
            var response = context.Response;
 | 
			
		||||
            var localPath = context.Request.Path.ToString();
 | 
			
		||||
 | 
			
		||||
            var req = new WebSocketSharpRequest(request, response, request.Path);
 | 
			
		||||
            return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Overridable method that can be used to implement a custom handler.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            var stopWatch = new Stopwatch();
 | 
			
		||||
            stopWatch.Start();
 | 
			
		||||
            var httpRes = httpReq.Response;
 | 
			
		||||
            string urlToLog = GetUrlToLog(urlString);
 | 
			
		||||
            string remoteIp = httpReq.RemoteIp;
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (_disposed)
 | 
			
		||||
                {
 | 
			
		||||
                    httpRes.StatusCode = 503;
 | 
			
		||||
                    httpRes.ContentType = "text/plain";
 | 
			
		||||
                    await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!ValidateHost(host))
 | 
			
		||||
                {
 | 
			
		||||
                    httpRes.StatusCode = 400;
 | 
			
		||||
                    httpRes.ContentType = "text/plain";
 | 
			
		||||
                    await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!ValidateRequest(remoteIp, httpReq.IsLocal))
 | 
			
		||||
                {
 | 
			
		||||
                    httpRes.StatusCode = 403;
 | 
			
		||||
                    httpRes.ContentType = "text/plain";
 | 
			
		||||
                    await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!ValidateSsl(httpReq.RemoteIp, urlString))
 | 
			
		||||
                {
 | 
			
		||||
                    RedirectToSecureUrl(httpReq, httpRes, urlString);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    httpRes.StatusCode = 200;
 | 
			
		||||
                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
 | 
			
		||||
                    {
 | 
			
		||||
                        httpRes.Headers.Add(key, value);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    httpRes.ContentType = "text/plain";
 | 
			
		||||
                    await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                    || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                    || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                    || string.IsNullOrEmpty(localPath)
 | 
			
		||||
                    || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    // Always redirect back to the default path if the base prefix is invalid or missing
 | 
			
		||||
                    _logger.LogDebug("Normalizing a URL at {0}", localPath);
 | 
			
		||||
                    httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrEmpty(GlobalResponse))
 | 
			
		||||
                {
 | 
			
		||||
                    // We don't want the address pings in ApplicationHost to fail
 | 
			
		||||
                    if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
 | 
			
		||||
                    {
 | 
			
		||||
                        httpRes.StatusCode = 503;
 | 
			
		||||
                        httpRes.ContentType = "text/html";
 | 
			
		||||
                        await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var handler = GetServiceHandler(httpReq);
 | 
			
		||||
                if (handler != null)
 | 
			
		||||
                {
 | 
			
		||||
                    await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    throw new FileNotFoundException();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception requestEx)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var requestInnerEx = GetActualException(requestEx);
 | 
			
		||||
                    var statusCode = GetStatusCode(requestInnerEx);
 | 
			
		||||
 | 
			
		||||
                    foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!httpRes.Headers.ContainsKey(key))
 | 
			
		||||
                        {
 | 
			
		||||
                            httpRes.Headers.Add(key, value);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    bool ignoreStackTrace =
 | 
			
		||||
                        requestInnerEx is SocketException
 | 
			
		||||
                        || requestInnerEx is IOException
 | 
			
		||||
                        || requestInnerEx is OperationCanceledException
 | 
			
		||||
                        || requestInnerEx is SecurityException
 | 
			
		||||
                        || requestInnerEx is AuthenticationException
 | 
			
		||||
                        || requestInnerEx is FileNotFoundException;
 | 
			
		||||
 | 
			
		||||
                    // Do not handle 500 server exceptions manually when in development mode.
 | 
			
		||||
                    // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
 | 
			
		||||
                    // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
 | 
			
		||||
                    // because it will log the stack trace when it handles the exception.
 | 
			
		||||
                    if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
 | 
			
		||||
                    {
 | 
			
		||||
                        throw;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception handlerException)
 | 
			
		||||
                {
 | 
			
		||||
                    var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
 | 
			
		||||
                    _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
 | 
			
		||||
 | 
			
		||||
                    if (_hostEnvironment.IsDevelopment())
 | 
			
		||||
                    {
 | 
			
		||||
                        throw aggregateEx;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                if (httpRes.StatusCode >= 500)
 | 
			
		||||
                {
 | 
			
		||||
                    _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                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);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task WebSocketRequestHandler(HttpContext context)
 | 
			
		||||
        {
 | 
			
		||||
            if (_disposed)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
 | 
			
		||||
 | 
			
		||||
                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                using var connection = new WebSocketConnection(
 | 
			
		||||
                    _loggerFactory.CreateLogger<WebSocketConnection>(),
 | 
			
		||||
                    webSocket,
 | 
			
		||||
                    context.Connection.RemoteIpAddress,
 | 
			
		||||
                    context.Request.Query)
 | 
			
		||||
                {
 | 
			
		||||
                    OnReceive = ProcessWebSocketMessageReceived
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
 | 
			
		||||
 | 
			
		||||
                await connection.ProcessAsync().ConfigureAwait(false);
 | 
			
		||||
                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
 | 
			
		||||
                if (!context.Response.HasStarted)
 | 
			
		||||
                {
 | 
			
		||||
                    context.Response.StatusCode = 500;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Get the default CORS headers.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="req"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
 | 
			
		||||
        {
 | 
			
		||||
            var origin = req.Headers["Origin"];
 | 
			
		||||
            if (origin == StringValues.Empty)
 | 
			
		||||
            {
 | 
			
		||||
                origin = req.Headers["Host"];
 | 
			
		||||
                if (origin == StringValues.Empty)
 | 
			
		||||
                {
 | 
			
		||||
                    origin = "*";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var headers = new Dictionary<string, string>();
 | 
			
		||||
            headers.Add("Access-Control-Allow-Origin", origin);
 | 
			
		||||
            headers.Add("Access-Control-Allow-Credentials", "true");
 | 
			
		||||
            headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
 | 
			
		||||
            headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
 | 
			
		||||
            return headers;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Entry point for HttpListener
 | 
			
		||||
        public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
 | 
			
		||||
        {
 | 
			
		||||
            var pathInfo = httpReq.PathInfo;
 | 
			
		||||
 | 
			
		||||
            pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
 | 
			
		||||
            var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
 | 
			
		||||
            if (restPath != null)
 | 
			
		||||
            {
 | 
			
		||||
                return new ServiceHandler(restPath, contentType);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _logger.LogError("Could not find handler for {PathInfo}", pathInfo);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
 | 
			
		||||
        {
 | 
			
		||||
            if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
 | 
			
		||||
            {
 | 
			
		||||
                var builder = new UriBuilder(uri)
 | 
			
		||||
                {
 | 
			
		||||
                    Port = _config.Configuration.PublicHttpsPort,
 | 
			
		||||
                    Scheme = "https"
 | 
			
		||||
                };
 | 
			
		||||
                url = builder.Uri.ToString();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            httpRes.Redirect(url);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds the rest handlers.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
 | 
			
		||||
        /// <param name="listeners">The web socket listeners.</param>
 | 
			
		||||
        /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
 | 
			
		||||
        public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
 | 
			
		||||
        {
 | 
			
		||||
            _webSocketListeners = listeners.ToArray();
 | 
			
		||||
            UrlPrefixes = urlPrefixes.ToArray();
 | 
			
		||||
 | 
			
		||||
            ServiceController.Init(this, serviceTypes);
 | 
			
		||||
 | 
			
		||||
            ResponseFilters = new Action<IRequest, HttpResponse, object>[]
 | 
			
		||||
            {
 | 
			
		||||
                new ResponseFilter(this, _logger).FilterResponse
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RouteAttribute[] GetRouteAttributes(Type requestType)
 | 
			
		||||
        {
 | 
			
		||||
            var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
 | 
			
		||||
            var clone = routes.ToList();
 | 
			
		||||
 | 
			
		||||
            foreach (var route in clone)
 | 
			
		||||
            {
 | 
			
		||||
                routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
 | 
			
		||||
                {
 | 
			
		||||
                    Notes = route.Notes,
 | 
			
		||||
                    Priority = route.Priority,
 | 
			
		||||
                    Summary = route.Summary
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
 | 
			
		||||
                {
 | 
			
		||||
                    Notes = route.Notes,
 | 
			
		||||
                    Priority = route.Priority,
 | 
			
		||||
                    Summary = route.Summary
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
 | 
			
		||||
                {
 | 
			
		||||
                    Notes = route.Notes,
 | 
			
		||||
                    Priority = route.Priority,
 | 
			
		||||
                    Summary = route.Summary
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return routes.ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Func<string, object> GetParseFn(Type propertyType)
 | 
			
		||||
        {
 | 
			
		||||
            return _funcParseFn(propertyType);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void SerializeToJson(object o, Stream stream)
 | 
			
		||||
        {
 | 
			
		||||
            _jsonSerializer.SerializeToStream(o, stream);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void SerializeToXml(object o, Stream stream)
 | 
			
		||||
        {
 | 
			
		||||
            _xmlSerializer.SerializeToStream(o, stream);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<object> DeserializeXml(Type type, Stream stream)
 | 
			
		||||
        {
 | 
			
		||||
            return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<object> DeserializeJson(Type type, Stream stream)
 | 
			
		||||
        {
 | 
			
		||||
            return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string NormalizeEmbyRoutePath(string path)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogDebug("Normalizing /emby route");
 | 
			
		||||
            return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string NormalizeMediaBrowserRoutePath(string path)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogDebug("Normalizing /mediabrowser route");
 | 
			
		||||
            return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string NormalizeCustomRoutePath(string path)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogDebug("Normalizing custom route {0}", path);
 | 
			
		||||
            return _baseUrlPrefix + NormalizeUrlPath(path);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Processes the web socket message received.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="result">The result.</param>
 | 
			
		||||
        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
 | 
			
		||||
        {
 | 
			
		||||
            if (_disposed)
 | 
			
		||||
            {
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            IEnumerable<Task> GetTasks()
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var x in _webSocketListeners)
 | 
			
		||||
                {
 | 
			
		||||
                    yield return x.ProcessMessageAsync(result);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.WhenAll(GetTasks());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,721 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.IO.Compression;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Runtime.Serialization;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using System.Xml;
 | 
			
		||||
using Emby.Server.Implementations.Services;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Class HttpResultFactory.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class HttpResultFactory : IHttpResultFactory
 | 
			
		||||
    {
 | 
			
		||||
        // Last-Modified and If-Modified-Since must follow strict date format,
 | 
			
		||||
        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
 | 
			
		||||
        private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
 | 
			
		||||
        // We specifically use en-US culture because both day of week and month names require it
 | 
			
		||||
        private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The logger.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly ILogger<HttpResultFactory> _logger;
 | 
			
		||||
        private readonly IFileSystem _fileSystem;
 | 
			
		||||
        private readonly IJsonSerializer _jsonSerializer;
 | 
			
		||||
        private readonly IStreamHelper _streamHelper;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
 | 
			
		||||
        {
 | 
			
		||||
            _fileSystem = fileSystem;
 | 
			
		||||
            _jsonSerializer = jsonSerializer;
 | 
			
		||||
            _streamHelper = streamHelper;
 | 
			
		||||
            _logger = loggerfactory.CreateLogger<HttpResultFactory>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="requestContext">The request context.</param>
 | 
			
		||||
        /// <param name="content">The content.</param>
 | 
			
		||||
        /// <param name="contentType">Type of the content.</param>
 | 
			
		||||
        /// <param name="responseHeaders">The response headers.</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            return GetHttpResult(null, content, contentType, true, responseHeaders);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public object GetRedirectResult(string url)
 | 
			
		||||
        {
 | 
			
		||||
            var responseHeaders = new Dictionary<string, string>();
 | 
			
		||||
            responseHeaders[HeaderNames.Location] = url;
 | 
			
		||||
 | 
			
		||||
            var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
 | 
			
		||||
 | 
			
		||||
            AddResponseHeaders(result, responseHeaders);
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the HTTP result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            var result = new StreamWriter(content, contentType);
 | 
			
		||||
 | 
			
		||||
            if (responseHeaders == null)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders = new Dictionary<string, string>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.Expires] = "0";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AddResponseHeaders(result, responseHeaders);
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the HTTP result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            string compressionType = null;
 | 
			
		||||
            bool isHeadRequest = false;
 | 
			
		||||
 | 
			
		||||
            if (requestContext != null)
 | 
			
		||||
            {
 | 
			
		||||
                compressionType = GetCompressionType(requestContext, content, contentType);
 | 
			
		||||
                isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            IHasHeaders result;
 | 
			
		||||
            if (string.IsNullOrEmpty(compressionType))
 | 
			
		||||
            {
 | 
			
		||||
                var contentLength = content.Length;
 | 
			
		||||
 | 
			
		||||
                if (isHeadRequest)
 | 
			
		||||
                {
 | 
			
		||||
                    content = Array.Empty<byte>();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                result = new StreamWriter(content, contentType, contentLength);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (responseHeaders == null)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders = new Dictionary<string, string>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.Expires] = "0";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AddResponseHeaders(result, responseHeaders);
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the HTTP result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            IHasHeaders result;
 | 
			
		||||
 | 
			
		||||
            var bytes = Encoding.UTF8.GetBytes(content);
 | 
			
		||||
 | 
			
		||||
            var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
 | 
			
		||||
 | 
			
		||||
            var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(compressionType))
 | 
			
		||||
            {
 | 
			
		||||
                var contentLength = bytes.Length;
 | 
			
		||||
 | 
			
		||||
                if (isHeadRequest)
 | 
			
		||||
                {
 | 
			
		||||
                    bytes = Array.Empty<byte>();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                result = new StreamWriter(bytes, contentType, contentLength);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (responseHeaders == null)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders = new Dictionary<string, string>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.Expires] = "0";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AddResponseHeaders(result, responseHeaders);
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the optimized result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T"></typeparam>
 | 
			
		||||
        public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
            where T : class
 | 
			
		||||
        {
 | 
			
		||||
            if (result == null)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentNullException(nameof(result));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (responseHeaders == null)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            responseHeaders[HeaderNames.Expires] = "0";
 | 
			
		||||
 | 
			
		||||
            return ToOptimizedResultInternal(requestContext, result, responseHeaders);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
 | 
			
		||||
        {
 | 
			
		||||
            if (responseContentType == null)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Per apple docs, hls manifests must be compressed
 | 
			
		||||
            if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
 | 
			
		||||
                responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
 | 
			
		||||
                responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
 | 
			
		||||
                responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
 | 
			
		||||
                responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (content.Length < 1024)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return GetCompressionType(request);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetCompressionType(IRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(acceptEncoding))
 | 
			
		||||
            {
 | 
			
		||||
                // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
                //    return "br";
 | 
			
		||||
 | 
			
		||||
                if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    return "deflate";
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    return "gzip";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Returns the optimized result for the IRequestContext.
 | 
			
		||||
        /// Does not use or store results in any cache.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="request"></param>
 | 
			
		||||
        /// <param name="dto"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public object ToOptimizedResult<T>(IRequest request, T dto)
 | 
			
		||||
        {
 | 
			
		||||
            return ToOptimizedResultInternal(request, dto);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
        {
 | 
			
		||||
            // TODO: @bond use Span and .Equals
 | 
			
		||||
            var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
            switch (contentType)
 | 
			
		||||
            {
 | 
			
		||||
                case "application/xml":
 | 
			
		||||
                case "text/xml":
 | 
			
		||||
                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
 | 
			
		||||
                    return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
 | 
			
		||||
 | 
			
		||||
                case "application/json":
 | 
			
		||||
                case "text/json":
 | 
			
		||||
                    return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
 | 
			
		||||
                default:
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
            var ms = new MemoryStream();
 | 
			
		||||
            var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
 | 
			
		||||
 | 
			
		||||
            writerFn(dto, ms);
 | 
			
		||||
 | 
			
		||||
            ms.Position = 0;
 | 
			
		||||
 | 
			
		||||
            if (isHeadRequest)
 | 
			
		||||
            {
 | 
			
		||||
                using (ms)
 | 
			
		||||
                {
 | 
			
		||||
                    return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return GetHttpResult(request, ms, contentType, true, responseHeaders);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private IHasHeaders GetCompressedResult(
 | 
			
		||||
            byte[] content,
 | 
			
		||||
            string requestedCompressionType,
 | 
			
		||||
            IDictionary<string, string> responseHeaders,
 | 
			
		||||
            bool isHeadRequest,
 | 
			
		||||
            string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            if (responseHeaders == null)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            content = Compress(content, requestedCompressionType);
 | 
			
		||||
            responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
 | 
			
		||||
 | 
			
		||||
            responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
 | 
			
		||||
 | 
			
		||||
            var contentLength = content.Length;
 | 
			
		||||
 | 
			
		||||
            if (isHeadRequest)
 | 
			
		||||
            {
 | 
			
		||||
                var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
 | 
			
		||||
                AddResponseHeaders(result, responseHeaders);
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var result = new StreamWriter(content, contentType, contentLength);
 | 
			
		||||
                AddResponseHeaders(result, responseHeaders);
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private byte[] Compress(byte[] bytes, string compressionType)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return Deflate(bytes);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return GZip(bytes);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw new NotSupportedException(compressionType);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static byte[] Deflate(byte[] bytes)
 | 
			
		||||
        {
 | 
			
		||||
            // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
 | 
			
		||||
            // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
 | 
			
		||||
            using (var ms = new MemoryStream())
 | 
			
		||||
            using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
 | 
			
		||||
            {
 | 
			
		||||
                zipStream.Write(bytes, 0, bytes.Length);
 | 
			
		||||
                zipStream.Dispose();
 | 
			
		||||
 | 
			
		||||
                return ms.ToArray();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static byte[] GZip(byte[] buffer)
 | 
			
		||||
        {
 | 
			
		||||
            using (var ms = new MemoryStream())
 | 
			
		||||
            using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
 | 
			
		||||
            {
 | 
			
		||||
                zipStream.Write(buffer, 0, buffer.Length);
 | 
			
		||||
                zipStream.Dispose();
 | 
			
		||||
 | 
			
		||||
                return ms.ToArray();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string SerializeToXmlString(object from)
 | 
			
		||||
        {
 | 
			
		||||
            using (var ms = new MemoryStream())
 | 
			
		||||
            {
 | 
			
		||||
                var xwSettings = new XmlWriterSettings();
 | 
			
		||||
                xwSettings.Encoding = new UTF8Encoding(false);
 | 
			
		||||
                xwSettings.OmitXmlDeclaration = false;
 | 
			
		||||
 | 
			
		||||
                using (var xw = XmlWriter.Create(ms, xwSettings))
 | 
			
		||||
                {
 | 
			
		||||
                    var serializer = new DataContractSerializer(from.GetType());
 | 
			
		||||
                    serializer.WriteObject(xw, from);
 | 
			
		||||
                    xw.Flush();
 | 
			
		||||
                    ms.Seek(0, SeekOrigin.Begin);
 | 
			
		||||
                    using (var reader = new StreamReader(ms))
 | 
			
		||||
                    {
 | 
			
		||||
                        return reader.ReadToEnd();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Pres the process optimized result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
 | 
			
		||||
        {
 | 
			
		||||
            bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
 | 
			
		||||
            AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
 | 
			
		||||
 | 
			
		||||
            if (!noCache)
 | 
			
		||||
            {
 | 
			
		||||
                if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
 | 
			
		||||
                {
 | 
			
		||||
                    _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
 | 
			
		||||
                {
 | 
			
		||||
                    AddAgeHeader(responseHeaders, options.DateLastModified);
 | 
			
		||||
 | 
			
		||||
                    var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
 | 
			
		||||
 | 
			
		||||
                    AddResponseHeaders(result, responseHeaders);
 | 
			
		||||
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<object> GetStaticFileResult(IRequest requestContext,
 | 
			
		||||
            string path,
 | 
			
		||||
            FileShare fileShare = FileShare.Read)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(path))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentNullException(nameof(path));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return GetStaticFileResult(requestContext, new StaticFileResultOptions
 | 
			
		||||
            {
 | 
			
		||||
                Path = path,
 | 
			
		||||
                FileShare = fileShare
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
 | 
			
		||||
        {
 | 
			
		||||
            var path = options.Path;
 | 
			
		||||
            var fileShare = options.FileShare;
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(path))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException("Path can't be empty.", nameof(options));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException("FileShare must be either Read or ReadWrite");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(options.ContentType))
 | 
			
		||||
            {
 | 
			
		||||
                options.ContentType = MimeTypes.GetMimeType(path);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!options.DateLastModified.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
 | 
			
		||||
 | 
			
		||||
            options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
            return GetStaticResult(requestContext, options);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the file stream.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="path">The path.</param>
 | 
			
		||||
        /// <param name="fileShare">The file share.</param>
 | 
			
		||||
        /// <returns>Stream.</returns>
 | 
			
		||||
        private Stream GetFileStream(string path, FileShare fileShare)
 | 
			
		||||
        {
 | 
			
		||||
            return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<object> GetStaticResult(IRequest requestContext,
 | 
			
		||||
            Guid cacheKey,
 | 
			
		||||
            DateTime? lastDateModified,
 | 
			
		||||
            TimeSpan? cacheDuration,
 | 
			
		||||
            string contentType,
 | 
			
		||||
            Func<Task<Stream>> factoryFn,
 | 
			
		||||
            IDictionary<string, string> responseHeaders = null,
 | 
			
		||||
            bool isHeadRequest = false)
 | 
			
		||||
        {
 | 
			
		||||
            return GetStaticResult(requestContext, new StaticResultOptions
 | 
			
		||||
            {
 | 
			
		||||
                CacheDuration = cacheDuration,
 | 
			
		||||
                ContentFactory = factoryFn,
 | 
			
		||||
                ContentType = contentType,
 | 
			
		||||
                DateLastModified = lastDateModified,
 | 
			
		||||
                IsHeadRequest = isHeadRequest,
 | 
			
		||||
                ResponseHeaders = responseHeaders
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
 | 
			
		||||
        {
 | 
			
		||||
            options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
            var contentType = options.ContentType;
 | 
			
		||||
            if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
 | 
			
		||||
            {
 | 
			
		||||
                // See if the result is already cached in the browser
 | 
			
		||||
                var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
 | 
			
		||||
 | 
			
		||||
                if (result != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // TODO: We don't really need the option value
 | 
			
		||||
            var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
            var factoryFn = options.ContentFactory;
 | 
			
		||||
            var responseHeaders = options.ResponseHeaders;
 | 
			
		||||
            AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
 | 
			
		||||
            AddAgeHeader(responseHeaders, options.DateLastModified);
 | 
			
		||||
 | 
			
		||||
            var rangeHeader = requestContext.Headers[HeaderNames.Range];
 | 
			
		||||
 | 
			
		||||
            if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
 | 
			
		||||
            {
 | 
			
		||||
                var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
 | 
			
		||||
                {
 | 
			
		||||
                    OnComplete = options.OnComplete,
 | 
			
		||||
                    OnError = options.OnError,
 | 
			
		||||
                    FileShare = options.FileShare
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
 | 
			
		||||
                return hasHeaders;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var stream = await factoryFn().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var totalContentLength = options.ContentLength;
 | 
			
		||||
            if (!totalContentLength.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    totalContentLength = stream.Length;
 | 
			
		||||
                }
 | 
			
		||||
                catch (NotSupportedException)
 | 
			
		||||
                {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
 | 
			
		||||
                {
 | 
			
		||||
                    OnComplete = options.OnComplete
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
 | 
			
		||||
                return hasHeaders;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (totalContentLength.HasValue)
 | 
			
		||||
                {
 | 
			
		||||
                    responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (isHeadRequest)
 | 
			
		||||
                {
 | 
			
		||||
                    using (stream)
 | 
			
		||||
                    {
 | 
			
		||||
                        return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var hasHeaders = new StreamWriter(stream, contentType)
 | 
			
		||||
                {
 | 
			
		||||
                    OnComplete = options.OnComplete,
 | 
			
		||||
                    OnError = options.OnError
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                AddResponseHeaders(hasHeaders, options.ResponseHeaders);
 | 
			
		||||
                return hasHeaders;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds the caching responseHeaders.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private void AddCachingHeaders(
 | 
			
		||||
            IDictionary<string, string> responseHeaders,
 | 
			
		||||
            TimeSpan? cacheDuration,
 | 
			
		||||
            bool noCache,
 | 
			
		||||
            DateTime? lastModifiedDate)
 | 
			
		||||
        {
 | 
			
		||||
            if (noCache)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
 | 
			
		||||
                responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (cacheDuration.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.CacheControl] = "public";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (lastModifiedDate.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds the age header.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="responseHeaders">The responseHeaders.</param>
 | 
			
		||||
        /// <param name="lastDateModified">The last date modified.</param>
 | 
			
		||||
        private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
 | 
			
		||||
        {
 | 
			
		||||
            if (lastDateModified.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Determines whether [is not modified] [the specified if modified since].
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="ifModifiedSince">If modified since.</param>
 | 
			
		||||
        /// <param name="cacheDuration">Duration of the cache.</param>
 | 
			
		||||
        /// <param name="dateModified">The date modified.</param>
 | 
			
		||||
        /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
 | 
			
		||||
        private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
 | 
			
		||||
        {
 | 
			
		||||
            if (dateModified.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                var lastModified = NormalizeDateForComparison(dateModified.Value);
 | 
			
		||||
                ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
 | 
			
		||||
 | 
			
		||||
                return lastModified <= ifModifiedSince;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (cacheDuration.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
 | 
			
		||||
 | 
			
		||||
                if (DateTime.UtcNow < cacheExpirationDate)
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="date">The date.</param>
 | 
			
		||||
        /// <returns>DateTime.</returns>
 | 
			
		||||
        private static DateTime NormalizeDateForComparison(DateTime date)
 | 
			
		||||
        {
 | 
			
		||||
            return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds the response headers.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="hasHeaders">The has options.</param>
 | 
			
		||||
        /// <param name="responseHeaders">The response headers.</param>
 | 
			
		||||
        private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in responseHeaders)
 | 
			
		||||
            {
 | 
			
		||||
                hasHeaders.Headers[item.Key] = item.Value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,212 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.Net.Http.Headers;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
 | 
			
		||||
    {
 | 
			
		||||
        private const int BufferSize = 81920;
 | 
			
		||||
 | 
			
		||||
        private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
 | 
			
		||||
 | 
			
		||||
        private List<KeyValuePair<long, long?>> _requestedRanges;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="rangeHeader">The range header.</param>
 | 
			
		||||
        /// <param name="contentLength">The content length.</param>
 | 
			
		||||
        /// <param name="source">The source.</param>
 | 
			
		||||
        /// <param name="contentType">Type of the content.</param>
 | 
			
		||||
        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
 | 
			
		||||
        public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(contentType))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentNullException(nameof(contentType));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            RangeHeader = rangeHeader;
 | 
			
		||||
            SourceStream = source;
 | 
			
		||||
            IsHeadRequest = isHeadRequest;
 | 
			
		||||
 | 
			
		||||
            ContentType = contentType;
 | 
			
		||||
            Headers[HeaderNames.ContentType] = contentType;
 | 
			
		||||
            Headers[HeaderNames.AcceptRanges] = "bytes";
 | 
			
		||||
            StatusCode = HttpStatusCode.PartialContent;
 | 
			
		||||
 | 
			
		||||
            SetRangeValues(contentLength);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the source stream.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The source stream.</value>
 | 
			
		||||
        private Stream SourceStream { get; set; }
 | 
			
		||||
        private string RangeHeader { get; set; }
 | 
			
		||||
        private bool IsHeadRequest { get; set; }
 | 
			
		||||
 | 
			
		||||
        private long RangeStart { get; set; }
 | 
			
		||||
        private long RangeEnd { get; set; }
 | 
			
		||||
        private long RangeLength { get; set; }
 | 
			
		||||
        private long TotalContentLength { get; set; }
 | 
			
		||||
 | 
			
		||||
        public Action OnComplete { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Additional HTTP Headers
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The headers.</value>
 | 
			
		||||
        public IDictionary<string, string> Headers => _options;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the requested ranges.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The requested ranges.</value>
 | 
			
		||||
        protected List<KeyValuePair<long, long?>> RequestedRanges
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                if (_requestedRanges == null)
 | 
			
		||||
                {
 | 
			
		||||
                    _requestedRanges = new List<KeyValuePair<long, long?>>();
 | 
			
		||||
 | 
			
		||||
                    // Example: bytes=0-,32-63
 | 
			
		||||
                    var ranges = RangeHeader.Split('=')[1].Split(',');
 | 
			
		||||
 | 
			
		||||
                    foreach (var range in ranges)
 | 
			
		||||
                    {
 | 
			
		||||
                        var vals = range.Split('-');
 | 
			
		||||
 | 
			
		||||
                        long start = 0;
 | 
			
		||||
                        long? end = null;
 | 
			
		||||
 | 
			
		||||
                        if (!string.IsNullOrEmpty(vals[0]))
 | 
			
		||||
                        {
 | 
			
		||||
                            start = long.Parse(vals[0], CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (!string.IsNullOrEmpty(vals[1]))
 | 
			
		||||
                        {
 | 
			
		||||
                            end = long.Parse(vals[1], CultureInfo.InvariantCulture);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return _requestedRanges;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string ContentType { get; set; }
 | 
			
		||||
 | 
			
		||||
        public IRequest RequestContext { get; set; }
 | 
			
		||||
 | 
			
		||||
        public object Response { get; set; }
 | 
			
		||||
 | 
			
		||||
        public int Status { get; set; }
 | 
			
		||||
 | 
			
		||||
        public HttpStatusCode StatusCode
 | 
			
		||||
        {
 | 
			
		||||
            get => (HttpStatusCode)Status;
 | 
			
		||||
            set => Status = (int)value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Sets the range values.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private void SetRangeValues(long contentLength)
 | 
			
		||||
        {
 | 
			
		||||
            var requestedRange = RequestedRanges[0];
 | 
			
		||||
 | 
			
		||||
            TotalContentLength = contentLength;
 | 
			
		||||
 | 
			
		||||
            // If the requested range is "0-", we can optimize by just doing a stream copy
 | 
			
		||||
            if (!requestedRange.Value.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                RangeEnd = TotalContentLength - 1;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                RangeEnd = requestedRange.Value.Value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            RangeStart = requestedRange.Key;
 | 
			
		||||
            RangeLength = 1 + RangeEnd - RangeStart;
 | 
			
		||||
 | 
			
		||||
            Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
            Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
 | 
			
		||||
 | 
			
		||||
            if (RangeStart > 0 && SourceStream.CanSeek)
 | 
			
		||||
            {
 | 
			
		||||
                SourceStream.Position = RangeStart;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // Headers only
 | 
			
		||||
                if (IsHeadRequest)
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                using (var source = SourceStream)
 | 
			
		||||
                {
 | 
			
		||||
                    // If the requested range is "0-", we can optimize by just doing a stream copy
 | 
			
		||||
                    if (RangeEnd >= TotalContentLength - 1)
 | 
			
		||||
                    {
 | 
			
		||||
                        await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                OnComplete?.Invoke();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            var array = ArrayPool<byte>.Shared.Rent(BufferSize);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                int bytesRead;
 | 
			
		||||
                while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var bytesToCopy = Math.Min(bytesRead, copyLength);
 | 
			
		||||
 | 
			
		||||
                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                    copyLength -= bytesToCopy;
 | 
			
		||||
 | 
			
		||||
                    if (copyLength <= 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                ArrayPool<byte>.Shared.Return(array);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,113 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Microsoft.Net.Http.Headers;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Class ResponseFilter.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ResponseFilter
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IHttpServer _server;
 | 
			
		||||
        private readonly ILogger _logger;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="ResponseFilter"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="server">The HTTP server.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        public ResponseFilter(IHttpServer server, ILogger logger)
 | 
			
		||||
        {
 | 
			
		||||
            _server = server;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Filters the response.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="req">The req.</param>
 | 
			
		||||
        /// <param name="res">The res.</param>
 | 
			
		||||
        /// <param name="dto">The dto.</param>
 | 
			
		||||
        public void FilterResponse(IRequest req, HttpResponse res, object dto)
 | 
			
		||||
        {
 | 
			
		||||
            foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
 | 
			
		||||
            {
 | 
			
		||||
                res.Headers.Add(key, value);
 | 
			
		||||
            }
 | 
			
		||||
            // Try to prevent compatibility view
 | 
			
		||||
            res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " +
 | 
			
		||||
                "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
 | 
			
		||||
                "Content-Type, Cookie, 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";
 | 
			
		||||
 | 
			
		||||
            if (dto is Exception exception)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl);
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrEmpty(exception.Message))
 | 
			
		||||
                {
 | 
			
		||||
                    var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
 | 
			
		||||
                    error = RemoveControlCharacters(error);
 | 
			
		||||
 | 
			
		||||
                    res.Headers.Add("X-Application-Error-Code", error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (dto is IHasHeaders hasHeaders)
 | 
			
		||||
            {
 | 
			
		||||
                if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
 | 
			
		||||
                {
 | 
			
		||||
                    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(HeaderNames.ContentLength, out string contentLength)
 | 
			
		||||
                    && !string.IsNullOrEmpty(contentLength))
 | 
			
		||||
                {
 | 
			
		||||
                    var length = long.Parse(contentLength, CultureInfo.InvariantCulture);
 | 
			
		||||
 | 
			
		||||
                    if (length > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        res.ContentLength = length;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Removes the control characters.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="inString">The in string.</param>
 | 
			
		||||
        /// <returns>System.String.</returns>
 | 
			
		||||
        public static string RemoveControlCharacters(string inString)
 | 
			
		||||
        {
 | 
			
		||||
            if (inString == null)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            else if (inString.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return inString;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var newString = new StringBuilder(inString.Length);
 | 
			
		||||
 | 
			
		||||
            foreach (var ch in inString)
 | 
			
		||||
            {
 | 
			
		||||
                if (!char.IsControl(ch))
 | 
			
		||||
                {
 | 
			
		||||
                    newString.Append(ch);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return newString.ToString();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +1,7 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Emby.Server.Implementations.SocketSharp;
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using Jellyfin.Data.Enums;
 | 
			
		||||
using MediaBrowser.Common.Net;
 | 
			
		||||
using MediaBrowser.Controller.Authentication;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Controller.Security;
 | 
			
		||||
using MediaBrowser.Controller.Session;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
    public class AuthService : IAuthService
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IAuthorizationContext _authorizationContext;
 | 
			
		||||
        private readonly ISessionManager _sessionManager;
 | 
			
		||||
        private readonly IServerConfigurationManager _config;
 | 
			
		||||
        private readonly INetworkManager _networkManager;
 | 
			
		||||
 | 
			
		||||
        public AuthService(
 | 
			
		||||
            IAuthorizationContext authorizationContext,
 | 
			
		||||
            IServerConfigurationManager config,
 | 
			
		||||
            ISessionManager sessionManager,
 | 
			
		||||
            INetworkManager networkManager)
 | 
			
		||||
            IAuthorizationContext authorizationContext)
 | 
			
		||||
        {
 | 
			
		||||
            _authorizationContext = authorizationContext;
 | 
			
		||||
            _config = config;
 | 
			
		||||
            _sessionManager = sessionManager;
 | 
			
		||||
            _networkManager = networkManager;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
 | 
			
		||||
        {
 | 
			
		||||
            ValidateUser(request, authAttributes);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
 | 
			
		||||
        {
 | 
			
		||||
            var req = new WebSocketSharpRequest(request, null, request.Path);
 | 
			
		||||
            var user = ValidateUser(req, authAttributes);
 | 
			
		||||
            return user;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AuthorizationInfo Authenticate(HttpRequest request)
 | 
			
		||||
@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
 | 
			
		||||
            return auth;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
 | 
			
		||||
        {
 | 
			
		||||
            // This code is executed before the service
 | 
			
		||||
            var auth = _authorizationContext.GetAuthorizationInfo(request);
 | 
			
		||||
 | 
			
		||||
            if (!IsExemptFromAuthenticationToken(authAttributes, request))
 | 
			
		||||
            {
 | 
			
		||||
                ValidateSecurityToken(request, auth.Token);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (authAttributes.AllowLocalOnly && !request.IsLocal)
 | 
			
		||||
            {
 | 
			
		||||
                throw new SecurityException("Operation not found.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var user = auth.User;
 | 
			
		||||
 | 
			
		||||
            if (user == null && auth.UserId != Guid.Empty)
 | 
			
		||||
            {
 | 
			
		||||
                throw new AuthenticationException("User with Id " + auth.UserId + " not found");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (user != null)
 | 
			
		||||
            {
 | 
			
		||||
                ValidateUserAccess(user, request, authAttributes);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var info = GetTokenInfo(request);
 | 
			
		||||
 | 
			
		||||
            if (!IsExemptFromRoles(auth, authAttributes, request, info))
 | 
			
		||||
            {
 | 
			
		||||
                var roles = authAttributes.GetRoles();
 | 
			
		||||
 | 
			
		||||
                ValidateRoles(roles, user);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrEmpty(auth.DeviceId) &&
 | 
			
		||||
                !string.IsNullOrEmpty(auth.Client) &&
 | 
			
		||||
                !string.IsNullOrEmpty(auth.Device))
 | 
			
		||||
            {
 | 
			
		||||
                _sessionManager.LogSessionActivity(
 | 
			
		||||
                    auth.Client,
 | 
			
		||||
                    auth.Version,
 | 
			
		||||
                    auth.DeviceId,
 | 
			
		||||
                    auth.Device,
 | 
			
		||||
                    request.RemoteIp,
 | 
			
		||||
                    user);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return user;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void ValidateUserAccess(
 | 
			
		||||
            User user,
 | 
			
		||||
            IRequest request,
 | 
			
		||||
            IAuthenticationAttributes authAttributes)
 | 
			
		||||
        {
 | 
			
		||||
            if (user.HasPermission(PermissionKind.IsDisabled))
 | 
			
		||||
            {
 | 
			
		||||
                throw new SecurityException("User account has been disabled.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
 | 
			
		||||
            {
 | 
			
		||||
                throw new SecurityException("User account has been disabled.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!user.HasPermission(PermissionKind.IsAdministrator)
 | 
			
		||||
                && !authAttributes.EscapeParentalControl
 | 
			
		||||
                && !user.IsParentalScheduleAllowed())
 | 
			
		||||
            {
 | 
			
		||||
                request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
 | 
			
		||||
 | 
			
		||||
                throw new SecurityException("This user account is not allowed access at this time.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (authAttribtues.AllowLocal && request.IsLocal)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (authAttribtues.AllowLocalOnly && request.IsLocal)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (authAttribtues.IgnoreLegacyAuth)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (authAttribtues.AllowLocal && request.IsLocal)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (authAttribtues.AllowLocalOnly && request.IsLocal)
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(auth.Token))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static void ValidateRoles(string[] roles, User user)
 | 
			
		||||
        {
 | 
			
		||||
            if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
 | 
			
		||||
                {
 | 
			
		||||
                    throw new SecurityException("User does not have admin access.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
 | 
			
		||||
                {
 | 
			
		||||
                    throw new SecurityException("User does not have delete access.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
 | 
			
		||||
                {
 | 
			
		||||
                    throw new SecurityException("User does not have download access.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static AuthenticationInfo GetTokenInfo(IRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
 | 
			
		||||
            return info as AuthenticationInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void ValidateSecurityToken(IRequest request, string token)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(token))
 | 
			
		||||
            {
 | 
			
		||||
                throw new AuthenticationException("Access token is required.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var info = GetTokenInfo(request);
 | 
			
		||||
 | 
			
		||||
            if (info == null)
 | 
			
		||||
            {
 | 
			
		||||
                throw new AuthenticationException("Access token is invalid or expired.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ using System.Net;
 | 
			
		||||
using MediaBrowser.Controller.Library;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Controller.Security;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.Net.Http.Headers;
 | 
			
		||||
 | 
			
		||||
@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
            _userManager = userManager;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AuthorizationInfo GetAuthorizationInfo(object requestContext)
 | 
			
		||||
        public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
 | 
			
		||||
        {
 | 
			
		||||
            return GetAuthorizationInfo((IRequest)requestContext);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
 | 
			
		||||
        {
 | 
			
		||||
            if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached))
 | 
			
		||||
            if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
 | 
			
		||||
            {
 | 
			
		||||
                return (AuthorizationInfo)cached;
 | 
			
		||||
            }
 | 
			
		||||
@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpReq">The HTTP req.</param>
 | 
			
		||||
        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 | 
			
		||||
        private AuthorizationInfo GetAuthorization(IRequest httpReq)
 | 
			
		||||
        private AuthorizationInfo GetAuthorization(HttpContext httpReq)
 | 
			
		||||
        {
 | 
			
		||||
            var auth = GetAuthorizationDictionary(httpReq);
 | 
			
		||||
            var (authInfo, originalAuthInfo) =
 | 
			
		||||
                GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
 | 
			
		||||
                GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
 | 
			
		||||
 | 
			
		||||
            if (originalAuthInfo != null)
 | 
			
		||||
            {
 | 
			
		||||
                httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
 | 
			
		||||
                httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            httpReq.Items["AuthorizationInfo"] = authInfo;
 | 
			
		||||
            httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
 | 
			
		||||
            return authInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpReq">The HTTP req.</param>
 | 
			
		||||
        /// <returns>Dictionary{System.StringSystem.String}.</returns>
 | 
			
		||||
        private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
 | 
			
		||||
        private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
 | 
			
		||||
        {
 | 
			
		||||
            var auth = httpReq.Headers["X-Emby-Authorization"];
 | 
			
		||||
            var auth = httpReq.Request.Headers["X-Emby-Authorization"];
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(auth))
 | 
			
		||||
            {
 | 
			
		||||
                auth = httpReq.Headers[HeaderNames.Authorization];
 | 
			
		||||
                auth = httpReq.Request.Headers[HeaderNames.Authorization];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return GetAuthorization(auth);
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,11 @@
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.Library;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Controller.Security;
 | 
			
		||||
using MediaBrowser.Controller.Session;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
{
 | 
			
		||||
@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
            _sessionManager = sessionManager;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public SessionInfo GetSession(IRequest requestContext)
 | 
			
		||||
        public SessionInfo GetSession(HttpContext requestContext)
 | 
			
		||||
        {
 | 
			
		||||
            var authorization = _authContext.GetAuthorizationInfo(requestContext);
 | 
			
		||||
 | 
			
		||||
            var user = authorization.User;
 | 
			
		||||
            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private AuthenticationInfo GetTokenInfo(IRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
 | 
			
		||||
            return info as AuthenticationInfo;
 | 
			
		||||
            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public SessionInfo GetSession(object requestContext)
 | 
			
		||||
        {
 | 
			
		||||
            return GetSession((IRequest)requestContext);
 | 
			
		||||
            return GetSession((HttpContext)requestContext);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public User GetUser(IRequest requestContext)
 | 
			
		||||
        public User GetUser(HttpContext requestContext)
 | 
			
		||||
        {
 | 
			
		||||
            var session = GetSession(requestContext);
 | 
			
		||||
 | 
			
		||||
@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 | 
			
		||||
 | 
			
		||||
        public User GetUser(object requestContext)
 | 
			
		||||
        {
 | 
			
		||||
            return GetUser((IRequest)requestContext);
 | 
			
		||||
            return GetUser((HttpContext)requestContext);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,120 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.Net.Http.Headers;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Class StreamWriter.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class StreamWriter : IAsyncStreamWriter, IHasHeaders
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The options.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="StreamWriter" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="source">The source.</param>
 | 
			
		||||
        /// <param name="contentType">Type of the content.</param>
 | 
			
		||||
        public StreamWriter(Stream source, string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(contentType))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentNullException(nameof(contentType));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SourceStream = source;
 | 
			
		||||
 | 
			
		||||
            Headers["Content-Type"] = contentType;
 | 
			
		||||
 | 
			
		||||
            if (source.CanSeek)
 | 
			
		||||
            {
 | 
			
		||||
                Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Headers[HeaderNames.ContentType] = contentType;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="StreamWriter"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="source">The source.</param>
 | 
			
		||||
        /// <param name="contentType">Type of the content.</param>
 | 
			
		||||
        /// <param name="contentLength">The content length.</param>
 | 
			
		||||
        public StreamWriter(byte[] source, string contentType, int contentLength)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(contentType))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentNullException(nameof(contentType));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SourceBytes = source;
 | 
			
		||||
 | 
			
		||||
            Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
            Headers[HeaderNames.ContentType] = contentType;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the source stream.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The source stream.</value>
 | 
			
		||||
        private Stream SourceStream { get; set; }
 | 
			
		||||
 | 
			
		||||
        private byte[] SourceBytes { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the options.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The options.</value>
 | 
			
		||||
        public IDictionary<string, string> Headers => _options;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Fires when complete.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Action OnComplete { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Fires when an error occours.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Action OnError { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var bytes = SourceBytes;
 | 
			
		||||
 | 
			
		||||
                if (bytes != null)
 | 
			
		||||
                {
 | 
			
		||||
                    await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    using (var src = SourceStream)
 | 
			
		||||
                    {
 | 
			
		||||
                        await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                OnError?.Invoke();
 | 
			
		||||
 | 
			
		||||
                throw;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                OnComplete?.Invoke();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								Emby.Server.Implementations/HttpServer/WebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								Emby.Server.Implementations/HttpServer/WebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.WebSockets;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Data.Events;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    public class WebSocketManager : IWebSocketManager
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ILogger<WebSocketManager> _logger;
 | 
			
		||||
        private readonly ILoggerFactory _loggerFactory;
 | 
			
		||||
 | 
			
		||||
        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
 | 
			
		||||
        private bool _disposed = false;
 | 
			
		||||
 | 
			
		||||
        public WebSocketManager(
 | 
			
		||||
            ILogger<WebSocketManager> logger,
 | 
			
		||||
            ILoggerFactory loggerFactory)
 | 
			
		||||
        {
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _loggerFactory = loggerFactory;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public async Task WebSocketRequestHandler(HttpContext context)
 | 
			
		||||
        {
 | 
			
		||||
            if (_disposed)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
 | 
			
		||||
 | 
			
		||||
                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                using var connection = new WebSocketConnection(
 | 
			
		||||
                    _loggerFactory.CreateLogger<WebSocketConnection>(),
 | 
			
		||||
                    webSocket,
 | 
			
		||||
                    context.Connection.RemoteIpAddress,
 | 
			
		||||
                    context.Request.Query)
 | 
			
		||||
                {
 | 
			
		||||
                    OnReceive = ProcessWebSocketMessageReceived
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
 | 
			
		||||
 | 
			
		||||
                await connection.ProcessAsync().ConfigureAwait(false);
 | 
			
		||||
                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
 | 
			
		||||
                if (!context.Response.HasStarted)
 | 
			
		||||
                {
 | 
			
		||||
                    context.Response.StatusCode = 500;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds the rest handlers.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="listeners">The web socket listeners.</param>
 | 
			
		||||
        public void Init(IEnumerable<IWebSocketListener> listeners)
 | 
			
		||||
        {
 | 
			
		||||
            _webSocketListeners = listeners.ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Processes the web socket message received.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="result">The result.</param>
 | 
			
		||||
        private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
 | 
			
		||||
        {
 | 
			
		||||
            if (_disposed)
 | 
			
		||||
            {
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            IEnumerable<Task> GetTasks()
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var x in _webSocketListeners)
 | 
			
		||||
                {
 | 
			
		||||
                    yield return x.ProcessMessageAsync(result);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.WhenAll(GetTasks());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -17,5 +17,8 @@
 | 
			
		||||
    "Genres": "Géneros",
 | 
			
		||||
    "Folders": "Carpetas",
 | 
			
		||||
    "Favorites": "Favoritos",
 | 
			
		||||
    "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}"
 | 
			
		||||
    "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
 | 
			
		||||
    "HeaderFavoriteSongs": "Canciones Favoritas",
 | 
			
		||||
    "HeaderFavoriteEpisodes": "Episodios Favoritos",
 | 
			
		||||
    "HeaderFavoriteArtists": "Artistas Favoritos"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@
 | 
			
		||||
    "NameSeasonNumber": "Sesong {0}",
 | 
			
		||||
    "NameSeasonUnknown": "Sesong ukjent",
 | 
			
		||||
    "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
 | 
			
		||||
    "NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig",
 | 
			
		||||
    "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
 | 
			
		||||
    "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
 | 
			
		||||
    "NotificationOptionAudioPlayback": "Lydavspilling startet",
 | 
			
		||||
    "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
 | 
			
		||||
 | 
			
		||||
@ -1,76 +1,117 @@
 | 
			
		||||
{
 | 
			
		||||
    "ProviderValue": "ผู้ให้บริการ: {0}",
 | 
			
		||||
    "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว",
 | 
			
		||||
    "PluginUninstalledWithName": "ถอนการติดตั้ง {0}",
 | 
			
		||||
    "PluginInstalledWithName": "{0} ได้รับการติดตั้ง",
 | 
			
		||||
    "Plugin": "Plugin",
 | 
			
		||||
    "Playlists": "รายการ",
 | 
			
		||||
    "PluginUpdatedWithName": "อัปเดต {0} แล้ว",
 | 
			
		||||
    "PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว",
 | 
			
		||||
    "PluginInstalledWithName": "ติดตั้ง {0} แล้ว",
 | 
			
		||||
    "Plugin": "ปลั๊กอิน",
 | 
			
		||||
    "Playlists": "เพลย์ลิสต์",
 | 
			
		||||
    "Photos": "รูปภาพ",
 | 
			
		||||
    "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video",
 | 
			
		||||
    "NotificationOptionVideoPlayback": "เริ่มแสดง Video",
 | 
			
		||||
    "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out",
 | 
			
		||||
    "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว",
 | 
			
		||||
    "NotificationOptionServerRestartRequired": "ควร Restart Server",
 | 
			
		||||
    "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว",
 | 
			
		||||
    "NotificationOptionPluginUninstalled": "ถอด Plugin",
 | 
			
		||||
    "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว",
 | 
			
		||||
    "NotificationOptionPluginError": "Plugin ล้มเหลว",
 | 
			
		||||
    "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว",
 | 
			
		||||
    "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว",
 | 
			
		||||
    "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload",
 | 
			
		||||
    "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง",
 | 
			
		||||
    "NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ",
 | 
			
		||||
    "NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ",
 | 
			
		||||
    "NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก",
 | 
			
		||||
    "NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว",
 | 
			
		||||
    "NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์",
 | 
			
		||||
    "NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว",
 | 
			
		||||
    "NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว",
 | 
			
		||||
    "NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว",
 | 
			
		||||
    "NotificationOptionPluginError": "ปลั๊กอินล้มเหลว",
 | 
			
		||||
    "NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว",
 | 
			
		||||
    "NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว",
 | 
			
		||||
    "NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
 | 
			
		||||
    "NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
 | 
			
		||||
    "NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
 | 
			
		||||
    "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว",
 | 
			
		||||
    "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว",
 | 
			
		||||
    "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่",
 | 
			
		||||
    "NameSeasonUnknown": "ไม่ทราบปี",
 | 
			
		||||
    "NameSeasonNumber": "ปี {0}",
 | 
			
		||||
    "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ",
 | 
			
		||||
    "MusicVideos": "MV",
 | 
			
		||||
    "Music": "เพลง",
 | 
			
		||||
    "Movies": "ภาพยนต์",
 | 
			
		||||
    "MixedContent": "รายการแบบผสม",
 | 
			
		||||
    "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว",
 | 
			
		||||
    "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว",
 | 
			
		||||
    "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}",
 | 
			
		||||
    "MessageApplicationUpdated": "Jellyfin Server update แล้ว",
 | 
			
		||||
    "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว",
 | 
			
		||||
    "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน",
 | 
			
		||||
    "NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
 | 
			
		||||
    "NameSeasonUnknown": "ไม่ทราบซีซัน",
 | 
			
		||||
    "NameSeasonNumber": "ซีซัน {0}",
 | 
			
		||||
    "NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
 | 
			
		||||
    "MusicVideos": "มิวสิควิดีโอ",
 | 
			
		||||
    "Music": "ดนตรี",
 | 
			
		||||
    "Movies": "ภาพยนตร์",
 | 
			
		||||
    "MixedContent": "เนื้อหาผสม",
 | 
			
		||||
    "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว",
 | 
			
		||||
    "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว",
 | 
			
		||||
    "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}",
 | 
			
		||||
    "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว",
 | 
			
		||||
    "Latest": "ล่าสุด",
 | 
			
		||||
    "LabelRunningTimeValue": "เวลาที่เล่น : {0}",
 | 
			
		||||
    "LabelIpAddressValue": "IP address: {0}",
 | 
			
		||||
    "ItemRemovedWithName": "{0} ถูกลบจากรายการ",
 | 
			
		||||
    "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ",
 | 
			
		||||
    "Inherit": "การสืบทอด",
 | 
			
		||||
    "HomeVideos": "วีดีโอส่วนตัว",
 | 
			
		||||
    "HeaderRecordingGroups": "ค่ายบันทึก",
 | 
			
		||||
    "LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
 | 
			
		||||
    "LabelIpAddressValue": "ที่อยู่ IP: {0}",
 | 
			
		||||
    "ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี",
 | 
			
		||||
    "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว",
 | 
			
		||||
    "Inherit": "สืบทอด",
 | 
			
		||||
    "HomeVideos": "โฮมวิดีโอ",
 | 
			
		||||
    "HeaderRecordingGroups": "กลุ่มการบันทึก",
 | 
			
		||||
    "HeaderNextUp": "ถัดไป",
 | 
			
		||||
    "HeaderLiveTV": "รายการสด",
 | 
			
		||||
    "HeaderFavoriteSongs": "เพลงโปรด",
 | 
			
		||||
    "HeaderFavoriteShows": "รายการโชว์โปรด",
 | 
			
		||||
    "HeaderFavoriteEpisodes": "ฉากโปรด",
 | 
			
		||||
    "HeaderFavoriteArtists": "นักแสดงโปรด",
 | 
			
		||||
    "HeaderFavoriteAlbums": "อัมบั้มโปรด",
 | 
			
		||||
    "HeaderContinueWatching": "ชมต่อจากเดิม",
 | 
			
		||||
    "HeaderCameraUploads": "Upload รูปภาพ",
 | 
			
		||||
    "HeaderAlbumArtists": "อัลบั้มนักแสดง",
 | 
			
		||||
    "HeaderLiveTV": "ทีวีสด",
 | 
			
		||||
    "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ",
 | 
			
		||||
    "HeaderFavoriteShows": "รายการที่ชื่นชอบ",
 | 
			
		||||
    "HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
 | 
			
		||||
    "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
 | 
			
		||||
    "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
 | 
			
		||||
    "HeaderContinueWatching": "ดูต่อ",
 | 
			
		||||
    "HeaderCameraUploads": "อัปโหลดรูปถ่าย",
 | 
			
		||||
    "HeaderAlbumArtists": "อัลบั้มศิลปิน",
 | 
			
		||||
    "Genres": "ประเภท",
 | 
			
		||||
    "Folders": "โฟลเดอร์",
 | 
			
		||||
    "Favorites": "รายการโปรด",
 | 
			
		||||
    "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}",
 | 
			
		||||
    "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ",
 | 
			
		||||
    "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ",
 | 
			
		||||
    "Collections": "ชุด",
 | 
			
		||||
    "ChapterNameValue": "บทที่ {0}",
 | 
			
		||||
    "Channels": "ชาแนล",
 | 
			
		||||
    "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}",
 | 
			
		||||
    "FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}",
 | 
			
		||||
    "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
 | 
			
		||||
    "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
 | 
			
		||||
    "Collections": "คอลเลกชัน",
 | 
			
		||||
    "ChapterNameValue": "บท {0}",
 | 
			
		||||
    "Channels": "ช่อง",
 | 
			
		||||
    "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
 | 
			
		||||
    "Books": "หนังสือ",
 | 
			
		||||
    "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ",
 | 
			
		||||
    "Artists": "นักแสดง",
 | 
			
		||||
    "Application": "แอปพลิเคชั่น",
 | 
			
		||||
    "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
 | 
			
		||||
    "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
 | 
			
		||||
    "Artists": "ศิลปิน",
 | 
			
		||||
    "Application": "แอพพลิเคชัน",
 | 
			
		||||
    "AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}",
 | 
			
		||||
    "Albums": "อัลบั้ม",
 | 
			
		||||
    "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
 | 
			
		||||
    "ScheduledTaskFailedWithName": "{0} ล้มเหลว",
 | 
			
		||||
    "Songs": "เพลง",
 | 
			
		||||
    "Shows": "แสดง",
 | 
			
		||||
    "ServerNameNeedsToBeRestarted": "{0} ต้องการรีสตาร์ท"
 | 
			
		||||
    "Shows": "รายการ",
 | 
			
		||||
    "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
 | 
			
		||||
    "TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
 | 
			
		||||
    "TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
 | 
			
		||||
    "TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
 | 
			
		||||
    "TaskRefreshChannels": "รีเฟรชช่อง",
 | 
			
		||||
    "TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน",
 | 
			
		||||
    "TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด",
 | 
			
		||||
    "TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ",
 | 
			
		||||
    "TaskUpdatePlugins": "อัปเดตปลั๊กอิน",
 | 
			
		||||
    "TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ",
 | 
			
		||||
    "TaskRefreshPeople": "รีเฟรชบุคคล",
 | 
			
		||||
    "TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน",
 | 
			
		||||
    "TaskCleanLogs": "ล้างไดเรกทอรีบันทึก",
 | 
			
		||||
    "TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา",
 | 
			
		||||
    "TaskRefreshLibrary": "สแกนไลบรารีสื่อ",
 | 
			
		||||
    "TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท",
 | 
			
		||||
    "TaskRefreshChapterImages": "แตกรูปภาพบท",
 | 
			
		||||
    "TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
 | 
			
		||||
    "TaskCleanCache": "ล้างไดเรกทอรีแคช",
 | 
			
		||||
    "TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
 | 
			
		||||
    "TasksApplicationCategory": "แอพพลิเคชัน",
 | 
			
		||||
    "TasksLibraryCategory": "ไลบรารี",
 | 
			
		||||
    "TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
 | 
			
		||||
    "VersionNumber": "เวอร์ชัน {0}",
 | 
			
		||||
    "ValueSpecialEpisodeName": "พิเศษ - {0}",
 | 
			
		||||
    "ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว",
 | 
			
		||||
    "UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}",
 | 
			
		||||
    "UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}",
 | 
			
		||||
    "UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}",
 | 
			
		||||
    "UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}",
 | 
			
		||||
    "UserOnlineFromDevice": "{0} ออนไลน์จาก {1}",
 | 
			
		||||
    "UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}",
 | 
			
		||||
    "UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก",
 | 
			
		||||
    "UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
 | 
			
		||||
    "UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
 | 
			
		||||
    "UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
 | 
			
		||||
    "User": "ผู้ใช้งาน",
 | 
			
		||||
    "TvShows": "รายการทีวี",
 | 
			
		||||
    "System": "ระบบ",
 | 
			
		||||
    "Sync": "ซิงค์",
 | 
			
		||||
    "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
 | 
			
		||||
    "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										285
									
								
								Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,285 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using MediaBrowser.Common;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller;
 | 
			
		||||
using MediaBrowser.Controller.Authentication;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using MediaBrowser.Controller.QuickConnect;
 | 
			
		||||
using MediaBrowser.Controller.Security;
 | 
			
		||||
using MediaBrowser.Model.QuickConnect;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.QuickConnect
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Quick connect implementation.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class QuickConnectManager : IQuickConnect, IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
 | 
			
		||||
        private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>();
 | 
			
		||||
 | 
			
		||||
        private readonly IServerConfigurationManager _config;
 | 
			
		||||
        private readonly ILogger<QuickConnectManager> _logger;
 | 
			
		||||
        private readonly IAuthenticationRepository _authenticationRepository;
 | 
			
		||||
        private readonly IAuthorizationContext _authContext;
 | 
			
		||||
        private readonly IServerApplicationHost _appHost;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
 | 
			
		||||
        /// Should only be called at server startup when a singleton is created.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="config">Configuration.</param>
 | 
			
		||||
        /// <param name="logger">Logger.</param>
 | 
			
		||||
        /// <param name="appHost">Application host.</param>
 | 
			
		||||
        /// <param name="authContext">Authentication context.</param>
 | 
			
		||||
        /// <param name="authenticationRepository">Authentication repository.</param>
 | 
			
		||||
        public QuickConnectManager(
 | 
			
		||||
            IServerConfigurationManager config,
 | 
			
		||||
            ILogger<QuickConnectManager> logger,
 | 
			
		||||
            IServerApplicationHost appHost,
 | 
			
		||||
            IAuthorizationContext authContext,
 | 
			
		||||
            IAuthenticationRepository authenticationRepository)
 | 
			
		||||
        {
 | 
			
		||||
            _config = config;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _appHost = appHost;
 | 
			
		||||
            _authContext = authContext;
 | 
			
		||||
            _authenticationRepository = authenticationRepository;
 | 
			
		||||
 | 
			
		||||
            ReloadConfiguration();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public int CodeLength { get; set; } = 6;
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public string TokenName { get; set; } = "QuickConnect";
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public int Timeout { get; set; } = 5;
 | 
			
		||||
 | 
			
		||||
        private DateTime DateActivated { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public void AssertActive()
 | 
			
		||||
        {
 | 
			
		||||
            if (State != QuickConnectState.Active)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException("Quick connect is not active on this server");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public void Activate()
 | 
			
		||||
        {
 | 
			
		||||
            DateActivated = DateTime.UtcNow;
 | 
			
		||||
            SetState(QuickConnectState.Active);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public void SetState(QuickConnectState newState)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
 | 
			
		||||
 | 
			
		||||
            ExpireRequests(true);
 | 
			
		||||
 | 
			
		||||
            State = newState;
 | 
			
		||||
            _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
 | 
			
		||||
            _config.SaveConfiguration();
 | 
			
		||||
 | 
			
		||||
            _logger.LogDebug("Configuration saved");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public QuickConnectResult TryConnect()
 | 
			
		||||
        {
 | 
			
		||||
            ExpireRequests();
 | 
			
		||||
 | 
			
		||||
            if (State != QuickConnectState.Active)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
 | 
			
		||||
                throw new AuthenticationException("Quick connect is not active on this server");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var code = GenerateCode();
 | 
			
		||||
            var result = new QuickConnectResult()
 | 
			
		||||
            {
 | 
			
		||||
                Secret = GenerateSecureRandom(),
 | 
			
		||||
                DateAdded = DateTime.UtcNow,
 | 
			
		||||
                Code = code
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            _currentRequests[code] = result;
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public QuickConnectResult CheckRequestStatus(string secret)
 | 
			
		||||
        {
 | 
			
		||||
            ExpireRequests();
 | 
			
		||||
            AssertActive();
 | 
			
		||||
 | 
			
		||||
            string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
 | 
			
		||||
 | 
			
		||||
            if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ResourceNotFoundException("Unable to find request with provided secret");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public string GenerateCode()
 | 
			
		||||
        {
 | 
			
		||||
            Span<byte> raw = stackalloc byte[4];
 | 
			
		||||
 | 
			
		||||
            int min = (int)Math.Pow(10, CodeLength - 1);
 | 
			
		||||
            int max = (int)Math.Pow(10, CodeLength);
 | 
			
		||||
 | 
			
		||||
            uint scale = uint.MaxValue;
 | 
			
		||||
            while (scale == uint.MaxValue)
 | 
			
		||||
            {
 | 
			
		||||
                _rng.GetBytes(raw);
 | 
			
		||||
                scale = BitConverter.ToUInt32(raw);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue)));
 | 
			
		||||
            return code.ToString(CultureInfo.InvariantCulture);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public bool AuthorizeRequest(Guid userId, string code)
 | 
			
		||||
        {
 | 
			
		||||
            ExpireRequests();
 | 
			
		||||
            AssertActive();
 | 
			
		||||
 | 
			
		||||
            if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
 | 
			
		||||
            {
 | 
			
		||||
                throw new ResourceNotFoundException("Unable to find request");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (result.Authenticated)
 | 
			
		||||
            {
 | 
			
		||||
                throw new InvalidOperationException("Request is already authorized");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
 | 
			
		||||
 | 
			
		||||
            // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
 | 
			
		||||
            var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
 | 
			
		||||
            result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
 | 
			
		||||
 | 
			
		||||
            _authenticationRepository.Create(new AuthenticationInfo
 | 
			
		||||
            {
 | 
			
		||||
                AppName = TokenName,
 | 
			
		||||
                AccessToken = result.Authentication,
 | 
			
		||||
                DateCreated = DateTime.UtcNow,
 | 
			
		||||
                DeviceId = _appHost.SystemId,
 | 
			
		||||
                DeviceName = _appHost.FriendlyName,
 | 
			
		||||
                AppVersion = _appHost.ApplicationVersionString,
 | 
			
		||||
                UserId = userId
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public int DeleteAllDevices(Guid user)
 | 
			
		||||
        {
 | 
			
		||||
            var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
 | 
			
		||||
            {
 | 
			
		||||
                DeviceId = _appHost.SystemId,
 | 
			
		||||
                UserId = user
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
 | 
			
		||||
 | 
			
		||||
            var removed = 0;
 | 
			
		||||
            foreach (var token in tokens)
 | 
			
		||||
            {
 | 
			
		||||
                _authenticationRepository.Delete(token);
 | 
			
		||||
                _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
 | 
			
		||||
                removed++;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return removed;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Dispose.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            Dispose(true);
 | 
			
		||||
            GC.SuppressFinalize(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Dispose.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="disposing">Dispose unmanaged resources.</param>
 | 
			
		||||
        protected virtual void Dispose(bool disposing)
 | 
			
		||||
        {
 | 
			
		||||
            if (disposing)
 | 
			
		||||
            {
 | 
			
		||||
                _rng?.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GenerateSecureRandom(int length = 32)
 | 
			
		||||
        {
 | 
			
		||||
            Span<byte> bytes = stackalloc byte[length];
 | 
			
		||||
            _rng.GetBytes(bytes);
 | 
			
		||||
 | 
			
		||||
            return Hex.Encode(bytes);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc/>
 | 
			
		||||
        public void ExpireRequests(bool expireAll = false)
 | 
			
		||||
        {
 | 
			
		||||
            // Check if quick connect should be deactivated
 | 
			
		||||
            if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogDebug("Quick connect time expired, deactivating");
 | 
			
		||||
                SetState(QuickConnectState.Available);
 | 
			
		||||
                expireAll = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Expire stale connection requests
 | 
			
		||||
            var code = string.Empty;
 | 
			
		||||
            var values = _currentRequests.Values.ToList();
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < values.Count; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var added = values[i].DateAdded ?? DateTime.UnixEpoch;
 | 
			
		||||
                if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
 | 
			
		||||
                {
 | 
			
		||||
                    code = values[i].Code;
 | 
			
		||||
                    _logger.LogDebug("Removing expired request {code}", code);
 | 
			
		||||
 | 
			
		||||
                    if (!_currentRequests.TryRemove(code, out _))
 | 
			
		||||
                    {
 | 
			
		||||
                        _logger.LogWarning("Request {code} already expired", code);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void ReloadConfiguration()
 | 
			
		||||
        {
 | 
			
		||||
            State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,64 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public class HttpResult
 | 
			
		||||
        : IHttpResult, IAsyncStreamWriter
 | 
			
		||||
    {
 | 
			
		||||
        public HttpResult(object response, string contentType, HttpStatusCode statusCode)
 | 
			
		||||
        {
 | 
			
		||||
            this.Headers = new Dictionary<string, string>();
 | 
			
		||||
 | 
			
		||||
            this.Response = response;
 | 
			
		||||
            this.ContentType = contentType;
 | 
			
		||||
            this.StatusCode = statusCode;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public object Response { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string ContentType { get; set; }
 | 
			
		||||
 | 
			
		||||
        public IDictionary<string, string> Headers { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public int Status { get; set; }
 | 
			
		||||
 | 
			
		||||
        public HttpStatusCode StatusCode
 | 
			
		||||
        {
 | 
			
		||||
            get => (HttpStatusCode)Status;
 | 
			
		||||
            set => Status = (int)value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IRequest RequestContext { get; set; }
 | 
			
		||||
 | 
			
		||||
        public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            var response = RequestContext?.Response;
 | 
			
		||||
 | 
			
		||||
            if (this.Response is byte[] bytesResponse)
 | 
			
		||||
            {
 | 
			
		||||
                var contentLength = bytesResponse.Length;
 | 
			
		||||
 | 
			
		||||
                if (response != null)
 | 
			
		||||
                {
 | 
			
		||||
                    response.ContentLength = contentLength;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (contentLength > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,51 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Emby.Server.Implementations.HttpServer;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public class RequestHelper
 | 
			
		||||
    {
 | 
			
		||||
        public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            switch (GetContentTypeWithoutEncoding(contentType))
 | 
			
		||||
            {
 | 
			
		||||
                case "application/xml":
 | 
			
		||||
                case "text/xml":
 | 
			
		||||
                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
 | 
			
		||||
                    return host.DeserializeXml;
 | 
			
		||||
 | 
			
		||||
                case "application/json":
 | 
			
		||||
                case "text/json":
 | 
			
		||||
                    return host.DeserializeJson;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            switch (GetContentTypeWithoutEncoding(contentType))
 | 
			
		||||
            {
 | 
			
		||||
                case "application/xml":
 | 
			
		||||
                case "text/xml":
 | 
			
		||||
                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
 | 
			
		||||
                    return host.SerializeToXml;
 | 
			
		||||
 | 
			
		||||
                case "application/json":
 | 
			
		||||
                case "text/json":
 | 
			
		||||
                    return host.SerializeToJson;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetContentTypeWithoutEncoding(string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            return contentType?.Split(';')[0].ToLowerInvariant().Trim();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,141 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Emby.Server.Implementations.HttpServer;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public static class ResponseHelper
 | 
			
		||||
    {
 | 
			
		||||
        public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            if (result == null)
 | 
			
		||||
            {
 | 
			
		||||
                if (response.StatusCode == (int)HttpStatusCode.OK)
 | 
			
		||||
                {
 | 
			
		||||
                    response.StatusCode = (int)HttpStatusCode.NoContent;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                response.ContentLength = 0;
 | 
			
		||||
                return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var httpResult = result as IHttpResult;
 | 
			
		||||
            if (httpResult != null)
 | 
			
		||||
            {
 | 
			
		||||
                httpResult.RequestContext = request;
 | 
			
		||||
                request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var defaultContentType = request.ResponseContentType;
 | 
			
		||||
 | 
			
		||||
            if (httpResult != null)
 | 
			
		||||
            {
 | 
			
		||||
                if (httpResult.RequestContext == null)
 | 
			
		||||
                {
 | 
			
		||||
                    httpResult.RequestContext = request;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                response.StatusCode = httpResult.Status;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (result is IHasHeaders responseOptions)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var responseHeaders in responseOptions.Headers)
 | 
			
		||||
                {
 | 
			
		||||
                    if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    response.Headers.Add(responseHeaders.Key, responseHeaders.Value);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // ContentType='text/html' is the default for a HttpResponse
 | 
			
		||||
            // Do not override if another has been set
 | 
			
		||||
            if (response.ContentType == null || response.ContentType == "text/html")
 | 
			
		||||
            {
 | 
			
		||||
                response.ContentType = defaultContentType;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (response.ContentType == "application/json")
 | 
			
		||||
            {
 | 
			
		||||
                response.ContentType += "; charset=utf-8";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            switch (result)
 | 
			
		||||
            {
 | 
			
		||||
                case IAsyncStreamWriter asyncStreamWriter:
 | 
			
		||||
                    return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken);
 | 
			
		||||
                case IStreamWriter streamWriter:
 | 
			
		||||
                    streamWriter.WriteTo(response.Body);
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                case FileWriter fileWriter:
 | 
			
		||||
                    return fileWriter.WriteToAsync(response, cancellationToken);
 | 
			
		||||
                case Stream stream:
 | 
			
		||||
                    return CopyStream(stream, response.Body);
 | 
			
		||||
                case byte[] bytes:
 | 
			
		||||
                    response.ContentType = "application/octet-stream";
 | 
			
		||||
                    response.ContentLength = bytes.Length;
 | 
			
		||||
 | 
			
		||||
                    if (bytes.Length > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
                case string responseText:
 | 
			
		||||
                    var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
 | 
			
		||||
                    response.ContentLength = responseTextAsBytes.Length;
 | 
			
		||||
 | 
			
		||||
                    if (responseTextAsBytes.Length > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return WriteObject(request, result, response);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static async Task CopyStream(Stream src, Stream dest)
 | 
			
		||||
        {
 | 
			
		||||
            using (src)
 | 
			
		||||
            {
 | 
			
		||||
                await src.CopyToAsync(dest).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static async Task WriteObject(IRequest request, object result, HttpResponse response)
 | 
			
		||||
        {
 | 
			
		||||
            var contentType = request.ResponseContentType;
 | 
			
		||||
            var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
 | 
			
		||||
 | 
			
		||||
            using (var ms = new MemoryStream())
 | 
			
		||||
            {
 | 
			
		||||
                serializer(result, ms);
 | 
			
		||||
 | 
			
		||||
                ms.Position = 0;
 | 
			
		||||
 | 
			
		||||
                var contentLength = ms.Length;
 | 
			
		||||
                response.ContentLength = contentLength;
 | 
			
		||||
 | 
			
		||||
                if (contentLength > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    await ms.CopyToAsync(response.Body).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,202 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Emby.Server.Implementations.HttpServer;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public delegate object ActionInvokerFn(object intance, object request);
 | 
			
		||||
 | 
			
		||||
    public delegate void VoidActionInvokerFn(object intance, object request);
 | 
			
		||||
 | 
			
		||||
    public class ServiceController
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ILogger<ServiceController> _logger;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="ServiceController"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="logger">The <see cref="ServiceController"/> logger.</param>
 | 
			
		||||
        public ServiceController(ILogger<ServiceController> logger)
 | 
			
		||||
        {
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var serviceType in serviceTypes)
 | 
			
		||||
            {
 | 
			
		||||
                RegisterService(appHost, serviceType);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void RegisterService(HttpListenerHost appHost, Type serviceType)
 | 
			
		||||
        {
 | 
			
		||||
            // Make sure the provided type implements IService
 | 
			
		||||
            if (!typeof(IService).IsAssignableFrom(serviceType))
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var processedReqs = new HashSet<Type>();
 | 
			
		||||
 | 
			
		||||
            var actions = ServiceExecGeneral.Reset(serviceType);
 | 
			
		||||
 | 
			
		||||
            foreach (var mi in serviceType.GetActions())
 | 
			
		||||
            {
 | 
			
		||||
                var requestType = mi.GetParameters()[0].ParameterType;
 | 
			
		||||
                if (processedReqs.Contains(requestType))
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                processedReqs.Add(requestType);
 | 
			
		||||
 | 
			
		||||
                ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
 | 
			
		||||
 | 
			
		||||
                // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
 | 
			
		||||
                // var responseType = returnMarker != null ?
 | 
			
		||||
                //      GetGenericArguments(returnMarker)[0]
 | 
			
		||||
                //    : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
 | 
			
		||||
                //      mi.ReturnType
 | 
			
		||||
                //    : Type.GetType(requestType.FullName + "Response");
 | 
			
		||||
 | 
			
		||||
                RegisterRestPaths(appHost, requestType, serviceType);
 | 
			
		||||
 | 
			
		||||
                appHost.AddServiceInfo(serviceType, requestType);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
 | 
			
		||||
 | 
			
		||||
        public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
 | 
			
		||||
        {
 | 
			
		||||
            var attrs = appHost.GetRouteAttributes(requestType);
 | 
			
		||||
            foreach (var attr in attrs)
 | 
			
		||||
            {
 | 
			
		||||
                var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description);
 | 
			
		||||
 | 
			
		||||
                RegisterRestPath(restPath);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
 | 
			
		||||
 | 
			
		||||
        public void RegisterRestPath(RestPath restPath)
 | 
			
		||||
        {
 | 
			
		||||
            if (restPath.Path[0] != '/')
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException(
 | 
			
		||||
                    string.Format(
 | 
			
		||||
                        CultureInfo.InvariantCulture,
 | 
			
		||||
                        "Route '{0}' on '{1}' must start with a '/'",
 | 
			
		||||
                        restPath.Path,
 | 
			
		||||
                        restPath.RequestType.GetMethodName()));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException(
 | 
			
		||||
                    string.Format(
 | 
			
		||||
                        CultureInfo.InvariantCulture,
 | 
			
		||||
                        "Route '{0}' on '{1}' contains invalid chars. ",
 | 
			
		||||
                        restPath.Path,
 | 
			
		||||
                        restPath.RequestType.GetMethodName()));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
 | 
			
		||||
            {
 | 
			
		||||
                pathsAtFirstMatch.Add(restPath);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
 | 
			
		||||
        {
 | 
			
		||||
            var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
 | 
			
		||||
 | 
			
		||||
            List<RestPath> firstMatches;
 | 
			
		||||
 | 
			
		||||
            var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
 | 
			
		||||
            foreach (var potentialHashMatch in yieldedHashMatches)
 | 
			
		||||
            {
 | 
			
		||||
                if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var bestScore = -1;
 | 
			
		||||
                RestPath bestMatch = null;
 | 
			
		||||
                foreach (var restPath in firstMatches)
 | 
			
		||||
                {
 | 
			
		||||
                    var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
 | 
			
		||||
                    if (score > bestScore)
 | 
			
		||||
                    {
 | 
			
		||||
                        bestScore = score;
 | 
			
		||||
                        bestMatch = restPath;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (bestScore > 0 && bestMatch != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return bestMatch;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
 | 
			
		||||
            foreach (var potentialHashMatch in yieldedWildcardMatches)
 | 
			
		||||
            {
 | 
			
		||||
                if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var bestScore = -1;
 | 
			
		||||
                RestPath bestMatch = null;
 | 
			
		||||
                foreach (var restPath in firstMatches)
 | 
			
		||||
                {
 | 
			
		||||
                    var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
 | 
			
		||||
                    if (score > bestScore)
 | 
			
		||||
                    {
 | 
			
		||||
                        bestScore = score;
 | 
			
		||||
                        bestMatch = restPath;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (bestScore > 0 && bestMatch != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return bestMatch;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
 | 
			
		||||
        {
 | 
			
		||||
            var requestType = requestDto.GetType();
 | 
			
		||||
            req.OperationName = requestType.Name;
 | 
			
		||||
 | 
			
		||||
            var serviceType = httpHost.GetServiceTypeByRequest(requestType);
 | 
			
		||||
 | 
			
		||||
            var service = httpHost.CreateInstance(serviceType);
 | 
			
		||||
 | 
			
		||||
            if (service is IRequiresRequest serviceRequiresContext)
 | 
			
		||||
            {
 | 
			
		||||
                serviceRequiresContext.Request = req;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Executes the service and returns the result
 | 
			
		||||
            return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,230 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public static class ServiceExecExtensions
 | 
			
		||||
    {
 | 
			
		||||
        public static string[] AllVerbs = new[] {
 | 
			
		||||
            "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
 | 
			
		||||
            "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK",    // RFC 2518
 | 
			
		||||
            "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
 | 
			
		||||
            "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY",  // RFC 3253
 | 
			
		||||
            "ORDERPATCH", // RFC 3648
 | 
			
		||||
            "ACL",        // RFC 3744
 | 
			
		||||
            "PATCH",      // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
 | 
			
		||||
            "SEARCH",     // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
 | 
			
		||||
            "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
 | 
			
		||||
            "POLL",  "SUBSCRIBE", "UNSUBSCRIBE"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        public static List<MethodInfo> GetActions(this Type serviceType)
 | 
			
		||||
        {
 | 
			
		||||
            var list = new List<MethodInfo>();
 | 
			
		||||
 | 
			
		||||
            foreach (var mi in serviceType.GetRuntimeMethods())
 | 
			
		||||
            {
 | 
			
		||||
                if (!mi.IsPublic)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (mi.IsStatic)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (mi.GetParameters().Length != 1)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var actionName = mi.Name;
 | 
			
		||||
                if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                list.Add(mi);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return list;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal static class ServiceExecGeneral
 | 
			
		||||
    {
 | 
			
		||||
        private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
 | 
			
		||||
 | 
			
		||||
        public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var actionCtx in actions)
 | 
			
		||||
            {
 | 
			
		||||
                if (execMap.ContainsKey(actionCtx.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                execMap[actionCtx.Id] = actionCtx;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
 | 
			
		||||
        {
 | 
			
		||||
            var actionName = request.Verb ?? "POST";
 | 
			
		||||
 | 
			
		||||
            if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext))
 | 
			
		||||
            {
 | 
			
		||||
                if (actionContext.RequestFilters != null)
 | 
			
		||||
                {
 | 
			
		||||
                    foreach (var requestFilter in actionContext.RequestFilters)
 | 
			
		||||
                    {
 | 
			
		||||
                        requestFilter.RequestFilter(request, request.Response, requestDto);
 | 
			
		||||
                        if (request.Response.HasStarted)
 | 
			
		||||
                        {
 | 
			
		||||
                            Task.FromResult<object>(null);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var response = actionContext.ServiceAction(instance, requestDto);
 | 
			
		||||
 | 
			
		||||
                if (response is Task taskResponse)
 | 
			
		||||
                {
 | 
			
		||||
                    return GetTaskResult(taskResponse);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return Task.FromResult(response);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
 | 
			
		||||
            throw new NotImplementedException(
 | 
			
		||||
                string.Format(
 | 
			
		||||
                    CultureInfo.InvariantCulture,
 | 
			
		||||
                    "Could not find method named {1}({0}) or Any({0}) on Service {2}",
 | 
			
		||||
                    requestDto.GetType().GetMethodName(),
 | 
			
		||||
                    expectedMethodName,
 | 
			
		||||
                    serviceType.GetMethodName()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static async Task<object> GetTaskResult(Task task)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (task is Task<object> taskObject)
 | 
			
		||||
                {
 | 
			
		||||
                    return await taskObject.ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await task.ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                var type = task.GetType().GetTypeInfo();
 | 
			
		||||
                if (!type.IsGenericType)
 | 
			
		||||
                {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var resultProperty = type.GetDeclaredProperty("Result");
 | 
			
		||||
                if (resultProperty == null)
 | 
			
		||||
                {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var result = resultProperty.GetValue(task);
 | 
			
		||||
 | 
			
		||||
                // hack alert
 | 
			
		||||
                if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
                {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return result;
 | 
			
		||||
            }
 | 
			
		||||
            catch (TypeAccessException)
 | 
			
		||||
            {
 | 
			
		||||
                return null; // return null for void Task's
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static List<ServiceMethod> Reset(Type serviceType)
 | 
			
		||||
        {
 | 
			
		||||
            var actions = new List<ServiceMethod>();
 | 
			
		||||
 | 
			
		||||
            foreach (var mi in serviceType.GetActions())
 | 
			
		||||
            {
 | 
			
		||||
                var actionName = mi.Name;
 | 
			
		||||
                var args = mi.GetParameters();
 | 
			
		||||
 | 
			
		||||
                var requestType = args[0].ParameterType;
 | 
			
		||||
                var actionCtx = new ServiceMethod
 | 
			
		||||
                {
 | 
			
		||||
                    Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
 | 
			
		||||
 | 
			
		||||
                var reqFilters = new List<IHasRequestFilter>();
 | 
			
		||||
 | 
			
		||||
                foreach (var attr in mi.GetCustomAttributes(true))
 | 
			
		||||
                {
 | 
			
		||||
                    if (attr is IHasRequestFilter hasReqFilter)
 | 
			
		||||
                    {
 | 
			
		||||
                        reqFilters.Add(hasReqFilter);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (reqFilters.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                actions.Add(actionCtx);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return actions;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
 | 
			
		||||
        {
 | 
			
		||||
            var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
 | 
			
		||||
            var serviceStrong = Expression.Convert(serviceParam, serviceType);
 | 
			
		||||
 | 
			
		||||
            var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
 | 
			
		||||
            var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
 | 
			
		||||
 | 
			
		||||
            Expression callExecute = Expression.Call(
 | 
			
		||||
            serviceStrong, mi, requestDtoStrong);
 | 
			
		||||
 | 
			
		||||
            if (mi.ReturnType != typeof(void))
 | 
			
		||||
            {
 | 
			
		||||
                var executeFunc = Expression.Lambda<ActionInvokerFn>(
 | 
			
		||||
                    callExecute,
 | 
			
		||||
                    serviceParam,
 | 
			
		||||
                    requestDtoParam).Compile();
 | 
			
		||||
 | 
			
		||||
                return executeFunc;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var executeFunc = Expression.Lambda<VoidActionInvokerFn>(
 | 
			
		||||
                    callExecute,
 | 
			
		||||
                    serviceParam,
 | 
			
		||||
                    requestDtoParam).Compile();
 | 
			
		||||
 | 
			
		||||
                return (service, request) =>
 | 
			
		||||
                {
 | 
			
		||||
                    executeFunc(service, request);
 | 
			
		||||
                    return null;
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,212 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Net.Mime;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Emby.Server.Implementations.HttpServer;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public class ServiceHandler
 | 
			
		||||
    {
 | 
			
		||||
        private RestPath _restPath;
 | 
			
		||||
 | 
			
		||||
        private string _responseContentType;
 | 
			
		||||
 | 
			
		||||
        internal ServiceHandler(RestPath restPath, string responseContentType)
 | 
			
		||||
        {
 | 
			
		||||
            _restPath = restPath;
 | 
			
		||||
            _responseContentType = responseContentType;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
 | 
			
		||||
            {
 | 
			
		||||
                var deserializer = RequestHelper.GetRequestReader(host, contentType);
 | 
			
		||||
                if (deserializer != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return deserializer.Invoke(requestType, httpReq.InputStream);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.FromResult(host.CreateInstance(requestType));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            contentType = null;
 | 
			
		||||
            var pos = pathInfo.LastIndexOf('.');
 | 
			
		||||
            if (pos != -1)
 | 
			
		||||
            {
 | 
			
		||||
                var format = pathInfo.AsSpan().Slice(pos + 1);
 | 
			
		||||
                contentType = GetFormatContentType(format);
 | 
			
		||||
                if (contentType != null)
 | 
			
		||||
                {
 | 
			
		||||
                    pathInfo = pathInfo.Substring(0, pos);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return pathInfo;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetFormatContentType(ReadOnlySpan<char> format)
 | 
			
		||||
        {
 | 
			
		||||
            if (format.Equals("json", StringComparison.Ordinal))
 | 
			
		||||
            {
 | 
			
		||||
                return MediaTypeNames.Application.Json;
 | 
			
		||||
            }
 | 
			
		||||
            else if (format.Equals("xml", StringComparison.Ordinal))
 | 
			
		||||
            {
 | 
			
		||||
                return MediaTypeNames.Application.Xml;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            httpReq.Items["__route"] = _restPath;
 | 
			
		||||
 | 
			
		||||
            if (_responseContentType != null)
 | 
			
		||||
            {
 | 
			
		||||
                httpReq.ResponseContentType = _responseContentType;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            httpHost.ApplyRequestFilters(httpReq, httpRes, request);
 | 
			
		||||
 | 
			
		||||
            httpRes.HttpContext.SetServiceStackRequest(httpReq);
 | 
			
		||||
            var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            // Apply response filters
 | 
			
		||||
            foreach (var responseFilter in httpHost.ResponseFilters)
 | 
			
		||||
            {
 | 
			
		||||
                responseFilter(httpReq, httpRes, response);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
 | 
			
		||||
        {
 | 
			
		||||
            var requestType = restPath.RequestType;
 | 
			
		||||
 | 
			
		||||
            if (RequireqRequestStream(requestType))
 | 
			
		||||
            {
 | 
			
		||||
                // Used by IRequiresRequestStream
 | 
			
		||||
                var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request);
 | 
			
		||||
                var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType));
 | 
			
		||||
 | 
			
		||||
                var rawReq = (IRequiresRequestStream)request;
 | 
			
		||||
                rawReq.RequestStream = httpReq.InputStream;
 | 
			
		||||
                return rawReq;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request);
 | 
			
		||||
 | 
			
		||||
                var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                return CreateRequest(httpReq, restPath, requestParams, requestDto);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static bool RequireqRequestStream(Type requestType)
 | 
			
		||||
        {
 | 
			
		||||
            var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
 | 
			
		||||
 | 
			
		||||
            return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
 | 
			
		||||
        {
 | 
			
		||||
            var pathInfo = !restPath.IsWildCardPath
 | 
			
		||||
                ? GetSanitizedPathInfo(httpReq.PathInfo, out _)
 | 
			
		||||
                : httpReq.PathInfo;
 | 
			
		||||
 | 
			
		||||
            return restPath.CreateRequest(pathInfo, requestParams, requestDto);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Duplicate Params are given a unique key by appending a #1 suffix
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private static Dictionary<string, string> GetRequestParams(HttpRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            var map = new Dictionary<string, string>();
 | 
			
		||||
 | 
			
		||||
            foreach (var pair in request.Query)
 | 
			
		||||
            {
 | 
			
		||||
                var values = pair.Value;
 | 
			
		||||
                if (values.Count == 1)
 | 
			
		||||
                {
 | 
			
		||||
                    map[pair.Key] = values[0];
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    for (var i = 0; i < values.Count; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
 | 
			
		||||
                && request.HasFormContentType)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var pair in request.Form)
 | 
			
		||||
                {
 | 
			
		||||
                    var values = pair.Value;
 | 
			
		||||
                    if (values.Count == 1)
 | 
			
		||||
                    {
 | 
			
		||||
                        map[pair.Key] = values[0];
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        for (var i = 0; i < values.Count; i++)
 | 
			
		||||
                        {
 | 
			
		||||
                            map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return map;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static bool IsMethod(string method, string expected)
 | 
			
		||||
            => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Duplicate params have their values joined together in a comma-delimited string.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            var map = new Dictionary<string, string>();
 | 
			
		||||
 | 
			
		||||
            foreach (var pair in request.Query)
 | 
			
		||||
            {
 | 
			
		||||
                map[pair.Key] = pair.Value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
 | 
			
		||||
                && request.HasFormContentType)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var pair in request.Form)
 | 
			
		||||
                {
 | 
			
		||||
                    map[pair.Key] = pair.Value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return map;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public class ServiceMethod
 | 
			
		||||
    {
 | 
			
		||||
        public string Id { get; set; }
 | 
			
		||||
 | 
			
		||||
        public ActionInvokerFn ServiceAction { get; set; }
 | 
			
		||||
 | 
			
		||||
        public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
 | 
			
		||||
 | 
			
		||||
        public static string Key(Type serviceType, string method, string requestDtoName)
 | 
			
		||||
        {
 | 
			
		||||
            return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,550 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    public class RestPath
 | 
			
		||||
    {
 | 
			
		||||
        private const string WildCard = "*";
 | 
			
		||||
        private const char WildCardChar = '*';
 | 
			
		||||
        private const string PathSeperator = "/";
 | 
			
		||||
        private const char PathSeperatorChar = '/';
 | 
			
		||||
        private const char ComponentSeperator = '.';
 | 
			
		||||
        private const string VariablePrefix = "{";
 | 
			
		||||
 | 
			
		||||
        private readonly bool[] componentsWithSeparators;
 | 
			
		||||
 | 
			
		||||
        private readonly string restPath;
 | 
			
		||||
        public bool IsWildCardPath { get; private set; }
 | 
			
		||||
 | 
			
		||||
        private readonly string[] literalsToMatch;
 | 
			
		||||
 | 
			
		||||
        private readonly string[] variablesNames;
 | 
			
		||||
 | 
			
		||||
        private readonly bool[] isWildcard;
 | 
			
		||||
        private readonly int wildcardCount = 0;
 | 
			
		||||
 | 
			
		||||
        internal static string[] IgnoreAttributesNamed = new[]
 | 
			
		||||
        {
 | 
			
		||||
            nameof(JsonIgnoreAttribute)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        private static Type _excludeType = typeof(Stream);
 | 
			
		||||
 | 
			
		||||
        public int VariableArgsCount { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The number of segments separated by '/' determinable by path.Split('/').Length
 | 
			
		||||
        /// e.g. /path/to/here.ext == 3
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int PathComponentsCount { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the total number of segments after subparts have been exploded ('.')
 | 
			
		||||
        /// e.g. /path/to/here.ext == 4.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int TotalComponentsCount { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string[] Verbs { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public Type RequestType { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public Type ServiceType { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public string Path => this.restPath;
 | 
			
		||||
 | 
			
		||||
        public string Summary { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public string Description { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public bool IsHidden { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public static string[] GetPathPartsForMatching(string pathInfo)
 | 
			
		||||
        {
 | 
			
		||||
            return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
 | 
			
		||||
        {
 | 
			
		||||
            var hashPrefix = pathPartsForMatching.Length + PathSeperator;
 | 
			
		||||
            return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
 | 
			
		||||
        {
 | 
			
		||||
            const string HashPrefix = WildCard + PathSeperator;
 | 
			
		||||
            return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
 | 
			
		||||
        {
 | 
			
		||||
            var list = new List<string>();
 | 
			
		||||
 | 
			
		||||
            foreach (var part in pathPartsForMatching)
 | 
			
		||||
            {
 | 
			
		||||
                list.Add(hashPrefix + part);
 | 
			
		||||
 | 
			
		||||
                if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var subParts = part.Split(ComponentSeperator);
 | 
			
		||||
                foreach (var subPart in subParts)
 | 
			
		||||
                {
 | 
			
		||||
                    list.Add(hashPrefix + subPart);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return list;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null)
 | 
			
		||||
        {
 | 
			
		||||
            this.RequestType = requestType;
 | 
			
		||||
            this.ServiceType = serviceType;
 | 
			
		||||
            this.Summary = summary;
 | 
			
		||||
            this.IsHidden = isHidden;
 | 
			
		||||
            this.Description = description;
 | 
			
		||||
            this.restPath = path;
 | 
			
		||||
 | 
			
		||||
            this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
 | 
			
		||||
            var componentsList = new List<string>();
 | 
			
		||||
 | 
			
		||||
            // We only split on '.' if the restPath has them. Allows for /{action}.{type}
 | 
			
		||||
            var hasSeparators = new List<bool>();
 | 
			
		||||
            foreach (var component in this.restPath.Split(PathSeperatorChar))
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrEmpty(component))
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
 | 
			
		||||
                    && component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1)
 | 
			
		||||
                {
 | 
			
		||||
                    hasSeparators.Add(true);
 | 
			
		||||
                    componentsList.AddRange(component.Split(ComponentSeperator));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    hasSeparators.Add(false);
 | 
			
		||||
                    componentsList.Add(component);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var components = componentsList.ToArray();
 | 
			
		||||
            this.TotalComponentsCount = components.Length;
 | 
			
		||||
 | 
			
		||||
            this.literalsToMatch = new string[this.TotalComponentsCount];
 | 
			
		||||
            this.variablesNames = new string[this.TotalComponentsCount];
 | 
			
		||||
            this.isWildcard = new bool[this.TotalComponentsCount];
 | 
			
		||||
            this.componentsWithSeparators = hasSeparators.ToArray();
 | 
			
		||||
            this.PathComponentsCount = this.componentsWithSeparators.Length;
 | 
			
		||||
            string firstLiteralMatch = null;
 | 
			
		||||
 | 
			
		||||
            for (var i = 0; i < components.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var component = components[i];
 | 
			
		||||
 | 
			
		||||
                if (component.StartsWith(VariablePrefix, StringComparison.Ordinal))
 | 
			
		||||
                {
 | 
			
		||||
                    var variableName = component.Substring(1, component.Length - 2);
 | 
			
		||||
                    if (variableName[variableName.Length - 1] == WildCardChar)
 | 
			
		||||
                    {
 | 
			
		||||
                        this.isWildcard[i] = true;
 | 
			
		||||
                        variableName = variableName.Substring(0, variableName.Length - 1);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    this.variablesNames[i] = variableName;
 | 
			
		||||
                    this.VariableArgsCount++;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    this.literalsToMatch[i] = component.ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
                    if (firstLiteralMatch == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        firstLiteralMatch = this.literalsToMatch[i];
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (var i = 0; i < components.Length - 1; i++)
 | 
			
		||||
            {
 | 
			
		||||
                if (!this.isWildcard[i])
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (this.literalsToMatch[i + 1] == null)
 | 
			
		||||
                {
 | 
			
		||||
                    throw new ArgumentException(
 | 
			
		||||
                        "A wildcard path component must be at the end of the path or followed by a literal path component.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.wildcardCount = this.isWildcard.Length;
 | 
			
		||||
            this.IsWildCardPath = this.wildcardCount > 0;
 | 
			
		||||
 | 
			
		||||
            this.FirstMatchHashKey = !this.IsWildCardPath
 | 
			
		||||
                ? this.PathComponentsCount + PathSeperator + firstLiteralMatch
 | 
			
		||||
                : WildCardChar + PathSeperator + firstLiteralMatch;
 | 
			
		||||
 | 
			
		||||
            this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
 | 
			
		||||
 | 
			
		||||
            _propertyNamesMap = new HashSet<string>(
 | 
			
		||||
                    GetSerializableProperties(RequestType).Select(x => x.Name),
 | 
			
		||||
                    StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var prop in GetPublicProperties(type))
 | 
			
		||||
            {
 | 
			
		||||
                if (prop.GetMethod == null
 | 
			
		||||
                    || _excludeType == prop.PropertyType)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var ignored = false;
 | 
			
		||||
                foreach (var attr in prop.GetCustomAttributes(true))
 | 
			
		||||
                {
 | 
			
		||||
                    if (IgnoreAttributesNamed.Contains(attr.GetType().Name))
 | 
			
		||||
                    {
 | 
			
		||||
                        ignored = true;
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!ignored)
 | 
			
		||||
                {
 | 
			
		||||
                    yield return prop;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
 | 
			
		||||
        {
 | 
			
		||||
            if (type.IsInterface)
 | 
			
		||||
            {
 | 
			
		||||
                var propertyInfos = new List<PropertyInfo>();
 | 
			
		||||
                var considered = new List<Type>()
 | 
			
		||||
                {
 | 
			
		||||
                    type
 | 
			
		||||
                };
 | 
			
		||||
                var queue = new Queue<Type>();
 | 
			
		||||
                queue.Enqueue(type);
 | 
			
		||||
 | 
			
		||||
                while (queue.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var subType = queue.Dequeue();
 | 
			
		||||
                    foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (considered.Contains(subInterface))
 | 
			
		||||
                        {
 | 
			
		||||
                            continue;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        considered.Add(subInterface);
 | 
			
		||||
                        queue.Enqueue(subInterface);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var newPropertyInfos = GetTypesPublicProperties(subType)
 | 
			
		||||
                        .Where(x => !propertyInfos.Contains(x));
 | 
			
		||||
 | 
			
		||||
                    propertyInfos.InsertRange(0, newPropertyInfos);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return propertyInfos;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return GetTypesPublicProperties(type)
 | 
			
		||||
                .Where(x => x.GetIndexParameters().Length == 0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var pi in subType.GetRuntimeProperties())
 | 
			
		||||
            {
 | 
			
		||||
                var mi = pi.GetMethod ?? pi.SetMethod;
 | 
			
		||||
                if (mi != null && mi.IsStatic)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                yield return pi;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Provide for quick lookups based on hashes that can be determined from a request url.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string FirstMatchHashKey { get; private set; }
 | 
			
		||||
 | 
			
		||||
        private readonly StringMapTypeDeserializer typeDeserializer;
 | 
			
		||||
 | 
			
		||||
        private readonly HashSet<string> _propertyNamesMap;
 | 
			
		||||
 | 
			
		||||
        public int MatchScore(string httpMethod, string[] withPathInfoParts)
 | 
			
		||||
        {
 | 
			
		||||
            var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount);
 | 
			
		||||
            if (!isMatch)
 | 
			
		||||
            {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Routes with least wildcard matches get the highest score
 | 
			
		||||
            var score = Math.Max(100 - wildcardMatchCount, 1) * 1000
 | 
			
		||||
                        // Routes with less variable (and more literal) matches
 | 
			
		||||
                        + Math.Max(10 - VariableArgsCount, 1) * 100;
 | 
			
		||||
 | 
			
		||||
            // Exact verb match is better than ANY
 | 
			
		||||
            if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                score += 10;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                score += 1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return score;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// For performance withPathInfoParts should already be a lower case string
 | 
			
		||||
        /// to minimize redundant matching operations.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
 | 
			
		||||
        {
 | 
			
		||||
            wildcardMatchCount = 0;
 | 
			
		||||
 | 
			
		||||
            if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!ExplodeComponents(ref withPathInfoParts))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            int pathIx = 0;
 | 
			
		||||
            for (var i = 0; i < this.TotalComponentsCount; i++)
 | 
			
		||||
            {
 | 
			
		||||
                if (this.isWildcard[i])
 | 
			
		||||
                {
 | 
			
		||||
                    if (i < this.TotalComponentsCount - 1)
 | 
			
		||||
                    {
 | 
			
		||||
                        // Continue to consume up until a match with the next literal
 | 
			
		||||
                        while (pathIx < withPathInfoParts.Length
 | 
			
		||||
                            && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
 | 
			
		||||
                        {
 | 
			
		||||
                            pathIx++;
 | 
			
		||||
                            wildcardMatchCount++;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Ensure there are still enough parts left to match the remainder
 | 
			
		||||
                        if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
 | 
			
		||||
                        {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        // A wildcard at the end matches the remainder of path
 | 
			
		||||
                        wildcardMatchCount += withPathInfoParts.Length - pathIx;
 | 
			
		||||
                        pathIx = withPathInfoParts.Length;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var literalToMatch = this.literalsToMatch[i];
 | 
			
		||||
                    if (literalToMatch == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        // Matching an ordinary (non-wildcard) variable consumes a single part
 | 
			
		||||
                        pathIx++;
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (withPathInfoParts.Length <= pathIx
 | 
			
		||||
                        || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    pathIx++;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return pathIx == withPathInfoParts.Length;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool ExplodeComponents(ref string[] withPathInfoParts)
 | 
			
		||||
        {
 | 
			
		||||
            var totalComponents = new List<string>();
 | 
			
		||||
            for (var i = 0; i < withPathInfoParts.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var component = withPathInfoParts[i];
 | 
			
		||||
                if (string.IsNullOrEmpty(component))
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (this.PathComponentsCount != this.TotalComponentsCount
 | 
			
		||||
                    && this.componentsWithSeparators[i])
 | 
			
		||||
                {
 | 
			
		||||
                    var subComponents = component.Split(ComponentSeperator);
 | 
			
		||||
                    if (subComponents.Length < 2)
 | 
			
		||||
                    {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    totalComponents.AddRange(subComponents);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    totalComponents.Add(component);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            withPathInfoParts = totalComponents.ToArray();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
 | 
			
		||||
        {
 | 
			
		||||
            var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
 | 
			
		||||
            ExplodeComponents(ref requestComponents);
 | 
			
		||||
 | 
			
		||||
            if (requestComponents.Length != this.TotalComponentsCount)
 | 
			
		||||
            {
 | 
			
		||||
                var isValidWildCardPath = this.IsWildCardPath
 | 
			
		||||
                    && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
 | 
			
		||||
 | 
			
		||||
                if (!isValidWildCardPath)
 | 
			
		||||
                {
 | 
			
		||||
                    throw new ArgumentException(
 | 
			
		||||
                        string.Format(
 | 
			
		||||
                            CultureInfo.InvariantCulture,
 | 
			
		||||
                            "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
 | 
			
		||||
                            pathInfo,
 | 
			
		||||
                            this.restPath));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var requestKeyValuesMap = new Dictionary<string, string>();
 | 
			
		||||
            var pathIx = 0;
 | 
			
		||||
            for (var i = 0; i < this.TotalComponentsCount; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var variableName = this.variablesNames[i];
 | 
			
		||||
                if (variableName == null)
 | 
			
		||||
                {
 | 
			
		||||
                    pathIx++;
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!this._propertyNamesMap.Contains(variableName))
 | 
			
		||||
                {
 | 
			
		||||
                    if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                    {
 | 
			
		||||
                        pathIx++;
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    throw new ArgumentException("Could not find property "
 | 
			
		||||
                        + variableName + " on " + RequestType.GetMethodName());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch
 | 
			
		||||
                if (value != null && this.isWildcard[i])
 | 
			
		||||
                {
 | 
			
		||||
                    if (i == this.TotalComponentsCount - 1)
 | 
			
		||||
                    {
 | 
			
		||||
                        // Wildcard at end of path definition consumes all the rest
 | 
			
		||||
                        var sb = new StringBuilder();
 | 
			
		||||
                        sb.Append(value);
 | 
			
		||||
                        for (var j = pathIx + 1; j < requestComponents.Length; j++)
 | 
			
		||||
                        {
 | 
			
		||||
                            sb.Append(PathSeperatorChar)
 | 
			
		||||
                                .Append(requestComponents[j]);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        value = sb.ToString();
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        // Wildcard in middle of path definition consumes up until it
 | 
			
		||||
                        // hits a match for the next element in the definition (which must be a literal)
 | 
			
		||||
                        // It may consume 0 or more path parts
 | 
			
		||||
                        var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
 | 
			
		||||
                        if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                        {
 | 
			
		||||
                            var sb = new StringBuilder(value);
 | 
			
		||||
                            pathIx++;
 | 
			
		||||
                            while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
                            {
 | 
			
		||||
                                sb.Append(PathSeperatorChar)
 | 
			
		||||
                                    .Append(requestComponents[pathIx++]);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            value = sb.ToString();
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            value = null;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // Variable consumes single path item
 | 
			
		||||
                    pathIx++;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                requestKeyValuesMap[variableName] = value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (queryStringAndFormData != null)
 | 
			
		||||
            {
 | 
			
		||||
                // Query String and form data can override variable path matches
 | 
			
		||||
                // path variables < query string < form data
 | 
			
		||||
                foreach (var name in queryStringAndFormData)
 | 
			
		||||
                {
 | 
			
		||||
                    requestKeyValuesMap[name.Key] = name.Value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class RestPathMap : SortedDictionary<string, List<RestPath>>
 | 
			
		||||
        {
 | 
			
		||||
            public RestPathMap() : base(StringComparer.OrdinalIgnoreCase)
 | 
			
		||||
            {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,118 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class StringMapTypeDeserializer
 | 
			
		||||
    {
 | 
			
		||||
        internal class PropertySerializerEntry
 | 
			
		||||
        {
 | 
			
		||||
            public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
 | 
			
		||||
            {
 | 
			
		||||
                PropertySetFn = propertySetFn;
 | 
			
		||||
                PropertyParseStringFn = propertyParseStringFn;
 | 
			
		||||
                PropertyType = propertyType;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public Action<object, object> PropertySetFn { get; private set; }
 | 
			
		||||
 | 
			
		||||
            public Func<string, object> PropertyParseStringFn { get; private set; }
 | 
			
		||||
 | 
			
		||||
            public Type PropertyType { get; private set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly Type type;
 | 
			
		||||
        private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap
 | 
			
		||||
            = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
        public Func<string, object> GetParseFn(Type propertyType)
 | 
			
		||||
        {
 | 
			
		||||
            if (propertyType == typeof(string))
 | 
			
		||||
            {
 | 
			
		||||
                return s => s;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _GetParseFn(propertyType);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly Func<Type, object> _CreateInstanceFn;
 | 
			
		||||
        private readonly Func<Type, Func<string, object>> _GetParseFn;
 | 
			
		||||
 | 
			
		||||
        public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type)
 | 
			
		||||
        {
 | 
			
		||||
            _CreateInstanceFn = createInstanceFn;
 | 
			
		||||
            _GetParseFn = getParseFn;
 | 
			
		||||
            this.type = type;
 | 
			
		||||
 | 
			
		||||
            foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
 | 
			
		||||
            {
 | 
			
		||||
                var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
 | 
			
		||||
                var propertyType = propertyInfo.PropertyType;
 | 
			
		||||
                var propertyParseStringFn = GetParseFn(propertyType);
 | 
			
		||||
                var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
 | 
			
		||||
 | 
			
		||||
                propertySetterMap[propertyInfo.Name] = propertySerializer;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
 | 
			
		||||
        {
 | 
			
		||||
            PropertySerializerEntry propertySerializerEntry = null;
 | 
			
		||||
 | 
			
		||||
            if (instance == null)
 | 
			
		||||
            {
 | 
			
		||||
                instance = _CreateInstanceFn(type);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var pair in keyValuePairs)
 | 
			
		||||
            {
 | 
			
		||||
                string propertyName = pair.Key;
 | 
			
		||||
                string propertyTextValue = pair.Value;
 | 
			
		||||
 | 
			
		||||
                if (propertyTextValue == null
 | 
			
		||||
                    || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
 | 
			
		||||
                    || propertySerializerEntry.PropertySetFn == null)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (propertySerializerEntry.PropertyType == typeof(bool))
 | 
			
		||||
                {
 | 
			
		||||
                    // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
 | 
			
		||||
                    propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
 | 
			
		||||
                if (value == null)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                propertySerializerEntry.PropertySetFn(instance, value);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return instance;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal static class TypeAccessor
 | 
			
		||||
    {
 | 
			
		||||
        public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo)
 | 
			
		||||
        {
 | 
			
		||||
            if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var setMethodInfo = propertyInfo.SetMethod;
 | 
			
		||||
            return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.Services
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Donated by Ivan Korneliuk from his post:
 | 
			
		||||
    /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
 | 
			
		||||
    ///
 | 
			
		||||
    /// Modified to only allow using routes matching the supplied HTTP Verb.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class UrlExtensions
 | 
			
		||||
    {
 | 
			
		||||
        public static string GetMethodName(this Type type)
 | 
			
		||||
        {
 | 
			
		||||
            var typeName = type.FullName != null // can be null, e.g. generic types
 | 
			
		||||
                ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
 | 
			
		||||
                    .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
 | 
			
		||||
                    .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
 | 
			
		||||
                : type.Name;
 | 
			
		||||
 | 
			
		||||
            return type.IsGenericParameter ? "'" + typeName : typeName;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1429,6 +1429,24 @@ namespace Emby.Server.Implementations.Session
 | 
			
		||||
            return AuthenticateNewSessionInternal(request, false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
 | 
			
		||||
        {
 | 
			
		||||
            var result = _authRepo.Get(new AuthenticationInfoQuery()
 | 
			
		||||
            {
 | 
			
		||||
                AccessToken = token,
 | 
			
		||||
                DeviceId = _appHost.SystemId,
 | 
			
		||||
                Limit = 1
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (result.TotalRecordCount == 0)
 | 
			
		||||
            {
 | 
			
		||||
                throw new SecurityException("Unknown quick connect token");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            request.UserId = result.Items[0].UserId;
 | 
			
		||||
            return AuthenticateNewSessionInternal(request, false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
 | 
			
		||||
        {
 | 
			
		||||
            CheckDisposed();
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session
 | 
			
		||||
        private readonly ILogger<SessionWebSocketListener> _logger;
 | 
			
		||||
        private readonly ILoggerFactory _loggerFactory;
 | 
			
		||||
 | 
			
		||||
        private readonly IHttpServer _httpServer;
 | 
			
		||||
        private readonly IWebSocketManager _webSocketManager;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The KeepAlive cancellation token.
 | 
			
		||||
@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="sessionManager">The session manager.</param>
 | 
			
		||||
        /// <param name="loggerFactory">The logger factory.</param>
 | 
			
		||||
        /// <param name="httpServer">The HTTP server.</param>
 | 
			
		||||
        /// <param name="webSocketManager">The HTTP server.</param>
 | 
			
		||||
        public SessionWebSocketListener(
 | 
			
		||||
            ILogger<SessionWebSocketListener> logger,
 | 
			
		||||
            ISessionManager sessionManager,
 | 
			
		||||
            ILoggerFactory loggerFactory,
 | 
			
		||||
            IHttpServer httpServer)
 | 
			
		||||
            IWebSocketManager webSocketManager)
 | 
			
		||||
        {
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _sessionManager = sessionManager;
 | 
			
		||||
            _loggerFactory = loggerFactory;
 | 
			
		||||
            _httpServer = httpServer;
 | 
			
		||||
            _webSocketManager = webSocketManager;
 | 
			
		||||
 | 
			
		||||
            httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
 | 
			
		||||
            webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
 | 
			
		||||
@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
 | 
			
		||||
            _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
 | 
			
		||||
            StopKeepAlive();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,248 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Net.Mime;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Common.Net;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Http.Extensions;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Microsoft.Extensions.Primitives;
 | 
			
		||||
using Microsoft.Net.Http.Headers;
 | 
			
		||||
using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
 | 
			
		||||
 | 
			
		||||
namespace Emby.Server.Implementations.SocketSharp
 | 
			
		||||
{
 | 
			
		||||
    public class WebSocketSharpRequest : IHttpRequest
 | 
			
		||||
    {
 | 
			
		||||
        private const string FormUrlEncoded = "application/x-www-form-urlencoded";
 | 
			
		||||
        private const string MultiPartFormData = "multipart/form-data";
 | 
			
		||||
        private const string Soap11 = "text/xml; charset=utf-8";
 | 
			
		||||
 | 
			
		||||
        private string _remoteIp;
 | 
			
		||||
        private Dictionary<string, object> _items;
 | 
			
		||||
        private string _responseContentType;
 | 
			
		||||
 | 
			
		||||
        public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName)
 | 
			
		||||
        {
 | 
			
		||||
            this.OperationName = operationName;
 | 
			
		||||
            this.Request = httpRequest;
 | 
			
		||||
            this.Response = httpResponse;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString();
 | 
			
		||||
 | 
			
		||||
        public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString();
 | 
			
		||||
 | 
			
		||||
        public HttpRequest Request { get; }
 | 
			
		||||
 | 
			
		||||
        public HttpResponse Response { get; }
 | 
			
		||||
 | 
			
		||||
        public string OperationName { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string RawUrl => Request.GetEncodedPathAndQuery();
 | 
			
		||||
 | 
			
		||||
        public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/');
 | 
			
		||||
 | 
			
		||||
        public string RemoteIp
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                if (_remoteIp != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return _remoteIp;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                IPAddress ip;
 | 
			
		||||
 | 
			
		||||
                // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
 | 
			
		||||
                // (if the server is behind a reverse proxy for example)
 | 
			
		||||
                if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
 | 
			
		||||
                {
 | 
			
		||||
                    if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
 | 
			
		||||
                    {
 | 
			
		||||
                        ip = Request.HttpContext.Connection.RemoteIpAddress;
 | 
			
		||||
 | 
			
		||||
                        // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
 | 
			
		||||
                        ip ??= IPAddress.Loopback;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return _remoteIp = NormalizeIp(ip).ToString();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
 | 
			
		||||
 | 
			
		||||
        public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>());
 | 
			
		||||
 | 
			
		||||
        public string ResponseContentType
 | 
			
		||||
        {
 | 
			
		||||
            get =>
 | 
			
		||||
                _responseContentType
 | 
			
		||||
                ?? (_responseContentType = GetResponseContentType(Request));
 | 
			
		||||
            set => _responseContentType = value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string PathInfo => Request.Path.Value;
 | 
			
		||||
 | 
			
		||||
        public string UserAgent => Request.Headers[HeaderNames.UserAgent];
 | 
			
		||||
 | 
			
		||||
        public IHeaderDictionary Headers => Request.Headers;
 | 
			
		||||
 | 
			
		||||
        public IQueryCollection QueryString => Request.Query;
 | 
			
		||||
 | 
			
		||||
        public bool IsLocal =>
 | 
			
		||||
            (Request.HttpContext.Connection.LocalIpAddress == null
 | 
			
		||||
            && Request.HttpContext.Connection.RemoteIpAddress == null)
 | 
			
		||||
            || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
 | 
			
		||||
 | 
			
		||||
        public string HttpMethod => Request.Method;
 | 
			
		||||
 | 
			
		||||
        public string Verb => HttpMethod;
 | 
			
		||||
 | 
			
		||||
        public string ContentType => Request.ContentType;
 | 
			
		||||
 | 
			
		||||
        public Uri UrlReferrer => Request.GetTypedHeaders().Referer;
 | 
			
		||||
 | 
			
		||||
        public Stream InputStream => Request.Body;
 | 
			
		||||
 | 
			
		||||
        public long ContentLength => Request.ContentLength ?? 0;
 | 
			
		||||
 | 
			
		||||
        private string GetHeader(string name) => Request.Headers[name].ToString();
 | 
			
		||||
 | 
			
		||||
        private static IPAddress NormalizeIp(IPAddress ip)
 | 
			
		||||
        {
 | 
			
		||||
            if (ip.IsIPv4MappedToIPv6)
 | 
			
		||||
            {
 | 
			
		||||
                return ip.MapToIPv4();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return ip;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static string GetResponseContentType(HttpRequest httpReq)
 | 
			
		||||
        {
 | 
			
		||||
            var specifiedContentType = GetQueryStringContentType(httpReq);
 | 
			
		||||
            if (!string.IsNullOrEmpty(specifiedContentType))
 | 
			
		||||
            {
 | 
			
		||||
                return specifiedContentType;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const string ServerDefaultContentType = MediaTypeNames.Application.Json;
 | 
			
		||||
 | 
			
		||||
            var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
 | 
			
		||||
            string defaultContentType = null;
 | 
			
		||||
            if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
 | 
			
		||||
            {
 | 
			
		||||
                defaultContentType = ServerDefaultContentType;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var acceptsAnything = false;
 | 
			
		||||
            var hasDefaultContentType = defaultContentType != null;
 | 
			
		||||
            if (acceptContentTypes != null)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes)
 | 
			
		||||
                {
 | 
			
		||||
                    ReadOnlySpan<char> contentType = acceptsType;
 | 
			
		||||
                    var index = contentType.IndexOf(';');
 | 
			
		||||
                    if (index != -1)
 | 
			
		||||
                    {
 | 
			
		||||
                        contentType = contentType.Slice(0, index);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    contentType = contentType.Trim();
 | 
			
		||||
                    acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
 | 
			
		||||
                    if (acceptsAnything)
 | 
			
		||||
                    {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (acceptsAnything)
 | 
			
		||||
                {
 | 
			
		||||
                    if (hasDefaultContentType)
 | 
			
		||||
                    {
 | 
			
		||||
                        return defaultContentType;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return ServerDefaultContentType;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (acceptContentTypes == null && httpReq.ContentType == Soap11)
 | 
			
		||||
            {
 | 
			
		||||
                return Soap11;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // We could also send a '406 Not Acceptable', but this is allowed also
 | 
			
		||||
            return ServerDefaultContentType;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
 | 
			
		||||
        {
 | 
			
		||||
            if (contentTypes == null || request.ContentType == null)
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var contentType in contentTypes)
 | 
			
		||||
            {
 | 
			
		||||
                if (IsContentType(request, contentType))
 | 
			
		||||
                {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static bool IsContentType(HttpRequest request, string contentType)
 | 
			
		||||
        {
 | 
			
		||||
            return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetQueryStringContentType(HttpRequest httpReq)
 | 
			
		||||
        {
 | 
			
		||||
            ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
 | 
			
		||||
            if (format == ReadOnlySpan<char>.Empty)
 | 
			
		||||
            {
 | 
			
		||||
                const int FormatMaxLength = 4;
 | 
			
		||||
                ReadOnlySpan<char> pi = httpReq.Path.ToString();
 | 
			
		||||
                if (pi == null || pi.Length <= FormatMaxLength)
 | 
			
		||||
                {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (pi[0] == '/')
 | 
			
		||||
                {
 | 
			
		||||
                    pi = pi.Slice(1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                format = pi.LeftPart('/');
 | 
			
		||||
                if (format.Length > FormatMaxLength)
 | 
			
		||||
                {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            format = format.LeftPart('.');
 | 
			
		||||
            if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return "application/json";
 | 
			
		||||
            }
 | 
			
		||||
            else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return "application/xml";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										154
									
								
								Jellyfin.Api/Controllers/QuickConnectController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								Jellyfin.Api/Controllers/QuickConnectController.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,154 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Helpers;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Controller.QuickConnect;
 | 
			
		||||
using MediaBrowser.Model.QuickConnect;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Api.Controllers
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Quick connect controller.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class QuickConnectController : BaseJellyfinApiController
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IQuickConnect _quickConnect;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
 | 
			
		||||
        public QuickConnectController(IQuickConnect quickConnect)
 | 
			
		||||
        {
 | 
			
		||||
            _quickConnect = quickConnect;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the current quick connect state.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <response code="200">Quick connect state returned.</response>
 | 
			
		||||
        /// <returns>The current <see cref="QuickConnectState"/>.</returns>
 | 
			
		||||
        [HttpGet("Status")]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        public ActionResult<QuickConnectState> GetStatus()
 | 
			
		||||
        {
 | 
			
		||||
            _quickConnect.ExpireRequests();
 | 
			
		||||
            return _quickConnect.State;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initiate a new quick connect request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <response code="200">Quick connect request successfully created.</response>
 | 
			
		||||
        /// <response code="401">Quick connect is not active on this server.</response>
 | 
			
		||||
        /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
 | 
			
		||||
        [HttpGet("Initiate")]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        public ActionResult<QuickConnectResult> Initiate()
 | 
			
		||||
        {
 | 
			
		||||
            return _quickConnect.TryConnect();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Attempts to retrieve authentication information.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="secret">Secret previously returned from the Initiate endpoint.</param>
 | 
			
		||||
        /// <response code="200">Quick connect result returned.</response>
 | 
			
		||||
        /// <response code="404">Unknown quick connect secret.</response>
 | 
			
		||||
        /// <returns>An updated <see cref="QuickConnectResult"/>.</returns>
 | 
			
		||||
        [HttpGet("Connect")]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
        public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                return _quickConnect.CheckRequestStatus(secret);
 | 
			
		||||
            }
 | 
			
		||||
            catch (ResourceNotFoundException)
 | 
			
		||||
            {
 | 
			
		||||
                return NotFound("Unknown secret");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Temporarily activates quick connect for five minutes.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <response code="204">Quick connect has been temporarily activated.</response>
 | 
			
		||||
        /// <response code="403">Quick connect is unavailable on this server.</response>
 | 
			
		||||
        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 | 
			
		||||
        [HttpPost("Activate")]
 | 
			
		||||
        [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
        public ActionResult Activate()
 | 
			
		||||
        {
 | 
			
		||||
            if (_quickConnect.State == QuickConnectState.Unavailable)
 | 
			
		||||
            {
 | 
			
		||||
                return Forbid("Quick connect is unavailable");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _quickConnect.Activate();
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Enables or disables quick connect.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="status">New <see cref="QuickConnectState"/>.</param>
 | 
			
		||||
        /// <response code="204">Quick connect state set successfully.</response>
 | 
			
		||||
        /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 | 
			
		||||
        [HttpPost("Available")]
 | 
			
		||||
        [Authorize(Policy = Policies.RequiresElevation)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status204NoContent)]
 | 
			
		||||
        public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
 | 
			
		||||
        {
 | 
			
		||||
            _quickConnect.SetState(status);
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Authorizes a pending quick connect request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="code">Quick connect code to authorize.</param>
 | 
			
		||||
        /// <response code="200">Quick connect result authorized successfully.</response>
 | 
			
		||||
        /// <response code="403">Unknown user id.</response>
 | 
			
		||||
        /// <returns>Boolean indicating if the authorization was successful.</returns>
 | 
			
		||||
        [HttpPost("Authorize")]
 | 
			
		||||
        [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
        public ActionResult<bool> Authorize([FromQuery, Required] string code)
 | 
			
		||||
        {
 | 
			
		||||
            var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
 | 
			
		||||
            if (!userId.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                return Forbid("Unknown user id");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _quickConnect.AuthorizeRequest(userId.Value, code);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deauthorize all quick connect devices for the current user.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <response code="200">All quick connect devices were deleted.</response>
 | 
			
		||||
        /// <returns>The number of devices that were deleted.</returns>
 | 
			
		||||
        [HttpPost("Deauthorize")]
 | 
			
		||||
        [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        public ActionResult<int> Deauthorize()
 | 
			
		||||
        {
 | 
			
		||||
            var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
 | 
			
		||||
            if (!userId.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                return 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _quickConnect.DeleteAllDevices(userId.Value);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -216,6 +216,40 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Authenticates a user with quick connect.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="request">The <see cref="QuickConnectDto"/> request.</param>
 | 
			
		||||
        /// <response code="200">User authenticated.</response>
 | 
			
		||||
        /// <response code="400">Missing token.</response>
 | 
			
		||||
        /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
 | 
			
		||||
        [HttpPost("AuthenticateWithQuickConnect")]
 | 
			
		||||
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
			
		||||
        public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
 | 
			
		||||
        {
 | 
			
		||||
            var auth = _authContext.GetAuthorizationInfo(Request);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var authRequest = new AuthenticationRequest
 | 
			
		||||
                {
 | 
			
		||||
                    App = auth.Client,
 | 
			
		||||
                    AppVersion = auth.Version,
 | 
			
		||||
                    DeviceId = auth.DeviceId,
 | 
			
		||||
                    DeviceName = auth.Device,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                return await _sessionManager.AuthenticateQuickConnect(
 | 
			
		||||
                    authRequest,
 | 
			
		||||
                    request.Token).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (SecurityException e)
 | 
			
		||||
            {
 | 
			
		||||
                // rethrow adding IP address to message
 | 
			
		||||
                throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates a user's password.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
 | 
			
		||||
@ -233,7 +233,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                    .First();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var list = primaryVersion.LinkedAlternateVersions.ToList();
 | 
			
		||||
            var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
 | 
			
		||||
 | 
			
		||||
            foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
 | 
			
		||||
            {
 | 
			
		||||
@ -241,17 +241,20 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
 | 
			
		||||
                await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                list.Add(new LinkedChild
 | 
			
		||||
                if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
 | 
			
		||||
                {
 | 
			
		||||
                    alternateVersionsOfPrimary.Add(new LinkedChild
 | 
			
		||||
                    {
 | 
			
		||||
                        Path = item.Path,
 | 
			
		||||
                        ItemId = item.Id
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                foreach (var linkedItem in item.LinkedAlternateVersions)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
 | 
			
		||||
                    if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(linkedItem);
 | 
			
		||||
                        alternateVersionsOfPrimary.Add(linkedItem);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -262,7 +265,7 @@ namespace Jellyfin.Api.Controllers
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            primaryVersion.LinkedAlternateVersions = list.ToArray();
 | 
			
		||||
            primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
 | 
			
		||||
            await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -130,6 +130,8 @@ namespace Jellyfin.Api.Helpers
 | 
			
		||||
        private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                int bytesRead;
 | 
			
		||||
                int totalBytesRead = 0;
 | 
			
		||||
 | 
			
		||||
@ -171,5 +173,10 @@ namespace Jellyfin.Api.Helpers
 | 
			
		||||
 | 
			
		||||
                return totalBytesRead;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                ArrayPool<byte>.Shared.Return(array);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Api.Models.UserDtos
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The quick connect request body.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class QuickConnectDto
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the quick connect token.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Required]
 | 
			
		||||
        public string? Token { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -5,6 +5,15 @@
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
 | 
			
		||||
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 | 
			
		||||
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
 | 
			
		||||
    <IncludeSymbols>true</IncludeSymbols>
 | 
			
		||||
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
 | 
			
		||||
    <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
 | 
			
		||||
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
@ -19,6 +28,10 @@
 | 
			
		||||
    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <!-- Code analysers-->
 | 
			
		||||
  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
 | 
			
		||||
    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
using Jellyfin.Server.Middleware;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
 | 
			
		||||
@ -46,5 +47,55 @@ namespace Jellyfin.Server.Extensions
 | 
			
		||||
                    c.RoutePrefix = $"{baseUrl}api-docs/redoc";
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds IP based access validation to the application pipeline.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appBuilder">The application builder.</param>
 | 
			
		||||
        /// <returns>The updated application builder.</returns>
 | 
			
		||||
        public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            return appBuilder.UseMiddleware<IpBasedAccessValidationMiddleware>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds LAN based access filtering to the application pipeline.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appBuilder">The application builder.</param>
 | 
			
		||||
        /// <returns>The updated application builder.</returns>
 | 
			
		||||
        public static IApplicationBuilder UseLanFiltering(this IApplicationBuilder appBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            return appBuilder.UseMiddleware<LanFilteringMiddleware>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds base url redirection to the application pipeline.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appBuilder">The application builder.</param>
 | 
			
		||||
        /// <returns>The updated application builder.</returns>
 | 
			
		||||
        public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            return appBuilder.UseMiddleware<BaseUrlRedirectionMiddleware>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds a custom message during server startup to the application pipeline.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appBuilder">The application builder.</param>
 | 
			
		||||
        /// <returns>The updated application builder.</returns>
 | 
			
		||||
        public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            return appBuilder.UseMiddleware<ServerStartupMessageMiddleware>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds a WebSocket request handler to the application pipeline.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appBuilder">The application builder.</param>
 | 
			
		||||
        /// <returns>The updated application builder.</returns>
 | 
			
		||||
        public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ using Jellyfin.Api.Constants;
 | 
			
		||||
using Jellyfin.Api.Controllers;
 | 
			
		||||
using Jellyfin.Server.Formatters;
 | 
			
		||||
using Jellyfin.Server.Models;
 | 
			
		||||
using MediaBrowser.Common;
 | 
			
		||||
using MediaBrowser.Common.Json;
 | 
			
		||||
using MediaBrowser.Model.Entities;
 | 
			
		||||
using Microsoft.AspNetCore.Authentication;
 | 
			
		||||
@ -135,10 +136,11 @@ namespace Jellyfin.Server.Extensions
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="serviceCollection">The service collection.</param>
 | 
			
		||||
        /// <param name="baseUrl">The base url for the API.</param>
 | 
			
		||||
        /// <param name="pluginAssemblies">An IEnumberable containing all plugin assemblies with API controllers.</param>
 | 
			
		||||
        /// <returns>The MVC builder.</returns>
 | 
			
		||||
        public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl)
 | 
			
		||||
        public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IEnumerable<Assembly> pluginAssemblies)
 | 
			
		||||
        {
 | 
			
		||||
            return serviceCollection
 | 
			
		||||
            IMvcBuilder mvcBuilder = serviceCollection
 | 
			
		||||
                .AddCors(options =>
 | 
			
		||||
                {
 | 
			
		||||
                    options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
 | 
			
		||||
@ -179,8 +181,14 @@ namespace Jellyfin.Server.Extensions
 | 
			
		||||
 | 
			
		||||
                    // From JsonDefaults.PascalCase
 | 
			
		||||
                    options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
 | 
			
		||||
                })
 | 
			
		||||
                .AddControllersAsServices();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            foreach (Assembly pluginAssembly in pluginAssemblies)
 | 
			
		||||
            {
 | 
			
		||||
                mvcBuilder.AddApplicationPart(pluginAssembly);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return mvcBuilder.AddControllersAsServices();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										62
									
								
								Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server.Middleware
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Redirect requests without baseurl prefix to the baseurl prefixed URL.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class BaseUrlRedirectionMiddleware
 | 
			
		||||
    {
 | 
			
		||||
        private readonly RequestDelegate _next;
 | 
			
		||||
        private readonly ILogger<BaseUrlRedirectionMiddleware> _logger;
 | 
			
		||||
        private readonly IConfiguration _configuration;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="next">The next delegate in the pipeline.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="configuration">The application configuration.</param>
 | 
			
		||||
        public BaseUrlRedirectionMiddleware(
 | 
			
		||||
            RequestDelegate next,
 | 
			
		||||
            ILogger<BaseUrlRedirectionMiddleware> logger,
 | 
			
		||||
            IConfiguration configuration)
 | 
			
		||||
        {
 | 
			
		||||
            _next = next;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _configuration = configuration;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Executes the middleware action.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpContext">The current HTTP context.</param>
 | 
			
		||||
        /// <param name="serverConfigurationManager">The server configuration manager.</param>
 | 
			
		||||
        /// <returns>The async task.</returns>
 | 
			
		||||
        public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
 | 
			
		||||
        {
 | 
			
		||||
            var localPath = httpContext.Request.Path.ToString();
 | 
			
		||||
            var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl;
 | 
			
		||||
 | 
			
		||||
            if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
 | 
			
		||||
                || string.IsNullOrEmpty(localPath)
 | 
			
		||||
                || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                // Always redirect back to the default path if the base prefix is invalid or missing
 | 
			
		||||
                _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
 | 
			
		||||
                httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _next(httpContext).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,76 @@
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Common.Extensions;
 | 
			
		||||
using MediaBrowser.Common.Net;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server.Middleware
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Validates the IP of requests coming from local networks wrt. remote access.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class IpBasedAccessValidationMiddleware
 | 
			
		||||
    {
 | 
			
		||||
        private readonly RequestDelegate _next;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="next">The next delegate in the pipeline.</param>
 | 
			
		||||
        public IpBasedAccessValidationMiddleware(RequestDelegate next)
 | 
			
		||||
        {
 | 
			
		||||
            _next = next;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Executes the middleware action.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpContext">The current HTTP context.</param>
 | 
			
		||||
        /// <param name="networkManager">The network manager.</param>
 | 
			
		||||
        /// <param name="serverConfigurationManager">The server configuration manager.</param>
 | 
			
		||||
        /// <returns>The async task.</returns>
 | 
			
		||||
        public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (httpContext.Request.IsLocal())
 | 
			
		||||
            {
 | 
			
		||||
                await _next(httpContext).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var remoteIp = httpContext.Request.RemoteIp();
 | 
			
		||||
 | 
			
		||||
            if (serverConfigurationManager.Configuration.EnableRemoteAccess)
 | 
			
		||||
            {
 | 
			
		||||
                var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
 | 
			
		||||
 | 
			
		||||
                if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp))
 | 
			
		||||
                {
 | 
			
		||||
                    if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (networkManager.IsAddressInSubnets(remoteIp, addressFilter))
 | 
			
		||||
                        {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter))
 | 
			
		||||
                        {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (!networkManager.IsInLocalNetwork(remoteIp))
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _next(httpContext).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Common.Net;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server.Middleware
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Validates the LAN host IP based on application configuration.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class LanFilteringMiddleware
 | 
			
		||||
    {
 | 
			
		||||
        private readonly RequestDelegate _next;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="next">The next delegate in the pipeline.</param>
 | 
			
		||||
        public LanFilteringMiddleware(RequestDelegate next)
 | 
			
		||||
        {
 | 
			
		||||
            _next = next;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Executes the middleware action.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpContext">The current HTTP context.</param>
 | 
			
		||||
        /// <param name="networkManager">The network manager.</param>
 | 
			
		||||
        /// <param name="serverConfigurationManager">The server configuration manager.</param>
 | 
			
		||||
        /// <returns>The async task.</returns>
 | 
			
		||||
        public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
 | 
			
		||||
        {
 | 
			
		||||
            var currentHost = httpContext.Request.Host.ToString();
 | 
			
		||||
            var hosts = serverConfigurationManager
 | 
			
		||||
                .Configuration
 | 
			
		||||
                .LocalNetworkAddresses
 | 
			
		||||
                .Select(NormalizeConfiguredLocalAddress)
 | 
			
		||||
                .ToList();
 | 
			
		||||
 | 
			
		||||
            if (hosts.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await _next(httpContext).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            currentHost ??= string.Empty;
 | 
			
		||||
 | 
			
		||||
            if (networkManager.IsInPrivateAddressSpace(currentHost))
 | 
			
		||||
            {
 | 
			
		||||
                hosts.Add("localhost");
 | 
			
		||||
                hosts.Add("127.0.0.1");
 | 
			
		||||
 | 
			
		||||
                if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1))
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _next(httpContext).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string NormalizeConfiguredLocalAddress(string address)
 | 
			
		||||
        {
 | 
			
		||||
            var add = address.AsSpan().Trim('/');
 | 
			
		||||
            int index = add.IndexOf('/');
 | 
			
		||||
            if (index != -1)
 | 
			
		||||
            {
 | 
			
		||||
                add = add.Slice(index + 1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return add.TrimStart('/').ToString();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
using System.Net.Mime;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Controller;
 | 
			
		||||
using MediaBrowser.Model.Globalization;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server.Middleware
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Shows a custom message during server startup.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class ServerStartupMessageMiddleware
 | 
			
		||||
    {
 | 
			
		||||
        private readonly RequestDelegate _next;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="next">The next delegate in the pipeline.</param>
 | 
			
		||||
        public ServerStartupMessageMiddleware(RequestDelegate next)
 | 
			
		||||
        {
 | 
			
		||||
            _next = next;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Executes the middleware action.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpContext">The current HTTP context.</param>
 | 
			
		||||
        /// <param name="serverApplicationHost">The server application host.</param>
 | 
			
		||||
        /// <param name="localizationManager">The localization manager.</param>
 | 
			
		||||
        /// <returns>The async task.</returns>
 | 
			
		||||
        public async Task Invoke(
 | 
			
		||||
            HttpContext httpContext,
 | 
			
		||||
            IServerApplicationHost serverApplicationHost,
 | 
			
		||||
            ILocalizationManager localizationManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (serverApplicationHost.CoreStartupHasCompleted)
 | 
			
		||||
            {
 | 
			
		||||
                await _next(httpContext).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
 | 
			
		||||
            httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
 | 
			
		||||
            httpContext.Response.ContentType = MediaTypeNames.Text.Html;
 | 
			
		||||
            await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Controller.Net;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server.Middleware
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Handles WebSocket requests.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class WebSocketHandlerMiddleware
 | 
			
		||||
    {
 | 
			
		||||
        private readonly RequestDelegate _next;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="next">The next delegate in the pipeline.</param>
 | 
			
		||||
        public WebSocketHandlerMiddleware(RequestDelegate next)
 | 
			
		||||
        {
 | 
			
		||||
            _next = next;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Executes the middleware action.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpContext">The current HTTP context.</param>
 | 
			
		||||
        /// <param name="webSocketManager">The WebSocket connection manager.</param>
 | 
			
		||||
        /// <returns>The async task.</returns>
 | 
			
		||||
        public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (!httpContext.WebSockets.IsWebSocketRequest)
 | 
			
		||||
            {
 | 
			
		||||
                await _next(httpContext).ConfigureAwait(false);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -11,7 +11,6 @@ using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using Emby.Server.Implementations;
 | 
			
		||||
using Emby.Server.Implementations.HttpServer;
 | 
			
		||||
using Emby.Server.Implementations.IO;
 | 
			
		||||
using Emby.Server.Implementations.Networking;
 | 
			
		||||
using Jellyfin.Api.Controllers;
 | 
			
		||||
@ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using Serilog.Extensions.Logging;
 | 
			
		||||
using SQLitePCL;
 | 
			
		||||
using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 | 
			
		||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server
 | 
			
		||||
@ -594,7 +594,7 @@ namespace Jellyfin.Server
 | 
			
		||||
            var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
 | 
			
		||||
            if (startupConfig != null && !startupConfig.HostWebClient())
 | 
			
		||||
            {
 | 
			
		||||
                inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger";
 | 
			
		||||
                inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return config
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,12 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
using Jellyfin.Api.TypeConverters;
 | 
			
		||||
using Jellyfin.Server.Extensions;
 | 
			
		||||
using Jellyfin.Server.Middleware;
 | 
			
		||||
using Jellyfin.Server.Models;
 | 
			
		||||
using MediaBrowser.Common;
 | 
			
		||||
using MediaBrowser.Common.Net;
 | 
			
		||||
using MediaBrowser.Controller;
 | 
			
		||||
using MediaBrowser.Controller.Configuration;
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
@ -20,14 +23,19 @@ namespace Jellyfin.Server
 | 
			
		||||
    public class Startup
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IServerConfigurationManager _serverConfigurationManager;
 | 
			
		||||
        private readonly IServerApplicationHost _serverApplicationHost;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="Startup" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="serverConfigurationManager">The server configuration manager.</param>
 | 
			
		||||
        public Startup(IServerConfigurationManager serverConfigurationManager)
 | 
			
		||||
        /// <param name="serverApplicationHost">The server application host.</param>
 | 
			
		||||
        public Startup(
 | 
			
		||||
            IServerConfigurationManager serverConfigurationManager,
 | 
			
		||||
            IServerApplicationHost serverApplicationHost)
 | 
			
		||||
        {
 | 
			
		||||
            _serverConfigurationManager = serverConfigurationManager;
 | 
			
		||||
            _serverApplicationHost = serverApplicationHost;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -38,7 +46,13 @@ namespace Jellyfin.Server
 | 
			
		||||
        {
 | 
			
		||||
            services.AddResponseCompression();
 | 
			
		||||
            services.AddHttpContextAccessor();
 | 
			
		||||
            services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'));
 | 
			
		||||
            services.AddHttpsRedirection(options =>
 | 
			
		||||
            {
 | 
			
		||||
                options.HttpsPort = _serverApplicationHost.HttpsPort;
 | 
			
		||||
            });
 | 
			
		||||
            services.AddJellyfinApi(
 | 
			
		||||
                _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'),
 | 
			
		||||
                _serverApplicationHost.GetApiPluginAssemblies());
 | 
			
		||||
 | 
			
		||||
            services.AddJellyfinApiSwagger();
 | 
			
		||||
 | 
			
		||||
@ -46,7 +60,23 @@ namespace Jellyfin.Server
 | 
			
		||||
            services.AddCustomAuthentication();
 | 
			
		||||
 | 
			
		||||
            services.AddJellyfinApiAuthorization();
 | 
			
		||||
            services.AddHttpClient();
 | 
			
		||||
 | 
			
		||||
            var productHeader = new ProductInfoHeaderValue(
 | 
			
		||||
                _serverApplicationHost.Name.Replace(' ', '-'),
 | 
			
		||||
                _serverApplicationHost.ApplicationVersionString);
 | 
			
		||||
            services
 | 
			
		||||
                .AddHttpClient(NamedClient.Default, c =>
 | 
			
		||||
                {
 | 
			
		||||
                    c.DefaultRequestHeaders.UserAgent.Add(productHeader);
 | 
			
		||||
                })
 | 
			
		||||
                .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
 | 
			
		||||
 | 
			
		||||
            services.AddHttpClient(NamedClient.MusicBrainz, c =>
 | 
			
		||||
                {
 | 
			
		||||
                    c.DefaultRequestHeaders.UserAgent.Add(productHeader);
 | 
			
		||||
                    c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})"));
 | 
			
		||||
                })
 | 
			
		||||
                .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -54,11 +84,9 @@ namespace Jellyfin.Server
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="app">The application builder.</param>
 | 
			
		||||
        /// <param name="env">The webhost environment.</param>
 | 
			
		||||
        /// <param name="serverApplicationHost">The server application host.</param>
 | 
			
		||||
        public void Configure(
 | 
			
		||||
            IApplicationBuilder app,
 | 
			
		||||
            IWebHostEnvironment env,
 | 
			
		||||
            IServerApplicationHost serverApplicationHost)
 | 
			
		||||
            IWebHostEnvironment env)
 | 
			
		||||
        {
 | 
			
		||||
            if (env.IsDevelopment())
 | 
			
		||||
            {
 | 
			
		||||
@ -73,12 +101,17 @@ namespace Jellyfin.Server
 | 
			
		||||
 | 
			
		||||
            app.UseResponseCompression();
 | 
			
		||||
 | 
			
		||||
            // TODO app.UseMiddleware<WebSocketMiddleware>();
 | 
			
		||||
            app.UseCors(ServerCorsPolicy.DefaultPolicyName);
 | 
			
		||||
 | 
			
		||||
            if (_serverConfigurationManager.Configuration.RequireHttps
 | 
			
		||||
                && _serverApplicationHost.ListenWithHttps)
 | 
			
		||||
            {
 | 
			
		||||
                app.UseHttpsRedirection();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            app.UseAuthentication();
 | 
			
		||||
            app.UseJellyfinApiSwagger(_serverConfigurationManager);
 | 
			
		||||
            app.UseRouting();
 | 
			
		||||
            app.UseCors(ServerCorsPolicy.DefaultPolicyName);
 | 
			
		||||
            app.UseAuthorization();
 | 
			
		||||
            if (_serverConfigurationManager.Configuration.EnableMetrics)
 | 
			
		||||
            {
 | 
			
		||||
@ -86,6 +119,12 @@ namespace Jellyfin.Server
 | 
			
		||||
                app.UseHttpMetrics();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            app.UseLanFiltering();
 | 
			
		||||
            app.UseIpBasedAccessValidation();
 | 
			
		||||
            app.UseBaseUrlRedirection();
 | 
			
		||||
            app.UseWebSocketHandler();
 | 
			
		||||
            app.UseServerStartupMessage();
 | 
			
		||||
 | 
			
		||||
            app.UseEndpoints(endpoints =>
 | 
			
		||||
            {
 | 
			
		||||
                endpoints.MapControllers();
 | 
			
		||||
@ -95,8 +134,6 @@ namespace Jellyfin.Server
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
 | 
			
		||||
 | 
			
		||||
            // Add type descriptor for legacy datetime parsing.
 | 
			
		||||
            TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using MediaBrowser.Common.Net;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Common.Extensions
 | 
			
		||||
@ -8,26 +9,55 @@ namespace MediaBrowser.Common.Extensions
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class HttpContextExtensions
 | 
			
		||||
    {
 | 
			
		||||
        private const string ServiceStackRequest = "ServiceStackRequest";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Set the ServiceStack request.
 | 
			
		||||
        /// Checks the origin of the HTTP request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpContext">The HttpContext instance.</param>
 | 
			
		||||
        /// <param name="request">The service stack request instance.</param>
 | 
			
		||||
        public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request)
 | 
			
		||||
        /// <param name="request">The incoming HTTP request.</param>
 | 
			
		||||
        /// <returns><c>true</c> if the request is coming from LAN, <c>false</c> otherwise.</returns>
 | 
			
		||||
        public static bool IsLocal(this HttpRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            httpContext.Items[ServiceStackRequest] = request;
 | 
			
		||||
            return (request.HttpContext.Connection.LocalIpAddress == null
 | 
			
		||||
                    && request.HttpContext.Connection.RemoteIpAddress == null)
 | 
			
		||||
                   || request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Get the ServiceStack request.
 | 
			
		||||
        /// Extracts the remote IP address of the caller of the HTTP request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="httpContext">The HttpContext instance.</param>
 | 
			
		||||
        /// <returns>The service stack request instance.</returns>
 | 
			
		||||
        public static IRequest GetServiceStackRequest(this HttpContext httpContext)
 | 
			
		||||
        /// <param name="request">The HTTP request.</param>
 | 
			
		||||
        /// <returns>The remote caller IP address.</returns>
 | 
			
		||||
        public static string RemoteIp(this HttpRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            return (IRequest)httpContext.Items[ServiceStackRequest];
 | 
			
		||||
            var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString();
 | 
			
		||||
            if (!string.IsNullOrEmpty(cachedRemoteIp))
 | 
			
		||||
            {
 | 
			
		||||
                return cachedRemoteIp;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            IPAddress ip;
 | 
			
		||||
 | 
			
		||||
            // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
 | 
			
		||||
            // (if the server is behind a reverse proxy for example)
 | 
			
		||||
            if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip))
 | 
			
		||||
            {
 | 
			
		||||
                if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), out ip))
 | 
			
		||||
                {
 | 
			
		||||
                    ip = request.HttpContext.Connection.RemoteIpAddress;
 | 
			
		||||
 | 
			
		||||
                    // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
 | 
			
		||||
                    ip ??= IPAddress.Loopback;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ip.IsIPv4MappedToIPv6)
 | 
			
		||||
            {
 | 
			
		||||
                ip = ip.MapToIPv4();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var normalizedIp = ip.ToString();
 | 
			
		||||
 | 
			
		||||
            request.HttpContext.Items["RemoteIp"] = normalizedIp;
 | 
			
		||||
            return normalizedIp;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Common.Plugins;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
@ -76,6 +77,12 @@ namespace MediaBrowser.Common
 | 
			
		||||
        /// <value>The plugins.</value>
 | 
			
		||||
        IReadOnlyList<IPlugin> Plugins { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all plugin assemblies which implement a custom rest api.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>An <see cref="IEnumerable{Assembly}"/> containing the plugin assemblies.</returns>
 | 
			
		||||
        IEnumerable<Assembly> GetApiPluginAssemblies();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Notifies the pending restart.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Common.Json.Converters
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Converts a nullable struct or value to/from JSON.
 | 
			
		||||
    /// Required - some clients send an empty string.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="T">The struct type.</typeparam>
 | 
			
		||||
    public class JsonNullableStructConverter<T> : JsonConverter<T?>
 | 
			
		||||
        where T : struct
 | 
			
		||||
    {
 | 
			
		||||
        private readonly JsonConverter<T?> _baseJsonConverter;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="JsonNullableStructConverter{T}"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="baseJsonConverter">The base json converter.</param>
 | 
			
		||||
        public JsonNullableStructConverter(JsonConverter<T?> baseJsonConverter)
 | 
			
		||||
        {
 | 
			
		||||
            _baseJsonConverter = baseJsonConverter;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
        {
 | 
			
		||||
            // Handle empty string.
 | 
			
		||||
            if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return _baseJsonConverter.Read(ref reader, typeToConvert, options);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
 | 
			
		||||
        {
 | 
			
		||||
            _baseJsonConverter.Write(writer, value, options);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -29,8 +29,14 @@ namespace MediaBrowser.Common.Json
 | 
			
		||||
                NumberHandling = JsonNumberHandling.AllowReadingFromString
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Get built-in converters for fallback converting.
 | 
			
		||||
            var baseNullableInt32Converter = (JsonConverter<int?>)options.GetConverter(typeof(int?));
 | 
			
		||||
            var baseNullableInt64Converter = (JsonConverter<long?>)options.GetConverter(typeof(long?));
 | 
			
		||||
 | 
			
		||||
            options.Converters.Add(new JsonGuidConverter());
 | 
			
		||||
            options.Converters.Add(new JsonStringEnumConverter());
 | 
			
		||||
            options.Converters.Add(new JsonNullableStructConverter<int>(baseNullableInt32Converter));
 | 
			
		||||
            options.Converters.Add(new JsonNullableStructConverter<long>(baseNullableInt64Converter));
 | 
			
		||||
 | 
			
		||||
            return options;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.7" />
 | 
			
		||||
    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
 | 
			
		||||
    <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
@ -32,6 +33,15 @@
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 | 
			
		||||
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
 | 
			
		||||
    <IncludeSymbols>true</IncludeSymbols>
 | 
			
		||||
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
 | 
			
		||||
    <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
 | 
			
		||||
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <!-- Code analyzers-->
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								MediaBrowser.Common/Net/DefaultHttpClientHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								MediaBrowser.Common/Net/DefaultHttpClientHandler.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Common.Net
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Default http client handler.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class DefaultHttpClientHandler : HttpClientHandler
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="DefaultHttpClientHandler"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DefaultHttpClientHandler()
 | 
			
		||||
        {
 | 
			
		||||
            // TODO change to DecompressionMethods.All with .NET5
 | 
			
		||||
            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								MediaBrowser.Common/Net/NamedClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								MediaBrowser.Common/Net/NamedClient.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
namespace MediaBrowser.Common.Net
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Registered http client names.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class NamedClient
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the value for the default named http client.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public const string Default = nameof(Default);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the value for the MusicBrainz named http client.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public const string MusicBrainz = nameof(MusicBrainz);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2633,6 +2633,7 @@ namespace MediaBrowser.Controller.Entities
 | 
			
		||||
        {
 | 
			
		||||
            return new T
 | 
			
		||||
            {
 | 
			
		||||
                Path = Path,
 | 
			
		||||
                MetadataCountryCode = GetPreferredMetadataCountryCode(),
 | 
			
		||||
                MetadataLanguage = GetPreferredMetadataLanguage(),
 | 
			
		||||
                Name = GetNameForMetadataLookup(),
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,12 @@ namespace MediaBrowser.Controller.Extensions
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class ConfigurationExtensions
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The key for a setting that specifies the default redirect path
 | 
			
		||||
        /// to use for requests where the URL base prefix is invalid or missing..
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public const string DefaultRedirectKey = "DefaultRedirectPath";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The key for a setting that indicates whether the application should host web client content.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,8 @@ namespace MediaBrowser.Controller
 | 
			
		||||
 | 
			
		||||
        IServiceProvider ServiceProvider { get; }
 | 
			
		||||
 | 
			
		||||
        bool CoreStartupHasCompleted { get; }
 | 
			
		||||
 | 
			
		||||
        bool CanLaunchWebBrowser { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -117,8 +119,7 @@ namespace MediaBrowser.Controller
 | 
			
		||||
        IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
 | 
			
		||||
 | 
			
		||||
        string ExpandVirtualPath(string path);
 | 
			
		||||
        string ReverseVirtualPath(string path);
 | 
			
		||||
 | 
			
		||||
        Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
 | 
			
		||||
        string ReverseVirtualPath(string path);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.7" />
 | 
			
		||||
    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
@ -32,6 +33,15 @@
 | 
			
		||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
			
		||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
			
		||||
    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
 | 
			
		||||
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 | 
			
		||||
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
 | 
			
		||||
    <IncludeSymbols>true</IncludeSymbols>
 | 
			
		||||
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
 | 
			
		||||
    <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
 | 
			
		||||
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <!-- Code Analyzers-->
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using MediaBrowser.Model.Dlna;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
{
 | 
			
		||||
@ -63,26 +62,20 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
        /// Gets or sets the id.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The id.</value>
 | 
			
		||||
        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
 | 
			
		||||
        public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
 | 
			
		||||
        public string MediaSourceId { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public string DeviceId { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public string Container { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the audio codec.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The audio codec.</value>
 | 
			
		||||
        [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public string AudioCodec { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public bool EnableAutoStreamCopy { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool AllowVideoStreamCopy { get; set; }
 | 
			
		||||
@ -95,7 +88,6 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
        /// Gets or sets the audio sample rate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The audio sample rate.</value>
 | 
			
		||||
        [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? AudioSampleRate { get; set; }
 | 
			
		||||
 | 
			
		||||
        public int? MaxAudioBitDepth { get; set; }
 | 
			
		||||
@ -104,105 +96,86 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
        /// Gets or sets the audio bit rate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The audio bit rate.</value>
 | 
			
		||||
        [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? AudioBitRate { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the audio channels.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The audio channels.</value>
 | 
			
		||||
        [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? AudioChannels { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? MaxAudioChannels { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public bool Static { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the profile.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The profile.</value>
 | 
			
		||||
        [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public string Profile { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the level.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The level.</value>
 | 
			
		||||
        [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public string Level { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the framerate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The framerate.</value>
 | 
			
		||||
        [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public float? Framerate { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public float? MaxFramerate { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public bool CopyTimestamps { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the start time ticks.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The start time ticks.</value>
 | 
			
		||||
        [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public long? StartTimeTicks { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the width.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The width.</value>
 | 
			
		||||
        [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? Width { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the height.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The height.</value>
 | 
			
		||||
        [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? Height { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the width of the max.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The width of the max.</value>
 | 
			
		||||
        [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? MaxWidth { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the height of the max.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The height of the max.</value>
 | 
			
		||||
        [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? MaxHeight { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the video bit rate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The video bit rate.</value>
 | 
			
		||||
        [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? VideoBitRate { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the index of the subtitle stream.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index of the subtitle stream.</value>
 | 
			
		||||
        [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? SubtitleStreamIndex { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public SubtitleDeliveryMethod SubtitleMethod { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? MaxRefFrames { get; set; }
 | 
			
		||||
 | 
			
		||||
        [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? MaxVideoBitDepth { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool RequireAvc { get; set; }
 | 
			
		||||
@ -223,7 +196,6 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
        /// Gets or sets the video codec.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The video codec.</value>
 | 
			
		||||
        [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public string VideoCodec { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string SubtitleCodec { get; set; }
 | 
			
		||||
@ -234,14 +206,12 @@ namespace MediaBrowser.Controller.MediaEncoding
 | 
			
		||||
        /// Gets or sets the index of the audio stream.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index of the audio stream.</value>
 | 
			
		||||
        [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? AudioStreamIndex { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the index of the video stream.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index of the video stream.</value>
 | 
			
		||||
        [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
 | 
			
		||||
        public int? VideoStreamIndex { get; set; }
 | 
			
		||||
 | 
			
		||||
        public EncodingContext Context { get; set; }
 | 
			
		||||
 | 
			
		||||
@ -1,76 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
{
 | 
			
		||||
    public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes
 | 
			
		||||
    {
 | 
			
		||||
        public static IAuthService AuthService { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the roles.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The roles.</value>
 | 
			
		||||
        public string Roles { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether [escape parental control].
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value><c>true</c> if [escape parental control]; otherwise, <c>false</c>.</value>
 | 
			
		||||
        public bool EscapeParentalControl { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether [allow before startup wizard].
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value><c>true</c> if [allow before startup wizard]; otherwise, <c>false</c>.</value>
 | 
			
		||||
        public bool AllowBeforeStartupWizard { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool AllowLocal { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The request filter is executed before the service.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="request">The http request wrapper.</param>
 | 
			
		||||
        /// <param name="response">The http response wrapper.</param>
 | 
			
		||||
        /// <param name="requestDto">The request DTO.</param>
 | 
			
		||||
        public void RequestFilter(IRequest request, HttpResponse response, object requestDto)
 | 
			
		||||
        {
 | 
			
		||||
            AuthService.Authenticate(request, this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Order in which Request Filters are executed.
 | 
			
		||||
        /// <0 Executed before global request filters
 | 
			
		||||
        /// >0 Executed after global request filters
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The priority.</value>
 | 
			
		||||
        public int Priority => 0;
 | 
			
		||||
 | 
			
		||||
        public string[] GetRoles()
 | 
			
		||||
        {
 | 
			
		||||
            return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool IgnoreLegacyAuth { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool AllowLocalOnly { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface IAuthenticationAttributes
 | 
			
		||||
    {
 | 
			
		||||
        bool EscapeParentalControl { get; }
 | 
			
		||||
 | 
			
		||||
        bool AllowBeforeStartupWizard { get; }
 | 
			
		||||
 | 
			
		||||
        bool AllowLocal { get; }
 | 
			
		||||
 | 
			
		||||
        bool AllowLocalOnly { get; }
 | 
			
		||||
 | 
			
		||||
        string[] GetRoles();
 | 
			
		||||
 | 
			
		||||
        bool IgnoreLegacyAuth { get; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,5 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
@ -11,21 +9,6 @@ namespace MediaBrowser.Controller.Net
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IAuthService
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Authenticate and authorize request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="request">Request.</param>
 | 
			
		||||
        /// <param name="authAttribtutes">Authorization attributes.</param>
 | 
			
		||||
        void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Authenticate and authorize request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="request">Request.</param>
 | 
			
		||||
        /// <param name="authAttribtutes">Authorization attributes.</param>
 | 
			
		||||
        /// <returns>Authenticated user.</returns>
 | 
			
		||||
        User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Authenticate request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
@ -13,14 +12,7 @@ namespace MediaBrowser.Controller.Net
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="requestContext">The request context.</param>
 | 
			
		||||
        /// <returns>AuthorizationInfo.</returns>
 | 
			
		||||
        AuthorizationInfo GetAuthorizationInfo(object requestContext);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the authorization information.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="requestContext">The request context.</param>
 | 
			
		||||
        /// <returns>AuthorizationInfo.</returns>
 | 
			
		||||
        AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
 | 
			
		||||
        AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the authorization information.
 | 
			
		||||
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Interface IHasResultFactory
 | 
			
		||||
    /// Services that require a ResultFactory should implement this
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IHasResultFactory : IRequiresRequest
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the result factory.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The result factory.</value>
 | 
			
		||||
        IHttpResultFactory ResultFactory { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,82 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Interface IHttpResultFactory.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IHttpResultFactory
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="content">The content.</param>
 | 
			
		||||
        /// <param name="contentType">Type of the content.</param>
 | 
			
		||||
        /// <param name="responseHeaders">The response headers.</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null);
 | 
			
		||||
 | 
			
		||||
        object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null);
 | 
			
		||||
 | 
			
		||||
        object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null);
 | 
			
		||||
 | 
			
		||||
        object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null);
 | 
			
		||||
 | 
			
		||||
        object GetRedirectResult(string url);
 | 
			
		||||
 | 
			
		||||
        object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
 | 
			
		||||
            where T : class;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the static result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="requestContext">The request context.</param>
 | 
			
		||||
        /// <param name="cacheKey">The cache key.</param>
 | 
			
		||||
        /// <param name="lastDateModified">The last date modified.</param>
 | 
			
		||||
        /// <param name="cacheDuration">Duration of the cache.</param>
 | 
			
		||||
        /// <param name="contentType">Type of the content.</param>
 | 
			
		||||
        /// <param name="factoryFn">The factory fn.</param>
 | 
			
		||||
        /// <param name="responseHeaders">The response headers.</param>
 | 
			
		||||
        /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        Task<object> GetStaticResult(IRequest requestContext,
 | 
			
		||||
            Guid cacheKey,
 | 
			
		||||
            DateTime? lastDateModified,
 | 
			
		||||
            TimeSpan? cacheDuration,
 | 
			
		||||
            string contentType, Func<Task<Stream>> factoryFn,
 | 
			
		||||
            IDictionary<string, string> responseHeaders = null,
 | 
			
		||||
            bool isHeadRequest = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the static result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="requestContext">The request context.</param>
 | 
			
		||||
        /// <param name="options">The options.</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the static file result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="requestContext">The request context.</param>
 | 
			
		||||
        /// <param name="path">The path.</param>
 | 
			
		||||
        /// <param name="fileShare">The file share.</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the static file result.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="requestContext">The request context.</param>
 | 
			
		||||
        /// <param name="options">The options.</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        Task<object> GetStaticFileResult(IRequest requestContext,
 | 
			
		||||
            StaticFileResultOptions options);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,50 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Data.Events;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Interface IHttpServer.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IHttpServer
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the URL prefix.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The URL prefix.</value>
 | 
			
		||||
        string[] UrlPrefixes { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Occurs when [web socket connected].
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inits this instance.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// If set, all requests will respond with this message.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string GlobalResponse { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The HTTP request handler.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="context"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        Task RequestHandler(HttpContext context);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Get the default CORS headers.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="req"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        IDictionary<string, string> GetDefaultCorsHeaders(IRequest req);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
using Jellyfin.Data.Entities;
 | 
			
		||||
using MediaBrowser.Controller.Session;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
{
 | 
			
		||||
@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.Net
 | 
			
		||||
 | 
			
		||||
        User GetUser(object requestContext);
 | 
			
		||||
 | 
			
		||||
        SessionInfo GetSession(IRequest requestContext);
 | 
			
		||||
        SessionInfo GetSession(HttpContext requestContext);
 | 
			
		||||
 | 
			
		||||
        User GetUser(IRequest requestContext);
 | 
			
		||||
        User GetUser(HttpContext requestContext);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								MediaBrowser.Controller/Net/IWebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								MediaBrowser.Controller/Net/IWebSocketManager.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Jellyfin.Data.Events;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Interface IHttpServer.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IWebSocketManager
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Occurs when [web socket connected].
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inits this instance.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="listeners">The websocket listeners.</param>
 | 
			
		||||
        void Init(IEnumerable<IWebSocketListener> listeners);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The HTTP request handler.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="context">The current HTTP context.</param>
 | 
			
		||||
        /// <returns>The task.</returns>
 | 
			
		||||
        Task WebSocketRequestHandler(HttpContext context);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Net
 | 
			
		||||
{
 | 
			
		||||
    public class StaticResultOptions
 | 
			
		||||
    {
 | 
			
		||||
        public string ContentType { get; set; }
 | 
			
		||||
 | 
			
		||||
        public TimeSpan? CacheDuration { get; set; }
 | 
			
		||||
 | 
			
		||||
        public DateTime? DateLastModified { get; set; }
 | 
			
		||||
 | 
			
		||||
        public Func<Task<Stream>> ContentFactory { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool IsHeadRequest { get; set; }
 | 
			
		||||
 | 
			
		||||
        public IDictionary<string, string> ResponseHeaders { get; set; }
 | 
			
		||||
 | 
			
		||||
        public Action OnComplete { get; set; }
 | 
			
		||||
 | 
			
		||||
        public Action OnError { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
        public long? ContentLength { get; set; }
 | 
			
		||||
 | 
			
		||||
        public FileShare FileShare { get; set; }
 | 
			
		||||
 | 
			
		||||
        public StaticResultOptions()
 | 
			
		||||
        {
 | 
			
		||||
            ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
            FileShare = FileShare.Read;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class StaticFileResultOptions : StaticResultOptions
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -14,6 +14,12 @@ namespace MediaBrowser.Controller.Providers
 | 
			
		||||
        /// <value>The name.</value>
 | 
			
		||||
        public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the path.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The path.</value>
 | 
			
		||||
        public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the metadata language.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										87
									
								
								MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
using System;
 | 
			
		||||
using MediaBrowser.Model.QuickConnect;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.QuickConnect
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Quick connect standard interface.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IQuickConnect
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the length of user facing codes.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int CodeLength { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the name of internal access tokens.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string TokenName { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the current state of quick connect.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        QuickConnectState State { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the time (in minutes) before quick connect will automatically deactivate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int Timeout { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Assert that quick connect is currently active and throws an exception if it is not.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        void AssertActive();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Temporarily activates quick connect for a short amount of time.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        void Activate();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Changes the state of quick connect.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="newState">New state to change to.</param>
 | 
			
		||||
        void SetState(QuickConnectState newState);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initiates a new quick connect request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
 | 
			
		||||
        QuickConnectResult TryConnect();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Checks the status of an individual request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="secret">Unique secret identifier of the request.</param>
 | 
			
		||||
        /// <returns>Quick connect result.</returns>
 | 
			
		||||
        QuickConnectResult CheckRequestStatus(string secret);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Authorizes a quick connect request to connect as the calling user.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="userId">User id.</param>
 | 
			
		||||
        /// <param name="code">Identifying code for the request.</param>
 | 
			
		||||
        /// <returns>A boolean indicating if the authorization completed successfully.</returns>
 | 
			
		||||
        bool AuthorizeRequest(Guid userId, string code);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="expireAll">If true, all requests will be expired.</param>
 | 
			
		||||
        void ExpireRequests(bool expireAll = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes all quick connect access tokens for the provided user.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="user">Guid of the user to delete tokens for.</param>
 | 
			
		||||
        /// <returns>A count of the deleted tokens.</returns>
 | 
			
		||||
        int DeleteAllDevices(Guid user);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Generates a short code to display to the user to uniquely identify this request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>A short, unique alphanumeric string.</returns>
 | 
			
		||||
        string GenerateCode();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -266,6 +266,14 @@ namespace MediaBrowser.Controller.Session
 | 
			
		||||
        /// <returns>Task{SessionInfo}.</returns>
 | 
			
		||||
        Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Authenticates a new session with quick connect.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="request">The request.</param>
 | 
			
		||||
        /// <param name="token">Quick connect access token.</param>
 | 
			
		||||
        /// <returns>Task{SessionInfo}.</returns>
 | 
			
		||||
        Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Creates the new session.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
 | 
			
		||||
@ -78,6 +78,11 @@ namespace MediaBrowser.Model.Configuration
 | 
			
		||||
        /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value>
 | 
			
		||||
        public bool IsPortAuthorized { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets if quick connect is available for use on this server.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool QuickConnectAvailable { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool AutoRunWebApp { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool EnableRemoteAccess { get; set; }
 | 
			
		||||
@ -299,6 +304,7 @@ namespace MediaBrowser.Model.Configuration
 | 
			
		||||
 | 
			
		||||
            AutoRunWebApp = true;
 | 
			
		||||
            EnableRemoteAccess = true;
 | 
			
		||||
            QuickConnectAvailable = false;
 | 
			
		||||
 | 
			
		||||
            EnableUPnP = false;
 | 
			
		||||
            MinResumePct = 5;
 | 
			
		||||
 | 
			
		||||
@ -20,9 +20,19 @@
 | 
			
		||||
    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
 | 
			
		||||
    <Nullable>enable</Nullable>
 | 
			
		||||
    <LangVersion>latest</LangVersion>
 | 
			
		||||
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 | 
			
		||||
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
 | 
			
		||||
    <IncludeSymbols>true</IncludeSymbols>
 | 
			
		||||
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
 | 
			
		||||
    <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
 | 
			
		||||
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.7" />
 | 
			
		||||
    <PackageReference Include="System.Globalization" Version="4.3.0" />
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										40
									
								
								MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.QuickConnect
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Stores the result of an incoming quick connect request.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class QuickConnectResult
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a value indicating whether this request is authorized.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool Authenticated => !string.IsNullOrEmpty(Authentication);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the secret value used to uniquely identify this request. Can be used to retrieve authentication information.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string? Secret { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the user facing code used so the user can quickly differentiate this request from others.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string? Code { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the private access token.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string? Authentication { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets an error message.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string? Error { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the DateTime that this request was created.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public DateTime? DateAdded { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								MediaBrowser.Model/QuickConnect/QuickConnectState.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								MediaBrowser.Model/QuickConnect/QuickConnectState.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
namespace MediaBrowser.Model.QuickConnect
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Quick connect state.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public enum QuickConnectState
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// This feature has not been opted into and is unavailable until the server administrator chooses to opt-in.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        Unavailable = 0,
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The feature is enabled for use on the server but is not currently accepting connection requests.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        Available = 1,
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The feature is actively accepting connection requests.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        Active = 2
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,65 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Identifies a single API endpoint.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
 | 
			
		||||
    public class ApiMemberAttribute : Attribute
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets verb to which applies attribute. By default applies to all verbs.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string Verb { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets parameter type: It can be only one of the following: path, query, body, form, or header.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string ParameterType { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets unique name for the parameter. Each name must be unique, even if they are associated with different paramType values.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// <para>
 | 
			
		||||
        /// Other notes on the name field:
 | 
			
		||||
        /// If paramType is body, the name is used only for UI and codegeneration.
 | 
			
		||||
        /// If paramType is path, the name field must correspond to the associated path segment from the path field in the api object.
 | 
			
		||||
        /// If paramType is query, the name field corresponds to the query param name.
 | 
			
		||||
        /// </para>
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the human-readable description for the parameter.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// For path, query, and header paramTypes, this field must be a primitive. For body, this can be a complex or container datatype.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string DataType { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// For path, this is always true. Otherwise, this field tells the client whether or not the field must be supplied.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool IsRequired { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// For query params, this specifies that a comma-separated list of values can be passed to the API. For path and body types, this field cannot be true.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool AllowMultiple { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets route to which applies attribute, matches using StartsWith. By default applies to all routes.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string Route { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Whether to exclude this property from being included in the ModelSchema.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool ExcludeInSchema { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,13 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IAsyncStreamWriter
 | 
			
		||||
    {
 | 
			
		||||
        Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IHasHeaders
 | 
			
		||||
    {
 | 
			
		||||
        IDictionary<string, string> Headers { get; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IHasRequestFilter
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the order in which Request Filters are executed.
 | 
			
		||||
        /// <0 Executed before global request filters.
 | 
			
		||||
        /// >0 Executed after global request filters.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int Priority { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The request filter is executed before the service.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="req">The http request wrapper.</param>
 | 
			
		||||
        /// <param name="res">The http response wrapper.</param>
 | 
			
		||||
        /// <param name="requestDto">The request DTO.</param>
 | 
			
		||||
        void RequestFilter(IRequest req, HttpResponse res, object requestDto);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IHttpRequest : IRequest
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the HTTP Verb.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string HttpMethod { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the value of the Accept HTTP Request Header.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string Accept { get; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System.Net;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IHttpResult : IHasHeaders
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The HTTP Response Status.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int Status { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The HTTP Response Status Code.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        HttpStatusCode StatusCode { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The HTTP Response ContentType.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string ContentType { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Response DTO.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        object Response { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Holds the request call context.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        IRequest RequestContext { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,93 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IRequest
 | 
			
		||||
    {
 | 
			
		||||
        HttpResponse Response { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The name of the service being called (e.g. Request DTO Name)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string OperationName { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The Verb / HttpMethod or Action for this request
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string Verb { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The request ContentType.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string ContentType { get; }
 | 
			
		||||
 | 
			
		||||
        bool IsLocal { get; }
 | 
			
		||||
 | 
			
		||||
        string UserAgent { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The expected Response ContentType for this request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string ResponseContentType { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Attach any data to this request that all filters and services can access.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        Dictionary<string, object> Items { get; }
 | 
			
		||||
 | 
			
		||||
        IHeaderDictionary Headers { get; }
 | 
			
		||||
 | 
			
		||||
        IQueryCollection QueryString { get; }
 | 
			
		||||
 | 
			
		||||
        string RawUrl { get; }
 | 
			
		||||
 | 
			
		||||
        string AbsoluteUri { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string RemoteIp { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The value of the Authorization Header used to send the Api Key, null if not available.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        string Authorization { get; }
 | 
			
		||||
 | 
			
		||||
        string[] AcceptTypes { get; }
 | 
			
		||||
 | 
			
		||||
        string PathInfo { get; }
 | 
			
		||||
 | 
			
		||||
        Stream InputStream { get; }
 | 
			
		||||
 | 
			
		||||
        long ContentLength { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The value of the Referrer, null if not available.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        Uri UrlReferrer { get; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface IHttpFile
 | 
			
		||||
    {
 | 
			
		||||
        string Name { get; }
 | 
			
		||||
 | 
			
		||||
        string FileName { get; }
 | 
			
		||||
 | 
			
		||||
        long ContentLength { get; }
 | 
			
		||||
 | 
			
		||||
        string ContentType { get; }
 | 
			
		||||
 | 
			
		||||
        Stream InputStream { get; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface IRequiresRequest
 | 
			
		||||
    {
 | 
			
		||||
        IRequest Request { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,14 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System.IO;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IRequiresRequestStream
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The raw Http Request Input Stream.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        Stream RequestStream { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    // marker interface
 | 
			
		||||
    public interface IService
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface IReturn { }
 | 
			
		||||
 | 
			
		||||
    public interface IReturn<T> : IReturn { }
 | 
			
		||||
 | 
			
		||||
    public interface IReturnVoid : IReturn { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System.IO;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IStreamWriter
 | 
			
		||||
    {
 | 
			
		||||
        void WriteTo(Stream responseStream);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,147 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using MediaBrowser.Model.Dto;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    // Remove this garbage class, it's just a bastard copy of NameValueCollection
 | 
			
		||||
    public class QueryParamCollection : List<NameValuePair>
 | 
			
		||||
    {
 | 
			
		||||
        public QueryParamCollection()
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static StringComparison GetStringComparison()
 | 
			
		||||
        {
 | 
			
		||||
            return StringComparison.OrdinalIgnoreCase;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Adds a new query parameter.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public void Add(string key, string value)
 | 
			
		||||
        {
 | 
			
		||||
            Add(new NameValuePair(key, value));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void Set(string key, string value)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(value))
 | 
			
		||||
            {
 | 
			
		||||
                var parameters = GetItems(key);
 | 
			
		||||
 | 
			
		||||
                foreach (var p in parameters)
 | 
			
		||||
                {
 | 
			
		||||
                    Remove(p);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var pair in this)
 | 
			
		||||
            {
 | 
			
		||||
                var stringComparison = GetStringComparison();
 | 
			
		||||
 | 
			
		||||
                if (string.Equals(key, pair.Name, stringComparison))
 | 
			
		||||
                {
 | 
			
		||||
                    pair.Value = value;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Add(key, value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string Get(string name)
 | 
			
		||||
        {
 | 
			
		||||
            var stringComparison = GetStringComparison();
 | 
			
		||||
 | 
			
		||||
            foreach (var pair in this)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.Equals(pair.Name, name, stringComparison))
 | 
			
		||||
                {
 | 
			
		||||
                    return pair.Value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<NameValuePair> GetItems(string name)
 | 
			
		||||
        {
 | 
			
		||||
            var stringComparison = GetStringComparison();
 | 
			
		||||
 | 
			
		||||
            var list = new List<NameValuePair>();
 | 
			
		||||
 | 
			
		||||
            foreach (var pair in this)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.Equals(pair.Name, name, stringComparison))
 | 
			
		||||
                {
 | 
			
		||||
                    list.Add(pair);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return list;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public virtual List<string> GetValues(string name)
 | 
			
		||||
        {
 | 
			
		||||
            var stringComparison = GetStringComparison();
 | 
			
		||||
 | 
			
		||||
            var list = new List<string>();
 | 
			
		||||
 | 
			
		||||
            foreach (var pair in this)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.Equals(pair.Name, name, stringComparison))
 | 
			
		||||
                {
 | 
			
		||||
                    list.Add(pair.Value);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return list;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<string> Keys
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                var keys = new string[this.Count];
 | 
			
		||||
 | 
			
		||||
                for (var i = 0; i < keys.Length; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    keys[i] = this[i].Name;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return keys;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a query parameter value by name. A query may contain multiple values of the same name
 | 
			
		||||
        /// (i.e. "x=1&x=2"), in which case the value is an array, which works for both getting and setting.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="name">The query parameter name.</param>
 | 
			
		||||
        /// <returns>The query parameter value or array of values.</returns>
 | 
			
		||||
        public string this[string name]
 | 
			
		||||
        {
 | 
			
		||||
            get => Get(name);
 | 
			
		||||
            set => Set(name, value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetQueryStringValue(NameValuePair pair)
 | 
			
		||||
        {
 | 
			
		||||
            return pair.Name + "=" + pair.Value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override string ToString()
 | 
			
		||||
        {
 | 
			
		||||
            var vals = this.Select(GetQueryStringValue).ToArray();
 | 
			
		||||
 | 
			
		||||
            return string.Join("&", vals);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,163 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Services
 | 
			
		||||
{
 | 
			
		||||
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
 | 
			
		||||
    public class RouteAttribute : Attribute
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        ///     <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para>
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="path">
 | 
			
		||||
        ///     <para>The path template to map to the request.  See
 | 
			
		||||
        ///        <see cref="Path">RouteAttribute.Path</see>
 | 
			
		||||
        ///        for details on the correct format.</para>
 | 
			
		||||
        /// </param>
 | 
			
		||||
        public RouteAttribute(string path)
 | 
			
		||||
            : this(path, null)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        ///     <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para>
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="path">
 | 
			
		||||
        ///     <para>The path template to map to the request.  See
 | 
			
		||||
        ///         <see cref="Path">RouteAttribute.Path</see>
 | 
			
		||||
        ///         for details on the correct format.</para>
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="verbs">A comma-delimited list of HTTP verbs supported by the
 | 
			
		||||
        ///     service.  If unspecified, all verbs are assumed to be supported.</param>
 | 
			
		||||
        public RouteAttribute(string path, string verbs)
 | 
			
		||||
        {
 | 
			
		||||
            Path = path;
 | 
			
		||||
            Verbs = verbs;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        ///     Gets or sets the path template to be mapped to the request.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///     A <see cref="String"/> value providing the path mapped to
 | 
			
		||||
        ///     the request.  Never <see langword="null"/>.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        ///     <para>Some examples of valid paths are:</para>
 | 
			
		||||
        ///
 | 
			
		||||
        ///     <list>
 | 
			
		||||
        ///         <item>"/Inventory"</item>
 | 
			
		||||
        ///         <item>"/Inventory/{Category}/{ItemId}"</item>
 | 
			
		||||
        ///         <item>"/Inventory/{ItemPath*}"</item>
 | 
			
		||||
        ///     </list>
 | 
			
		||||
        ///
 | 
			
		||||
        ///     <para>Variables are specified within "{}"
 | 
			
		||||
        ///     brackets.  Each variable in the path is mapped to the same-named property
 | 
			
		||||
        ///     on the request DTO.  At runtime, ServiceStack will parse the
 | 
			
		||||
        ///     request URL, extract the variable values, instantiate the request DTO,
 | 
			
		||||
        ///     and assign the variable values into the corresponding request properties,
 | 
			
		||||
        ///     prior to passing the request DTO to the service object for processing.</para>
 | 
			
		||||
        ///
 | 
			
		||||
        ///     <para>It is not necessary to specify all request properties as
 | 
			
		||||
        ///     variables in the path.  For unspecified properties, callers may provide
 | 
			
		||||
        ///     values in the query string.  For example: the URL
 | 
			
		||||
        ///     "http://services/Inventory?Category=Books&ItemId=12345" causes the same
 | 
			
		||||
        ///     request DTO to be processed as "http://services/Inventory/Books/12345",
 | 
			
		||||
        ///     provided that the paths "/Inventory" (which supports the first URL) and
 | 
			
		||||
        ///     "/Inventory/{Category}/{ItemId}" (which supports the second URL)
 | 
			
		||||
        ///     are both mapped to the request DTO.</para>
 | 
			
		||||
        ///
 | 
			
		||||
        ///     <para>Please note that while it is possible to specify property values
 | 
			
		||||
        ///     in the query string, it is generally considered to be less RESTful and
 | 
			
		||||
        ///     less desirable than to specify them as variables in the path.  Using the
 | 
			
		||||
        ///     query string to specify property values may also interfere with HTTP
 | 
			
		||||
        ///     caching.</para>
 | 
			
		||||
        ///
 | 
			
		||||
        ///     <para>The final variable in the path may contain a "*" suffix
 | 
			
		||||
        ///     to grab all remaining segments in the path portion of the request URL and assign
 | 
			
		||||
        ///     them to a single property on the request DTO.
 | 
			
		||||
        ///     For example, if the path "/Inventory/{ItemPath*}" is mapped to the request DTO,
 | 
			
		||||
        ///     then the request URL "http://services/Inventory/Books/12345" will result
 | 
			
		||||
        ///     in a request DTO whose ItemPath property contains "Books/12345".
 | 
			
		||||
        ///     You may only specify one such variable in the path, and it must be positioned at
 | 
			
		||||
        ///     the end of the path.</para>
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        ///    Gets or sets short summary of what the route does.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string Summary { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool IsHidden { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        ///    Gets or sets longer text to explain the behaviour of the route.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string Notes { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        ///     Gets or sets a comma-delimited list of HTTP verbs supported by the service, such as
 | 
			
		||||
        ///     "GET,PUT,POST,DELETE".
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///     A <see cref="String"/> providing a comma-delimited list of HTTP verbs supported
 | 
			
		||||
        ///     by the service, <see langword="null"/> or empty if all verbs are supported.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Verbs { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Used to rank the precedences of route definitions in reverse routing.
 | 
			
		||||
        /// i.e. Priorities below 0 are auto-generated have less precedence.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int Priority { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected bool Equals(RouteAttribute other)
 | 
			
		||||
        {
 | 
			
		||||
            return base.Equals(other)
 | 
			
		||||
                && string.Equals(Path, other.Path)
 | 
			
		||||
                && string.Equals(Summary, other.Summary)
 | 
			
		||||
                && string.Equals(Notes, other.Notes)
 | 
			
		||||
                && string.Equals(Verbs, other.Verbs)
 | 
			
		||||
                && Priority == other.Priority;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override bool Equals(object obj)
 | 
			
		||||
        {
 | 
			
		||||
            if (ReferenceEquals(null, obj))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ReferenceEquals(this, obj))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (obj.GetType() != this.GetType())
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Equals((RouteAttribute)obj);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override int GetHashCode()
 | 
			
		||||
        {
 | 
			
		||||
            unchecked
 | 
			
		||||
            {
 | 
			
		||||
                var hashCode = base.GetHashCode();
 | 
			
		||||
                hashCode = (hashCode * 397) ^ (Path != null ? Path.GetHashCode() : 0);
 | 
			
		||||
                hashCode = (hashCode * 397) ^ (Summary != null ? Summary.GetHashCode() : 0);
 | 
			
		||||
                hashCode = (hashCode * 397) ^ (Notes != null ? Notes.GetHashCode() : 0);
 | 
			
		||||
                hashCode = (hashCode * 397) ^ (Verbs != null ? Verbs.GetHashCode() : 0);
 | 
			
		||||
                hashCode = (hashCode * 397) ^ Priority;
 | 
			
		||||
                return hashCode;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,7 +2,6 @@
 | 
			
		||||
#pragma warning disable CS1591
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using MediaBrowser.Model.Services;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Model.Session
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
using Emby.Server.Implementations.HttpServer;
 | 
			
		||||
using Xunit;
 | 
			
		||||
 | 
			
		||||
namespace Jellyfin.Server.Implementations.Tests.HttpServer
 | 
			
		||||
{
 | 
			
		||||
    public class ResponseFilterTests
 | 
			
		||||
    {
 | 
			
		||||
        [Theory]
 | 
			
		||||
        [InlineData(null, null)]
 | 
			
		||||
        [InlineData("", "")]
 | 
			
		||||
        [InlineData("This is a clean string.", "This is a clean string.")]
 | 
			
		||||
        [InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")]
 | 
			
		||||
        public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result)
 | 
			
		||||
        {
 | 
			
		||||
            Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user