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 { /// /// Specify the download activities prior to the launch. /// This enables self-updating services. /// 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; } /// /// Constructs the download setting sfrom the XML entry /// /// XML element /// The required attribute is missing or the configuration is invalid internal Download(XmlElement n) { this.From = XmlHelper.SingleAttribute(n, "from"); this.To = XmlHelper.SingleAttribute(n, "to"); // All arguments below are optional this.FailOnError = XmlHelper.SingleAttribute(n, "failOnError", false); this.Proxy = XmlHelper.SingleAttribute(n, "proxy", null); this.Auth = XmlHelper.EnumAttribute(n, "auth", AuthType.None); this.Username = XmlHelper.SingleAttribute(n, "user", null); this.Password = XmlHelper.SingleAttribute(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; } /// /// Downloads the requested file and puts it to the specified target. /// /// /// Download failure. FailOnError flag should be processed outside. /// 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; } } } }