614 lines
23 KiB
C#

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<string, string> _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);
}
}
/// <returns> new HttpClientHandler that fits current state of the WebStorageConnection </returns>
/// <remarks> HttpClientHandler should either be disposed explicitly or fed to an HttpClient -
/// it will then be disposed by the HttpClient when the HttpClient is disposed </remarks>
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<byte[]> 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<string, string>()
{
{ ".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
}
}