Merge pull request #1584 from Bond-009/checksum

Check checksum for plugin downloads
This commit is contained in:
dkanada
2019-08-15 01:00:33 -07:00
committed by GitHub
17 changed files with 143 additions and 427 deletions

View File

@@ -1537,8 +1537,6 @@ namespace Emby.Server.Implementations
{
Url = Url,
LogErrorResponseBody = false,
LogErrors = false,
LogRequest = false,
BufferContent = false,
CancellationToken = cancellationToken
}).ConfigureAwait(false))
@@ -1690,8 +1688,8 @@ namespace Emby.Server.Implementations
private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
{
if (address.Equals(IPAddress.Loopback) ||
address.Equals(IPAddress.IPv6Loopback))
if (address.Equals(IPAddress.Loopback)
|| address.Equals(IPAddress.IPv6Loopback))
{
return true;
}
@@ -1704,12 +1702,6 @@ namespace Emby.Server.Implementations
return cachedResult;
}
#if DEBUG
const bool LogPing = true;
#else
const bool LogPing = false;
#endif
try
{
using (var response = await HttpClient.SendAsync(
@@ -1717,8 +1709,6 @@ namespace Emby.Server.Implementations
{
Url = apiUrl,
LogErrorResponseBody = false,
LogErrors = LogPing,
LogRequest = LogPing,
BufferContent = false,
CancellationToken = cancellationToken
}, HttpMethod.Post).ConfigureAwait(false))

View File

@@ -5,9 +5,6 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -20,7 +17,7 @@ using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpClientManager
{
/// <summary>
/// Class HttpClientManager
/// Class HttpClientManager.
/// </summary>
public class HttpClientManager : IHttpClient
{
@@ -45,19 +42,9 @@ namespace Emby.Server.Implementations.HttpClientManager
IFileSystem fileSystem,
Func<string> defaultUserAgentFn)
{
if (appPaths == null)
{
throw new ArgumentNullException(nameof(appPaths));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_logger = logger;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_fileSystem = fileSystem;
_appPaths = appPaths;
_appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths));
_defaultUserAgentFn = defaultUserAgentFn;
}
@@ -118,7 +105,7 @@ namespace Emby.Server.Implementations.HttpClientManager
request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
}
//request.Headers.Add(HeaderNames.CacheControl, "no-cache");
// request.Headers.Add(HeaderNames.CacheControl, "no-cache");
/*
if (!string.IsNullOrWhiteSpace(userInfo))
@@ -196,7 +183,7 @@ namespace Emby.Server.Implementations.HttpClientManager
}
var url = options.Url;
var urlHash = url.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
@@ -239,7 +226,13 @@ namespace Emby.Server.Implementations.HttpClientManager
{
Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath));
using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true))
using (var fileStream = new FileStream(
responseCachePath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
StreamDefaults.DefaultFileStreamBufferSize,
true))
{
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
@@ -278,16 +271,11 @@ namespace Emby.Server.Implementations.HttpClientManager
}
}
if (options.LogRequest)
{
_logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url);
}
options.CancellationToken.ThrowIfCancellationRequested();
var response = await client.SendAsync(
httpWebRequest,
options.BufferContent ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
options.CancellationToken).ConfigureAwait(false);
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
@@ -308,138 +296,6 @@ namespace Emby.Server.Implementations.HttpClientManager
public Task<HttpResponseInfo> Post(HttpRequestOptions options)
=> SendAsync(options, HttpMethod.Post);
/// <summary>
/// Downloads the contents of a given url into a temporary location
/// </summary>
/// <param name="options">The options.</param>
/// <returns>Task{System.String}.</returns>
public async Task<string> GetTempFile(HttpRequestOptions options)
{
var response = await GetTempFileResponse(options).ConfigureAwait(false);
return response.TempFilePath;
}
public async Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options)
{
ValidateParams(options);
Directory.CreateDirectory(_appPaths.TempDirectory);
var tempFile = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
if (options.Progress == null)
{
throw new ArgumentException("Options did not have a Progress value.", nameof(options));
}
options.CancellationToken.ThrowIfCancellationRequested();
var httpWebRequest = GetRequestMessage(options, HttpMethod.Get);
options.Progress.Report(0);
if (options.LogRequest)
{
_logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
}
var client = GetHttpClient(options.Url);
try
{
options.CancellationToken.ThrowIfCancellationRequested();
using (var response = (await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false)))
{
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
options.CancellationToken.ThrowIfCancellationRequested();
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
{
await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
}
options.Progress.Report(100);
var responseInfo = new HttpResponseInfo(response.Headers, response.Content.Headers)
{
TempFilePath = tempFile,
StatusCode = response.StatusCode,
ContentType = response.Content.Headers.ContentType?.MediaType,
ContentLength = response.Content.Headers.ContentLength
};
return responseInfo;
}
}
catch (Exception ex)
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
throw GetException(ex, options);
}
}
private Exception GetException(Exception ex, HttpRequestOptions options)
{
if (ex is HttpException)
{
return ex;
}
var webException = ex as WebException
?? ex.InnerException as WebException;
if (webException != null)
{
if (options.LogErrors)
{
_logger.LogError(webException, "Error {Status} getting response from {Url}", webException.Status, options.Url);
}
var exception = new HttpException(webException.Message, webException);
using (var response = webException.Response as HttpWebResponse)
{
if (response != null)
{
exception.StatusCode = response.StatusCode;
}
}
if (!exception.StatusCode.HasValue)
{
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
webException.Status == WebExceptionStatus.ConnectFailure)
{
exception.IsTimedOut = true;
}
}
return exception;
}
var operationCanceledException = ex as OperationCanceledException
?? ex.InnerException as OperationCanceledException;
if (operationCanceledException != null)
{
return GetCancellationException(options, options.CancellationToken, operationCanceledException);
}
if (options.LogErrors)
{
_logger.LogError(ex, "Error getting response from {Url}", options.Url);
}
return ex;
}
private void ValidateParams(HttpRequestOptions options)
{
if (string.IsNullOrEmpty(options.Url))
@@ -471,35 +327,6 @@ namespace Emby.Server.Implementations.HttpClientManager
return url;
}
/// <summary>
/// Throws the cancellation exception.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="exception">The exception.</param>
/// <returns>Exception.</returns>
private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception)
{
// If the HttpClient's timeout is reached, it will cancel the Task internally
if (!cancellationToken.IsCancellationRequested)
{
var msg = string.Format("Connection to {0} timed out", options.Url);
if (options.LogErrors)
{
_logger.LogError(msg);
}
// Throw an HttpException so that the caller doesn't think it was cancelled by user code
return new HttpException(msg, exception)
{
IsTimedOut = true
};
}
return exception;
}
private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
{
if (response.IsSuccessStatusCode)
@@ -507,8 +334,11 @@ namespace Emby.Server.Implementations.HttpClientManager
return;
}
var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
_logger.LogError("HTTP request failed with message: {Message}", msg);
if (options.LogErrorResponseBody)
{
var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
_logger.LogError("HTTP request failed with message: {Message}", msg);
}
throw new HttpException(response.ReasonPhrase)
{

View File

@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
try
{
await _installationManager.InstallPackage(package, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -87,8 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
// Update progress
lock (progress)
{
numComplete++;
progress.Report(90.0 * numComplete / packagesToInstall.Count + 10);
progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10);
}
}

View File

@@ -4,13 +4,14 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Progress;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Events;
@@ -126,13 +127,16 @@ namespace Emby.Server.Implementations.Updates
/// <returns>Task{List{PackageInfo}}.</returns>
public async Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken)
{
using (var response = await _httpClient.SendAsync(new HttpRequestOptions
{
Url = "https://repo.jellyfin.org/releases/plugin/manifest.json",
CancellationToken = cancellationToken,
CacheLength = GetCacheLength()
}, HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
using (var response = await _httpClient.SendAsync(
new HttpRequestOptions
{
Url = "https://repo.jellyfin.org/releases/plugin/manifest.json",
CancellationToken = cancellationToken,
CacheMode = CacheMode.Unconditional,
CacheLength = GetCacheLength()
},
HttpMethod.Get).ConfigureAwait(false))
using (Stream stream = response.Content)
{
return FilterPackages(await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(stream).ConfigureAwait(false));
}
@@ -275,12 +279,7 @@ namespace Emby.Server.Implementations.Updates
var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
?? availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (package == null)
{
return null;
}
return package.versions
return package?.versions
.OrderByDescending(x => x.Version)
.FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, currentServerVersion));
}
@@ -304,32 +303,18 @@ namespace Emby.Server.Implementations.Updates
var latestPluginInfo = GetLatestCompatibleVersion(catalog, p.Name, p.Id.ToString(), applicationVersion, systemUpdateLevel);
return latestPluginInfo != null && latestPluginInfo.Version > p.Version ? latestPluginInfo : null;
}).Where(i => i != null)
.Where(p => !string.IsNullOrEmpty(p.sourceUrl) && !CompletedInstallations.Any(i => string.Equals(i.AssemblyGuid, p.guid, StringComparison.OrdinalIgnoreCase)));
}
/// <summary>
/// Installs the package.
/// </summary>
/// <param name="package">The package.</param>
/// <param name="isPlugin">if set to <c>true</c> [is plugin].</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">package</exception>
public async Task InstallPackage(PackageVersionInfo package, IProgress<double> progress, CancellationToken cancellationToken)
/// <inheritdoc />
public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken)
{
if (package == null)
{
throw new ArgumentNullException(nameof(package));
}
if (progress == null)
{
throw new ArgumentNullException(nameof(progress));
}
var installationInfo = new InstallationInfo
{
Id = Guid.NewGuid(),
@@ -349,16 +334,6 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Add(tuple);
}
var innerProgress = new ActionableProgress<double>();
// Whenever the progress updates, update the outer progress object and InstallationInfo
innerProgress.RegisterAction(percent =>
{
progress.Report(percent);
installationInfo.PercentComplete = percent;
});
var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
var installationEventArgs = new InstallationEventArgs
@@ -371,7 +346,7 @@ namespace Emby.Server.Implementations.Updates
try
{
await InstallPackageInternal(package, innerProgress, linkedToken).ConfigureAwait(false);
await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
lock (_currentInstallations)
{
@@ -423,20 +398,16 @@ namespace Emby.Server.Implementations.Updates
/// Installs the package internal.
/// </summary>
/// <param name="package">The package.</param>
/// <param name="isPlugin">if set to <c>true</c> [is plugin].</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task InstallPackageInternal(PackageVersionInfo package, IProgress<double> progress, CancellationToken cancellationToken)
/// <returns><see cref="Task" />.</returns>
private async Task InstallPackageInternal(PackageVersionInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase))
?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase));
string targetPath = plugin == null ? null : plugin.AssemblyFilePath;
// Do the install
await PerformPackageInstallation(progress, targetPath, package, cancellationToken).ConfigureAwait(false);
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing
if (plugin == null)
@@ -455,76 +426,57 @@ namespace Emby.Server.Implementations.Updates
_applicationHost.NotifyPendingRestart();
}
private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken)
private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken)
{
// TODO: Remove the `string target` argument as it is not used any longer
var extension = Path.GetExtension(package.targetFilename);
var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase);
if (!isArchive)
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename);
return;
}
// Always override the passed-in target (which is a file) and figure it out again
target = Path.Combine(_appPaths.PluginsPath, package.name);
_logger.LogDebug("Installing plugin to {Filename}.", target);
string targetDir = Path.Combine(_appPaths.PluginsPath, package.name);
// Download to temporary file so that, if interrupted, it won't destroy the existing installation
_logger.LogDebug("Downloading ZIP.");
var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
{
Url = package.sourceUrl,
CancellationToken = cancellationToken,
Progress = progress
}).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
// TODO: Validate with a checksum, *properly*
// Check if the target directory already exists, and remove it if so
if (Directory.Exists(target))
{
_logger.LogDebug("Deleting existing plugin at {Filename}.", target);
Directory.Delete(target, true);
}
// Success - move it to the real target
try
{
_logger.LogDebug("Extracting ZIP {TempFile} to {Filename}.", tempFile, target);
using (var stream = File.OpenRead(tempFile))
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
using (var res = await _httpClient.SendAsync(
new HttpRequestOptions
{
_zipClient.ExtractAllFromZip(stream, target, true);
}
}
catch (IOException ex)
Url = package.sourceUrl,
CancellationToken = cancellationToken,
// We need it to be buffered for setting the position
BufferContent = true
},
HttpMethod.Get).ConfigureAwait(false))
using (var stream = res.Content)
using (var md5 = MD5.Create())
{
_logger.LogError(ex, "Error attempting to extract {TempFile} to {TargetFile}", tempFile, target);
throw;
cancellationToken.ThrowIfCancellationRequested();
var hash = HexHelper.ToHexString(md5.ComputeHash(stream));
if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("{0}, {1}", package.checksum, hash);
throw new InvalidDataException($"The checksums didn't match while installing {package.name}.");
}
if (Directory.Exists(targetDir))
{
Directory.Delete(targetDir);
}
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
}
try
{
_logger.LogDebug("Deleting temporary file {Filename}.", tempFile);
_fileSystem.DeleteFile(tempFile);
}
catch (IOException ex)
{
// Don't fail because of this
_logger.LogError(ex, "Error deleting temp file {TempFile}", tempFile);
}
#pragma warning restore CA5351
}
/// <summary>
/// Uninstalls a plugin
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <exception cref="ArgumentException"></exception>
public void UninstallPlugin(IPlugin plugin)
{
plugin.OnUninstalling();