322 lines
16 KiB
C#
322 lines
16 KiB
C#
// Copyright (C) 2024, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.Library.Modules.Builtin
|
|
{
|
|
public class HttpOptions : Duplicati.Library.Interface.IConnectionModule, IDisposable
|
|
{
|
|
private const string OPTION_DISABLE_EXPECT100 = "disable-expect100-continue";
|
|
private const string OPTION_DISABLE_NAGLING = "disable-nagling";
|
|
private const string OPTION_ACCEPT_SPECIFIED_CERTIFICATE = "accept-specified-ssl-hash";
|
|
private const string OPTION_ACCEPT_ANY_CERTIFICATE = "accept-any-ssl-certificate";
|
|
private const string OPTION_OAUTH_URL = "oauth-url";
|
|
private const string OPTION_SSL_VERSIONS = "allowed-ssl-versions";
|
|
|
|
private const string OPTION_BUFFER_REQUESTS = "http-enable-buffering";
|
|
private const string OPTION_OPERATION_TIMEOUT = "http-operation-timeout";
|
|
private const string OPTION_OPERATION_SHORT_TIMEOUT = "http-operation-short-timeout";
|
|
private const string OPTION_READWRITE_TIMEOUT = "http-readwrite-timeout";
|
|
|
|
|
|
private bool m_useNagle;
|
|
private bool m_useExpect;
|
|
private System.Net.SecurityProtocolType m_securityProtocol;
|
|
|
|
private bool m_dispose;
|
|
|
|
private bool m_resetNagle;
|
|
private bool m_resetExpect;
|
|
private bool m_resetSecurity;
|
|
|
|
/// <summary>
|
|
/// The handle to the call-context http settings
|
|
/// </summary>
|
|
private IDisposable m_httpsettings;
|
|
|
|
/// <summary>
|
|
/// The handle to the call-context oauth settings
|
|
/// </summary>
|
|
private IDisposable m_oauthsettings;
|
|
|
|
private static Dictionary<string, int> SecurityProtocols
|
|
{
|
|
get
|
|
{
|
|
var res = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var val in Enum.GetNames(typeof(System.Net.SecurityProtocolType)).Zip(Enum.GetValues(typeof(System.Net.SecurityProtocolType)).Cast<int>(), (x, y) => new KeyValuePair<string, int>(x, y)))
|
|
res[val.Key] = val.Value;
|
|
|
|
return res;
|
|
}
|
|
}
|
|
|
|
private static System.Net.SecurityProtocolType ParseSSLProtocols(string names)
|
|
{
|
|
var ptr = SecurityProtocols;
|
|
var res = 0;
|
|
foreach (var s in names.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()))
|
|
if (ptr.ContainsKey(s))
|
|
res = res | ptr[s];
|
|
|
|
return (System.Net.SecurityProtocolType)res;
|
|
}
|
|
|
|
#region IGenericModule Members
|
|
|
|
public string Key
|
|
{
|
|
get { return "http-options"; }
|
|
}
|
|
|
|
public string DisplayName
|
|
{
|
|
get { return Strings.HttpOptions.DisplayName; }
|
|
}
|
|
|
|
public string Description
|
|
{
|
|
get { return Strings.HttpOptions.Description; }
|
|
}
|
|
|
|
public bool LoadAsDefault
|
|
{
|
|
get { return true; }
|
|
}
|
|
|
|
private static Interface.ICommandLineArgument[] GetArguments()
|
|
{
|
|
var sslnames = SecurityProtocols.Select(x => x.Key).ToArray();
|
|
var defaultssl = System.Net.SecurityProtocolType.SystemDefault.ToString();
|
|
|
|
return [
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_DISABLE_EXPECT100, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.HttpOptions.DisableExpect100Short, Strings.HttpOptions.DisableExpect100Long, "false"),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_DISABLE_NAGLING, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.HttpOptions.DisableNagleShort, Strings.HttpOptions.DisableNagleLong, "false", null, null, "This option is deprecated and no longer has any effect."),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_ACCEPT_SPECIFIED_CERTIFICATE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.HttpOptions.DescriptionAcceptHashShort, Strings.HttpOptions.DescriptionAcceptHashLong2),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_ACCEPT_ANY_CERTIFICATE, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.HttpOptions.DescriptionAcceptAnyCertificateShort, Strings.HttpOptions.DescriptionAcceptAnyCertificateLong),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_OAUTH_URL, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.HttpOptions.OauthurlShort, Strings.HttpOptions.OauthurlLong, OAuthHelper.DUPLICATI_OAUTH_SERVICE),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_SSL_VERSIONS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Flags, Strings.HttpOptions.SslversionsShort, Strings.HttpOptions.SslversionsLong, defaultssl, null, sslnames),
|
|
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_OPERATION_TIMEOUT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.HttpOptions.OperationtimeoutShort, Strings.HttpOptions.OperationtimeoutLong),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_OPERATION_SHORT_TIMEOUT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.HttpOptions.OperationshorttimeoutShort, Strings.HttpOptions.OperationshorttimeoutLong),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_READWRITE_TIMEOUT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.HttpOptions.ReadwritetimeoutShort, Strings.HttpOptions.ReadwritetimeoutLong),
|
|
new Duplicati.Library.Interface.CommandLineArgument(OPTION_BUFFER_REQUESTS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.HttpOptions.BufferrequestsShort, Strings.HttpOptions.BufferrequestsLong, "false"),
|
|
];
|
|
}
|
|
|
|
private static readonly Duplicati.Library.Interface.ICommandLineArgument[] ARGUMENTS = GetArguments();
|
|
|
|
public static IEnumerable<Duplicati.Library.Interface.ICommandLineArgument> GetHttpArguments(bool includeSsl = false, bool includeBuffer = false, bool includeReadWrite = false, bool includeOAuth = false, bool includeShortTimeout = false)
|
|
{
|
|
var res = new List<string>();
|
|
res.AddRange([OPTION_DISABLE_EXPECT100, OPTION_OPERATION_TIMEOUT]);
|
|
if (includeShortTimeout)
|
|
res.Add(OPTION_OPERATION_SHORT_TIMEOUT);
|
|
if (includeReadWrite)
|
|
res.Add(OPTION_READWRITE_TIMEOUT);
|
|
if (includeBuffer)
|
|
res.Add(OPTION_BUFFER_REQUESTS);
|
|
if (includeSsl)
|
|
res.AddRange([OPTION_ACCEPT_ANY_CERTIFICATE, OPTION_ACCEPT_SPECIFIED_CERTIFICATE, OPTION_SSL_VERSIONS]);
|
|
if (includeOAuth)
|
|
res.Add(OPTION_OAUTH_URL);
|
|
|
|
return ARGUMENTS.Where(x => res.Contains(x.Name));
|
|
}
|
|
|
|
public IList<Duplicati.Library.Interface.ICommandLineArgument> SupportedCommands => ARGUMENTS;
|
|
|
|
public sealed record HttpModuleSettings(
|
|
TimeSpan OperationTimeout,
|
|
TimeSpan ShortOperationTimeout,
|
|
TimeSpan ReadWriteTimeout,
|
|
bool BufferRequests,
|
|
bool AcceptAllCertificates,
|
|
string[] AcceptCertificates,
|
|
bool EnableExpect100,
|
|
System.Net.SecurityProtocolType SecurityProtocols
|
|
)
|
|
{
|
|
public int ReadWriteTimeoutMilliseconds
|
|
=> ReadWriteTimeout == Timeout.InfiniteTimeSpan
|
|
? Timeout.Infinite
|
|
: (int)ReadWriteTimeout.TotalMilliseconds;
|
|
|
|
public int OperationTimeoutMilliseconds
|
|
=> OperationTimeout == Timeout.InfiniteTimeSpan
|
|
? Timeout.Infinite
|
|
: (int)OperationTimeout.TotalMilliseconds;
|
|
|
|
public int ShortOperationTimeoutMilliseconds
|
|
=> ShortOperationTimeout == Timeout.InfiniteTimeSpan
|
|
? Timeout.Infinite
|
|
: (int)ShortOperationTimeout.TotalMilliseconds;
|
|
}
|
|
|
|
public static HttpModuleSettings ParseSettings(IDictionary<string, string> options)
|
|
{
|
|
var operationTimeout = options.TryGetValue(OPTION_OPERATION_TIMEOUT, out var timetmp) && !string.IsNullOrWhiteSpace(timetmp)
|
|
? Utility.Timeparser.ParseTimeSpan(timetmp)
|
|
: Timeout.InfiniteTimeSpan;
|
|
|
|
var shortOperationTimeout = options.TryGetValue(OPTION_OPERATION_SHORT_TIMEOUT, out timetmp) && !string.IsNullOrWhiteSpace(timetmp)
|
|
? Utility.Timeparser.ParseTimeSpan(timetmp)
|
|
: operationTimeout;
|
|
|
|
var readwriteTimeout = options.TryGetValue(OPTION_READWRITE_TIMEOUT, out timetmp) && !string.IsNullOrWhiteSpace(timetmp)
|
|
? Utility.Timeparser.ParseTimeSpan(timetmp)
|
|
: Timeout.InfiniteTimeSpan;
|
|
|
|
options.TryGetValue(OPTION_ACCEPT_SPECIFIED_CERTIFICATE, out var certHash);
|
|
options.TryGetValue(OPTION_SSL_VERSIONS, out var sslprotocol);
|
|
|
|
return new HttpModuleSettings(
|
|
operationTimeout,
|
|
shortOperationTimeout,
|
|
readwriteTimeout,
|
|
Utility.Utility.ParseBoolOption(options, OPTION_BUFFER_REQUESTS),
|
|
Utility.Utility.ParseBoolOption(options, OPTION_ACCEPT_ANY_CERTIFICATE),
|
|
certHash == null ? null : certHash.Split(new string[] { ",", ";" }, StringSplitOptions.RemoveEmptyEntries),
|
|
// Legacy handling, the "default" was enabled, but now the default is "disabled"
|
|
options.ContainsKey(OPTION_DISABLE_EXPECT100) ? !Utility.Utility.ParseBoolOption(options, OPTION_DISABLE_EXPECT100) : false,
|
|
string.IsNullOrWhiteSpace(sslprotocol) ? System.Net.SecurityProtocolType.SystemDefault : ParseSSLProtocols(sslprotocol)
|
|
);
|
|
}
|
|
|
|
public static HttpClient CreateHttpClient(HttpModuleSettings settings, HttpClientHandler handler = null)
|
|
{
|
|
handler ??= new HttpClientHandler();
|
|
if (settings.AcceptAllCertificates)
|
|
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
|
else if (settings.AcceptCertificates != null && settings.AcceptCertificates.Length > 0)
|
|
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => settings.AcceptCertificates.Contains(cert.GetCertHashString());
|
|
|
|
var client = HttpClientHelper.CreateClient(handler);
|
|
client.Timeout = settings.OperationTimeout == Timeout.InfiniteTimeSpan ? Timeout.InfiniteTimeSpan : settings.OperationTimeout;
|
|
client.DefaultRequestHeaders.Add(HttpRequestHeader.UserAgent.ToString(), "Duplicati v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
|
|
client.DefaultRequestHeaders.ExpectContinue = settings.EnableExpect100;
|
|
|
|
return client;
|
|
}
|
|
|
|
// TODO: The configure method below is legacy code and should be removed
|
|
// once all code is updated to use the HttpClientFactory pattern
|
|
|
|
public void Configure(IDictionary<string, string> commandlineOptions)
|
|
{
|
|
m_dispose = true;
|
|
TimeSpan operationTimeout = new TimeSpan(0);
|
|
TimeSpan readwriteTimeout = new TimeSpan(0);
|
|
|
|
string timetmp;
|
|
commandlineOptions.TryGetValue(OPTION_OPERATION_TIMEOUT, out timetmp);
|
|
if (!string.IsNullOrWhiteSpace(timetmp))
|
|
operationTimeout = Utility.Timeparser.ParseTimeSpan(timetmp);
|
|
|
|
commandlineOptions.TryGetValue(OPTION_READWRITE_TIMEOUT, out timetmp);
|
|
if (!string.IsNullOrWhiteSpace(timetmp))
|
|
readwriteTimeout = Utility.Timeparser.ParseTimeSpan(timetmp);
|
|
|
|
bool accepAllCertificates = Utility.Utility.ParseBoolOption(commandlineOptions, OPTION_ACCEPT_ANY_CERTIFICATE);
|
|
|
|
string certHash;
|
|
commandlineOptions.TryGetValue(OPTION_ACCEPT_SPECIFIED_CERTIFICATE, out certHash);
|
|
|
|
m_httpsettings = Duplicati.Library.Utility.HttpContextSettings.StartSession(
|
|
operationTimeout,
|
|
readwriteTimeout,
|
|
Utility.Utility.ParseBoolOption(commandlineOptions, OPTION_BUFFER_REQUESTS),
|
|
accepAllCertificates,
|
|
certHash == null ? null : certHash.Split(new string[] { ",", ";" }, StringSplitOptions.RemoveEmptyEntries)
|
|
);
|
|
|
|
bool disableNagle = Utility.Utility.ParseBoolOption(commandlineOptions, OPTION_DISABLE_NAGLING);
|
|
bool disableExpect100 = Utility.Utility.ParseBoolOption(commandlineOptions, OPTION_DISABLE_EXPECT100);
|
|
|
|
// TODO: This is done to avoid conflicting settings,
|
|
// but ideally, we should run each operation in a separate
|
|
// app-domain to ensure that multiple invocations of this module
|
|
// does not interfere, as the options are shared in the app-domain
|
|
m_resetNagle = commandlineOptions.ContainsKey(OPTION_DISABLE_NAGLING);
|
|
m_resetExpect = commandlineOptions.ContainsKey(OPTION_DISABLE_EXPECT100);
|
|
m_resetSecurity = commandlineOptions.ContainsKey(OPTION_SSL_VERSIONS);
|
|
|
|
m_useNagle = System.Net.ServicePointManager.UseNagleAlgorithm;
|
|
m_useExpect = System.Net.ServicePointManager.Expect100Continue;
|
|
m_securityProtocol = System.Net.ServicePointManager.SecurityProtocol;
|
|
|
|
if (m_resetNagle)
|
|
System.Net.ServicePointManager.UseNagleAlgorithm = !disableNagle;
|
|
if (m_resetExpect)
|
|
System.Net.ServicePointManager.Expect100Continue = !disableExpect100;
|
|
|
|
string sslprotocol;
|
|
commandlineOptions.TryGetValue(OPTION_SSL_VERSIONS, out sslprotocol);
|
|
if (!string.IsNullOrWhiteSpace(sslprotocol) && m_resetSecurity)
|
|
System.Net.ServicePointManager.SecurityProtocol = ParseSSLProtocols(sslprotocol);
|
|
|
|
string url;
|
|
commandlineOptions.TryGetValue(OPTION_OAUTH_URL, out url);
|
|
if (!string.IsNullOrWhiteSpace(url))
|
|
m_oauthsettings = OAuthContextSettings.StartSession(url);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable Members
|
|
|
|
public void Dispose()
|
|
{
|
|
if (m_dispose)
|
|
{
|
|
m_dispose = false;
|
|
if (m_resetNagle)
|
|
System.Net.ServicePointManager.UseNagleAlgorithm = m_useNagle;
|
|
if (m_resetExpect)
|
|
System.Net.ServicePointManager.Expect100Continue = m_useExpect;
|
|
if (m_resetSecurity)
|
|
System.Net.ServicePointManager.SecurityProtocol = m_securityProtocol;
|
|
}
|
|
|
|
if (m_httpsettings != null)
|
|
{
|
|
m_httpsettings.Dispose();
|
|
m_httpsettings = null;
|
|
}
|
|
|
|
if (m_oauthsettings != null)
|
|
{
|
|
m_oauthsettings.Dispose();
|
|
m_oauthsettings = null;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|