Compare commits

...

57 Commits

Author SHA1 Message Date
Jeff Leung 5cad97c9ac Move more constant texts in a constant string 2024-09-10 20:20:45 -07:00
Jeff Leung cc61b02959 Update README for extensbility hints. 2024-02-27 21:14:37 -08:00
Jeff Leung 0dfe68656d Merge branch 'master' of ssh://git.startmywifi.com:2251/AS1024/GeoFeed 2024-02-27 20:53:28 -08:00
Jeff Leung 6544dc5a3d Some refactoring here 2024-02-27 20:53:18 -08:00
Jeff Leung 08e518c340 Remove reference to musl x64 as this was breaking multiarch builds 2024-02-27 20:27:46 -08:00
Jeff Leung e82d198ee7 Merge branch 'master' of ssh://git.startmywifi.com:2251/AS1024/GeoFeed 2024-02-27 20:17:36 -08:00
Jeff Leung 4c6a9e6dec Add /tmp 2024-02-27 20:16:39 -08:00
Jeff Leung feb1326cee Add bare image option 2024-02-27 19:29:32 -08:00
Jeff Leung 626ab99888 Add comment on example YAML file 2024-02-27 16:52:25 -08:00
Jeff Leung 9c8189bc33 Fix port to 8080 for Caddy reverse proxy 2024-02-27 16:51:15 -08:00
Jeff Leung 956aa8b3b0 Add example docker compose files to the repository 2024-02-27 16:49:43 -08:00
Jeff Leung 7d5b2a7aac Mark the same for the aot minimal version 2024-02-27 15:38:31 -08:00
Jeff Leung 5f089fb01f Use a stream instead of putting the entire string in memory 2024-02-27 15:30:13 -08:00
Jeff Leung af3cc81180 Code cleanup 2024-01-18 19:07:18 -08:00
Jeff Leung e0189381f1 Rename the GetGeofeed Method to Async 2024-01-18 16:15:41 -08:00
Jeff Leung 3a56e2426e Move geofeed web logic to a seperate project as core lib as asp.net core project is causing weird issues 2024-01-18 16:15:04 -08:00
Jeff Leung 0c3edd64f3 Unify logic of generating the webside unlike a different version we had for MVC and Minimal API 2024-01-18 15:57:01 -08:00
Jeff Leung 2d95fcb1a1 Fix preloading issues 2024-01-16 23:45:33 -08:00
Jeff Leung 7c952c134a Preload GeoFeed on start 2024-01-16 13:16:33 -08:00
Jeff Leung 7721dfb669 Update the sqlite provider to say it was retrieved from the persistent cache 2024-01-16 13:06:16 -08:00
Jeff Leung c1ed05a335 Append when the GeoFeed is retrieved and if it was generated from in memory cache 2024-01-16 13:05:58 -08:00
Jeff Leung 690a117ffd Update README 2024-01-14 00:08:14 -08:00
Jeff Leung 9dfbded5b8 Update the readme file 2024-01-14 00:04:14 -08:00
Jeff Leung 7b7d422890 Update the README.md file 2024-01-13 23:56:06 -08:00
Jeff Leung 2d6083f530 Don't add unecessary binaries 2024-01-13 23:38:18 -08:00
Jeff Leung 0e56d778bc Mark class as abstract so we don't instantiate or inject it by accident 2024-01-13 19:43:29 -08:00
Jeff Leung 8523ddd60f Add Alpine deployment 2024-01-13 19:41:34 -08:00
Jeff Leung 83826cc930 Switch over to jammy 2024-01-13 19:28:03 -08:00
Jeff Leung cc1717e4a3 Return a file instead of just a CSV text 2024-01-13 18:11:07 -08:00
Jeff Leung 48932ec2a5 Don't include the symbols 2024-01-13 17:44:44 -08:00
Jeff Leung 42aacf497c Some minor housekeeping 2024-01-13 17:10:18 -08:00
Jeff Leung e8152186c1 Bring the caching services over 2024-01-13 17:05:21 -08:00
Jeff Leung 43b34143a3 Remove uneeded usings 2024-01-13 17:05:08 -08:00
Jeff Leung 8ed021754c Remove uneeded files 2024-01-13 17:04:55 -08:00
Jeff Leung a44b4f9421 Initial work on getting minimal API to work 2024-01-13 16:44:03 -08:00
Jeff Leung 810993cbf3 Update launch settings 2024-01-13 16:43:44 -08:00
Jeff Leung 8d6999c95c Move it to a base class so we can override the deserializer in a different project 2024-01-13 14:20:38 -08:00
Jeff Leung 704d6b24dc Don't reference to EF Core as AOT does not yet support it 2024-01-13 13:44:20 -08:00
Jeff Leung 81d01f28a3 Add initial work to get minimal API's going 2024-01-13 13:30:28 -08:00
Jeff Leung bdd247e6e0 Inital work on breaking out the core logic out of the MVC API Web App in preparation to migrating to minimal API's in a seperate project 2024-01-13 12:42:46 -08:00
Jeff Leung 52708bd2c2 Add self contained runtime deployment 2024-01-13 12:15:55 -08:00
Jeff Leung 9c5fbc314f Merge branch 'master' of ssh://git.startmywifi.com:2251/AS1024/GeoFeed 2024-01-08 23:21:17 -08:00
Jeff Leung 1f3778095c Make the code cleaner to read 2024-01-08 23:18:26 -08:00
Jeff Leung 2d5c318689 Update license text 2024-01-08 14:59:18 -08:00
Jeff Leung dd643ab8ab Update the readme file 2024-01-08 10:48:48 -08:00
Jeff Leung 2d68115f25 Complete the implementation of returning cached data from the local disk cache 2024-01-08 10:41:25 -08:00
Jeff Leung 020db780a6 Add example connection string 2024-01-08 10:23:02 -08:00
Jeff Leung 5fec5f4f92 Forward all cancellation tokens and run the local cache first 2024-01-08 10:22:51 -08:00
Jeff Leung c43b218974 Add necessary tools 2024-01-08 10:08:46 -08:00
Jeff Leung c18e486750 Add initial DB Migrations 2024-01-08 10:08:36 -08:00
Jeff Leung 9974bb5f0c Create various scopes - forgot that EF Core DBContexts don't work in a singleton service 2024-01-08 10:08:28 -08:00
Jeff Leung 37841db663 Inject the necessary services into the DI container 2024-01-08 09:57:51 -08:00
Jeff Leung e974635cfb Break it out to individual files and initial cache service 2024-01-08 09:57:37 -08:00
Jeff Leung 2fa8f32f44 Initial work on caching without calling dependencies 2024-01-08 09:18:35 -08:00
Jeff Leung 6e9d58c6e3 Initial skeleton work of periodic on disk caching 2024-01-08 00:13:08 -08:00
Jeff Leung ade2861395 Merge branch 'master' of ssh://git.startmywifi.com:2251/AS1024/GeoFeed 2024-01-07 14:58:49 -08:00
Jeff Leung 4ea9f2ca7f Update readme and license text 2024-01-07 11:11:20 -08:00
43 changed files with 1152 additions and 130 deletions

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedCacheDbContext : DbContext
{
public GeoFeedCacheDbContext(DbContextOptions options)
: base(options)
{
}
public virtual DbSet<GeoFeedCacheEntry> GeoFeedCacheEntries { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using AS1024.GeoFeed.Models;
using System.ComponentModel.DataAnnotations;
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedCacheEntry : IPGeoFeed
{
[Key]
public int Id { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedDesignTimeMigration : IDesignTimeDbContextFactory<GeoFeedCacheDbContext>
{
public GeoFeedCacheDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<GeoFeedCacheDbContext>();
builder.UseSqlite("Data Source=migratedb.db");
return new GeoFeedCacheDbContext(builder.Options);
}
}
}

View File

@ -0,0 +1,72 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Core.Tools;
using AS1024.GeoFeed.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedSqliteCache : IGeoFeedPersistentCacheProvider
{
protected readonly GeoFeedCacheDbContext dbContext;
private readonly IGeoFeedProvider feedProvider;
public GeoFeedSqliteCache(GeoFeedCacheDbContext geoFeedCacheDb,
IHost host,
IGeoFeedProvider provider)
{
dbContext = geoFeedCacheDb;
feedProvider = provider;
}
public string ProviderName => "sqlite";
public async Task<bool> CacheGeoFeed(IList<IPGeoFeed> pGeoFeeds)
{
await DBContextMigrate();
List<GeoFeedCacheEntry> geoFeedCacheEntry = [];
var results = pGeoFeeds.ToList();
results.ForEach(x =>
{
geoFeedCacheEntry.Add(new()
{
Prefix = x.Prefix,
GeolocCity = x.GeolocCity,
GeolocCountry = x.GeolocCountry,
GeolocHasLocation = x.GeolocHasLocation,
GeolocPostalCode = x.GeolocPostalCode,
GeolocRegion = x.GeolocRegion
});
});
if (dbContext.GeoFeedCacheEntries.Any())
{
dbContext.GeoFeedCacheEntries.RemoveRange(dbContext.GeoFeedCacheEntries.ToArray());
}
await dbContext.AddRangeAsync(geoFeedCacheEntry);
await dbContext.SaveChangesAsync();
return true;
}
public string GetGeoFeed()
{
var results =
dbContext.GeoFeedCacheEntries.ToList();
List<IPGeoFeed> cachedData = [];
results.ForEach(cachedData.Add);
return cachedData.ToGeoFeedCsv(true, true);
}
private async Task DBContextMigrate()
{
if (dbContext.Database.GetPendingMigrations().Any())
{
await dbContext.Database.MigrateAsync();
}
}
}
}

View File

@ -0,0 +1,54 @@
// <auto-generated />
using System;
using AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AS1024.GeoFeed.Core.SqliteGeoFeedCache.Migrations
{
[DbContext(typeof(GeoFeedCacheDbContext))]
[Migration("20240114002908_InitialMigration")]
partial class InitialMigration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache.GeoFeedCacheEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("GeolocCity")
.HasColumnType("TEXT");
b.Property<string>("GeolocCountry")
.HasColumnType("TEXT");
b.Property<bool?>("GeolocHasLocation")
.HasColumnType("INTEGER");
b.Property<string>("GeolocPostalCode")
.HasColumnType("TEXT");
b.Property<string>("GeolocRegion")
.HasColumnType("TEXT");
b.Property<string>("Prefix")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("GeoFeedCacheEntries");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AS1024.GeoFeed.Core.SqliteGeoFeedCache.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "GeoFeedCacheEntries",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GeolocCity = table.Column<string>(type: "TEXT", nullable: true),
GeolocCountry = table.Column<string>(type: "TEXT", nullable: true),
GeolocHasLocation = table.Column<bool>(type: "INTEGER", nullable: true),
GeolocRegion = table.Column<string>(type: "TEXT", nullable: true),
GeolocPostalCode = table.Column<string>(type: "TEXT", nullable: true),
Prefix = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GeoFeedCacheEntries", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GeoFeedCacheEntries");
}
}
}

View File

@ -0,0 +1,51 @@
// <auto-generated />
using System;
using AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AS1024.GeoFeed.Core.SqliteGeoFeedCache.Migrations
{
[DbContext(typeof(GeoFeedCacheDbContext))]
partial class GeoFeedCacheDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache.GeoFeedCacheEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("GeolocCity")
.HasColumnType("TEXT");
b.Property<string>("GeolocCountry")
.HasColumnType("TEXT");
b.Property<bool?>("GeolocHasLocation")
.HasColumnType("INTEGER");
b.Property<string>("GeolocPostalCode")
.HasColumnType("TEXT");
b.Property<string>("GeolocRegion")
.HasColumnType("TEXT");
b.Property<string>("Prefix")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("GeoFeedCacheEntries");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,72 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Core.Tools;
using AS1024.GeoFeed.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Text;
namespace AS1024.GeoFeed.Core.WebLogic
{
public class GeoFeedReturn
{
private const string GeoFeedCacheKey = "GeoFeedData";
private const string GeoFeedMimeTypeReturn = "text/csv";
private const string GeoFeedFileName = "geofeed.csv";
private readonly IGeoFeedProvider provider;
private readonly ILogger<GeoFeedReturn> logger;
private readonly IGeoFeedPersistentCacheProvider cacheProvider;
private readonly IMemoryCache memoryCache;
private readonly IWebHostEnvironment environment;
public GeoFeedReturn(IGeoFeedProvider provider,
ILogger<GeoFeedReturn> logger,
IGeoFeedPersistentCacheProvider cacheProvider,
IMemoryCache memoryCache,
IWebHostEnvironment environment)
{
this.provider = provider;
this.logger = logger;
this.cacheProvider = cacheProvider;
this.memoryCache = memoryCache;
this.environment = environment;
}
public async Task<IResult> GetGeoFeed()
{
bool isCached = true;
try
{
if (!memoryCache.TryGetValue(GeoFeedCacheKey, out List<IPGeoFeed>? feed))
{
isCached = false;
feed = await provider.GetGeoFeedDataAsync();
if (environment.IsProduction())
{
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(15));
memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions);
}
}
return Results.File(Encoding.UTF8.GetBytes(feed.ToGeoFeedCsv(true, isCached)),
GeoFeedMimeTypeReturn,
GeoFeedFileName);
}
catch (HttpRequestException ex)
{
logger.LogWarning($"Temporary failure of retrieving GeoData from upstream. {ex}");
string geoFeedData = cacheProvider.GetGeoFeed();
return Results.File(Encoding.UTF8.GetBytes(geoFeedData),
GeoFeedMimeTypeReturn,
GeoFeedFileName);
}
catch (Exception ex)
{
logger.LogError($"Error: {ex}");
}
return Results.NoContent();
}
}
}

View File

@ -0,0 +1,12 @@
{
"profiles": {
"AS1024.GeoFeed.Core.WebLogic": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:54455;http://localhost:54456"
}
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,58 @@
using AS1024.GeoFeed.Core.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.Core.CacheService
{
public class GeoFeedCacheService : IHostedService
{
private readonly ILogger<GeoFeedCacheService> logger;
private readonly IGeoFeedProvider feedProvider;
private readonly IHost host;
public GeoFeedCacheService(ILogger<GeoFeedCacheService> logger,
IGeoFeedProvider feedProvider,
IHost host)
{
this.logger = logger;
this.feedProvider = feedProvider;
this.host = host;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = StartPerioidicSync(cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public async Task<bool> StartPerioidicSync(CancellationToken Token)
{
while (!Token.IsCancellationRequested)
{
try
{
var scope = host.Services.CreateScope();
var persistentCacheProvider =
scope.ServiceProvider.GetRequiredService<IGeoFeedPersistentCacheProvider>();
var results = await feedProvider.GetGeoFeedDataAsync();
await persistentCacheProvider.CacheGeoFeed(results);
}
catch (Exception)
{
logger.LogWarning("On disk cache failed to run. Waiting on 30 minutes before retry...");
}
await Task.Delay(TimeSpan.FromMinutes(30));
}
return false;
}
}
}

View File

@ -0,0 +1,49 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Core.Tools;
using AS1024.GeoFeed.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.Core.CacheService
{
public class GeoFeedLocalFileCache : IGeoFeedPersistentCacheProvider
{
private readonly IConfiguration configuration;
private readonly ILogger<GeoFeedLocalFileCache> logger;
public GeoFeedLocalFileCache(IConfiguration _configuration,
ILogger<GeoFeedLocalFileCache> logger)
{
configuration = _configuration;
this.logger = logger;
}
string IGeoFeedPersistentCacheProvider.ProviderName => "file";
Task<bool> IGeoFeedPersistentCacheProvider.CacheGeoFeed(IList<IPGeoFeed> pGeoFeeds)
{
string? tempPath = GetTempPath();
logger.LogInformation($"Writing geofeed data to: {tempPath}");
File.WriteAllText(tempPath, pGeoFeeds.ToList().
ToGeoFeedCsv());
return Task.FromResult(true);
}
private string GetTempPath()
{
string? tempPath = Path.Combine(Path.GetTempPath(), "geoFeedCache.csv");
logger.LogInformation($"Getting GeoFeed data from: {tempPath}");
if (!string.IsNullOrEmpty(configuration["TempCache"]))
tempPath = configuration["TempCache"];
return tempPath;
}
string IGeoFeedPersistentCacheProvider.GetGeoFeed()
{
return File.ReadAllText(GetTempPath());
}
}
}

View File

@ -1,21 +1,23 @@

using AS1024.GeoFeed.Interfaces;
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.GeoFeedBuilder
namespace AS1024.GeoFeed.Core.GeoFeedPreloader
{
public class PreLoadGeoFeed : IHostedService
{
private readonly ILogger<PreLoadGeoFeed> logger;
private readonly IGeoFeedProvider provider;
private readonly IMemoryCache memoryCache;
private readonly IWebHostEnvironment environment;
private readonly IHostEnvironment environment;
private const string GeoFeedCacheKey = "GeoFeedData";
public PreLoadGeoFeed(ILogger<PreLoadGeoFeed> logger,
IGeoFeedProvider provider,
IMemoryCache memoryCache,
IWebHostEnvironment environment)
IHostEnvironment environment)
{
this.logger = logger;
this.provider = provider;
@ -29,7 +31,8 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
{
if (environment.IsProduction())
await StartPreLoad();
} catch (Exception ex)
}
catch (Exception ex)
{
logger.LogWarning($"Failed to preload, exception settings below:\n{ex}");
}
@ -38,7 +41,7 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
private async Task StartPreLoad()
{
logger.LogInformation("Preloading GeoFeed data in memory...");
List<Models.IPGeoFeed> feed = await provider.GetGeoFeedData();
List<IPGeoFeed> feed = await provider.GetGeoFeedDataAsync();
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(45));
memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions);
}

View File

@ -0,0 +1,14 @@
using AS1024.GeoFeed.Core.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.Core.GeoFeedProviders
{
public class NetBoxGeoFeedProvider : NetBoxGeoFeedProviderBase, IGeoFeedProvider
{
public NetBoxGeoFeedProvider(IConfiguration configuration, ILogger<NetBoxGeoFeedProvider> logger, IHttpClientFactory httpClientFactory)
: base(configuration, logger, httpClientFactory)
{
}
}
}

View File

@ -1,26 +1,28 @@
using AS1024.GeoFeed.Interfaces;
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text.Json;
using System.Web;
namespace AS1024.GeoFeed.GeoFeedBuilder
namespace AS1024.GeoFeed.Core.GeoFeedProviders
{
public class NetBoxGeoFeedProvider : IGeoFeedProvider
public abstract class NetBoxGeoFeedProviderBase : IGeoFeedProvider
{
private readonly IConfiguration configuration;
private readonly ILogger<NetBoxGeoFeedProvider> logger;
private readonly IList<AddressFamily> addressFamilies = new List<AddressFamily>()
protected readonly IConfiguration configuration;
protected readonly ILogger<NetBoxGeoFeedProvider> logger;
protected readonly IList<AddressFamily> addressFamilies = new List<AddressFamily>()
{
AddressFamily.InterNetwork,
AddressFamily.InterNetworkV6
};
private readonly IHttpClientFactory httpClientFactory;
protected readonly IHttpClientFactory httpClientFactory;
string IGeoFeedProvider.GeoFeedProviderName => "netbox";
public NetBoxGeoFeedProvider(IConfiguration configuration,
public NetBoxGeoFeedProviderBase(IConfiguration configuration,
ILogger<NetBoxGeoFeedProvider> logger,
IHttpClientFactory httpClientFactory)
{
@ -29,9 +31,9 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
this.httpClientFactory = httpClientFactory;
}
public async Task<List<IPGeoFeed>> GetGeoFeedData()
public async Task<List<IPGeoFeed>> GetGeoFeedDataAsync()
{
List<IPGeoFeed> geoFeed = new List<IPGeoFeed>();
List<IPGeoFeed> geoFeed = [];
using HttpClient client = httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", configuration["APIKey"]);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
@ -46,45 +48,44 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
logger.LogDebug($"Making request to {uri}...");
using HttpResponseMessage result = await client.GetAsync(uri);
if (result.IsSuccessStatusCode)
if (!result.IsSuccessStatusCode)
{
string stringResult = await result.Content.ReadAsStringAsync();
jsonData = JsonSerializer.Deserialize<NetboxData>(stringResult, new JsonSerializerOptions {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
break;
}
var stringResult = await result.Content.ReadAsStreamAsync();
jsonData = DeserializeJsonData(stringResult);
if (jsonData?.Results == null || jsonData.Results.Count == 0)
{
break;
}
if (jsonData?.Results == null || jsonData.Results.Count == 0)
{
break;
}
foreach (Result data in jsonData.Results)
foreach (Result data in jsonData.Results)
{
try
{
try
geoFeed.Add(new IPGeoFeed
{
geoFeed.Add(new IPGeoFeed
{
Prefix = data.Prefix,
GeolocCity = data.CustomFields.GeolocCity,
GeolocRegion = data.CustomFields.GeolocRegion,
GeolocCountry = data.CustomFields.GeolocCountry,
GeolocHasLocation = data.CustomFields.GeolocHasLocation,
GeolocPostalCode = data.CustomFields.GeolocPostalCode
});
}
catch (Exception ex)
{
logger.LogError(ex.ToString());
}
Prefix = data.Prefix,
GeolocCity = data.CustomFields.GeolocCity,
GeolocRegion = data.CustomFields.GeolocRegion,
GeolocCountry = data.CustomFields.GeolocCountry,
GeolocHasLocation = data.CustomFields.GeolocHasLocation,
GeolocPostalCode = data.CustomFields.GeolocPostalCode
});
}
if (!string.IsNullOrEmpty(jsonData.Next))
catch (Exception ex)
{
uri = new Uri(jsonData.Next);
continue;
logger.LogError(ex.ToString());
}
}
if (!string.IsNullOrEmpty(jsonData.Next))
{
uri = new Uri(jsonData.Next);
continue;
}
break;
}
}
@ -92,6 +93,14 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
return geoFeed;
}
protected virtual NetboxData? DeserializeJsonData(Stream stringResult)
{
return JsonSerializer.Deserialize<NetboxData>(stringResult, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
}
protected Uri BuildNetBoxURI(AddressFamily family)
{
System.Collections.Specialized.NameValueCollection queryParameters = HttpUtility.ParseQueryString(string.Empty);

View File

@ -0,0 +1,26 @@
using AS1024.GeoFeed.Models;
namespace AS1024.GeoFeed.Core.Interfaces
{
/// <summary>
/// Represents a persistent cache GeoFeed provider
/// </summary>
public interface IGeoFeedPersistentCacheProvider
{
/// <summary>
/// Name of the provider
/// </summary>
public string ProviderName { get; }
/// <summary>
/// Returns the GeoFeed
/// </summary>
/// <returns>String of the CSV geofeed</returns>
public string GetGeoFeed();
/// <summary>
/// Stores the GeoFeed in the cache backend
/// </summary>
/// <param name="pGeoFeeds">GeoFeed retrieved from the backend</param>
/// <returns></returns>
public Task<bool> CacheGeoFeed(IList<IPGeoFeed> pGeoFeeds);
}
}

View File

@ -1,10 +1,10 @@
using AS1024.GeoFeed.Models;
namespace AS1024.GeoFeed.Interfaces
namespace AS1024.GeoFeed.Core.Interfaces
{
public interface IGeoFeedProvider
{
public string GeoFeedProviderName { get; }
public Task<List<IPGeoFeed>> GetGeoFeedData();
public Task<List<IPGeoFeed>> GetGeoFeedDataAsync();
}
}

View File

@ -0,0 +1,39 @@
using AS1024.GeoFeed.Models;
using System.Text;
namespace AS1024.GeoFeed.Core.Tools
{
public static class GeoFeedTools
{
/// <summary>
/// Returns a CSV string for a given GeoFeed retrieved from various sources.
/// </summary>
/// <param name="geoFeeds">GeoFeed returned from the source of truth.</param>
/// <param name="timeStamp">If a timestamp should be appended at the header.</param>
/// <param name="isCached">If the result is cached.</param>
/// <returns>CSV formatted string of GeoFeed data.</returns>
public static string ToGeoFeedCsv(this List<IPGeoFeed> geoFeeds, bool timeStamp = false, bool isCached = false)
{
if (geoFeeds == null) throw new ArgumentNullException(nameof(geoFeeds));
StringBuilder csvContent = new();
// Append timestamp header if required
if (timeStamp)
csvContent.AppendFormat("# GeoFeed generated on {0:R}\n", DateTime.UtcNow);
// Append cache status if required
if (isCached)
csvContent.AppendLine("# Geofeed data is returned from local in memory cache");
// Iterate over each GeoFeed entry to append its details to the CSV content
foreach (IPGeoFeed feed in geoFeeds)
{
// Using AppendFormat for a cleaner and more readable approach to constructing CSV lines
csvContent.AppendFormat("{0},{1},{2},{3},{4}\n", feed.Prefix, feed.GeolocCountry, feed.GeolocRegion, feed.GeolocCity, feed.GeolocPostalCode);
}
return csvContent.ToString();
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishAot>true</PublishAot>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core.WebLogic\AS1024.GeoFeed.Core.WebLogic.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
using AS1024.GeoFeed.Models;
using System.Text.Json.Serialization;
namespace AS1024.GeoFeed.MinimalAPI
{
[JsonSerializable(typeof(NetboxData))]
[JsonSerializable(typeof(Result))]
[JsonSerializable(typeof(CustomFields))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
}

View File

@ -0,0 +1,29 @@
#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/runtime:8.0-jammy-chiseled AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
clang zlib1g-dev
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["AS1024.GeoFeed.MinimalAPI/AS1024.GeoFeed.MinimalAPI.csproj", "AS1024.GeoFeed.MinimalAPI/"]
RUN dotnet restore "./AS1024.GeoFeed.MinimalAPI/./AS1024.GeoFeed.MinimalAPI.csproj"
COPY . .
WORKDIR "/src/AS1024.GeoFeed.MinimalAPI"
RUN dotnet build "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true /p:DebugType=None /p:DebugSymbols=false
FROM base AS final
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["./AS1024.GeoFeed.MinimalAPI"]

View File

@ -0,0 +1,55 @@
#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 alpine:latest AS base
ENV \
# UID of the non-root user 'app'
APP_UID=1654 \
# Configure web servers to bind to port 8080 when present
ASPNETCORE_HTTP_PORTS=8080 \
# Enable detection of running in a container
DOTNET_RUNNING_IN_CONTAINER=true \
# Set the invariant mode since ICU package isn't included (see https://github.com/dotnet/announcements/issues/20)
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true
#RUN apk add --upgrade --no-cache \
# ca-certificates-bundle \
# \
# # .NET dependencies
# libgcc \
# libssl3 \
# libstdc++ \
# zlib
# Create a non-root user and group
RUN addgroup \
--gid=$APP_UID \
app \
&& adduser \
--uid=$APP_UID \
--ingroup=app \
--disabled-password \
app
WORKDIR /app
USER app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apk add clang zlib-dev musl-dev libc6-compat
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["AS1024.GeoFeed.MinimalAPI/AS1024.GeoFeed.MinimalAPI.csproj", "AS1024.GeoFeed.MinimalAPI/"]
RUN dotnet restore "./AS1024.GeoFeed.MinimalAPI/./AS1024.GeoFeed.MinimalAPI.csproj"
COPY . .
WORKDIR "/src/AS1024.GeoFeed.MinimalAPI"
RUN dotnet build "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true /p:DebugType=None /p:DebugSymbols=false
FROM base AS final
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["./AS1024.GeoFeed.MinimalAPI"]

View File

@ -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/sdk:8.0-alpine AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apk add clang zlib-static zlib-dev musl-dev libc6-compat cmake autoconf make openssl-dev openssl-libs-static icu-static icu-dev
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["AS1024.GeoFeed.MinimalAPI/AS1024.GeoFeed.MinimalAPI.csproj", "AS1024.GeoFeed.MinimalAPI/"]
RUN dotnet restore "./AS1024.GeoFeed.MinimalAPI/./AS1024.GeoFeed.MinimalAPI.csproj"
COPY . .
WORKDIR "/src/AS1024.GeoFeed.MinimalAPI"
RUN dotnet build "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:StaticOpenSslLinking=true /p:StaticExecutable=true /p:StaticallyLinked=true /p:StripSymbols=true /p:DebugType=None /p:DebugSymbols=false
FROM scratch AS final
WORKDIR /tmp
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .
COPY --from=build /etc/ssl/certs/* /etc/ssl/certs/
ENTRYPOINT ["./AS1024.GeoFeed.MinimalAPI"]

View File

@ -0,0 +1,22 @@
using AS1024.GeoFeed.Core.GeoFeedProviders;
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using System.Text.Json;
namespace AS1024.GeoFeed.MinimalAPI
{
internal class NetboxAoTGeoFeedProvider : NetBoxGeoFeedProvider, IGeoFeedProvider
{
public NetboxAoTGeoFeedProvider(IConfiguration configuration,
ILogger<NetBoxGeoFeedProvider> logger,
IHttpClientFactory httpClientFactory)
: base(configuration, logger, httpClientFactory)
{
}
protected override NetboxData? DeserializeJsonData(Stream stringResult)
{
return JsonSerializer.Deserialize(stringResult, AppJsonSerializerContext.Default.NetboxData);
}
}
}

View File

@ -0,0 +1,41 @@
using AS1024.GeoFeed.Core.CacheService;
using AS1024.GeoFeed.Core.GeoFeedPreloader;
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Core.WebLogic;
namespace AS1024.GeoFeed.MinimalAPI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddTransient<IGeoFeedProvider, NetboxAoTGeoFeedProvider>();
builder.Services.AddHostedService<GeoFeedCacheService>();
builder.Services.AddHostedService<PreLoadGeoFeed>();
builder.Services.AddTransient<IGeoFeedPersistentCacheProvider, GeoFeedLocalFileCache>();
builder.Services.AddScoped<GeoFeedReturn>();
builder.Services.AddMemoryCache();
builder.Services.AddLogging();
builder.Services.AddHttpClient();
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
var app = builder.Build();
app.Map("/geofeed.csv", async (GeoFeedReturn feedReturn) =>
{
return await feedReturn.GetGeoFeed();
});
app.Map("/geofeed", async (GeoFeedReturn feedReturn) =>
{
return await feedReturn.GetGeoFeed();
});
app.Run();
}
}
}

View File

@ -0,0 +1,24 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "geofeed.csv",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5127"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/todos",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json"
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -1,11 +1,10 @@
using System.Text.Json.Serialization;
namespace AS1024.GeoFeed.Models
namespace AS1024.GeoFeed.Models
{
public class NetboxData
{
public List<Result>? Results { get; set; }
public string? Next { get; set; }
public string? Previous { get; set; }
public string? Previous { get; set; }
}
public class Result
@ -41,7 +40,8 @@ namespace AS1024.GeoFeed.Models
/// <summary>
/// This class represents the IP GeoFeed Entry
/// </summary>
public class IPGeoFeed : CustomFields {
public class IPGeoFeed : CustomFields
{
/// <summary>
/// Represents the IP Prefix for the associated GeoFeed entry
/// </summary>

View File

@ -3,7 +3,20 @@ 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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed", "AS1024.GeoFeed\AS1024.GeoFeed.csproj", "{6292097C-7F35-45BB-B2B0-1918DF49FE7D}"
ProjectSection(ProjectDependencies) = postProject
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5} = {8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.Models", "AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj", "{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.Core", "AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj", "{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.MinimalAPI", "AS1024.GeoFeed.MinimalAPI\AS1024.GeoFeed.MinimalAPI.csproj", "{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.Core.SqliteGeoFeedCache", "AS1024.GeoFeed.Core.SqliteGeoFeedCache\AS1024.GeoFeed.Core.SqliteGeoFeedCache.csproj", "{3459BB31-FA7A-44D1-872D-C5338ACFBF80}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS1024.GeoFeed.Core.WebLogic", "AS1024.GeoFeed.Core.WebLogic\AS1024.GeoFeed.Core.WebLogic.csproj", "{58BDCE89-FCC0-478F-BBDE-B89833712AAB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -15,6 +28,26 @@ Global
{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
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Release|Any CPU.Build.0 = Release|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Release|Any CPU.Build.0 = Release|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Release|Any CPU.Build.0 = Release|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Release|Any CPU.Build.0 = Release|Any CPU
{58BDCE89-FCC0-478F-BBDE-B89833712AAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58BDCE89-FCC0-478F-BBDE-B89833712AAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58BDCE89-FCC0-478F-BBDE-B89833712AAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58BDCE89-FCC0-478F-BBDE-B89833712AAB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -9,7 +9,23 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core.SqliteGeoFeedCache\AS1024.GeoFeed.Core.SqliteGeoFeedCache.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Core.WebLogic\AS1024.GeoFeed.Core.WebLogic.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project>

View File

@ -1,9 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using AS1024.GeoFeed.Interfaces;
using AS1024.GeoFeed.GeoFeedBuilder;
using Microsoft.Extensions.Caching.Memory;
using AS1024.GeoFeed.Models;
using System.Text;
using AS1024.GeoFeed.Core.WebLogic;
using Microsoft.AspNetCore.Mvc;
namespace AS1024.GeoFeed.Controllers
{
@ -13,52 +9,18 @@ namespace AS1024.GeoFeed.Controllers
public class GeofeedController : ControllerBase
{
private readonly IGeoFeedProvider builder;
private readonly IMemoryCache memoryCache;
private readonly IWebHostEnvironment environment;
private readonly ILogger<GeofeedController> logger;
private const string GeoFeedCacheKey = "GeoFeedData";
private readonly GeoFeedReturn feedReturn;
public GeofeedController(IGeoFeedProvider builder,
IMemoryCache memoryCache,
IWebHostEnvironment environment,
ILogger<GeofeedController> logger) {
this.logger = logger;
this.builder = builder;
this.memoryCache = memoryCache;
this.environment = environment;
public GeofeedController(GeoFeedReturn feedReturn)
{
this.feedReturn = feedReturn;
}
[HttpGet]
[Route("")]
public async Task<IActionResult> Get()
public async Task<IResult> Get()
{
try
{
if (!memoryCache.TryGetValue(GeoFeedCacheKey, out List<IPGeoFeed>? feed))
{
feed = await builder.GetGeoFeedData();
if (environment.IsProduction())
{
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(15));
memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions);
}
}
string csvContent = feed.ToGeoFeedCsv(); // Assuming ToGeoFeedCsv() returns a string in CSV format.
byte[] contentBytes = Encoding.UTF8.GetBytes(csvContent);
string contentType = "text/csv";
return new FileContentResult(contentBytes, contentType)
{
FileDownloadName = "geofeed.csv"
};
} catch (Exception ex)
{
logger.LogError($"Geofeed generation failed. Exception: {ex}");
return StatusCode(500);
}
return await feedReturn.GetGeoFeed();
}
}
}

View File

@ -0,0 +1,54 @@
#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 alpine:latest AS base
ENV \
# UID of the non-root user 'app'
APP_UID=1654 \
# Configure web servers to bind to port 8080 when present
ASPNETCORE_HTTP_PORTS=8080 \
# Enable detection of running in a container
DOTNET_RUNNING_IN_CONTAINER=true \
# Set the invariant mode since ICU package isn't included (see https://github.com/dotnet/announcements/issues/20)
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true
RUN apk add --upgrade --no-cache \
ca-certificates-bundle \
\
# .NET dependencies
libgcc \
libssl3 \
libstdc++ \
zlib
# Create a non-root user and group
RUN addgroup \
--gid=$APP_UID \
app \
&& adduser \
--uid=$APP_UID \
--ingroup=app \
--disabled-password \
app
WORKDIR /app
USER app
EXPOSE 8080
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
ARG BUILD_CONFIGURATION=Release
ARG TARGETARCH
WORKDIR /src
COPY ["AS1024.GeoFeed/AS1024.GeoFeed.csproj", "AS1024.GeoFeed/"]
RUN dotnet restore "./AS1024.GeoFeed/./AS1024.GeoFeed.csproj" -a $TARGETARCH
COPY . .
WORKDIR "/src/AS1024.GeoFeed"
RUN dotnet build "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/build -a $TARGETARCH /p:StaticLink=true
FROM --platform=$BUILDPLATFORM build AS publish
ARG BUILD_CONFIGURATION=Release
ARG TARGETARCH
RUN dotnet publish "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/publish -a $TARGETARCH --self-contained
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["/app/AS1024.GeoFeed"]

View File

@ -1,20 +0,0 @@
using AS1024.GeoFeed.Models;
using System.Text;
namespace AS1024.GeoFeed.GeoFeedBuilder
{
public static class GeoFeedTools
{
public static string ToGeoFeedCsv(this List<IPGeoFeed> geoFeeds)
{
StringBuilder csvContent = new();
foreach (IPGeoFeed feed in geoFeeds)
{
csvContent.AppendLine($"{feed.Prefix},{feed.GeolocCountry},{feed.GeolocRegion},{feed.GeolocCity},{feed.GeolocPostalCode}");
}
return csvContent.ToString();
}
}
}

View File

@ -1,5 +1,10 @@
using AS1024.GeoFeed.GeoFeedBuilder;
using AS1024.GeoFeed.Interfaces;
using AS1024.GeoFeed.Core.CacheService;
using AS1024.GeoFeed.Core.GeoFeedPreloader;
using AS1024.GeoFeed.Core.GeoFeedProviders;
using AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache;
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Core.WebLogic;
using Microsoft.EntityFrameworkCore;
namespace AS1024.GeoFeed
{
@ -11,6 +16,14 @@ namespace AS1024.GeoFeed
builder.Services.AddHostedService<PreLoadGeoFeed>();
builder.Services.AddTransient<IGeoFeedProvider, NetBoxGeoFeedProvider>();
builder.Services.AddScoped<GeoFeedReturn>();
builder.Services.AddDbContext<GeoFeedCacheDbContext>(
options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("LocalFeedCache"));
});
builder.Services.AddScoped<IGeoFeedPersistentCacheProvider, GeoFeedSqliteCache>();
builder.Services.AddHostedService<GeoFeedCacheService>();
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
// Add services to the container.

View File

@ -5,6 +5,9 @@
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"LocalFeedCache": "Data Source=localcache.db"
},
"AllowedHosts": "*",
"APIKey": "",
"NetBoxHost": ""

View File

@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) [year], [fullname]
Copyright (c) 2024, 12393239 Canada Inc.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

View File

@ -6,21 +6,56 @@ This web application provides a self-published IP geolocation feed, conforming t
The application is implemented in C# using .NET 8.0, ensuring a robust and modern back-end architecture. It's built to communicate securely over HTTPS with the NetBox API, adhering to best practices for data transmission and security.
This project was inspired by the GitHub project [GeoBox](https://github.com/FrumentumNL/GeoBox), which provided foundational ideas for the development of this application.
## Application Variants
### AS1024.GeoFeed - MVC Version (Recommended)
The standard version of this application, named `AS1024.GeoFeed`, is built using the Model-View-Controller (MVC) architecture. This variant is fully supported and recommended for most use cases. It offers a complete set of features and is optimized for robustness and scalability.
### AS1024.GeoFeed.MinimalAPI - MinimalAPI Version (Experimental)
In addition to the standard MVC version, there is an experimental MinimalAPI version of the application named `AS1024.GeoFeed.MinimalAPI`. This variant is designed for environments that require extremely fast startup times, such as serverless containers. However, it comes with limited support and a reduced feature set. We recommend using the `AS1024.GeoFeed.MinimalAPI` version only if your deployment environment necessitates near-instant startup times and you are comfortable with its experimental nature and limitations.
**Note**: While the `AS1024.GeoFeed.MinimalAPI` version offers performance benefits in specific scenarios, we strongly recommend deploying the `AS1024.GeoFeed` MVC version for most applications to ensure full functionality and support.
## Features
- **GeoFeed Generation**: Dynamically generates a geolocation feed in CSV format as specified in RFC 8805.
- **Caching Mechanism**: Implements an efficient caching strategy to reduce redundant API calls and enhance performance.
- **Local Disk Fallback Caching Mechanism**: In the event of a failure to communicate with the NetBox instance specified, the web app will return data that is locally cached inside a SQLite database. In the minimal API version of the GeoFeed application, this will be stored as a file, rather than a SQLite database as Minimal API does not yet support Entity Framework Core at this time.
- **Secure Communication**: Ensures secure data retrieval from NetBox over HTTPS.
## Configuration
The application requires the following configuration variables to be set:
The application requires the following configuration variables to be set, depending on the version you are using:
### For AS1024.GeoFeed - MVC Version
1. **APIKey**: This is the API key used for authenticating with the NetBox API. Ensure this key has the necessary permissions to access the required resources.
2. **NetBoxHost**: The hostname of the NetBox instance from which the application retrieves data. For example, `netbox.example.com`.
3. **LocalFeedCache**: This connection string is for a local SQLite Database (using EF Core) that caches the geofeed data from Netbox. The syntax should follow the standard SQLite EF Core connection string format.
### For AS1024.GeoFeed.MinimalAPI - MinimalAPI Version
1. **APIKey**: This is the API key used for authenticating with the NetBox API. Ensure this key has the necessary permissions to access the required resources.
2. **NetBoxHost**: The hostname of the NetBox instance from which the application retrieves data. For example, `netbox.example.com`.
3. **TempCache** (optional): This configuration is specific to the Minimal API version. It is optional, but if users wish to specify a different location for storing and serving the cached geofeed data, this value can be adjusted to point to the desired save location.
These variables can be set in your application's configuration file or through environment variables, depending on your deployment strategy.
## NetBox Custom Fields
Ensure that your NetBox instance is configured with the following custom fields:
- `geoloc_city`: (Text) Represents the city and is not required to be filled in.
- `geoloc_country`: (Selection) Represents the country and is not required to be filled in.
- `geoloc_has_location`: (Boolean) Indicates if there is geolocation data available and is required.
- `geoloc_postal_code`: (Text) Represents the postal code and is not required to be filled in.
- `geoloc_region`: (Selection) Represents the region and is not required to be filled in.
These fields are critical for the application to accurately retrieve and format geolocation data.
## Getting Started
To build and run the application, follow these steps:
@ -32,9 +67,36 @@ To build and run the application, follow these steps:
5. After a successful build, you can start the application by running `dotnet run`.
6. The application will start, and you can access the endpoints as specified.
## Docker Build
## Building a Docker Image
A Dockerfile is provided for your convienence. This has not been tested as of yet.
### Building a Docker Image for the Minimal API Version
To containerize the `AS1024.GeoFeed` version using Docker, you can follow these steps to build a Docker image:
1. Open your terminal or command line interface.
2. Navigate to the root directory of this repository.
3. Execute the following Docker build command: ``docker buildx build --platform=linux/amd64,linux/arm64 -f .\AS1024.GeoFeed\Dockerfile .``
### Building a Docker Image for the Minimal API Version
To containerize the `AS1024.GeoFeed.MinimalAPI` version using Docker, you can follow these steps to build a Docker image:
1. Open your terminal or command line interface.
2. Navigate to the root directory of this repository.
3. Execute the following Docker build command: ``docker buildx build --platform=linux/amd64 -f .\AS1024.GeoFeed.MinimalAPI\Dockerfile.alpine-selfcontained .``
**Currently the minimal API version does not support being cross compiled for different CPU architectures. It is best built with the same CPU architecture where the build machine is targeting to.**
Ensure you have Docker installed and configured on your machine before executing this command. This Docker image can then be used to deploy the application in containerized environments such as Kubernetes or Docker Compose setups.
## Docker Deployment
To deploy the Docker container of this GeoFeed application, simply pull the image for the following variants:
- MVC Variant ``docker pull git.startmywifi.com/as1024/geofeed:latest``
- Minimal API Variant ``docker pull git.startmywifi.com/as1024/geofeed:aot-minimal``
To configure the container, please refer to the Configuration section above. You should be able to configure this with either an environment variable or map an ``appSettings.json`` file to the container.
## Endpoints
@ -47,6 +109,8 @@ The application provides the following key endpoints:
This application is designed to always communicate over HTTPS with NetBox, ensuring that the data transfer is encrypted and secure.
---
## Extending Beyond NetBox
For more information about configuring and using this application, please refer to the official .NET documentation and the NetBox API guide.
If your current IPAM solution is not netbox and wish to extend this web application to use the desired IPAM solution of choice, the interface `IGeoFeedProvider` is available for extensibility. To use your custom IPAM backend ensure that `NetboxAoTGeoFeedProvider` and `NetboxGeoFeedProvider` are not registered in the dependency injection container in the Web Apps. Once unregistered, register your custom IPAM communication backend provider to your needs and the web app should work in both AOT and MVC mode.
Currently the Minimal API implementation of this web application only supports code that does not require reflection. This is a known limitation of native AOT deployments. If your code utilizes reflection or is not properly adapted for source generation, the minimal API version will **not work**.

14
docker/Caddyfile Normal file
View File

@ -0,0 +1,14 @@
{$GEOFEEDDOMAIN}:443 {
log {
level INFO
output file {$LOG_FILE} {
roll_size 10MB
roll_keep 10
}
}
# Use the ACME HTTP-01 challenge to get a cert for the configured domain.
tls {$EMAIL}
reverse_proxy geofeed:8080
}

28
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,28 @@
version: '3.7'
services:
geofeed:
# use the image tag aot-minimal for the AOT version for fastest startup performance
image: git.startmywifi.com/as1024/geofeed:latest
restart: always
volumes:
- './data:/data'
environment:
- ASPNETCORE_URLS=http://+:8080
- ConnectionString__LocalFeedCache=Data Source=/data/geofeed-cache.db
- APIKey=APIKeyHere
- NetBoxHost=netboxhosthere
caddy:
image: caddy:2
restart: always
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-config:/config
- ./caddy-data:/data
environment:
GEOFEEDDOMAIN: "https://geofeed.exampleas.net"
EMAIL: "noc@example.com" # The email address to use for ACME registration.
LOG_FILE: "/data/access.log"