using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Net; using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Net.Http.Headers; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Security; namespace Intel.WebStorage { public class WebStorageConnection : IWebStorageConnection { #region Constants private const string MAIN_DIRECTORY = "amt-storage"; private const string INTEL_RESERVED = "intel"; private const string CONTENT_TYPE_VALUE = "application/x-www-form-urlencoded"; private const string CONTENT_TYPE_HEADER = "Content-Type"; private const string CONTENT_LENGTH_HEADER = "Content-Length"; private const string KERBEROS_AUTH = "Negotiate"; private const int MAX_LENGTH = 11; private const int MAX_CONTENT = 6000; public const int MAX_PWD_LENGTH = 32; public const int MIN_PWD_LENGTH = 8; public const int MAX_USERNAME_LENGTH = 32; #endregion #region Private Members private Uri _host; private string _username; private SecureString _password; private bool _secure; private AuthenticationScheme _auth; private Dictionary _contentTypeDictionary; private WebStorageConnectionOptions _options; private HttpClient _client = null; private bool _changed = false; private readonly Regex _lettersAndDigits = new Regex("^[0-9a-zA-Z]+$"); #endregion #region Constructors public WebStorageConnection(bool acceptSelfSignedCert = false) { _options = new WebStorageConnectionOptions(); if (acceptSelfSignedCert) { _options.ServerCertificateValidationCallback = SelfSignedCertificateCallback; } _host = null; _username = string.Empty; _password = new SecureString(); _secure = false; _auth = AuthenticationScheme.Digest; SetContentTypeOptions(); } public WebStorageConnection(string host, string username, SecureString pwd, bool secure, bool acceptSelfSignedCert = false) : this(host, username, pwd, secure, AuthenticationScheme.Digest, acceptSelfSignedCert) { } public WebStorageConnection(string host, string username, SecureString pwd, bool secure, AuthenticationScheme authenticationScheme, bool acceptSelfSignedCert = false) { _options = new WebStorageConnectionOptions(); Host = host; UserName = username; Password = pwd; if (acceptSelfSignedCert) { _options.ServerCertificateValidationCallback = SelfSignedCertificateCallback; } Secure = secure; Authentication = authenticationScheme; SetContentTypeOptions(); } #endregion //Constructors #region Properties public string Host { get => _host.Host; set { if (Uri.CheckHostName(value) == UriHostNameType.Unknown) throw new WebStorageException("Invalid Argument - Unknown Host name."); var newHost = new Uri(GetHostUriString(value)); _changed = _changed || (_host != newHost); _host = newHost; } } public string UserName { get => _username; set { if (string.IsNullOrEmpty(value)) { //Accepting null value for Kerberos connection (setting value to empty string) if (Authentication == AuthenticationScheme.Kerberos) value = string.Empty; else throw new WebStorageException("Invalid Argument - Username is null or an empty string."); } _changed = _changed || (_username != value); _username = value; if (_username.Length > MAX_USERNAME_LENGTH) throw new WebStorageException("Invalid Argument - Username can contain up to 32 characters."); } } public SecureString Password { get => _password; set { if (value == null || value.Length == 0) { //Accepting null value for Kerberos connection (setting value to empty string) if (Authentication == AuthenticationScheme.Kerberos) { _changed = _changed || (_password.Length > 0); _password?.Dispose(); _password = new SecureString(); } else throw new WebStorageException("Invalid Argument - Password can contain between 8 to 32 characters."); } else { if (value.Length < MIN_PWD_LENGTH || value.Length > MAX_PWD_LENGTH) throw new WebStorageException("Invalid Argument - Password can contain between 8 to 32 characters."); _changed = _changed || (_password != value); _password?.Dispose(); _password = value; } } } public AuthenticationScheme Authentication { get => _auth; set { _changed = _changed || (_auth != value); _auth = value; } } public bool Secure { get => _secure; set { var changed = _secure != value; _secure = value; if (changed) { _host = new Uri(GetHostUriString(_host.Host)); } _changed = _changed || changed; } } public IWebStorageConnectionOptions Options => _options; #endregion //Properties #region IWebStorageConnection Implementation public void Put(string filePath) { Headers headers = GetHeaders(filePath, null); // WebStorage headers to be sent with the request HandlePutRequest(filePath, null, null, headers); } public void Put(string filePath, Headers headers) { HandlePutRequest(filePath, null, null, headers); } public void Put(string filePath, string directory, string subdirectory, string webUILinkName) { ValidateArgument(directory, "Directory"); ValidateArgument(subdirectory, "Subdirectory"); Headers headers = GetHeaders(filePath, webUILinkName); // WebStorage headers to be sent with the request HandlePutRequest(filePath, directory, subdirectory, headers); } public void Put(string filePath, string directory, string subdirectory, Headers headers) { ValidateArgument(directory, "Directory"); ValidateArgument(subdirectory, "Subdirectory"); HandlePutRequest(filePath, directory, subdirectory, headers); } public byte[] Get(string directory, string subdirectory, string fileName) { ValidateArgument(directory, "Directory"); ValidateArgument(subdirectory, "Subdirectory"); Uri absoluteUri = GetURI(directory, subdirectory, fileName); return Send(HttpRequestMethod.GET, absoluteUri, null, null); } public string GetFileContent(string directory, string subdirectory, string fileName) { byte[] byteRes = Get(directory, subdirectory, fileName); return Encoding.UTF8.GetString(byteRes); } public string GetContent(string directory, string subdirectory) { //subdirectory directory provided only if (string.IsNullOrEmpty(directory) && !string.IsNullOrEmpty(subdirectory)) throw new WebStorageException("Directory name has not been provided."); Uri absoluteUri = GetURI(directory, subdirectory, null); byte[] byteRes = Send(HttpRequestMethod.GET, absoluteUri, null, null); return Encoding.UTF8.GetString(byteRes); } public void Delete(string directory, string subdirectory, string fileName) { if (string.IsNullOrEmpty(directory) && (string.IsNullOrEmpty(subdirectory) == false)) throw new WebStorageException("Must provide directory name."); if (!string.IsNullOrEmpty(directory)) ValidateArgument(directory, "Directory"); if (!string.IsNullOrEmpty(subdirectory)) ValidateArgument(subdirectory, "Subdirectory"); Uri absoluteUri = GetURI(directory, subdirectory, fileName); Send(HttpRequestMethod.DELETE, absoluteUri, null, null); } #endregion //IWebStorageConnection Implementation #region IDisposable Implementation private bool _disposed = false; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _client?.Dispose(); _client = null; _password?.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } ~WebStorageConnection() { Dispose(false); } #endregion #region Private Functions private void HandlePutRequest(string filePath, string directory, string subdirectory, Headers headers) { string fileName = GetFileName(filePath); Uri absoluteUri = GetURI(directory, subdirectory, fileName); using (FileStream fStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { bool firstPacket = true; //Indicates whether to add WebStorage headers int byteReadCounter = 0; byte[] byteContent = new byte[MAX_CONTENT]; //Content of file to send do { if (firstPacket) { byte[] headerBytes = headers.GetBytes(); Array.Copy(headerBytes, byteContent, headerBytes.Length); byteReadCounter = fStream.Read(byteContent, headerBytes.Length, byteContent.Length - headerBytes.Length); if (byteReadCounter == 0) throw new WebStorageException("Failed to read the contents of the file."); Send(HttpRequestMethod.PUT, absoluteUri, byteContent, headerBytes.Length + byteReadCounter); absoluteUri = new Uri(absoluteUri.ToString() + "?append="); firstPacket = false; } else { byteReadCounter = fStream.Read(byteContent, 0, byteContent.Length); if (byteReadCounter != 0) Send(HttpRequestMethod.PUT, absoluteUri, byteContent, byteReadCounter); } } while (byteReadCounter != 0); } } /// new HttpClientHandler that fits current state of the WebStorageConnection /// HttpClientHandler should either be disposed explicitly or fed to an HttpClient - /// it will then be disposed by the HttpClient when the HttpClient is disposed private HttpClientHandler CreateHttpHandler() { CredentialCache cache = null; var handler = new HttpClientHandler(); handler.PreAuthenticate = true; handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.Proxy = _options.Proxy; if (_options.ClientCertificate != null) { handler.ClientCertificates.Add(_options.ClientCertificate); } //Negotiate - Kerberos switch (_auth) { case AuthenticationScheme.Kerberos: int pos = _username.IndexOf('\\'); string domain = null; string user = null; if (pos > 0) { domain = _username.Substring(0, pos); user = _username.Substring(pos + 1); } if (domain != null && !domain.Equals(string.Empty)) { cache = new CredentialCache { { _host, KERBEROS_AUTH, new NetworkCredential(user, _password.Copy(), domain) } }; } if (!AuthenticationManager.CustomTargetNameDictionary.ContainsKey(_host.OriginalString) && _host != null && _host.Authority != null) { var spnPort = "HTTP/" + _host.Authority.ToUpper(); AuthenticationManager.CustomTargetNameDictionary.Add(_host.OriginalString, spnPort); } handler.PreAuthenticate = true; break; case AuthenticationScheme.Digest: cache = new CredentialCache { { _host, _auth.ToString(), new NetworkCredential(_username, _password.Copy()) } }; break; } //Add credentials to the connection if (cache != null) { handler.Credentials = cache; } else { handler.Credentials = CredentialCache.DefaultNetworkCredentials.GetCredential(_host, KERBEROS_AUTH); } return handler; } private void CreateHttpClient() { _client = new HttpClient(CreateHttpHandler()); //the handler will be disposed when the HttpClient is disposed on Dispose() _client.BaseAddress = _host; _client.Timeout = TimeSpan.FromMilliseconds(_options.TimeOut); _client.DefaultRequestHeaders.ConnectionClose = false; _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); _client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate"); _client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { NoCache = true }; _client.DefaultRequestHeaders.Connection.Add("keep-alive"); } private byte[] Send(HttpRequestMethod method, Uri address, byte[] data, Int32? dataLength) { return Task.Run(() => SendAsync(method, address, data, dataLength)).GetAwaiter().GetResult(); } private async Task SendAsync(HttpRequestMethod method, Uri address, byte[] data, Int32? dataLength) { byte[] responseArr = null; HttpStatusCode statusCode = HttpStatusCode.OK; if (_changed) { _client?.Dispose(); _client = null; _changed = false; } if (_client == null) { CreateHttpClient(); } try { using (var httpRequestMessage = new HttpRequestMessage(new HttpMethod(method.ToString()), address)) { if (data != null && dataLength != null) { var messageContent = new ByteArrayContent(data, 0, dataLength.Value); messageContent.Headers.Remove(CONTENT_TYPE_HEADER); // Remove default headers before adding the needed ones messageContent.Headers.Add(CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE); messageContent.Headers.Add(CONTENT_LENGTH_HEADER, dataLength.Value.ToString()); httpRequestMessage.Content = messageContent; } using (var responseMessage = await _client.SendAsync(httpRequestMessage).ConfigureAwait(false)) { responseArr = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false); statusCode = responseMessage.StatusCode; responseMessage.EnsureSuccessStatusCode(); } } } catch (HttpRequestException httpReqExp) { //Check if received response from the AMT if (responseArr != null && responseArr.Length > 0) { if (statusCode == HttpStatusCode.NotFound) { throw new WebStorageConnectionException( $"File at {address.AbsolutePath} not found. Returned Status: {(int)statusCode} {statusCode}", httpReqExp); } throw new WebStorageConnectionException( $"Failed to establish connection to the AMT. Returned Status: {(int)statusCode} {statusCode}", httpReqExp); } throw new WebStorageConnectionException( $"Failed to perform {method} request. Reason: {(int)statusCode} {statusCode}", httpReqExp); } catch (Exception exp) { throw new WebStorageException($"Failed to perform {method} request.", exp); } return responseArr; } private string GetHostUriString(string host) { string scheme = _secure ? "https://" : "http://"; string port = _secure ? "16993" : "16992"; if (Uri.CheckHostName(host) == UriHostNameType.IPv6) { return scheme + "[" + host + "]:" + port + "/" + MAIN_DIRECTORY + "/"; } return scheme + host + ":" + port + "/" + MAIN_DIRECTORY + "/"; } private Uri GetURI(string directory, string subdirectory, string fileName) { StringBuilder uri = new StringBuilder(_host.OriginalString); if (!string.IsNullOrEmpty(directory)) uri.Append(directory + "/"); if (!string.IsNullOrEmpty(subdirectory)) uri.Append(subdirectory + "/"); if (!string.IsNullOrEmpty(fileName)) uri.Append(fileName); return new Uri(uri.ToString()); } private Headers GetHeaders(string filePath, string webUILinkName) { Headers header = new Headers(); string ext = GetFileExtension(filePath); //Extension of the file //Set content type if (_contentTypeDictionary.ContainsKey(ext)) header.ContentType = _contentTypeDictionary[ext]; else throw new WebStorageException("Invalid file extension."); //Set Encoding type (if needed) if (ext.CompareTo(".gzip") == 0 || ext.CompareTo(".gz") == 0) header.ContentEncoding = "gzip"; if (!string.IsNullOrEmpty(webUILinkName)) header.Link = webUILinkName; return header; } private void SetContentTypeOptions() { if (_contentTypeDictionary == null) { _contentTypeDictionary = new Dictionary() { { ".js", "application/javascript" }, { ".jsx", "text/jscript" }, { ".css", "text/css" }, { ".htm", "text/html" }, { ".html", "text/html" }, { ".gzip", "text/html" }, { ".gz", "text/html" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, { ".png", "image/png" }, { ".bmp", "image/bmp" }, { ".gif", "image/gif" }, { ".ico", "image/x-icon" }, { ".xml", "application/xml" }, { ".xls", "application/vnd.ms-excel" }, { ".pdf", "application/pdf" }, }; } } private string GetFileExtension(string filePath) { return Path.GetExtension(filePath).ToLower(); } private string GetFileName(string filePath) { if (!System.IO.File.Exists(filePath)) throw new WebStorageException("File not found."); FileInfo fileInfo = new FileInfo(filePath); if (fileInfo.Length == 0) throw new WebStorageException("The selected file is empty."); string res = Path.GetFileName(filePath); if (res.Length > MAX_LENGTH) throw new WebStorageException("Filename, including extension, can contain only up to 11 characters."); string fileNameWOExtension = Path.GetFileNameWithoutExtension(filePath); if (!_lettersAndDigits.IsMatch(fileNameWOExtension)) throw new WebStorageException("File name can contain only letters and numbers."); return res; } private void ValidateArgument(string arg, string argName) { if (string.IsNullOrEmpty(arg)) throw new WebStorageException(argName + " name cannot be null or an empty string."); if (arg.Length > MAX_LENGTH) throw new WebStorageException(argName + " name can contain up to 11 characters."); if (arg.ToLower().CompareTo(INTEL_RESERVED) == 0) throw new WebStorageException("'Intel' namespace is reserved for Intel use only."); if (!_lettersAndDigits.IsMatch(arg)) throw new WebStorageException(argName + " name can contain only letters and numbers."); } private static bool SelfSignedCertificateCallback(X509Certificate certificate, System.Net.Security.SslPolicyErrors error) { //If certificate is self signed, ignore all errors if (certificate.Subject.Equals(certificate.Issuer)) { return true; } if (error == System.Net.Security.SslPolicyErrors.None) { return true; } return false; } #endregion //Private Functions } }