614 lines
23 KiB
C#
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
|
|
}
|
|
} |