Add IPMonitor

This commit is contained in:
2025-04-27 00:33:08 -04:00
parent 11c6f7357b
commit 9ad444762a
14 changed files with 699 additions and 52 deletions

86
IPMonitor/CommandArgs.cs Normal file
View File

@@ -0,0 +1,86 @@
using System.Text;
using MarketData.Utils;
namespace IPMonitor
{
public class CommandArgs : Dictionary<String,NVP>
{
private String[] arguments = default;
public String[] GetArguments()
{
return arguments;
}
// While the commands are converted to uppercase care must be taken to preserve the case of the arguments.
public CommandArgs(String[] args)
{
arguments = args;
for(int index=1;index<args.Length;index++)
{
String command=args[index];
String[] nvp=null;
if(command.StartsWith("/"))
{
command=command.Substring(1);
nvp=command.Split(':');
if(nvp.Length>2)
{
StringBuilder sb=new StringBuilder();
for(int subIndex=1;subIndex<nvp.Length;subIndex++)
{
sb.Append(nvp[subIndex]);
if(subIndex<nvp.Length-1)sb.Append(":");
}
nvp=new String[]{nvp[0].ToUpper(),sb.ToString()};
}
}
else nvp=command.Split('=');
if(2!=nvp.Length)continue;
if(ContainsKey(nvp[0].ToUpper()))continue;
Add(nvp[0].ToUpper(),new NVP(nvp[0].ToUpper(),nvp[1]));
}
}
public bool Contains(String name)
{
return ContainsKey(name);
}
public bool Has(String commandList)
{
commandList=commandList.ToUpper();
String[] commands=commandList.Split(',');
if(0==commands.Length)return false;
foreach(String command in commands)
{
if(!ContainsKey(command))return false;
}
return true;
}
public T Get<T>(String name)
{
T result=default(T);
try {result = (T)Convert.ChangeType(this[name].Value, typeof(T));}
catch {result = default(T);}
return result;
}
public T Coalesce<T>(String name,T coalesce)
{
T result=default(T);
try
{
if(!Contains(name))result=coalesce;
else result = (T)Convert.ChangeType(this[name].Value, typeof(T));
}
catch {result = default(T);}
return result;
}
public T Coalesce<T>(String name)
{
T result=default(T);
try {result = (T)Convert.ChangeType(this[name].Value, typeof(T));}
catch {result = default(T);}
return result;
}
}
}

26
IPMonitor/Executor.cs Executable file
View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
namespace IPMonitor
{
public class Executor
{
private readonly IConfiguration _configuration;
private readonly IMainService _mainService;
private readonly IArguments _arguments;
public Executor(IMainService mainService,IConfigurationRoot configuration,IArguments arguments)
{
_configuration = configuration ?? throw new ArgumentException(nameof(configuration));
_mainService = mainService ?? throw new ArgumentException(nameof(mainService));
_arguments = arguments ?? throw new ArgumentException(nameof(arguments));
}
/// <summary>
/// This is essentially the starting point where we bootstrap the legacy entry point
/// </summary>
public void Execute()
{
_mainService.RunService(_arguments.GetArguments(), _configuration);
}
}
}

23
IPMonitor/IPMonitor.csproj Executable file
View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ipmonitor</AssemblyName>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<NoWarn>CA1416;CS8769;CS0108;CS8602;CS8601;CS8620;CS8618;CS8603;CS8767;CS8625;CS8604;CS8600;CS8604</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../MarketData/MarketDataLib/MarketDataLib.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

22
IPMonitor/IPMonitor.sln Executable file
View File

@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.21005.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPMonitor", "IPMonitor.csproj", "{45646B53-1805-4678-AC93-20F87BE46A95}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{45646B53-1805-4678-AC93-20F87BE46A95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45646B53-1805-4678-AC93-20F87BE46A95}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45646B53-1805-4678-AC93-20F87BE46A95}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45646B53-1805-4678-AC93-20F87BE46A95}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,7 @@
namespace IPMonitor
{
public interface IArguments
{
public String[] GetArguments();
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.Extensions.Configuration;
namespace IPMonitor
{
public interface IMainService
{
public void RunService(String[] args,IConfiguration configuration);
}
}

22
IPMonitor/Program.cs Executable file
View File

@@ -0,0 +1,22 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace IPMonitor
{
class Program
{
static void Main(string[] args)
{
IConfigurationBuilder builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
IConfigurationRoot configurationRoot = builder.Build();
Arguments arguments = new Arguments(args);
IServiceCollection services = new ServiceCollection();
services.AddSingleton<IArguments>(arguments);
services.AddSingleton<IConfigurationRoot>(configurationRoot);
services.AddSingleton<IMainService,MainService>();
services.AddSingleton<Executor,Executor>();
services.BuildServiceProvider().GetService<Executor>().Execute();
}
}
}

17
IPMonitor/Services/Arguments.cs Executable file
View File

@@ -0,0 +1,17 @@
namespace IPMonitor
{
public class Arguments : IArguments
{
private readonly String[] _args;
public Arguments(String[] args)
{
_args = args ?? throw new ArgumentNullException(nameof(args));
}
public String[] GetArguments()
{
return _args;
}
}
}

348
IPMonitor/Services/MainService.cs Executable file
View File

@@ -0,0 +1,348 @@
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Web;
using MarketData;
using MarketData.Configuration;
using MarketData.Integration;
using MarketData.Utils;
using Microsoft.Extensions.Configuration;
namespace IPMonitor
{
public class MainService : IMainService
{
/// <summary>
/// This is the main entry point.
/// Arguments: /FORCE:true|false if true forces a refresh on ZoneEdit.
/// Force an update to ZoneEdit at the top of every hour
/// cron every hour
/// DOTNET_ROOT=/opt/dotnet
/// CRON_DIR_IPMONITOR=/opt/MarketData/IPMonitor
/// 0 * * * * cd $CRON_DIR_IPMONITOR ; /opt/MarketData/IPMonitor/ipmonitor /FORCE:true > /dev/null 2>&1
/// cron 10 minute intervals except top of hour
/// 10,20,30,40,50 * * * * cd $CRON_DIR_IPMONITOR ; /opt/MarketData/IPMonitor/ipmonitor > /dev/null 2>&1
/// </summary>
/// <param name="args"></param>
/// <param name="configuration"></param>
public void RunService(String[] args,IConfiguration configuration)
{
DateTime currentDate = DateTime.Now;
GlobalConfig.Instance.Configuration = configuration; // This call sets up configuration stuff so it needs to be first.
string arg = "ipmonitor";
CommandArgs commandArgs = new CommandArgs(args);
if(!CreateLogging(arg))
{
Console.WriteLine("CreateLogging returned false.");
return;
}
try
{
Profiler profiler=new Profiler();
profiler.Start();
MDTrace.WriteLine(LogLevel.DEBUG,$"[RunService] Started @ {Utility.DateTimeToStringYYYYHMMHDDHHMMSSTT(currentDate)} in {Directory.GetCurrentDirectory()}");
MDTrace.WriteLine(LogLevel.DEBUG,$"[RunService] Argument {arg}");
bool force=false;
if(commandArgs.Has("FORCE"))
{
force=commandArgs.Coalesce<bool>("FORCE");
MDTrace.WriteLine(LogLevel.DEBUG,$"FORCE={force}");
}
String ipAddress=GetPublicIPAddress();
UpdateIPAddress(ipAddress,force);
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Exception:{0}",exception.ToString()));
}
}
private static bool CreateLogging(String task)
{
if(String.IsNullOrEmpty(task))return false;
task=task.ToLower();
MDTrace.LogLevel = LogLevel.DEBUG;
String logFolder = "/logs";
DateTime currentDate=DateTime.Now;
String strLogFile = "marketdata_" + task + ".log";
String currentWorkingDirectory = Directory.GetCurrentDirectory();
Console.WriteLine($"Current directory is {currentWorkingDirectory}");
Utility.EnsureLogFolder(currentWorkingDirectory+logFolder);
Utility.ExpireLogs(currentWorkingDirectory+logFolder,1);
Trace.Listeners.Remove("Default");
Console.WriteLine($"Adding Trace Listener :{currentWorkingDirectory+logFolder+"/"+strLogFile}");
Trace.Listeners.Add(new TextWriterTraceListener(currentWorkingDirectory+logFolder+"/"+strLogFile));
MDTrace.WriteLine($"Trace Listener added.");
Utility.ShowLogs(currentWorkingDirectory + logFolder);
return true;
}
/// <summary>
/// Retains a record of current ip address in ipaddress.txt file and updates that ip address to ZoneEdit for DNS
/// </summary>
/// <param name="ipAddress"></param>
public static void UpdateIPAddress(String ipAddress,bool force)
{
try
{
String strPathFileName="ipaddress.txt";
if(null==ipAddress)return;
if(force)
{
File.Delete(GetPathFileName(strPathFileName));
}
if(!File.Exists(GetPathFileName(strPathFileName)))
{
WriteFile(GetPathFileName(strPathFileName), ipAddress);
ZoneEditResponses zoneEditResponses=UpdateZoneEditRecord(ipAddress);
MDTrace.WriteLine(LogLevel.DEBUG,$"IPAddress {ipAddress}. ZoneEditUpdated={zoneEditResponses.IsSuccess()}");
SendSMSEmail(String.Format("IPAddress {0}. ZoneEditUpdated={1}",ipAddress,zoneEditResponses.IsSuccess()));
if(!zoneEditResponses.IsSuccess())
{
File.Delete(GetPathFileName(strPathFileName));
}
}
else
{
String currentIPAddress=ReadFile(GetPathFileName(strPathFileName));
if(null!=currentIPAddress && !currentIPAddress.Equals(ipAddress))
{
WriteFile(GetPathFileName(strPathFileName),ipAddress);
ZoneEditResponses zoneEditResponses=UpdateZoneEditRecord(ipAddress);
SendSMSEmail(String.Format("IPAddress {0}. ZoneEditUpdated={1}",ipAddress,zoneEditResponses.IsSuccess()));
}
else if(null!=currentIPAddress && currentIPAddress.Equals(ipAddress))
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Public IP:{ipAddress} matches latest fetched IP:{currentIPAddress}");
}
else if(null==currentIPAddress)
{
SendSMSEmail("IPMonitor "+ipAddress+ ". IPMonitor encountered an issue reading the IPAddress file.");
}
}
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Exception:{0}",exception.ToString()));
}
}
/// <summary>
/// Retrieves the public IP Address
/// </summary>
/// <param name="ipAddress"></param>
public static String GetPublicIPAddress()
{
int MAX_RETRIES=5;
int TIMEOUT_BETWEEN_ATTEMPTS=30000;
String request="http://checkip.dyndns.org/";
try
{
String address = null;
for(int index=0;index<MAX_RETRIES && null==address;index++)
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Requesting IPAddress from {request}");
if(!NetworkStatus.IsNetworkAvailable(0))
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The Network seems to be disconnected. Will retry after {0} (ms) ",request,TIMEOUT_BETWEEN_ATTEMPTS));
try{Thread.Sleep(TIMEOUT_BETWEEN_ATTEMPTS);}catch{;}
continue;
}
address = GetHttpRequest(request);
if(null==address)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Request failed {0}. Will retry after {1} (ms) ",request,TIMEOUT_BETWEEN_ATTEMPTS));
try{Thread.Sleep(TIMEOUT_BETWEEN_ATTEMPTS);}catch{;}
continue;
}
}
if(null==address)
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Unable to obtain IP Address. Attempts:{MAX_RETRIES}");
return null;
}
try
{
int first = address.IndexOf("Address: ") + 9;
int last = address.LastIndexOf("</body>");
address = address.Substring(first, last - first);
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Request succeeded: {0} -> {1}",request,address));
return address;
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Exception parsing address: {0}",exception.ToString()));
return null;
}
}
catch(Exception exception)
{
String message = String.Format("Exception:{0}",exception.ToString());
MDTrace.WriteLine(LogLevel.DEBUG,message);
SendSMSEmail("IPMonitor encountered an issue retrieving public IPAddress.");
return null;
}
}
/// <summary>
/// Sends an HttpRequest and receives the response string. Used by GetPublicIPAddress
/// </summary>
/// <param name="ipAddress"></param>
public static String GetHttpRequest(String strRequest)
{
try
{
WebRequest request = WebRequest.Create(strRequest);
using WebResponse response = request.GetResponse();
using (StreamReader stream = new StreamReader(response.GetResponseStream()))
{
return stream.ReadToEnd();
}
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Request {strRequest} Failed with {1}",exception.ToString()));
return null;
}
finally
{
}
}
/// <summary>
/// Updates ZoneEdit with the given ipaddress.
/// </summary>
/// <param name="ipAddress"></param>
/// <returns>ZoneEditResponses</returns>
public static ZoneEditResponses UpdateZoneEditRecord(String ipAddress)
{
HttpClient client = default;
HttpNetResponse httpNetResponse=default;
try
{
client = new HttpClient();
String user=GlobalConfig.Instance.Configuration["zoneedit_user"];
String password=GlobalConfig.Instance.Configuration["zoneedit_password"];
String host=GlobalConfig.Instance.Configuration["zoneedit_host"];
String strRequest=null;
StringBuilder sb=new StringBuilder();
sb = new StringBuilder();
sb.Append("https://");
sb.Append("dynamic.zoneedit.com/auth/dynamic.html");
sb.Append("?host=").Append(HttpUtility.UrlEncode(host));
sb.Append("&dnsto=").Append(HttpUtility.UrlEncode(ipAddress));
strRequest=sb.ToString();
httpNetResponse = HttpNetRequest.GetRequestNoEncodingZoneEdit(strRequest,user,password);
if(!httpNetResponse.Success)
{
return new ZoneEditResponses();
}
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0}",httpNetResponse.ResponseString));
return new ZoneEditResponses(httpNetResponse.ResponseString);
}
finally
{
if(default!=client)
{
client.Dispose();
}
if(default!=httpNetResponse)
{
httpNetResponse.Dispose();
}
}
}
/// <summary>
/// Writes the given ip address into the specified file
/// </summary>
/// <param name="strPathFileName"></param>
/// <param name="ipAddress"></param>
public static void WriteFile(String strPathFileName,String ipAddress)
{
try
{
if(File.Exists(strPathFileName))File.Delete(strPathFileName);
using FileStream fileStream=new FileStream(strPathFileName,FileMode.Create,FileAccess.Write,FileShare.Read);
using StreamWriter streamWriter=new StreamWriter(fileStream);
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Creating address file:{0}",strPathFileName));
streamWriter.WriteLine(ipAddress);
streamWriter.Flush();
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Exception:{0}",exception.ToString()));
return;
}
}
/// <summary>
/// Reads a string from the specified file
/// </summary>
/// <param name="strPathFileName"></param>
/// <param name="ipAddress"></param>
public static String ReadFile(String strPathFileName)
{
try
{
using FileStream fileStream=new FileStream(strPathFileName,FileMode.Open,FileAccess.ReadWrite,FileShare.Read);
using StreamReader streamReader=new StreamReader(fileStream);
String item=null;
item=streamReader.ReadLine();
return item;
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Exception:{0}",exception.ToString()));
return null;
}
}
/// <summary>
/// Determines the fully qualified path file name for the provided file based on current directory and working folder
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static String GetPathFileName(String fileName)
{
String currentWorkingDirectory = Directory.GetCurrentDirectory();
String workingFolder = GlobalConfig.Instance.Configuration["working_folder"];
if(workingFolder.StartsWith("/"))
{
String fullPath = currentWorkingDirectory+workingFolder;
if(!Directory.Exists(fullPath))Directory.CreateDirectory(fullPath);
return fullPath+"/"+fileName;
}
else
{
String fullPath = currentWorkingDirectory+"/"+workingFolder;
if(!Directory.Exists(fullPath))Directory.CreateDirectory(fullPath);
return fullPath+"/"+fileName;
}
}
/// <summary>
/// Sends an email.
/// </summary>
/// <param name="message"></param>
public static void SendSMSEmail(string message)
{
String smsSMTPAddress = GlobalConfig.Instance.Configuration["sms_smtpaddress"];
String smsUserName = GlobalConfig.Instance.Configuration["sms_smsusername"];
String smsPassword = GlobalConfig.Instance.Configuration["sms_smspassword"];
String[] smsRecipients = GlobalConfig.Instance.Configuration["sms_smsrecipients"].Split(',');
SMSClient.SendSMSEmail(message, smsUserName, smsRecipients, smsSMTPAddress, smsUserName, smsPassword);
}
}
}

79
IPMonitor/ZoneEditResponse.cs Executable file
View File

@@ -0,0 +1,79 @@
using System.Text;
using MarketData.Utils;
namespace IPMonitor
{
public class ZoneEditResponses : List<ZoneEditResponse>
{
public ZoneEditResponses()
{
}
public bool IsSuccess()
{
if(Count==0)return false;
return this.Any(x => x.SuccessCode.Equals("200") || x.SuccessCode.Equals("201"));
}
public override string ToString()
{
StringBuilder sb=new StringBuilder();
for(int index=0;index<Count;index++)
{
ZoneEditResponse zoneEditResponse=this[index];
sb.Append(zoneEditResponse).ToString();
if(index<Count-1)sb.Append("\n");
}
return sb.ToString();
}
public ZoneEditResponses(String httpResponseString)
{
if(null==httpResponseString)return;
String[] responseItems = httpResponseString.Split('\n');
foreach(String item in responseItems)
{
int successCodeIndex= item.IndexOf("SUCCESS CODE=");
int textIndex=item.IndexOf("TEXT=");
int zoneIndex=item.IndexOf("ZONE=");
if(-1==successCodeIndex || -1==textIndex || -1==zoneIndex)continue;
String successCode=Utility.BetweenString(item.Substring(successCodeIndex),"\"","\"");
String text=Utility.BetweenString(item.Substring(textIndex),"\"","\"");
String zone=Utility.BetweenString(item.Substring(zoneIndex),"\"","\"");
ZoneEditResponse zoneEditResponse= new ZoneEditResponse(successCode, text, zone);
Add(zoneEditResponse);
}
}
}
// ResponseString "<SUCCESS CODE=\"201\" TEXT=\"no update required for diversified-software.com to 67.191.114.201\" ZONE=\"diversified-software.com\">\n<SUCCESS CODE=\"200\" TEXT=\"diversified-software.com updated to 67.191.114.201\" ZONE=\"diversified-software.com\">\n" string
public class ZoneEditResponse
{
public String SuccessCode { get; set; }
public String Text { get; set; }
public String Zone { get; set; }
public ZoneEditResponse()
{
}
public ZoneEditResponse(String successCode, String text, String zone)
{
SuccessCode=successCode;
Text=text;
Zone=zone;
}
public override String ToString()
{
StringBuilder sb = new StringBuilder();
sb.Append("SUCCESS CODE=").Append(SuccessCode);
sb.Append(",");
sb.Append("TEXT=").Append(Text);
sb.Append(",");
sb.Append("ZONE=").Append(Zone);
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,10 @@
{
"working_folder" : "/ipaddress",
"sms_smtpaddress" : "smtp.gmail.com",
"sms_smsusername" : "skessler1964@gmail.com",
"sms_smspassword" : "xjfo isnf gmyi zovr",
"sms_smsrecipients" : "skessler1964@gmail.com",
"zoneedit_user" : "skessler1964",
"zoneedit_password" : "4A536A7920309A11",
"zoneedit_host" : "diversified-software.com"
}