diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/AS1024.GeoFeed.sln b/AS1024.GeoFeed.sln
new file mode 100644
index 0000000..848634e
--- /dev/null
+++ b/AS1024.GeoFeed.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34330.188
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS1024.GeoFeed", "AS1024.GeoFeed\AS1024.GeoFeed.csproj", "{6292097C-7F35-45BB-B2B0-1918DF49FE7D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {66AEF076-9E39-4263-9719-4197027D1059}
+ EndGlobalSection
+EndGlobal
diff --git a/AS1024.GeoFeed/AS1024.GeoFeed.csproj b/AS1024.GeoFeed/AS1024.GeoFeed.csproj
new file mode 100644
index 0000000..3d04b4f
--- /dev/null
+++ b/AS1024.GeoFeed/AS1024.GeoFeed.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ Linux
+
+
+
+
+
+
+
+
diff --git a/AS1024.GeoFeed/AS1024.GeoFeed.http b/AS1024.GeoFeed/AS1024.GeoFeed.http
new file mode 100644
index 0000000..513a611
--- /dev/null
+++ b/AS1024.GeoFeed/AS1024.GeoFeed.http
@@ -0,0 +1,6 @@
+@AS1024.GeoFeed_HostAddress = http://localhost:5140
+
+GET {{AS1024.GeoFeed_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/AS1024.GeoFeed/Controllers/GeofeedController.cs b/AS1024.GeoFeed/Controllers/GeofeedController.cs
new file mode 100644
index 0000000..1dbe7b7
--- /dev/null
+++ b/AS1024.GeoFeed/Controllers/GeofeedController.cs
@@ -0,0 +1,48 @@
+using Microsoft.AspNetCore.Mvc;
+using AS1024.GeoFeed.Interfaces;
+using AS1024.GeoFeed.GeoFeedBuilder;
+using Microsoft.Extensions.Caching.Memory;
+using AS1024.GeoFeed.Models;
+using System.Text;
+
+namespace AS1024.GeoFeed.Controllers
+{
+ [ApiController]
+ [Route("[controller]")]
+ [Route("[controller].csv")]
+
+ public class GeofeedController : ControllerBase
+ {
+ private readonly IGeoFeedProvider builder;
+ private readonly IMemoryCache memoryCache;
+ private const string GeoFeedCacheKey = "GeoFeedData";
+
+ public GeofeedController(IGeoFeedProvider builder,
+ IMemoryCache memoryCache) {
+ this.builder = builder;
+ this.memoryCache = memoryCache;
+ }
+
+ [HttpGet]
+ [Route("")]
+ public async Task Get()
+ {
+ if (!memoryCache.TryGetValue(GeoFeedCacheKey, out List? feed))
+ {
+ feed = await builder.GetGeoFeedData();
+ var cacheEntryOptions = new MemoryCacheEntryOptions()
+ .SetAbsoluteExpiration(TimeSpan.FromHours(1));
+ memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions);
+ }
+
+ var csvContent = feed.ToGeoFeedCsv(); // Assuming ToGeoFeedCsv() returns a string in CSV format.
+ var contentBytes = Encoding.UTF8.GetBytes(csvContent);
+ var contentType = "text/csv";
+
+ return new FileContentResult(contentBytes, contentType)
+ {
+ FileDownloadName = "geofeed.csv"
+ };
+ }
+ }
+}
diff --git a/AS1024.GeoFeed/Dockerfile b/AS1024.GeoFeed/Dockerfile
new file mode 100644
index 0000000..04fbf23
--- /dev/null
+++ b/AS1024.GeoFeed/Dockerfile
@@ -0,0 +1,24 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
+WORKDIR /app
+EXPOSE 8080
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["AS1024.GeoFeed/AS1024.GeoFeed.csproj", "AS1024.GeoFeed/"]
+RUN dotnet restore "./AS1024.GeoFeed/./AS1024.GeoFeed.csproj"
+COPY . .
+WORKDIR "/src/AS1024.GeoFeed"
+RUN dotnet build "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "AS1024.GeoFeed.dll"]
\ No newline at end of file
diff --git a/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedBuilder.cs b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedBuilder.cs
new file mode 100644
index 0000000..53f0fc5
--- /dev/null
+++ b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedBuilder.cs
@@ -0,0 +1,36 @@
+
+using AS1024.GeoFeed.Interfaces;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace AS1024.GeoFeed.GeoFeedBuilder
+{
+ public class PreLoadGeoFeed : IHostedService
+ {
+ private readonly ILogger logger;
+ private readonly IGeoFeedProvider provider;
+ private readonly IMemoryCache memoryCache;
+ private const string GeoFeedCacheKey = "GeoFeedData";
+
+ public PreLoadGeoFeed(ILogger logger,
+ IGeoFeedProvider provider,
+ IMemoryCache memoryCache)
+ {
+ this.logger = logger;
+ this.provider = provider;
+ this.memoryCache = memoryCache;
+ }
+
+ async Task IHostedService.StartAsync(CancellationToken cancellationToken)
+ {
+ logger.LogInformation("Preloading GeoFeed data in memory...");
+ var feed = await provider.GetGeoFeedData();
+ var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromHours(1));
+ memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions);
+ }
+
+ Task IHostedService.StopAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs
new file mode 100644
index 0000000..c76634b
--- /dev/null
+++ b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs
@@ -0,0 +1,20 @@
+using AS1024.GeoFeed.Models;
+using System.Text;
+
+namespace AS1024.GeoFeed.GeoFeedBuilder
+{
+ public static class GeoFeedTools
+ {
+ public static string ToGeoFeedCsv(this List geoFeeds)
+ {
+ StringBuilder csvContent = new();
+
+ foreach (var feed in geoFeeds)
+ {
+ csvContent.AppendLine($"{feed.Prefix},{feed.GeolocCountry},{feed.GeolocRegion},{feed.GeolocCity}");
+ }
+
+ return csvContent.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AS1024.GeoFeed/GeoFeedBuilder/NetBoxGeoFeedProvider.cs b/AS1024.GeoFeed/GeoFeedBuilder/NetBoxGeoFeedProvider.cs
new file mode 100644
index 0000000..57aae4e
--- /dev/null
+++ b/AS1024.GeoFeed/GeoFeedBuilder/NetBoxGeoFeedProvider.cs
@@ -0,0 +1,122 @@
+using AS1024.GeoFeed.Interfaces;
+using AS1024.GeoFeed.Models;
+using Newtonsoft.Json;
+using System.Net.Http.Headers;
+using System.Net.Sockets;
+using System.Web;
+
+namespace AS1024.GeoFeed.GeoFeedBuilder
+{
+ public class NetBoxGeoFeedProvider : IGeoFeedProvider
+ {
+ private readonly IConfiguration configuration;
+ private readonly ILogger logger;
+ private readonly IList addressFamilies = new List()
+ {
+ AddressFamily.InterNetwork,
+ AddressFamily.InterNetworkV6
+ };
+ private readonly IHttpClientFactory httpClientFactory;
+
+ string IGeoFeedProvider.GeoFeedProviderName => "netbox";
+
+ public NetBoxGeoFeedProvider(IConfiguration configuration,
+ ILogger logger,
+ IHttpClientFactory httpClientFactory)
+ {
+ this.configuration = configuration;
+ this.logger = logger;
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ public async Task> GetGeoFeedData()
+ {
+ var geoFeed = new List();
+ using var client = httpClientFactory.CreateClient();
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", configuration["APIKey"]);
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ foreach (var family in addressFamilies)
+ {
+ Uri uri = BuildNetBoxURI(family);
+ NetboxData? jsonData = null;
+
+ while (true)
+ {
+ logger.LogDebug($"Making request to {uri}...");
+
+ using var result = await client.GetAsync(uri);
+ if (result.IsSuccessStatusCode)
+ {
+ var stringResult = await result.Content.ReadAsStringAsync();
+ jsonData = JsonConvert.DeserializeObject(stringResult);
+
+ if (jsonData?.Results == null || jsonData.Results.Count == 0)
+ {
+ break;
+ }
+
+ foreach (var data in jsonData.Results)
+ {
+ try
+ {
+ geoFeed.Add(new IPGeoFeed
+ {
+ Prefix = data.Prefix,
+ GeolocCity = data.CustomFields.GeolocCity,
+ GeolocRegion = data.CustomFields.GeolocRegion,
+ GeolocCountry = data.CustomFields.GeolocCountry,
+ GeolocHasLocation = data.CustomFields.GeolocHasLocation
+ });
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex.ToString());
+ }
+ }
+
+ if (!string.IsNullOrEmpty(jsonData.Next))
+ {
+ uri = new Uri(jsonData.Next);
+ continue;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return geoFeed;
+ }
+
+ protected Uri BuildNetBoxURI(AddressFamily family)
+ {
+ var queryParameters = HttpUtility.ParseQueryString(string.Empty);
+ queryParameters["cf_geoloc_has_location"] = "true";
+ queryParameters["limit"] = "5";
+
+ switch (family)
+ {
+ case AddressFamily.InterNetwork:
+ queryParameters["mask_length__lte"] = "24";
+ queryParameters["family"] = "4";
+ break;
+
+ case AddressFamily.InterNetworkV6:
+ queryParameters["mask_length__lte"] = "48";
+ queryParameters["family"] = "6";
+ break;
+ }
+
+ var endUrl = new UriBuilder
+ {
+ Path = "api/ipam/prefixes/",
+ Query = queryParameters.ToString(),
+ Host = configuration["NetBoxHost"],
+ Scheme = "https"
+ };
+
+ return endUrl.Uri;
+ }
+ }
+}
diff --git a/AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs b/AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs
new file mode 100644
index 0000000..f547e95
--- /dev/null
+++ b/AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs
@@ -0,0 +1,10 @@
+using AS1024.GeoFeed.Models;
+
+namespace AS1024.GeoFeed.Interfaces
+{
+ public interface IGeoFeedProvider
+ {
+ public string GeoFeedProviderName { get; }
+ public Task> GetGeoFeedData();
+ }
+}
diff --git a/AS1024.GeoFeed/Models/GeoFeed.cs b/AS1024.GeoFeed/Models/GeoFeed.cs
new file mode 100644
index 0000000..ee54101
--- /dev/null
+++ b/AS1024.GeoFeed/Models/GeoFeed.cs
@@ -0,0 +1,35 @@
+using Newtonsoft.Json;
+
+namespace AS1024.GeoFeed.Models
+{
+ public class NetboxData
+ {
+ public List? Results { get; set; }
+ public string? Next { get; set; }
+ public string? Previous { get; set; }
+ }
+
+ public class Result
+ {
+ public string? Prefix { get; set; }
+ [JsonProperty("custom_fields")]
+ public CustomFields? CustomFields { get; set; }
+ }
+
+ public class CustomFields
+ {
+ [JsonProperty("geoloc_city")]
+ public string? GeolocCity { get; set; }
+ [JsonProperty("geoloc_country")]
+ public string? GeolocCountry { get; set; }
+ [JsonProperty("geoloc_has_location")]
+ public bool? GeolocHasLocation { get; set; }
+ [JsonProperty("geoloc_region")]
+ public string? GeolocRegion { get; set; }
+ }
+
+
+ public class IPGeoFeed : CustomFields {
+ public string? Prefix { get; set; }
+ }
+}
diff --git a/AS1024.GeoFeed/Program.cs b/AS1024.GeoFeed/Program.cs
new file mode 100644
index 0000000..c61fac7
--- /dev/null
+++ b/AS1024.GeoFeed/Program.cs
@@ -0,0 +1,32 @@
+using AS1024.GeoFeed.GeoFeedBuilder;
+using AS1024.GeoFeed.Interfaces;
+
+namespace AS1024.GeoFeed
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddHostedService();
+ builder.Services.AddTransient();
+ builder.Services.AddHttpClient();
+ builder.Services.AddMemoryCache();
+ // Add services to the container.
+
+ builder.Services.AddControllers();
+
+ var app = builder.Build();
+
+ // Configure the HTTP request pipeline.
+
+ app.UseAuthorization();
+
+
+ app.MapControllers();
+
+ app.Run();
+ }
+ }
+}
diff --git a/AS1024.GeoFeed/Properties/launchSettings.json b/AS1024.GeoFeed/Properties/launchSettings.json
new file mode 100644
index 0000000..b659ae9
--- /dev/null
+++ b/AS1024.GeoFeed/Properties/launchSettings.json
@@ -0,0 +1,40 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "geofeed",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:5140"
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "geofeed",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Docker": {
+ "commandName": "Docker",
+ "launchBrowser": true,
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_HTTP_PORTS": "8080"
+ },
+ "publishAllPorts": true
+ }
+ },
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:30039",
+ "sslPort": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/AS1024.GeoFeed/appsettings.Development.json b/AS1024.GeoFeed/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/AS1024.GeoFeed/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/AS1024.GeoFeed/appsettings.json b/AS1024.GeoFeed/appsettings.json
new file mode 100644
index 0000000..fbff1fd
--- /dev/null
+++ b/AS1024.GeoFeed/appsettings.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "APIKey": "",
+ "NetBoxHost": ""
+}