Skip to content

Instantly share code, notes, and snippets.

@dylanlangston
Last active July 25, 2023 06:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dylanlangston/7db8f6912010e119556914985f68dc1a to your computer and use it in GitHub Desktop.
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
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