mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-03 12:52:56 +01:00
Merge branch 'master' into async
This commit is contained in:
@@ -18,15 +18,17 @@ namespace Jellyfin.Server
|
||||
|
||||
public override bool CanSelfRestart => StartupOptions.RestartPath != null;
|
||||
|
||||
protected override bool SupportsDualModeSockets => true;
|
||||
|
||||
protected override void RestartInternal() => Program.Restart();
|
||||
|
||||
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
|
||||
=> new[] { typeof(CoreAppHost).Assembly };
|
||||
{
|
||||
yield return typeof(CoreAppHost).Assembly;
|
||||
}
|
||||
|
||||
protected override void ShutdownInternal() => Program.Shutdown();
|
||||
|
||||
protected override bool SupportsDualModeSockets => true;
|
||||
|
||||
protected override IHttpListener CreateHttpListener()
|
||||
=> new WebSocketSharpListener(
|
||||
Logger,
|
||||
@@ -37,7 +39,6 @@ namespace Jellyfin.Server
|
||||
CryptographyProvider,
|
||||
SupportsDualModeSockets,
|
||||
FileSystemManager,
|
||||
EnvironmentInfo
|
||||
);
|
||||
EnvironmentInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- We need C# 7.1 for async main-->
|
||||
<LangVersion>latest</LangVersion>
|
||||
<!-- Disable documentation warnings (for now) -->
|
||||
<NoWarn>SA1600;CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -20,6 +23,10 @@
|
||||
<EmbeddedResource Include="Resources/Configuration/*" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code analysers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" />
|
||||
@@ -41,9 +48,8 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="1.68.0" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.12" />
|
||||
<PackageReference Include="SQLitePCLRaw.core" Version="1.1.12" />
|
||||
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.12" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.13" />
|
||||
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.13" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.AspNetCore;
|
||||
@@ -56,13 +57,28 @@ namespace Jellyfin.Server
|
||||
errs => Task.FromResult(0)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
if (!_tokenSource.IsCancellationRequested)
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Restart()
|
||||
{
|
||||
_restartOnShutdown = true;
|
||||
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
private static async Task StartApp(StartupOptions options)
|
||||
{
|
||||
ServerApplicationPaths appPaths = CreateApplicationPaths(options);
|
||||
|
||||
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
|
||||
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
|
||||
await CreateLogger(appPaths);
|
||||
await CreateLogger(appPaths).ConfigureAwait(false);
|
||||
_logger = _loggerFactory.CreateLogger("Main");
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += (sender, e)
|
||||
@@ -75,6 +91,7 @@ namespace Jellyfin.Server
|
||||
{
|
||||
return; // Already shutting down
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
_logger.LogInformation("Ctrl+C, shutting down");
|
||||
Environment.ExitCode = 128 + 2;
|
||||
@@ -88,6 +105,7 @@ namespace Jellyfin.Server
|
||||
{
|
||||
return; // Already shutting down
|
||||
}
|
||||
|
||||
_logger.LogInformation("Received a SIGTERM signal, shutting down");
|
||||
Environment.ExitCode = 128 + 15;
|
||||
Shutdown();
|
||||
@@ -101,7 +119,7 @@ namespace Jellyfin.Server
|
||||
SQLitePCL.Batteries_V2.Init();
|
||||
|
||||
// Allow all https requests
|
||||
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
|
||||
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; } );
|
||||
|
||||
var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, null, appPaths.TempDirectory, true);
|
||||
|
||||
@@ -114,18 +132,18 @@ namespace Jellyfin.Server
|
||||
new NullImageEncoder(),
|
||||
new NetworkManager(_loggerFactory, environmentInfo)))
|
||||
{
|
||||
await appHost.Init();
|
||||
await appHost.Init(new ServiceCollection()).ConfigureAwait(false);
|
||||
|
||||
appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager);
|
||||
|
||||
await appHost.RunStartupTasks();
|
||||
await appHost.RunStartupTasks().ConfigureAwait(false);
|
||||
|
||||
// TODO: read input for a stop command
|
||||
|
||||
try
|
||||
{
|
||||
// Block main thread until shutdown
|
||||
await Task.Delay(-1, _tokenSource.Token);
|
||||
await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
@@ -139,112 +157,156 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the data, config and log paths from the variety of inputs(command line args,
|
||||
/// environment variables) or decide on what default to use. For Windows it's %AppPath%
|
||||
/// for everything else the XDG approach is followed:
|
||||
/// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
/// </summary>
|
||||
/// <param name="options">StartupOptions</param>
|
||||
/// <returns>ServerApplicationPaths</returns>
|
||||
private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
|
||||
{
|
||||
string programDataPath = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH");
|
||||
if (string.IsNullOrEmpty(programDataPath))
|
||||
// dataDir
|
||||
// IF --datadir
|
||||
// ELSE IF $JELLYFIN_DATA_PATH
|
||||
// ELSE IF windows, use <%APPDATA%>/jellyfin
|
||||
// ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
|
||||
// ELSE use $HOME/.local/share/jellyfin
|
||||
var dataDir = options.DataDir;
|
||||
|
||||
if (string.IsNullOrEmpty(dataDir))
|
||||
{
|
||||
if (options.DataDir != null)
|
||||
{
|
||||
programDataPath = options.DataDir;
|
||||
}
|
||||
else
|
||||
dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH");
|
||||
|
||||
if (string.IsNullOrEmpty(dataDir))
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
dataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored.
|
||||
programDataPath = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||
// If $XDG_DATA_HOME is either not set or empty, $HOME/.local/share should be used.
|
||||
if (string.IsNullOrEmpty(programDataPath))
|
||||
dataDir = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
|
||||
|
||||
// If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used.
|
||||
if (string.IsNullOrEmpty(dataDir))
|
||||
{
|
||||
programDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share");
|
||||
dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share");
|
||||
}
|
||||
}
|
||||
|
||||
programDataPath = Path.Combine(programDataPath, "jellyfin");
|
||||
dataDir = Path.Combine(dataDir, "jellyfin");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(programDataPath))
|
||||
{
|
||||
Console.WriteLine("Cannot continue without path to program data folder (try -programdata)");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(programDataPath);
|
||||
}
|
||||
// configDir
|
||||
// IF --configdir
|
||||
// ELSE IF $JELLYFIN_CONFIG_DIR
|
||||
// ELSE IF --datadir, use <datadir>/config (assume portable run)
|
||||
// ELSE IF <datadir>/config exists, use that
|
||||
// ELSE IF windows, use <datadir>/config
|
||||
// ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin
|
||||
// ELSE $HOME/.config/jellyfin
|
||||
var configDir = options.ConfigDir;
|
||||
|
||||
string configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
|
||||
if (string.IsNullOrEmpty(configDir))
|
||||
{
|
||||
if (options.ConfigDir != null)
|
||||
configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
|
||||
|
||||
if (string.IsNullOrEmpty(configDir))
|
||||
{
|
||||
configDir = options.ConfigDir;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Let BaseApplicationPaths set up the default value
|
||||
configDir = null;
|
||||
if (options.DataDir != null || Directory.Exists(Path.Combine(dataDir, "config")) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Hang config folder off already set dataDir
|
||||
configDir = Path.Combine(dataDir, "config");
|
||||
}
|
||||
else
|
||||
{
|
||||
// $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored.
|
||||
configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
|
||||
|
||||
// If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME /.config should be used.
|
||||
if (string.IsNullOrEmpty(configDir))
|
||||
{
|
||||
configDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
|
||||
}
|
||||
|
||||
configDir = Path.Combine(configDir, "jellyfin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configDir != null)
|
||||
{
|
||||
Directory.CreateDirectory(configDir);
|
||||
}
|
||||
// cacheDir
|
||||
// IF --cachedir
|
||||
// ELSE IF $JELLYFIN_CACHE_DIR
|
||||
// ELSE IF windows, use <datadir>/cache
|
||||
// ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin
|
||||
// ELSE HOME/.cache/jellyfin
|
||||
var cacheDir = options.CacheDir;
|
||||
|
||||
string cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
|
||||
if (string.IsNullOrEmpty(cacheDir))
|
||||
{
|
||||
if (options.CacheDir != null)
|
||||
cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
|
||||
|
||||
if (string.IsNullOrEmpty(cacheDir))
|
||||
{
|
||||
cacheDir = options.CacheDir;
|
||||
}
|
||||
else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// $XDG_CACHE_HOME defines the base directory relative to which user specific non-essential data files should be stored.
|
||||
cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
|
||||
// If $XDG_CACHE_HOME is either not set or empty, $HOME/.cache should be used.
|
||||
if (string.IsNullOrEmpty(cacheDir))
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache");
|
||||
// Hang cache folder off already set dataDir
|
||||
cacheDir = Path.Combine(dataDir, "cache");
|
||||
}
|
||||
else
|
||||
{
|
||||
// $XDG_CACHE_HOME defines the base directory relative to which user specific non-essential data files should be stored.
|
||||
cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
|
||||
|
||||
// If $XDG_CACHE_HOME is either not set or empty, a default equal to $HOME/.cache should be used.
|
||||
if (string.IsNullOrEmpty(cacheDir))
|
||||
{
|
||||
cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache");
|
||||
}
|
||||
|
||||
cacheDir = Path.Combine(cacheDir, "jellyfin");
|
||||
}
|
||||
cacheDir = Path.Combine(cacheDir, "jellyfin");
|
||||
}
|
||||
}
|
||||
|
||||
if (cacheDir != null)
|
||||
{
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
}
|
||||
// logDir
|
||||
// IF --logdir
|
||||
// ELSE IF $JELLYFIN_LOG_DIR
|
||||
// ELSE IF --datadir, use <datadir>/log (assume portable run)
|
||||
// ELSE <datadir>/log
|
||||
var logDir = options.LogDir;
|
||||
|
||||
string logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
|
||||
if (string.IsNullOrEmpty(logDir))
|
||||
{
|
||||
if (options.LogDir != null)
|
||||
logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
|
||||
|
||||
if (string.IsNullOrEmpty(logDir))
|
||||
{
|
||||
logDir = options.LogDir;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Let BaseApplicationPaths set up the default value
|
||||
logDir = null;
|
||||
// Hang log folder off already set dataDir
|
||||
logDir = Path.Combine(dataDir, "log");
|
||||
}
|
||||
}
|
||||
|
||||
if (logDir != null)
|
||||
// Ensure the main folders exist before we continue
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dataDir);
|
||||
Directory.CreateDirectory(logDir);
|
||||
Directory.CreateDirectory(configDir);
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Console.Error.WriteLine("Error whilst attempting to create folder");
|
||||
Console.Error.WriteLine(ex.ToString());
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
string appPath = AppContext.BaseDirectory;
|
||||
|
||||
return new ServerApplicationPaths(programDataPath, appPath, appPath, logDir, configDir, cacheDir);
|
||||
return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir);
|
||||
}
|
||||
|
||||
private static async Task CreateLogger(IApplicationPaths appPaths)
|
||||
@@ -263,6 +325,7 @@ namespace Jellyfin.Server
|
||||
await rscstr.CopyToAsync(fstr).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(appPaths.ConfigurationDirectoryPath)
|
||||
.AddJsonFile("logging.json")
|
||||
@@ -290,7 +353,7 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
public static IImageEncoder GetImageEncoder(
|
||||
private static IImageEncoder GetImageEncoder(
|
||||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
ILocalizationManager localizationManager)
|
||||
@@ -331,26 +394,12 @@ namespace Jellyfin.Server
|
||||
{
|
||||
return MediaBrowser.Model.System.OperatingSystem.BSD;
|
||||
}
|
||||
|
||||
throw new Exception($"Can't resolve OS with description: '{osDescription}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
if (!_tokenSource.IsCancellationRequested)
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Restart()
|
||||
{
|
||||
_restartOnShutdown = true;
|
||||
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
private static void StartNewInstance(StartupOptions options)
|
||||
{
|
||||
_logger.LogInformation("Starting new instance");
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
{
|
||||
internal static string GetParameter(string header, string attr)
|
||||
{
|
||||
int ap = header.IndexOf(attr);
|
||||
int ap = header.IndexOf(attr, StringComparison.Ordinal);
|
||||
if (ap == -1)
|
||||
{
|
||||
return null;
|
||||
@@ -82,9 +82,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// We use a substream, as in 2.x we will support large uploads streamed to disk,
|
||||
//
|
||||
var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
|
||||
files[e.Name] = sub;
|
||||
}
|
||||
@@ -127,8 +125,12 @@ namespace Jellyfin.Server.SocketSharp
|
||||
|
||||
public string Authorization => string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"];
|
||||
|
||||
protected bool validate_cookies, validate_query_string, validate_form;
|
||||
protected bool checked_cookies, checked_query_string, checked_form;
|
||||
protected bool validate_cookies { get; set; }
|
||||
protected bool validate_query_string { get; set; }
|
||||
protected bool validate_form { get; set; }
|
||||
protected bool checked_cookies { get; set; }
|
||||
protected bool checked_query_string { get; set; }
|
||||
protected bool checked_form { get; set; }
|
||||
|
||||
private static void ThrowValidationException(string name, string key, string value)
|
||||
{
|
||||
@@ -138,8 +140,12 @@ namespace Jellyfin.Server.SocketSharp
|
||||
v = v.Substring(0, 16) + "...\"";
|
||||
}
|
||||
|
||||
string msg = string.Format("A potentially dangerous Request.{0} value was " +
|
||||
"detected from the client ({1}={2}).", name, key, v);
|
||||
string msg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"A potentially dangerous Request.{0} value was detected from the client ({1}={2}).",
|
||||
name,
|
||||
key,
|
||||
v);
|
||||
|
||||
throw new Exception(msg);
|
||||
}
|
||||
@@ -179,6 +185,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
for (int idx = 1; idx < len; idx++)
|
||||
{
|
||||
char next = val[idx];
|
||||
|
||||
// See http://secunia.com/advisories/14325
|
||||
if (current == '<' || current == '\xff1c')
|
||||
{
|
||||
@@ -256,6 +263,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
value.Append((char)c);
|
||||
}
|
||||
}
|
||||
|
||||
if (c == -1)
|
||||
{
|
||||
AddRawKeyValue(form, key, value);
|
||||
@@ -271,6 +279,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
key.Append((char)c);
|
||||
}
|
||||
}
|
||||
|
||||
if (c == -1)
|
||||
{
|
||||
AddRawKeyValue(form, key, value);
|
||||
@@ -308,6 +317,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
result.Append(key);
|
||||
result.Append('=');
|
||||
}
|
||||
|
||||
result.Append(pair.Value);
|
||||
}
|
||||
|
||||
@@ -429,13 +439,13 @@ namespace Jellyfin.Server.SocketSharp
|
||||
real = position + d;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(nameof(origin));
|
||||
throw new ArgumentException("Unknown SeekOrigin value", nameof(origin));
|
||||
}
|
||||
|
||||
long virt = real - offset;
|
||||
if (virt < 0 || virt > Length)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
throw new ArgumentException("Invalid position", nameof(d));
|
||||
}
|
||||
|
||||
position = s.Seek(real, SeekOrigin.Begin);
|
||||
@@ -491,11 +501,6 @@ namespace Jellyfin.Server.SocketSharp
|
||||
public Stream InputStream => stream;
|
||||
}
|
||||
|
||||
private class Helpers
|
||||
{
|
||||
public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture;
|
||||
}
|
||||
|
||||
internal static class StrUtils
|
||||
{
|
||||
public static bool StartsWith(string str1, string str2, bool ignore_case)
|
||||
@@ -533,12 +538,17 @@ namespace Jellyfin.Server.SocketSharp
|
||||
|
||||
public class Element
|
||||
{
|
||||
public string ContentType;
|
||||
public string Name;
|
||||
public string Filename;
|
||||
public Encoding Encoding;
|
||||
public long Start;
|
||||
public long Length;
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Filename { get; set; }
|
||||
|
||||
public Encoding Encoding { get; set; }
|
||||
|
||||
public long Start { get; set; }
|
||||
|
||||
public long Length { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
@@ -547,15 +557,23 @@ namespace Jellyfin.Server.SocketSharp
|
||||
}
|
||||
}
|
||||
|
||||
private Stream data;
|
||||
private string boundary;
|
||||
private byte[] boundary_bytes;
|
||||
private byte[] buffer;
|
||||
private bool at_eof;
|
||||
private Encoding encoding;
|
||||
private StringBuilder sb;
|
||||
private const byte LF = (byte)'\n';
|
||||
|
||||
private const byte LF = (byte)'\n', CR = (byte)'\r';
|
||||
private const byte CR = (byte)'\r';
|
||||
|
||||
private Stream data;
|
||||
|
||||
private string boundary;
|
||||
|
||||
private byte[] boundaryBytes;
|
||||
|
||||
private byte[] buffer;
|
||||
|
||||
private bool atEof;
|
||||
|
||||
private Encoding encoding;
|
||||
|
||||
private StringBuilder sb;
|
||||
|
||||
// See RFC 2046
|
||||
// In the case of multipart entities, in which one or more different
|
||||
@@ -570,18 +588,48 @@ namespace Jellyfin.Server.SocketSharp
|
||||
public HttpMultipart(Stream data, string b, Encoding encoding)
|
||||
{
|
||||
this.data = data;
|
||||
//DB: 30/01/11: cannot set or read the Position in HttpListener in Win.NET
|
||||
//var ms = new MemoryStream(32 * 1024);
|
||||
//data.CopyTo(ms);
|
||||
//this.data = ms;
|
||||
|
||||
boundary = b;
|
||||
boundary_bytes = encoding.GetBytes(b);
|
||||
buffer = new byte[boundary_bytes.Length + 2]; // CRLF or '--'
|
||||
boundaryBytes = encoding.GetBytes(b);
|
||||
buffer = new byte[boundaryBytes.Length + 2]; // CRLF or '--'
|
||||
this.encoding = encoding;
|
||||
sb = new StringBuilder();
|
||||
}
|
||||
|
||||
public Element ReadNextElement()
|
||||
{
|
||||
if (atEof || ReadBoundary())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var elem = new Element();
|
||||
string header;
|
||||
while ((header = ReadHeaders()) != null)
|
||||
{
|
||||
if (StrUtils.StartsWith(header, "Content-Disposition:", true))
|
||||
{
|
||||
elem.Name = GetContentDispositionAttribute(header, "name");
|
||||
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
|
||||
}
|
||||
else if (StrUtils.StartsWith(header, "Content-Type:", true))
|
||||
{
|
||||
elem.ContentType = header.Substring("Content-Type:".Length).Trim();
|
||||
elem.Encoding = GetEncoding(elem.ContentType);
|
||||
}
|
||||
}
|
||||
|
||||
long start = data.Position;
|
||||
elem.Start = start;
|
||||
long pos = MoveToNextBoundary();
|
||||
if (pos == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
elem.Length = pos - start;
|
||||
return elem;
|
||||
}
|
||||
|
||||
private string ReadLine()
|
||||
{
|
||||
// CRLF or LF are ok as line endings.
|
||||
@@ -600,6 +648,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
got_cr = b == CR;
|
||||
sb.Append((char)b);
|
||||
}
|
||||
@@ -769,7 +818,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!CompareBytes(boundary_bytes, buffer))
|
||||
if (!CompareBytes(boundaryBytes, buffer))
|
||||
{
|
||||
state = 0;
|
||||
data.Position = retval + 2;
|
||||
@@ -785,7 +834,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
|
||||
if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-')
|
||||
{
|
||||
at_eof = true;
|
||||
atEof = true;
|
||||
}
|
||||
else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF)
|
||||
{
|
||||
@@ -800,6 +849,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
c = data.ReadByte();
|
||||
continue;
|
||||
}
|
||||
|
||||
data.Position = retval + 2;
|
||||
if (got_cr)
|
||||
{
|
||||
@@ -818,42 +868,6 @@ namespace Jellyfin.Server.SocketSharp
|
||||
return retval;
|
||||
}
|
||||
|
||||
public Element ReadNextElement()
|
||||
{
|
||||
if (at_eof || ReadBoundary())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var elem = new Element();
|
||||
string header;
|
||||
while ((header = ReadHeaders()) != null)
|
||||
{
|
||||
if (StrUtils.StartsWith(header, "Content-Disposition:", true))
|
||||
{
|
||||
elem.Name = GetContentDispositionAttribute(header, "name");
|
||||
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
|
||||
}
|
||||
else if (StrUtils.StartsWith(header, "Content-Type:", true))
|
||||
{
|
||||
elem.ContentType = header.Substring("Content-Type:".Length).Trim();
|
||||
elem.Encoding = GetEncoding(elem.ContentType);
|
||||
}
|
||||
}
|
||||
|
||||
long start = 0;
|
||||
start = data.Position;
|
||||
elem.Start = start;
|
||||
long pos = MoveToNextBoundary();
|
||||
if (pos == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
elem.Length = pos - start;
|
||||
return elem;
|
||||
}
|
||||
|
||||
private static string StripPath(string path)
|
||||
{
|
||||
if (path == null || path.Length == 0)
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
|
||||
private TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed = false;
|
||||
|
||||
public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger)
|
||||
{
|
||||
@@ -40,9 +41,9 @@ namespace Jellyfin.Server.SocketSharp
|
||||
_logger = logger;
|
||||
WebSocket = socket;
|
||||
|
||||
socket.OnMessage += socket_OnMessage;
|
||||
socket.OnClose += socket_OnClose;
|
||||
socket.OnError += socket_OnError;
|
||||
socket.OnMessage += OnSocketMessage;
|
||||
socket.OnClose += OnSocketClose;
|
||||
socket.OnError += OnSocketError;
|
||||
}
|
||||
|
||||
public Task ConnectAsServerAsync()
|
||||
@@ -53,29 +54,22 @@ namespace Jellyfin.Server.SocketSharp
|
||||
return _taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
void socket_OnError(object sender, SocketHttpListener.ErrorEventArgs e)
|
||||
private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e)
|
||||
{
|
||||
_logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty);
|
||||
//Closed?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
// Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
void socket_OnClose(object sender, SocketHttpListener.CloseEventArgs e)
|
||||
private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e)
|
||||
{
|
||||
_taskCompletionSource.TrySetResult(true);
|
||||
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
void socket_OnMessage(object sender, SocketHttpListener.MessageEventArgs e)
|
||||
private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e)
|
||||
{
|
||||
//if (!string.IsNullOrEmpty(e.Data))
|
||||
//{
|
||||
// if (OnReceive != null)
|
||||
// {
|
||||
// OnReceive(e.Data);
|
||||
// }
|
||||
// return;
|
||||
//}
|
||||
if (OnReceiveBytes != null)
|
||||
{
|
||||
OnReceiveBytes(e.RawData);
|
||||
@@ -118,6 +112,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,16 +121,23 @@ namespace Jellyfin.Server.SocketSharp
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
WebSocket.OnMessage -= socket_OnMessage;
|
||||
WebSocket.OnClose -= socket_OnClose;
|
||||
WebSocket.OnError -= socket_OnError;
|
||||
WebSocket.OnMessage -= OnSocketMessage;
|
||||
WebSocket.OnClose -= OnSocketClose;
|
||||
WebSocket.OnError -= OnSocketError;
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
|
||||
WebSocket.CloseAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -143,11 +145,5 @@ namespace Jellyfin.Server.SocketSharp
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
public Action<byte[]> OnReceiveBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the on receive.
|
||||
/// </summary>
|
||||
/// <value>The on receive.</value>
|
||||
public Action<string> OnReceive { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +91,11 @@ namespace Jellyfin.Server.SocketSharp
|
||||
{
|
||||
var url = request.Url.ToString();
|
||||
|
||||
logger.LogInformation("{0} {1}. UserAgent: {2}",
|
||||
request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, url, request.UserAgent ?? string.Empty);
|
||||
logger.LogInformation(
|
||||
"{0} {1}. UserAgent: {2}",
|
||||
request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod,
|
||||
url,
|
||||
request.UserAgent ?? string.Empty);
|
||||
}
|
||||
|
||||
private Task InitTask(HttpListenerContext context, CancellationToken cancellationToken)
|
||||
@@ -200,7 +203,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
//TODO Investigate and properly fix.
|
||||
// TODO: Investigate and properly fix.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -222,38 +225,39 @@ namespace Jellyfin.Server.SocketSharp
|
||||
public Task Stop()
|
||||
{
|
||||
_disposeCancellationTokenSource.Cancel();
|
||||
|
||||
if (_listener != null)
|
||||
{
|
||||
_listener.Close();
|
||||
}
|
||||
_listener?.Close();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources and disposes of the managed resources used.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private readonly object _disposeLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources and disposes of the managed resources used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Whether or not the managed resources should be disposed</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
lock (_disposeLock)
|
||||
if (_disposed)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
//release unmanaged resources here...
|
||||
_disposed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Stop().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
@@ -24,7 +25,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
this.request = httpContext.Request;
|
||||
this.response = new WebSocketSharpResponse(logger, httpContext.Response, this);
|
||||
|
||||
//HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
|
||||
// HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
|
||||
}
|
||||
|
||||
private static string GetHandlerPathIfAny(string listenerUrl)
|
||||
@@ -41,7 +42,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
}
|
||||
|
||||
var startHostUrl = listenerUrl.Substring(pos + "://".Length);
|
||||
var endPos = startHostUrl.IndexOf('/');
|
||||
var endPos = startHostUrl.IndexOf('/', StringComparison.Ordinal);
|
||||
if (endPos == -1)
|
||||
{
|
||||
return null;
|
||||
@@ -69,9 +70,11 @@ namespace Jellyfin.Server.SocketSharp
|
||||
|
||||
public string UserHostAddress => request.UserHostAddress;
|
||||
|
||||
public string XForwardedFor => string.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"];
|
||||
public string XForwardedFor
|
||||
=> string.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"];
|
||||
|
||||
public int? XForwardedPort => string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"]);
|
||||
public int? XForwardedPort
|
||||
=> string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
|
||||
|
||||
public string XForwardedProtocol => string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"];
|
||||
|
||||
@@ -107,6 +110,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
switch (crlf)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
if (c == '\r')
|
||||
{
|
||||
crlf = 1;
|
||||
@@ -121,29 +125,39 @@ namespace Jellyfin.Server.SocketSharp
|
||||
{
|
||||
throw new ArgumentException("net_WebHeaderInvalidControlChars");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 1:
|
||||
{
|
||||
if (c == '\n')
|
||||
{
|
||||
crlf = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
if (c == ' ' || c == '\t')
|
||||
{
|
||||
crlf = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crlf != 0)
|
||||
{
|
||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -156,6 +170,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -343,6 +358,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo);
|
||||
this.pathInfo = NormalizePathInfo(pathInfo, mode);
|
||||
}
|
||||
|
||||
return this.pathInfo;
|
||||
}
|
||||
}
|
||||
@@ -444,7 +460,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
|
||||
public string ContentType => request.ContentType;
|
||||
|
||||
public Encoding contentEncoding;
|
||||
private Encoding contentEncoding;
|
||||
public Encoding ContentEncoding
|
||||
{
|
||||
get => contentEncoding ?? request.ContentEncoding;
|
||||
@@ -502,6 +518,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return httpFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
|
||||
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
|
||||
using IRequest = MediaBrowser.Model.Services.IRequest;
|
||||
|
||||
|
||||
namespace Jellyfin.Server.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpResponse : IHttpResponse
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly HttpListenerResponse _response;
|
||||
|
||||
public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request)
|
||||
@@ -30,7 +30,9 @@ namespace Jellyfin.Server.SocketSharp
|
||||
}
|
||||
|
||||
public IRequest Request { get; private set; }
|
||||
|
||||
public Dictionary<string, object> Items { get; private set; }
|
||||
|
||||
public object OriginalResponse => _response;
|
||||
|
||||
public int StatusCode
|
||||
@@ -51,7 +53,7 @@ namespace Jellyfin.Server.SocketSharp
|
||||
set => _response.ContentType = value;
|
||||
}
|
||||
|
||||
//public ICookies Cookies { get; set; }
|
||||
public QueryParamCollection Headers => _response.Headers;
|
||||
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
@@ -64,8 +66,6 @@ namespace Jellyfin.Server.SocketSharp
|
||||
_response.AddHeader(name, value);
|
||||
}
|
||||
|
||||
public QueryParamCollection Headers => _response.Headers;
|
||||
|
||||
public string GetHeader(string name)
|
||||
{
|
||||
return _response.Headers[name];
|
||||
@@ -114,9 +114,9 @@ namespace Jellyfin.Server.SocketSharp
|
||||
|
||||
public void SetContentLength(long contentLength)
|
||||
{
|
||||
//you can happily set the Content-Length header in Asp.Net
|
||||
//but HttpListener will complain if you do - you have to set ContentLength64 on the response.
|
||||
//workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
|
||||
// you can happily set the Content-Length header in Asp.Net
|
||||
// but HttpListener will complain if you do - you have to set ContentLength64 on the response.
|
||||
// workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
|
||||
_response.ContentLength64 = contentLength;
|
||||
}
|
||||
|
||||
@@ -147,15 +147,12 @@ namespace Jellyfin.Server.SocketSharp
|
||||
{
|
||||
sb.Append($";domain={cookie.Domain}");
|
||||
}
|
||||
//else if (restrictAllCookiesToDomain != null)
|
||||
//{
|
||||
// sb.Append($";domain={restrictAllCookiesToDomain}");
|
||||
//}
|
||||
|
||||
if (cookie.Secure)
|
||||
{
|
||||
sb.Append(";Secure");
|
||||
}
|
||||
|
||||
if (cookie.HttpOnly)
|
||||
{
|
||||
sb.Append(";HttpOnly");
|
||||
@@ -164,7 +161,6 @@ namespace Jellyfin.Server.SocketSharp
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
public bool SendChunked
|
||||
{
|
||||
get => _response.SendChunked;
|
||||
|
||||
Reference in New Issue
Block a user