winsw/src/WinSW.Core/Download.cs

230 lines
8.3 KiB
C#

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using log4net;
using WinSW.Util;
namespace WinSW
{
/// <summary>
/// Specify the download activities prior to the launch.
/// This enables self-updating services.
/// </summary>
public class Download
{
public enum AuthType
{
None = 0,
Sspi,
Basic
}
private static readonly ILog Logger = LogManager.GetLogger(typeof(Download));
public readonly string From;
public readonly string To;
public readonly AuthType Auth;
public readonly string? Username;
public readonly string? Password;
public readonly bool UnsecureAuth;
public readonly bool FailOnError;
public readonly string? Proxy;
public string ShortId => $"(download from {this.From})";
#if NET461
static Download()
{
// If your app runs on .NET Framework 4.7 or later versions, but targets an earlier version
AppContext.SetSwitch("Switch.System.Net.DontEnableSystemDefaultTlsVersions", false);
}
#endif
// internal
public Download(
string from,
string to,
bool failOnError = false,
AuthType auth = AuthType.None,
string? username = null,
string? password = null,
bool unsecureAuth = false,
string? proxy = null)
{
this.From = from;
this.To = to;
this.FailOnError = failOnError;
this.Proxy = proxy;
this.Auth = auth;
this.Username = username;
this.Password = password;
this.UnsecureAuth = unsecureAuth;
}
/// <summary>
/// Constructs the download setting sfrom the XML entry
/// </summary>
/// <param name="n">XML element</param>
/// <exception cref="InvalidDataException">The required attribute is missing or the configuration is invalid</exception>
internal Download(XmlElement n)
{
this.From = XmlHelper.SingleAttribute<string>(n, "from");
this.To = XmlHelper.SingleAttribute<string>(n, "to");
// All arguments below are optional
this.FailOnError = XmlHelper.SingleAttribute(n, "failOnError", false);
this.Proxy = XmlHelper.SingleAttribute<string>(n, "proxy", null);
this.Auth = XmlHelper.EnumAttribute(n, "auth", AuthType.None);
this.Username = XmlHelper.SingleAttribute<string>(n, "user", null);
this.Password = XmlHelper.SingleAttribute<string>(n, "password", null);
this.UnsecureAuth = XmlHelper.SingleAttribute(n, "unsecureAuth", false);
if (this.Auth == AuthType.Basic)
{
// Allow it only for HTTPS or for UnsecureAuth
if (!this.From.StartsWith("https:") && !this.UnsecureAuth)
{
throw new InvalidDataException("Warning: you're sending your credentials in clear text to the server " + this.ShortId +
"If you really want this you must enable 'unsecureAuth' in the configuration");
}
// Also fail if there is no user/password
if (this.Username is null)
{
throw new InvalidDataException("Basic Auth is enabled, but username is not specified " + this.ShortId);
}
if (this.Password is null)
{
throw new InvalidDataException("Basic Auth is enabled, but password is not specified " + this.ShortId);
}
}
}
// Source: http://stackoverflow.com/questions/2764577/forcing-basic-authentication-in-webrequest
private void SetBasicAuthHeader(WebRequest request, string username, string password)
{
string authInfo = username + ":" + password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
request.Headers["Authorization"] = "Basic " + authInfo;
}
/// <summary>
/// Downloads the requested file and puts it to the specified target.
/// </summary>
/// <exception cref="WebException">
/// Download failure. FailOnError flag should be processed outside.
/// </exception>
public async Task PerformAsync()
{
WebRequest request = WebRequest.Create(this.From);
if (!string.IsNullOrEmpty(this.Proxy))
{
CustomProxyInformation proxyInformation = new CustomProxyInformation(this.Proxy!);
if (proxyInformation.Credentials != null)
{
request.Proxy = new WebProxy(proxyInformation.ServerAddress, false, null, proxyInformation.Credentials);
}
else
{
request.Proxy = new WebProxy(proxyInformation.ServerAddress);
}
}
switch (this.Auth)
{
case AuthType.None:
// Do nothing
break;
case AuthType.Sspi:
request.UseDefaultCredentials = true;
request.PreAuthenticate = true;
request.Credentials = CredentialCache.DefaultCredentials;
break;
case AuthType.Basic:
this.SetBasicAuthHeader(request, this.Username!, this.Password!);
break;
default:
throw new WebException("Code defect. Unsupported authentication type: " + this.Auth);
}
bool supportsIfModifiedSince = false;
if (request is HttpWebRequest httpRequest && File.Exists(this.To))
{
supportsIfModifiedSince = true;
httpRequest.IfModifiedSince = File.GetLastWriteTime(this.To);
}
DateTime lastModified = default;
string tmpFilePath = this.To + ".tmp";
try
{
using (WebResponse response = await request.GetResponseAsync())
using (Stream responseStream = response.GetResponseStream())
using (FileStream tmpStream = new FileStream(tmpFilePath, FileMode.Create))
{
if (supportsIfModifiedSince)
{
lastModified = ((HttpWebResponse)response).LastModified;
}
await responseStream.CopyToAsync(tmpStream);
}
FileHelper.MoveOrReplaceFile(this.To + ".tmp", this.To);
if (supportsIfModifiedSince)
{
File.SetLastWriteTime(this.To, lastModified);
}
}
catch (WebException e)
{
if (supportsIfModifiedSince && ((HttpWebResponse?)e.Response)?.StatusCode == HttpStatusCode.NotModified)
{
Logger.Info($"Skipped downloading unmodified resource '{this.From}'");
}
else
{
throw;
}
}
}
}
public class CustomProxyInformation
{
public string ServerAddress { get; }
public NetworkCredential? Credentials { get; }
public CustomProxyInformation(string proxy)
{
if (proxy.Contains("@"))
{
// Extract proxy credentials
int credsFrom = proxy.IndexOf("://") + 3;
int credsTo = proxy.LastIndexOf("@");
string completeCredsStr = proxy.Substring(credsFrom, credsTo - credsFrom);
int credsSeparator = completeCredsStr.IndexOf(":");
string username = completeCredsStr.Substring(0, credsSeparator);
string password = completeCredsStr.Substring(credsSeparator + 1);
this.Credentials = new NetworkCredential(username, password);
this.ServerAddress = proxy.Replace(completeCredsStr + "@", null);
}
else
{
this.ServerAddress = proxy;
}
}
}
}