Last active
July 25, 2023 06:49
-
-
Save dylanlangston/7db8f6912010e119556914985f68dc1a to your computer and use it in GitHub Desktop.
Sample Program to show how to create/update an ImageSilo user with FTP User/Admin account
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Win32; | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.IO; | |
using System.IO.IsolatedStorage; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Security.Cryptography; // You might need to add a reference to System.Security manually | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using System.Xml; | |
using static System.String; | |
/// <summary> | |
/// This is a single file implementation of a mostly useless C# program that interacts with the PVE API Web Services. | |
/// Outside of the standard libraries referenced above all code and resources are contained here spread amoungst various classes. | |
/// This is not an example of best practices but should provide an overview of interacting with the PVE API at a low level. | |
/// | |
/// There are a bunch of test cases we run through: | |
/// - Logging in | |
/// - Create a PVE User with FTP User account | |
/// - Update PVE User to have FTP Admin Account | |
/// - Delete PVE User | |
/// - Logging out | |
/// | |
/// Written for use with ImageSilo r87+ | |
/// </summary> | |
namespace Create_FTP_User | |
{ | |
class Program | |
{ | |
/// <summary> | |
/// Determine if program should log the HTTP requests. | |
/// On by default when Debugging. This can be enabled at any time by using the /dolog flag | |
/// </summary> | |
#if DEBUG | |
private static bool doLogging = true; | |
#else | |
private static bool doLogging = false; | |
#endif | |
/// <summary> | |
/// Entry Point into the application. | |
/// </summary> | |
/// <param name="args">Command Line arguments passed to the program on startup.</param> | |
static void Main(string[] args) | |
{ | |
// Convert Args Array into List, and replace "-" with "/" to make /help and -help aliases. | |
List<string> argsList = new List<string>(); args.ToList().ForEach(arg => argsList.Add(arg.Replace("-", "/"))); | |
// If run with -help, -?, or -h display version and other possible flags. | |
if (argsList.Contains("/help", StringComparer.OrdinalIgnoreCase) || argsList.Contains("/h", StringComparer.OrdinalIgnoreCase) || argsList.Contains("/?", StringComparer.OrdinalIgnoreCase)) | |
{ | |
IO_Utilities.HelpMessage(); | |
return; | |
} | |
// Check if run with /reset flag, delete entropy. | |
if (argsList.Contains("/reset", StringComparer.OrdinalIgnoreCase)) | |
Security_Utilities.DeleteEntropy(); | |
// Check if run with /dolog flag, enable http logging. | |
if (argsList.Contains("/dolog", StringComparer.OrdinalIgnoreCase)) | |
doLogging = true; | |
// Setup PVE API Interfaces | |
PVE_HTTPInterface httpInterface = new PVE_HTTPInterface(doLogging); | |
// Create URI, EntityID, and loginRequest variable to populate and reuse | |
Uri uri = null; | |
string entityID = null; | |
XmlDocument loginRequest = null; | |
// Try create login request with Token auth, fall back to username and password. | |
try | |
{ | |
// Check if entropy (randomly generated encryption key) exists and try to use to decrypt Token | |
if (!Security_Utilities.EntropyExist(out byte[] entropy)) | |
throw new System.ArgumentNullException("entropy", "Unable to find entropy."); | |
IO_Utilities.WriteTimestampedMessage("Detected Token Auth."); | |
uri = new Uri(Security_Utilities.GetCred(entropy, "URI")); | |
entityID = Security_Utilities.GetCred(entropy, "ENT"); | |
// Write LoginToken Request | |
loginRequest = XML_Utilities.BuildPVERequest("LoginToken", new Dictionary<string, object>() { | |
{ "EntityID", entityID }, | |
{ "TokenID", Security_Utilities.GetCred(entropy, "TID") ?? throw new System.ArgumentNullException("TokenID", "Unable to find tokenid.") }, | |
{ "TokenCode", Security_Utilities.GetCred(entropy, "TCD") ?? throw new System.ArgumentNullException("TokenCode", "Unable to find tokencode.")}, | |
{ "SourceIP", null }, | |
{ "CoreLicType", null }, | |
{ "ClientPingable", false } | |
}); | |
} | |
catch (Exception e) | |
{ | |
if (e is ArgumentNullException || e is UriFormatException || e is CryptographicException) | |
IO_Utilities.WriteTimestampedMessage("Unable to get token. Falling back to manual auth."); | |
else | |
throw e; // Throw any unexpected excpetions instead of handling them. | |
} | |
finally | |
{ | |
// Fallback behavior, manually enter credentials to login with | |
if (loginRequest == null) | |
{ | |
IO_Utilities.WriteTimestampedMessage("Prompting for any missing credentials."); | |
uri = new Uri("https://login.imagesilo.com"); | |
entityID = IO_Utilities.CheckForArgs(args, "/ent", "Entity ID", @"\d+"); | |
// Write LoginUserEx4 Request | |
loginRequest = XML_Utilities.BuildPVERequest("LoginUserEx4", new Dictionary<string, object>() { | |
{ "EntityID", entityID }, | |
{ "Username", IO_Utilities.CheckForArgs(args, "/user", "Username", @"[\w\d\s]+") }, // Get Username | |
{ "Password", IO_Utilities.CheckForArgs(args, "/pass", "Password", @"[\w\d\s]+|") }, // Get Password | |
{ "SourceIP", null }, | |
{ "CoreLicType", null }, | |
{ "ClientPingable", false }, | |
{ "RemoteAuth", false }, | |
{ "RequestToken", true }, | |
{ "AppIdent", Process.GetCurrentProcess().ProcessName }, | |
{ "MFACode", IO_Utilities.CheckForArgs(args, "/mfa", "MFA Code", @"[\w\d\s]+|") } | |
}); | |
} | |
} | |
// Login using HTTPInterface | |
Task<XmlDocument> login = httpInterface.CallHTTPInterface(uri, loginRequest); | |
login.Wait(); | |
// Check login was successful | |
if (!httpInterface.CheckLoginSuccess(login.Result)) | |
{ | |
IO_Utilities.WriteTimestampedMessage("Error Logging In."); | |
IO_Utilities.WriteLineAndReadLine(XML_Utilities.BeautifyXML(login.Result)); | |
return; | |
} | |
else | |
IO_Utilities.WriteTimestampedMessage("Logged In."); | |
// Get Session ID to reuse | |
string sessionID = login.Result.GetTag("SessionID"); | |
// Check logged in under Entity Admin account as those permissions are required to continue. | |
if (login.Result.GetTag("EntityAdminSystem") != "1") | |
{ | |
IO_Utilities.WriteTimestampedMessage("Not logged in as Entity Admin, Exiting."); | |
Logout(); | |
return; | |
} | |
// Check for token in the login response, if present save credentials for future use. | |
try | |
{ | |
XmlDocument UserToken = new XmlDocument(); | |
UserToken.LoadXml("<UserToken>" + login.Result.GetTag("UserToken") + "</UserToken>"); | |
byte[] newEntropy = Security_Utilities.GetNewEntropy(); | |
Security_Utilities.SaveCred(newEntropy, "ENT", entityID); | |
Security_Utilities.SaveCred(newEntropy, "URI", uri.ToString()); | |
Security_Utilities.SaveCred(newEntropy, "TID", UserToken.GetTag("TokenID")); | |
Security_Utilities.SaveCred(newEntropy, "TCD", UserToken.GetTag("TokenCode")); | |
Security_Utilities.SaveEntropy(newEntropy); | |
IO_Utilities.WriteTimestampedMessage("Saved Token for future use."); | |
} | |
catch (System.ArgumentException) { } // Handle missing token gracefully and do nothing | |
// Username to create | |
string userName = "Test User"; | |
// Create a user with an FTP User Account | |
Task<XmlDocument> aDAddUserEx3 = httpInterface.CallHTTPInterface(uri, XML_Utilities.BuildPVERequest("ADAddUserEx3", new Dictionary<string, object>() { | |
{ "EntityID", entityID }, | |
{ "SessionID", sessionID }, | |
{ "Username", userName }, | |
{ "Fullname", null }, | |
{ "Password", "foobar" }, | |
{ "ExpirePWD", 0 }, | |
{ "ChangePWD", 1 }, | |
{ "UserType", 0 }, | |
{ "Email", null }, | |
{ "UserSettings", System.Security.SecurityElement.Escape(@"<?xml version=""1.0"" encoding=""UTF-8""?><PVDM_UserSettings><USERRIGHTS><PASSWORDNEVEREXPIRES>0</PASSWORDNEVEREXPIRES><CHANGEPASSWORD>1</CHANGEPASSWORD></USERRIGHTS><DISABLED>0</DISABLED><FTP_RIGHTS>1</FTP_RIGHTS></PVDM_UserSettings>") }, // FTP_RIGHTS 1 = FTP User | |
{ "Notifications", -1 } | |
})); | |
aDAddUserEx3.Wait(); | |
// Check creating a user was successful | |
if (httpInterface.CheckForError(aDAddUserEx3.Result)) | |
{ | |
IO_Utilities.WriteTimestampedMessage("Error Creating User."); | |
IO_Utilities.WriteLineAndReadLine(XML_Utilities.BeautifyXML(aDAddUserEx3.Result)); | |
Logout(); | |
return; | |
} | |
else | |
IO_Utilities.WriteTimestampedMessage("User Created with FTP User Account."); | |
// Save UserID for reuse | |
int userId = Int32.Parse(aDAddUserEx3.Result.GetTag("NewUserId")); | |
// Update User account so it has a FTP Account | |
Task<XmlDocument> aDUpdateUserEx4 = httpInterface.CallHTTPInterface(uri, XML_Utilities.BuildPVERequest("ADUpdateUserEx4", new Dictionary<string, object>() { | |
{ "EntityID", entityID }, | |
{ "SessionID", sessionID }, | |
{ "UserId", userId }, | |
{ "Fullname", null }, | |
{ "ChangePWD", 1 }, | |
{ "ExpirePWD", 0 }, | |
{ "UserType", 0 }, | |
{ "Email", null }, | |
{ "UserSettings", System.Security.SecurityElement.Escape(@"<?xml version=""1.0"" encoding=""UTF-8""?><PVDM_UserSettings><USERRIGHTS><PASSWORDNEVEREXPIRES>0</PASSWORDNEVEREXPIRES><CHANGEPASSWORD>1</CHANGEPASSWORD></USERRIGHTS><DISABLED>0</DISABLED><FTP_RIGHTS>2</FTP_RIGHTS></PVDM_UserSettings>") }, // FTP_RIGHTS 2 = FTP Admin | |
{ "Notifications", -1 } | |
})); | |
aDUpdateUserEx4.Wait(); | |
// Check updating user was successful | |
if (httpInterface.CheckForError(aDUpdateUserEx4.Result)) | |
{ | |
IO_Utilities.WriteTimestampedMessage("Error Updating User to have FTP Admin account."); | |
IO_Utilities.WriteLineAndReadLine(XML_Utilities.BeautifyXML(aDUpdateUserEx4.Result)); | |
} | |
else | |
IO_Utilities.WriteTimestampedMessage("User Updating to have FTP Admin."); | |
// Allow time to check user account was created correct. | |
IO_Utilities.WriteLineAndReadLine("Please review accounts in ImageSilo. Press ENTER when done to delete user account."); | |
// Delete User | |
Task<XmlDocument> aaDDeleteUsers = httpInterface.CallHTTPInterface(uri, XML_Utilities.BuildPVERequest("ADDeleteUsers", new Dictionary<string, object>() { | |
{ "EntityID", entityID }, | |
{ "SessionID", sessionID }, | |
{ "SourceIP", null }, | |
{ "NameList", userName }, | |
{ "UserIDList", userId } | |
})); | |
aaDDeleteUsers.Wait(); | |
// Check deleting user was successful | |
if (httpInterface.CheckForError(aaDDeleteUsers.Result)) | |
{ | |
IO_Utilities.WriteTimestampedMessage("Error deleting user."); | |
IO_Utilities.WriteLineAndReadLine(XML_Utilities.BeautifyXML(aaDDeleteUsers.Result)); | |
} | |
else | |
IO_Utilities.WriteTimestampedMessage("User Deleted."); | |
// Logout of PVE | |
void Logout() | |
{ | |
// Logout using HTTPInterface | |
Task<XmlDocument> logout = httpInterface.CallHTTPInterface(uri, XML_Utilities.BuildPVERequest("KillSession", new Dictionary<string, object>() { | |
{ "EntityID", entityID }, | |
{ "SessionID", sessionID }, | |
{ "SourceIP", null } | |
})); | |
logout.Wait(); | |
// Check login was successful | |
if (!httpInterface.CheckLogoutSuccess(logout.Result)) | |
{ | |
IO_Utilities.WriteTimestampedMessage("Error logging out."); | |
IO_Utilities.WriteLineAndReadLine(XML_Utilities.BeautifyXML(logout.Result)); | |
return; | |
} | |
else | |
IO_Utilities.WriteTimestampedMessage("Logged Out."); | |
IO_Utilities.WriteLineAndReadLine("\nPress ENTER key to exit."); | |
} | |
Logout(); | |
} | |
} | |
/// <summary> | |
/// Class with methods to communicate with and parse the responses from the HTTPInterface.asp | |
/// </summary> | |
class PVE_HTTPInterface : HTTP_Service | |
{ | |
public PVE_HTTPInterface(bool logging = false) : base(logging) { } | |
/// <summary> | |
/// Sanatize URI to point to HTTPInterface. | |
/// </summary> | |
/// <param name="uri">PVE Host Server</param> | |
/// <returns>Uri that points to HTTPInterface</returns> | |
internal override Uri SanatizeURI(Uri uri) | |
{ | |
return new Uri(uri.Scheme + "://" + uri.Host + (uri.IsDefaultPort ? "" : ":" + uri.Port) + "/httpinterface.asp"); | |
} | |
/// <summary> | |
/// Send request to HTTPInterface | |
/// </summary> | |
/// <param name="uri">URI of the PVE server</param> | |
/// <param name="XML">XmlDocument request to send</param> | |
/// <returns>XmlDocument of the response</returns> | |
public async Task<XmlDocument> CallHTTPInterface(Uri uri, XmlDocument XML) | |
{ | |
return (await base.CallService(uri, uri.ToString(), "text/xml", XML) as XmlDocument) ?? XmlError(); | |
XmlDocument XmlError() { XmlDocument x = new XmlDocument(); x.LoadXml(errors.GetXmlString()); x.InnerXml = x.InnerXml.Replace("exception>", "HttpInterface-Error>"); return x; }; | |
} | |
/// <summary> | |
/// Check that login request was successful | |
/// </summary> | |
/// <param name="response">XmlDocument response to check</param> | |
/// <returns>true if logged in successfully</returns> | |
public bool CheckLoginSuccess(XmlDocument response) | |
{ | |
if (response.DocumentElement.SelectSingleNode("//RESULTVAL") != null) | |
return response.DocumentElement.SelectSingleNode("//RESULTVAL").InnerText == "0"; | |
else | |
return false; | |
} | |
/// <summary> | |
/// Check that logout request was successful | |
/// </summary> | |
/// <param name="response">XmlDocument response to check</param> | |
/// <returns>true if logged out successfully</returns> | |
public bool CheckLogoutSuccess(XmlDocument response) | |
{ | |
if (response.DocumentElement.SelectSingleNode("/PVDM_KillSession/GOODSESSION") != null) | |
return response.DocumentElement.SelectSingleNode("//GOODSESSION").InnerText == "1"; | |
else | |
return false; | |
} | |
/// <summary> | |
/// Check for error in response XmlDocument | |
/// </summary> | |
/// <param name="response">XmlDocument response to check for errors</param> | |
/// <returns>true if error exists</returns> | |
public bool CheckForError(XmlDocument response) | |
{ | |
return response.DocumentElement.SelectNodes("//ERROR").Count > 0 || response.DocumentElement.SelectNodes("//HttpInterface-Error").Count > 0; | |
} | |
} | |
/// <summary> | |
/// Abstract Class that contains common logging code that all the services use. | |
/// </summary> | |
internal abstract class HTTP_Service | |
{ | |
/// <summary> | |
/// This stores that last exception to be referenced if you need to check the last error. | |
/// </summary> | |
internal Exception errors = null; | |
/// <summary> | |
/// Track if logging is enabled | |
/// </summary> | |
internal bool doLogging; | |
/// <summary> | |
/// Default Service Constructor | |
/// </summary> | |
/// <param name="logging"></param> | |
internal HTTP_Service(bool logging = false) { doLogging = logging; } | |
/// <summary> | |
/// TODO: Santize URI to point to service | |
/// </summary> | |
/// <param name="uri">Host Server</param> | |
/// <returns>URI that points to the service</returns> | |
internal abstract Uri SanatizeURI(Uri uri); | |
/// <summary> | |
/// Generic method to call service. This handles most of the logging functionality whereas HTTP_Utilities.PostRequest handles the actual http requests. | |
/// Has some basic error handling that throws exceptions containing page responses. | |
/// </summary> | |
/// <param name="uri">Server URI</param> | |
/// <param name="soapAction">SOAPAction to use with request, can be an empty string</param> | |
/// <param name="contentType">Content type for the request</param> | |
/// <param name="body">body of the request, either an XmlDocument or string</param> | |
/// <param name="bodyIsString">specify if the body is an XmlDocument (false - default) or is a string (true)</param> | |
/// <returns>Object Response, either an XmlDocument or Stream. Returns Null on Error to be handled elsewhere.</returns> | |
internal async Task<Object> CallService(Uri uri, string soapAction, string contentType, object body) | |
{ | |
Guid id = Guid.NewGuid(); // Get Unique GUID to identify each request | |
string uriString = SanatizeURI(uri).ToString(); // Convert URI to string | |
try | |
{ | |
Type bodyType = body.GetType(); | |
if (doLogging) | |
{ | |
string logContent = String.Empty; | |
if (bodyType == typeof(XmlDocument)) | |
logContent = XML_Utilities.BeautifyXML(((XmlDocument)body)); | |
if (bodyType == typeof(string)) | |
logContent = (string)body; | |
if (bodyType == typeof(MultipartFormDataContent)) | |
logContent = Join("\n", ((MultipartFormDataContent)body).ToArray().Select(c => c.Headers.ToArray().Select(h => h.Key + Join("\n", h.Value)) + c.ReadAsStringAsync().Result)); | |
IO_Utilities.Logging(" - " + uriString + " Request: " + id.ToString() + "\n" + logContent); | |
} | |
// Post Request | |
HttpResponseMessage response = null; | |
string bodyString = String.Empty; | |
if (bodyType == typeof(XmlDocument)) | |
{ | |
bodyString = ((XmlDocument)body).OuterXml; | |
response = await HTTP_Utilities.PostRequest(uriString, soapAction, contentType, bodyString); | |
} | |
if (bodyType == typeof(string)) | |
{ | |
bodyString = (string)body; | |
response = await HTTP_Utilities.PostRequest(uriString, soapAction, contentType, bodyString); | |
} | |
if (bodyType == typeof(MultipartFormDataContent)) | |
{ | |
response = await HTTP_Utilities.PostRequest(uriString, (MultipartFormDataContent)body); | |
} | |
// Check for Errors | |
if (!response.IsSuccessStatusCode) | |
throw new HttpRequestException((int)response.StatusCode + " " + response.ReasonPhrase + Environment.NewLine + (await response.Content?.ReadAsStringAsync() ?? "")); | |
response.EnsureSuccessStatusCode(); | |
// Determine response type and parse | |
Object responseContent; | |
if (response.Content.Headers.ContentType.MediaType == "text/xml" || response.Content.Headers.ContentType.MediaType == "text/html") | |
{ | |
try | |
{ | |
responseContent = new XmlDocument(); | |
((XmlDocument)responseContent).LoadXml(await response.Content.ReadAsStringAsync()); | |
} | |
catch (XmlException) | |
{ | |
throw new HttpRequestException((int)response.StatusCode + " " + response.ReasonPhrase + Environment.NewLine + (await response.Content?.ReadAsStringAsync() ?? "")); | |
} | |
if (doLogging) | |
IO_Utilities.Logging(" - " + uriString + " Response: " + id.ToString() + "\n" + XML_Utilities.BeautifyXML(((XmlDocument)responseContent))); | |
} | |
else | |
{ | |
responseContent = await response.Content.ReadAsStreamAsync(); | |
if (doLogging) | |
IO_Utilities.Logging(" - " + uriString + " Response: " + id.ToString() + "\nStreamed Data."); | |
} | |
return responseContent; | |
} | |
catch (Exception e) | |
{ | |
// Handle errors | |
errors = new Exception("Uri: " + uriString, e); | |
if (doLogging) | |
IO_Utilities.Logging(" - " + uriString + " Error in Response: " + id.ToString() + "\n" + e.GetXmlString()); | |
return null; | |
} | |
} | |
/// <summary> | |
/// Generic Method to get file as stream. | |
/// This handles most of the logging functionality whereas HTTP_Utilities.GetFileStream handles the actual http requests. | |
/// Has some basic error handling. | |
/// </summary> | |
/// <param name="Url">Url of the file to get a stream of</param> | |
/// <returns>Stream of the file</returns> | |
public Task<Stream> GetFile(string Url) | |
{ | |
try | |
{ | |
if (doLogging) | |
IO_Utilities.Logging(" - Downloaded file:\n" + Url); | |
return HTTP_Utilities.GetFileStream(Url); | |
} | |
catch (Exception e) | |
{ | |
// Handle errors | |
errors = new Exception("Uri: " + Url, e); | |
if (doLogging) | |
IO_Utilities.Logging(" - Error Downloading:\n" + Url); | |
return null; | |
} | |
} | |
} | |
/// <summary> | |
/// Utility methods to send/recieve http requests | |
/// </summary> | |
class HTTP_Utilities | |
{ | |
/// <summary> | |
/// Hard coded timeout for the request. | |
/// </summary> | |
private static readonly int timeout = 120; // 2 Minutes | |
/// <summary> | |
/// Takes the XML body and posts it to the specified Uri with soap header. Simple way to send SOAP request. | |
/// </summary> | |
/// <param name="Url">Server URL</param> | |
/// <param name="xmlString">XML String to Post</param> | |
/// <returns></returns> | |
public static async Task<HttpResponseMessage> PostRequest(string Url, string soapAction, string contentType, string body) | |
{ | |
var httpContent = new StringContent(body, Encoding.UTF8, contentType); | |
httpContent.Headers.Add("SOAPAction", soapAction); | |
return await PostRequest(Url, httpContent); | |
} | |
public static async Task<HttpResponseMessage> PostRequest(string Url, HttpContent httpContent) | |
{ | |
using (var httpClient = new HttpClient()) | |
{ | |
httpClient.Timeout = TimeSpan.FromSeconds(timeout); | |
return await httpClient.PostAsync(Url, httpContent); | |
} | |
} | |
/// <summary> | |
/// Get File Stream | |
/// </summary> | |
/// <param name="Url">Uri of file to get stream of</param> | |
/// <returns>Stream of file</returns> | |
public static async Task<Stream> GetFileStream(string Url) | |
{ | |
using (var httpClient = new HttpClient()) | |
{ | |
httpClient.Timeout = TimeSpan.FromSeconds(timeout); | |
var file = await httpClient.GetAsync(Url); | |
file.EnsureSuccessStatusCode(); | |
return await file.Content.ReadAsStreamAsync(); | |
} | |
} | |
} | |
/// <summary> | |
/// Utility methods to create/parse XML | |
/// </summary> | |
class XML_Utilities | |
{ | |
/// <summary> | |
/// Build a PVE API Request XmlDocument semi-programatically. | |
/// </summary> | |
/// <param name="method">Method name to execute</param> | |
/// <param name="parameters">A dictonary containing a string and object that correspond to the paramater name and value.</param> | |
/// <returns>XmlDocument httpinterface reqeust</returns> | |
public static XmlDocument BuildPVERequest(string method, Dictionary<string, object> parameters) | |
{ | |
XmlDocument doc = new XmlDocument(); | |
StringBuilder xmlString = new StringBuilder("<PVDM_" + method + "><FUNCTION><NAME>" + method.ToUpper() + "</NAME><PARAMETERS>"); | |
foreach (var param in parameters) | |
xmlString.Append($"<{param.Key.ToUpper()}>{param.Value}</{param.Key.ToUpper()}>"); | |
xmlString.Append("</PARAMETERS></FUNCTION></PVDM_" + method + ">"); | |
doc.LoadXml(xmlString.ToString()); | |
return doc; | |
} | |
/// <summary> | |
/// Format an XML document for display. A little slow due to overhead of using regex. | |
/// </summary> | |
/// <param name="document">XmlDocument to beautify</param> | |
/// <param name="removePassword">bool specifying if the password should be removed from the XML, default true.</param> | |
/// <returns>Formatted XML as a string.</returns> | |
public static string BeautifyXML(XmlDocument document, bool removePassword = true) | |
{ | |
string result; | |
MemoryStream mStream = new MemoryStream(); | |
XmlTextWriter writer = new XmlTextWriter(mStream, Encoding.Unicode); | |
// Try and read the XML string as XML stream. Then write stream back to string with indentation. | |
try | |
{ | |
writer.Formatting = Formatting.Indented; | |
document.WriteContentTo(writer); | |
writer.Flush(); | |
mStream.Flush(); | |
mStream.Position = 0; | |
StreamReader sReader = new StreamReader(mStream); | |
string formattedXml = sReader.ReadToEnd(); | |
// Remove Header | |
result = Regex.Replace(formattedXml, "<\\?xml version=\"1.0\".*\\?>" + System.Environment.NewLine, ""); | |
// Remove Password is specified | |
if (removePassword) | |
{ | |
result = Regex.Replace(result, "<PASSWORD>.*</PASSWORD>", "<PASSWORD>REDACTED</PASSWORD>"); | |
result = Regex.Replace(result, "<TOKENCODE>.*</TOKENCODE>", "<TOKENCODE>REDACTED</TOKENCODE>"); | |
} | |
} | |
// Handle Exceptions. | |
catch (Exception e) { mStream.Close(); writer.Close(); return e.GetXmlString(); } | |
mStream.Close(); | |
writer.Close(); | |
return result; | |
} | |
} | |
/// <summary> | |
/// Utility method to securely store credentials (token) for reuse | |
/// </summary> | |
class Security_Utilities | |
{ | |
/// <summary> | |
/// Name of the registry to save the entropy as. | |
/// </summary> | |
private static readonly string entropyKeyName = "entropy"; | |
/// <summary> | |
/// Check that Entropy Exists in user registry | |
/// </summary> | |
/// <param name="outputEntropy">the byte array of entropy to output</param> | |
/// <returns>bool indicating if the entropy exists in the registry</returns> | |
public static bool EntropyExist(out byte[] outputEntropy) | |
{ | |
// Check for the Entropy key in windows registry | |
outputEntropy = IO_Utilities.ReadRegistry(entropyKeyName); | |
if (outputEntropy.Length == 0) { return false; } | |
return true; | |
} | |
/// <summary> | |
/// Generate entropy using RNGCryptoServiceProvider. Used to encrypt date using the SaveCred method | |
/// </summary> | |
/// <returns>byte array of entropy</returns> | |
public static byte[] GetNewEntropy() | |
{ | |
byte[] entropy = new byte[20]; | |
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) | |
rng.GetBytes(entropy); | |
return entropy; | |
} | |
/// <summary> | |
/// Save entropy to Registry | |
/// </summary> | |
/// <param name="Entropy">byte array of entropy</param> | |
public static void SaveEntropy(byte[] Entropy) | |
{ | |
IO_Utilities.WriteRegistry(entropyKeyName, Entropy); // Write Entropy to registry | |
} | |
/// <summary> | |
/// Delete entropy to Registry | |
/// </summary> | |
public static void DeleteEntropy() | |
{ | |
IO_Utilities.WriteRegistry(entropyKeyName, new byte[0]); // Write Entropy to registry | |
} | |
/// <summary> | |
/// Get decrypted credential as string using the supplied entropy | |
/// </summary> | |
/// <param name="entropy">entropy used to decrypt credential</param> | |
/// <param name="Credential">Name of credential to get</param> | |
/// <returns>decrypted string of the requested credential</returns> | |
public static string GetCred(byte[] entropy, string Credential) | |
{ | |
string value = IO_Utilities.ReadIsolatedStorage(Credential); // Get cipher username | |
if (string.IsNullOrEmpty(value)) // Check value isn't blank | |
return null; | |
byte[] cipherValue = value.StringToBytes(); // Get cipher value bytes | |
byte[] valueBytes = ProtectedData.Unprotect(cipherValue, entropy, DataProtectionScope.CurrentUser); // De-cipher | |
return Encoding.Default.GetString(valueBytes); | |
} | |
/// <summary> | |
/// Encrypt and Save Credential to IsolatedStorage | |
/// </summary> | |
/// <param name="entropy">entropy to encrypt credential with</param> | |
/// <param name="Credential">Name of the credential</param> | |
/// <param name="Value">Text value to encrypt and save</param> | |
public static void SaveCred(byte[] entropy, string Credential, string Value) | |
{ | |
string cipheredValue = BitConverter.ToString(ProtectedData.Protect(Encoding.Default.GetBytes(Value), entropy, DataProtectionScope.CurrentUser)); // cipher value | |
IO_Utilities.WriteIsolatedStorage(Credential, cipheredValue); // Write to isolatedstorage | |
} | |
} | |
/// <summary> | |
/// Utility methods to handle Input/Output related tasks. | |
/// </summary> | |
class IO_Utilities | |
{ | |
/// <summary> | |
/// Runs Console.WriteLine() using the supplied text and returns the output of Console.ReadLine() | |
/// </summary> | |
/// <param name="text">Message to write</param> | |
/// <returns>output of Console.ReadLine()</returns> | |
public static string WriteLineAndReadLine(string text) | |
{ | |
Console.WriteLine(text); return Console.ReadLine(); | |
} | |
/// <summary> | |
/// Write message to console with timestamp | |
/// </summary> | |
/// <param name="text">Message to write</param> | |
public static void WriteTimestampedMessage(string text) | |
{ | |
Console.WriteLine(DateTime.Now.ToString() + " - " + text); | |
} | |
/// <summary> | |
/// Help Message that specifies what command line arguments are valid. | |
/// </summary> | |
public static void HelpMessage() | |
{ | |
string Repeater(char c, int n) { n = n > 0 ? n : 0; return new String(c, n); } | |
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly(); | |
FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location); | |
string version = fvi.FileVersion; | |
string process = Process.GetCurrentProcess().ProcessName; | |
Console.WriteLine(@"-------------------------------------------------------------------- | |
| " + process + " - Version: " + version + (Repeater(' ', (5 - version.Length - process.Length))) + @"| | |
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | | |
| Run with '/reset' to reset previous token auth. | | |
| (This will not revoke the token from PVE.) | | |
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | | |
| Run with '/dolog' to write all HTTP request/response to the log. | | |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -| | |
| '/ent' followed by the entity id. | | |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -| | |
| '/user' followed by the username to login with. | | |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -| | |
| '/pass' followed by the password. | | |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -| | |
| '/mfa' followed by the mfa code to use when logging in. | | |
--------------------------------------------------------------------"); | |
} | |
/// <summary> | |
/// Write string to log file | |
/// </summary> | |
/// <param name="log">string to write</param> | |
public static void Logging(string log) | |
{ | |
string message = "Timestamp - " + DateTime.Now.ToString() + log + Environment.NewLine + Environment.NewLine; | |
WriteTextToFileAsync(Process.GetCurrentProcess().ProcessName.ToLower().Replace(" ", "-") + "-http.log", message); | |
} | |
/// <summary> | |
/// Write text to a file, meant to be used with multiple async processes accessing the same file | |
/// | |
/// Writes to the executing folder or Desktop depending on permissions. | |
/// </summary> | |
/// <param name="fileName"></param> | |
/// <param name="text"></param> | |
public static void WriteTextToFileAsync(string fileName, string text) | |
{ | |
string f = null; | |
try | |
{ | |
// Write to Executing folder | |
f = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); | |
CreateFolder(f); | |
} | |
catch | |
{ | |
// Write to Desktop | |
f = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), fileName); | |
CreateFolder(f); | |
} | |
finally | |
{ | |
// Attempt to write to the file, on failure retry up to 10 times. | |
int counter = 0; | |
Random r = new Random(); | |
while (counter < 10) | |
{ | |
counter++; | |
try | |
{ | |
using (FileStream file = new FileStream(f, FileMode.Append, FileAccess.Write, FileShare.Write)) | |
using (StreamWriter stream = new System.IO.StreamWriter(file)) | |
stream.Write(text); | |
break; | |
} | |
catch | |
{ | |
Thread.Sleep(r.Next(100, 1000)); // Sleep a random amount of time before attempting to re-write | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// Save a stream to a file. | |
/// | |
/// Writes to the executing folder or Desktop depending on permissions. | |
/// </summary> | |
/// <param name="filename">file name to save file</param> | |
/// <param name="stream">stream to save</param> | |
public static void SaveStream(string filename, Stream stream) | |
{ | |
string f = null; | |
try | |
{ | |
// Write to Executing folder | |
f = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filename); | |
CreateFolder(f); | |
} | |
catch | |
{ | |
// Write to Desktop | |
f = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), filename); | |
CreateFolder(f); | |
} | |
finally | |
{ | |
using (Stream s = File.Create(f)) | |
stream.CopyTo(s); | |
} | |
} | |
/// <summary> | |
/// Try to create a folder for the specified file. | |
/// </summary> | |
/// <param name="filename">Filename to create folder for.</param> | |
public static void CreateFolder(string filename) | |
{ | |
if (!Directory.Exists(Path.GetDirectoryName(filename))) | |
Directory.CreateDirectory(Path.GetDirectoryName(filename)); | |
} | |
/// <summary> | |
/// Check for arguments in array and prompt if they aren't present | |
/// </summary> | |
/// <param name="args">string array of arguments passed to the program</param> | |
/// <param name="argument">string argument to search for</param> | |
/// <param name="name">string display name of the argument, used when prompting for missing arguments</param> | |
/// <param name="regex">regex to validate against.</param> | |
/// <param name="value">regex to validate against.</param> | |
/// <returns>The string value for the requested argument</returns> | |
public static string CheckForArgs(string[] args, string argument, string name, string regex) | |
{ | |
void ValidateArgs(ref string val, string n, string reg) | |
{ | |
Regex r = new Regex("^" + reg + "$"); | |
if (!(r.IsMatch(val))) | |
while (!(r.IsMatch(val))) | |
{ | |
Console.WriteLine($"Invalid {n} - '{val}'"); | |
Console.Write($"Type your {n}: "); | |
val = Console.ReadLine(); | |
} | |
} | |
string value; | |
if (args.Contains(argument, StringComparer.OrdinalIgnoreCase)) | |
{ | |
value = Regex.Match("" + String.Concat(args) + "", $"({argument} |{argument})({regex})", RegexOptions.IgnoreCase).Groups[2].Value; | |
ValidateArgs(ref value, name, regex); | |
} | |
else | |
{ | |
Console.Write($"Type your {name}: "); | |
value = Console.ReadLine(); | |
ValidateArgs(ref value, name, regex); | |
} | |
return value; | |
} | |
/// <summary> | |
/// Write byte array key to registry key | |
/// </summary> | |
/// <param name="name">Key name to save</param> | |
/// <param name="value">Key value to safe</param> | |
public static void WriteRegistry(string name, byte[] value) | |
{ | |
RegistryKey key = Registry.CurrentUser.OpenSubKey("Software", true); | |
key.CreateSubKey(Process.GetCurrentProcess().ProcessName.Replace(" ", "")); | |
key = key.OpenSubKey(Process.GetCurrentProcess().ProcessName.Replace(" ", ""), true); | |
key.SetValue(name, value); // Write registry Key | |
key.Close(); | |
} | |
/// <summary> | |
/// Read registry key value | |
/// </summary> | |
/// <param name="keyName">string name for the registry key to read</param> | |
/// <returns>byte array contents</returns> | |
public static byte[] ReadRegistry(string keyName) | |
{ | |
RegistryKey key = Registry.CurrentUser.OpenSubKey("Software", false); | |
key = key.OpenSubKey(Process.GetCurrentProcess().ProcessName.Replace(" ", ""), false); | |
byte[] valueExist = key != null ? key.GetValue(keyName) != null ? (byte[])key.GetValue(keyName) : new byte[0] : new byte[0]; // Return byte array if it exists, otherwise return empty array. | |
if (key != null) { key.Close(); } | |
return valueExist; | |
} | |
/// <summary> | |
/// Write string to file in progrom IsolatedStorage store | |
/// </summary> | |
/// <param name="fileName">String filename to safe</param> | |
/// <param name="value">String contents of the file</param> | |
public static void WriteIsolatedStorage(string fileName, string value) | |
{ | |
IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null); | |
if (isoStore.FileExists(fileName)) // If File already exists... | |
isoStore.DeleteFile(fileName); // Delete it. | |
using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(fileName, FileMode.CreateNew, isoStore)) | |
using (StreamWriter writer = new StreamWriter(isoStream)) | |
writer.WriteLine(value); // Write value to file. | |
} | |
/// <summary> | |
/// Read contents of file stored in program IsolatedStorage store | |
/// </summary> | |
/// <param name="filename">String name of the file to read</param> | |
/// <returns>String contents of file</returns> | |
public static string ReadIsolatedStorage(string filename) | |
{ | |
IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null); | |
if (isoStore.FileExists(filename)) | |
using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(filename, FileMode.Open, isoStore)) | |
using (StreamReader reader = new StreamReader(isoStream)) | |
return reader.ReadToEnd(); // Return requested file contents | |
return ""; | |
} | |
} | |
/// <summary> | |
/// Various extension methods, no particular order. | |
/// </summary> | |
public static class Extensions | |
{ | |
/// <summary> | |
/// Extract the value of a specific tag from the XmlDocument | |
/// </summary> | |
/// <param name="doc">XmlDocument to parse</param> | |
/// <param name="tagName">Tag to extract the value from</param> | |
/// <param name="tagUppercase">Bool which specifies if the tag gets converted to uppercase or not. Default true.</param> | |
/// <returns>String of the inner XML</returns> | |
public static string GetTag(this XmlDocument doc, string tagName, bool tagUppercase = true) | |
{ | |
tagName = tagUppercase ? tagName.ToUpper() : tagName; | |
try | |
{ | |
return doc.DocumentElement.SelectSingleNode("/" + tagName)?.InnerXml ?? doc.DocumentElement.SelectSingleNode("//" + tagName).InnerXml; | |
} | |
catch (System.NullReferenceException) | |
{ | |
throw new System.ArgumentException("Error: Unable to get Tag '" + tagName + "'.\n" + doc.OuterXml); | |
} | |
} | |
/// <summary> | |
/// Undo BitConverter.ToString(); | |
/// </summary> | |
/// <param name="String">String output from BitConverter.ToString()</param> | |
/// <returns>Byte array</returns> | |
public static byte[] StringToBytes(this string String) | |
{ | |
byte[] array = String.Replace("\n", String.Empty).Replace("\r", String.Empty).Split('-').Select(b => Convert.ToByte(b, 16)).ToArray(); | |
return array; | |
} | |
/// <summary> | |
/// Write Exceptions as XML. | |
/// </summary> | |
/// <param name="exception">The exception</param> | |
/// <returns>Exception as a string of XML</returns> | |
public static string GetXmlString(this Exception exception) | |
{ | |
void WriteException(XmlWriter writer, string name, Exception ex) | |
{ | |
if (ex == null) return; | |
writer.WriteStartElement(name); | |
writer.WriteElementString("Message", ex.Message); | |
writer.WriteElementString("Source", ex.Source); | |
WriteException(writer, "InnerException", ex.InnerException); | |
writer.WriteEndElement(); | |
} | |
if (exception == null) throw new ArgumentNullException("exception"); | |
StringWriter sw = new StringWriter(); | |
XmlWriterSettings settings = new XmlWriterSettings() | |
{ | |
OmitXmlDeclaration = true, | |
ConformanceLevel = ConformanceLevel.Fragment, | |
CloseOutput = false | |
}; | |
using (XmlWriter xw = XmlWriter.Create(sw, settings)) | |
WriteException(xw, "exception", exception); | |
return sw.ToString(); | |
} | |
/// <summary> | |
/// Create new random string of specified length | |
/// </summary> | |
/// <param name="random">random method to use</param> | |
/// <param name="length">length of string to create</param> | |
/// <returns>random string of specified length</returns> | |
public static string RandomString(this Random random, int length) | |
{ | |
StringBuilder strbuilder = new StringBuilder(); | |
for (int i = 0; i < length; i++) | |
{ | |
// Generate floating point numbers | |
double myFloat = random.NextDouble(); | |
// Generate the char | |
var myChar = Convert.ToChar(Convert.ToInt32(Math.Floor(25 * myFloat) + 65)); | |
strbuilder.Append(myChar); | |
} | |
return strbuilder.ToString().ToLower(); | |
} | |
/// <summary> | |
/// Replace the first occurence of text in a string with specified string. | |
/// </summary> | |
/// <param name="text">The string to modify</param> | |
/// <param name="search">the string to search for</param> | |
/// <param name="replace">the string to replace with</param> | |
/// <returns>the new string</returns> | |
public static string ReplaceFirst(this string text, string search, string replace) | |
{ | |
int p = text.IndexOf(search); | |
if (p < 0) | |
return text; | |
return text.Substring(0, p) + replace + text.Substring(p + search.Length); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment