发布v1.0.0

- 实现动态创建WebApplication
- 实现自动申请Http证书
- 实现自动续期Http证书
- 实现QPS实时预览
- 实现每天请求数据统计
This commit is contained in:
token
2024-04-03 00:45:30 +08:00
parent 47316ac1dc
commit 56edb3f211
32 changed files with 440 additions and 451 deletions
+3 -1
View File
@@ -103,7 +103,9 @@ StyleCopReport.xml
*.pidb
*.svclog
*.scc
*.db*
yarn.lock
package-lock.json
# Chutzpah Test files
_Chutzpah*
+19
View File
@@ -0,0 +1,19 @@
namespace FastGateway.Core;
public enum ApiServiceType : byte
{
/// <summary>
/// 静态代理
/// </summary>
StaticProxy = 1,
/// <summary>
/// 单个服务
/// </summary>
SingleService = 2,
/// <summary>
/// 负载均衡
/// </summary>
LoadBalance = 3,
}
+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.3",
"commands": [
"dotnet-ef"
]
}
}
}
@@ -30,6 +30,9 @@ public sealed class RenewSslBackgroundService(IServiceProvider serviceProvider)
masterDbContext.Certs.Update(certItem);
await masterDbContext.SaveChangesAsync(stoppingToken);
// 成功以后需要刷新证书列表
await CertService.LoadCerts(masterDbContext);
}
catch (Exception e)
{
@@ -10,8 +10,8 @@ public sealed class StatisticsBackgroundService(IServiceProvider serviceProvider
private static readonly ConcurrentDictionary<string, StatisticRequestCountDto> RequestCountDic = new();
private static readonly ConcurrentDictionary<string,StatisticIpDto> IpDic = new();
private static readonly ConcurrentDictionary<string, StatisticIpDto> IpDic = new();
private static DateTime _lastTime = DateTime.Now;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -36,7 +36,7 @@ public sealed class StatisticsBackgroundService(IServiceProvider serviceProvider
IpDic.TryAdd(ipDto.Ip, ipDto);
}
}
private async Task RequestCountAsync()
{
await using var scope = serviceProvider.CreateAsyncScope();
@@ -45,6 +45,18 @@ public sealed class StatisticsBackgroundService(IServiceProvider serviceProvider
{
var requestCount = await StatisticRequestChannel.Reader.ReadAsync();
// 判断是否超过5分钟,如果超过5分钟就将之前的数据写入数据库
if (DateTime.Now - _lastTime >= TimeSpan.FromMinutes(5))
{
_lastTime = DateTime.Now;
foreach (var item in RequestCountDic)
{
await CheckAddOrUpdateAsync(masterDbContext, item.Value);
}
RequestCountDic.Clear();
}
// 判断是否存在缓存
if (RequestCountDic.TryGetValue(requestCount.ServiceId, out var statisticRequestCount))
{
@@ -57,20 +69,36 @@ public sealed class StatisticsBackgroundService(IServiceProvider serviceProvider
RequestCountDic.TryAdd(requestCount.ServiceId, requestCount);
}
//每隔5分钟写入数据库
if (DateTime.Now - _lastTime <= TimeSpan.FromMinutes(5)) continue;
_lastTime = DateTime.Now;
foreach (var item in RequestCountDic)
{
await CheckAddOrUpdateAsync(masterDbContext, item.Value);
}
RequestCountDic.Clear();
}
}
private async ValueTask CheckAddOrUpdateAsync(MasterDbContext masterDbContext,
public static StatisticRequestCountDto GetCurrentRequestCount(string? serviceId)
{
if (string.IsNullOrWhiteSpace(serviceId))
{
return new StatisticRequestCountDto
{
RequestCount = RequestCountDic.Values.Sum(x => x.RequestCount),
Error4xxCount = RequestCountDic.Values.Sum(x => x.Error4xxCount),
Error5xxCount = RequestCountDic.Values.Sum(x => x.Error5xxCount),
};
}
if (RequestCountDic.TryGetValue(serviceId, out var statisticRequestCount))
{
return statisticRequestCount;
}
return new StatisticRequestCountDto
{
ServiceId = serviceId,
RequestCount = 0,
Error4xxCount = 0,
Error5xxCount = 0
};
}
private static async ValueTask CheckAddOrUpdateAsync(MasterDbContext masterDbContext,
StatisticRequestCountDto requestCountDto)
{
// 判断数据库是否存在今年今月今日的数据
+2
View File
@@ -34,6 +34,8 @@ public class Location
/// </summary>
public string[]? TryFiles { get; set; }
public ApiServiceType Type { get; set; }
/// <summary>
/// 负载均衡模型
/// </summary>
+4
View File
@@ -2,6 +2,8 @@
public class LocationInput
{
public string? Id { get; set; }
/// <summary>
/// 绑定服务ID
/// </summary>
@@ -32,6 +34,8 @@ public class LocationInput
/// </summary>
public string[]? TryFiles { get; set; }
public ApiServiceType Type { get; set; }
/// <summary>
/// 负载均衡模型
/// </summary>
+2
View File
@@ -2,6 +2,8 @@
public class ServiceInput
{
public string? Id { get; set; }
/// <summary>
/// 多个服务名称
/// </summary>
@@ -20,7 +20,7 @@ public sealed class StatisticRequestCountDto
/// <summary>
/// 关联服务ID
/// </summary>
public string ServiceId { get; set; }
public string? ServiceId { get; set; }
public DateTime CreatedTime { get; set; }
}
+11
View File
@@ -0,0 +1,11 @@
namespace FastGateway;
public class FastContext
{
public static IQpsService QpsService { get; protected set; } = null!;
public static void SetQpsService(IQpsService qpsService)
{
QpsService = qpsService;
}
}
+8 -4
View File
@@ -4,14 +4,13 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServerGarbageCollection>true</ServerGarbageCollection>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<!-- <PackageReference Include="FastMiniApis" Version="0.1.2" />-->
<PackageReference Include="Certes" Version="3.0.4" />
<PackageReference Include="FastMiniApis" Version="0.1.2" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -20,7 +19,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
@@ -30,4 +28,10 @@
<ProjectReference Include="..\FastGateway.Core\FastGateway.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="gateway.pfx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
-2
View File
@@ -1,10 +1,8 @@
global using FastGateway.EFCore;
global using FastMiniApis.Core;
global using Mapster;
global using Microsoft.EntityFrameworkCore;
global using FastGateway.Domain;
global using FastGateway.Dto;
global using FastMiniApis;
global using System.Text.Json;
global using FastGateway.Core;
global using FastGateway.Services;
@@ -4,8 +4,6 @@ namespace FastGateway.Middlewares;
public sealed class StatisticsMiddleware(ICurrentContext currentContext) : IMiddleware
{
private readonly IQpsService _qpsService = FastApp.GetRequiredService<IQpsService>();
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await next(context);
@@ -16,8 +14,8 @@ public sealed class StatisticsMiddleware(ICurrentContext currentContext) : IMidd
StatisticsBackgroundService.WriteAsync(new StatisticRequestCountDto()
{
CreatedTime = DateTime.Now,
ServiceId = currentContext.ServiceId,
Error4xxCount = 1,
Error5xxCount = 0,
RequestCount = 1,
});
break;
@@ -25,7 +23,7 @@ public sealed class StatisticsMiddleware(ICurrentContext currentContext) : IMidd
StatisticsBackgroundService.WriteAsync(new StatisticRequestCountDto()
{
CreatedTime = DateTime.Now,
Error4xxCount = 0,
ServiceId = currentContext.ServiceId,
Error5xxCount = 1,
RequestCount = 1,
});
@@ -34,24 +32,12 @@ public sealed class StatisticsMiddleware(ICurrentContext currentContext) : IMidd
StatisticsBackgroundService.WriteAsync(new StatisticRequestCountDto()
{
CreatedTime = DateTime.Now,
Error4xxCount = 0,
Error5xxCount = 0,
ServiceId = currentContext.ServiceId,
RequestCount = 1,
});
break;
}
_qpsService.IncrementServiceRequests(currentContext.ServiceId);
//
//
// // 获取ip
// var ip = context.Connection.RemoteIpAddress?.ToString();
//
// // 获取请求头中的IP
// if (context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwardedFor))
// {
// ip = forwardedFor;
// }
FastContext.QpsService.IncrementServiceRequests(currentContext.ServiceId);
}
}
@@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace FastGateway.Migrations
{
[DbContext(typeof(MasterDbContext))]
[Migration("20240401173039_Initial")]
[Migration("20240402152541_Initial")]
partial class Initial
{
/// <inheritdoc />
@@ -90,6 +90,9 @@ namespace FastGateway.Migrations
b.Property<string>("TryFiles")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("UpStreams")
.IsRequired()
.HasColumnType("TEXT");
@@ -82,6 +82,7 @@ namespace FastGateway.Migrations
AddHeader = table.Column<string>(type: "TEXT", nullable: false),
Root = table.Column<string>(type: "TEXT", nullable: true),
TryFiles = table.Column<string>(type: "TEXT", nullable: true),
Type = table.Column<int>(type: "INTEGER", nullable: false),
LoadType = table.Column<int>(type: "INTEGER", nullable: false),
UpStreams = table.Column<string>(type: "TEXT", nullable: false)
},
@@ -1,236 +0,0 @@
// <auto-generated />
using System;
using FastGateway.EFCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FastGateway.Migrations
{
[DbContext(typeof(MasterDbContext))]
partial class MasterDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.3");
modelBuilder.Entity("FastGateway.Domain.Cert", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<bool>("AutoRenew")
.HasColumnType("INTEGER");
b.Property<string>("Certs")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Domains")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("Expired")
.HasColumnType("INTEGER");
b.Property<string>("Issuer")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("NotAfter")
.HasColumnType("TEXT");
b.Property<int>("RenewStats")
.HasColumnType("INTEGER");
b.Property<DateTime?>("RenewTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("cert", (string)null);
});
modelBuilder.Entity("FastGateway.Domain.Location", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("AddHeader")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("LoadType")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProxyPass")
.HasColumnType("TEXT");
b.Property<string>("Root")
.HasColumnType("TEXT");
b.Property<string>("ServiceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TryFiles")
.HasColumnType("TEXT");
b.Property<string>("UpStreams")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("location", (string)null);
});
modelBuilder.Entity("FastGateway.Domain.Service", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<bool>("Enable")
.HasColumnType("INTEGER");
b.Property<bool>("EnableFlowMonitoring")
.HasColumnType("INTEGER");
b.Property<bool>("EnableHttp3")
.HasColumnType("INTEGER");
b.Property<bool>("EnableRequestSource")
.HasColumnType("INTEGER");
b.Property<bool>("EnableTunnel")
.HasColumnType("INTEGER");
b.Property<bool>("IsHttps")
.HasColumnType("INTEGER");
b.Property<ushort>("Listen")
.HasColumnType("INTEGER");
b.Property<string>("ServiceNames")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SslCertificate")
.HasColumnType("TEXT");
b.Property<string>("SslCertificatePassword")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("service", (string)null);
});
modelBuilder.Entity("FastGateway.Domain.StatisticIp", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Count")
.HasColumnType("INTEGER");
b.Property<byte>("Day")
.HasColumnType("INTEGER");
b.Property<string>("Ip")
.HasColumnType("TEXT");
b.Property<string>("Location")
.HasColumnType("TEXT");
b.Property<byte>("Month")
.HasColumnType("INTEGER");
b.Property<string>("ServiceId")
.HasColumnType("TEXT");
b.Property<ushort>("Year")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("statistic_ip", (string)null);
});
modelBuilder.Entity("FastGateway.Domain.StatisticRequestCount", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte>("Day")
.HasColumnType("INTEGER");
b.Property<int>("Error4xxCount")
.HasColumnType("INTEGER");
b.Property<int>("Error5xxCount")
.HasColumnType("INTEGER");
b.Property<byte>("Month")
.HasColumnType("INTEGER");
b.Property<int>("RequestCount")
.HasColumnType("INTEGER");
b.Property<string>("ServiceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ushort>("Year")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("statistic_request_count", (string)null);
});
modelBuilder.Entity("FastGateway.Domain.Location", b =>
{
b.HasOne("FastGateway.Domain.Service", null)
.WithMany("Locations")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FastGateway.Domain.StatisticRequestCount", b =>
{
b.HasOne("FastGateway.Domain.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("FastGateway.Domain.Service", b =>
{
b.Navigation("Locations");
});
#pragma warning restore 612, 618
}
}
}
+103 -38
View File
@@ -1,7 +1,6 @@
using FastGateway;
using FastGateway.BackgroundServices;
using FastGateway.Options;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
@@ -22,37 +21,9 @@ builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new JsonDateTimeConverter());
});
builder.Services.AddSingleton<IQpsService,QpsService>();
builder.Services.AddSingleton<IQpsService, QpsService>();
builder.Services.AddHostedService<RenewSslBackgroundService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "FastGateway", Version = "v1" });
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer",
},
},
Array.Empty<string>()
}
});
});
builder.Services.AddHostedService<StatisticsBackgroundService>();
builder.Services.AddMapster();
@@ -61,33 +32,127 @@ builder.Services.AddDbContext<MasterDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("Default"));
});
builder.Services.AddService();
builder.Services
.AddAuthorization()
.AddJwtBearerAuthentication();
var app = builder.Build();
app.MapFastMiniApis();
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/.well-known/acme-challenge/{token}", AcmeChallenge.Challenge);
app.MapPost("/.well-known/acme-challenge/{token}", AcmeChallenge.Challenge);
#region Authorize
var authorizeService = app.MapGroup("/api/v1/Authorize")
.RequireAuthorization();
authorizeService.MapPost(string.Empty,
AuthorizeService.CreateAsync);
#endregion
#region ApiService
var apiService = app.MapGroup("/api/v1/ApiService")
.RequireAuthorization();
apiService.MapGet("/List",
ApiServiceService.GetListAsync);
apiService.MapGet(string.Empty,
ApiServiceService.GetAsync);
apiService.MapPost(string.Empty,
ApiServiceService.CreateAsync);
apiService.MapPut(string.Empty,
ApiServiceService.UpdateAsync);
apiService.MapDelete(string.Empty,
ApiServiceService.DeleteAsync);
apiService.MapGet("/SelectList",
ApiServiceService.GetSelectListAsync);
apiService.MapPost("/StartService",
ApiServiceService.StartServiceAsync);
apiService.MapPost("/StopService",
ApiServiceService.StopServiceAsync);
apiService.MapPost("/RestartService",
ApiServiceService.RestartServiceAsync);
apiService.MapPost("/ServiceStats",
ApiServiceService.ServiceStats);
apiService.MapPost("/CheckDirectoryExistence",
ApiServiceService.CheckDirectoryExistenceAsync);
#endregion
#region Cert
var certService = app.MapGroup("/api/v1/Cert")
.RequireAuthorization();
certService.MapGet("/List",
CertService.GetListAsync);
certService.MapGet(string.Empty,
CertService.GetAsync);
certService.MapPost(string.Empty,
CertService.CreateAsync);
certService.MapPut(string.Empty,
CertService.UpdateAsync);
certService.MapDelete(string.Empty,
CertService.DeleteAsync);
#endregion
#region Qps
var apiQpsService = app.MapGroup("/api/v1/Qps")
.RequireAuthorization();
apiQpsService.MapGet(string.Empty,
ApiQpsService.GetAsync);
#endregion
#region Statistic
var statisticService = app.MapGroup("/api/v1/Statistic")
.RequireAuthorization();
statisticService.MapGet("/DayRequestCount",
StatisticRequestService.GetDayStatisticAsync);
statisticService.MapGet("/TotalRequestCount",
StatisticRequestService.GetTotalStatisticAsync);
#endregion
FastContext.SetQpsService(app.Services.GetRequiredService<IQpsService>());
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MasterDbContext>();
await db.Database.MigrateAsync();
await ApiServiceService.LoadServices(db);
await CertService.LoadCerts(db);
}
app.Run();
await app.RunAsync();
@@ -12,7 +12,7 @@
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5283",
"environmentVariables": {
@@ -22,7 +22,7 @@
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7115;http://localhost:5283",
"environmentVariables": {
+66 -47
View File
@@ -1,14 +1,16 @@
using System.Net;
using System.Security.Cryptography.X509Certificates;
using FastGateway.Infrastructures;
using FastGateway.Middlewares;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.Transforms;
namespace FastGateway.Services;
public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
public class ApiServiceService
{
private static readonly Dictionary<string, WebApplication> WebApplications = new();
@@ -33,7 +35,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task CreateAsync(ServiceInput input, MasterDbContext masterDbContext)
public static async Task CreateAsync(ServiceInput input, MasterDbContext masterDbContext)
{
var service = input.Adapt<Service>();
@@ -54,7 +56,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task UpdateAsync(string id, ServiceInput input, MasterDbContext masterDbContext)
public static async Task UpdateAsync(string id, ServiceInput input, MasterDbContext masterDbContext)
{
var service = await masterDbContext.Services.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id);
@@ -72,7 +74,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task DeleteAsync(string id, MasterDbContext masterDbContext)
public static async Task DeleteAsync(string id, MasterDbContext masterDbContext)
{
try
{
@@ -105,7 +107,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public ResultDto<bool> CheckDirectoryExistenceAsync(string path)
public static ResultDto<bool> CheckDirectoryExistenceAsync(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
@@ -130,7 +132,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task<Service?> GetAsync(string id, MasterDbContext masterDbContext)
public static async Task<Service?> GetAsync(string id, MasterDbContext masterDbContext)
{
return await masterDbContext.Services.AsNoTracking()
.Include(x => x.Locations)
@@ -138,7 +140,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task<ResultDto<PageResultDto<Service>>> GetListAsync(int page, int pageSize,
public static async Task<ResultDto<PageResultDto<Service>>> GetListAsync(int page, int pageSize,
MasterDbContext masterDbContext)
{
if (page < 1)
@@ -166,7 +168,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task<ResultDto<List<ServiceSelectDto>>> GetSelectListAsync(MasterDbContext masterDbContext)
public static async Task<ResultDto<List<ServiceSelectDto>>> GetSelectListAsync(MasterDbContext masterDbContext)
{
var result = await masterDbContext.Services
.AsNoTracking()
@@ -180,7 +182,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public ResultDto<Dictionary<string, bool>> ServiceStats([FromBody] List<string> ids)
public static ResultDto<Dictionary<string, bool>> ServiceStats([FromBody] List<string> ids)
{
var result = new Dictionary<string, bool>();
@@ -196,7 +198,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task StartServiceAsync(string id, MasterDbContext masterDbContext)
public static async Task StartServiceAsync(string id, MasterDbContext masterDbContext)
{
if (WebApplications.TryGetValue(id, out var app))
{
@@ -213,7 +215,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task StopServiceAsync(string id)
public static async Task StopServiceAsync(string id)
{
if (WebApplications.TryGetValue(id, out var app))
{
@@ -223,7 +225,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
[Authorize]
public async Task RestartServiceAsync(string id, MasterDbContext masterDbContext)
public static async Task RestartServiceAsync(string id, MasterDbContext masterDbContext)
{
if (WebApplications.TryGetValue(id, out var app))
{
@@ -242,20 +244,29 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
var service = (Service)state;
var builder = WebApplication.CreateBuilder([]);
var qpsService = FastApp.GetRequiredService<IQpsService>();
qpsService?.AddServiceQps(service.Id);
FastContext.QpsService?.AddServiceQps(service.Id);
IContentTypeProvider defaultContentTypeProvider = new DefaultContentTypeProvider();
builder.WebHost.UseKestrel(options =>
{
options.Listen(IPAddress.Parse("0.0.0.0"), service.Listen, listenOptions =>
options.ConfigureHttpsDefaults(adapterOptions =>
{
if (service.IsHttps)
{
// listenOptions.UseHttps(service.SslCertificate, service.SslCertificatePassword);
adapterOptions.ServerCertificateSelector = (context, name) =>
CertService.Certs.TryGetValue(name, out var cert)
? new X509Certificate2(cert.File, cert.Password)
: new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "gateway.pfx"), "010426");
}
});
options.Listen(IPAddress.Parse("0.0.0.0"), service.Listen, listenOptions =>
{
if (!service.IsHttps) return;
listenOptions.UseHttps();
if (service.EnableHttp3)
{
@@ -266,7 +277,6 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
var routes = new List<RouteConfig>();
var clusters = new List<ClusterConfig>();
var staticFiles = new List<Location>();
// 绑定路由到service
foreach (var location in service.Locations)
@@ -278,7 +288,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
ClusterId = clusterId,
Match = new RouteMatch()
{
Path = location.Path + "{**catch-all}",
Path = location.Path.TrimEnd('/') + "/{**catch-all}",
Hosts = service.ServiceNames
}
};
@@ -291,8 +301,18 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
ClusterId = clusterId
};
// 如果配置了 UpStreams ,多个则负载均衡
if (location.UpStreams.Count > 0)
if (location.Type == ApiServiceType.SingleService)
{
destinations.Add(location.ProxyPass, new DestinationConfig()
{
Address = location.ProxyPass,
Host = new Uri(location.ProxyPass).Host,
});
clusters.Add(cluster);
continue;
}
else if (location.Type == ApiServiceType.LoadBalance)
{
foreach (var upStream in location.UpStreams.Where(x => !string.IsNullOrEmpty(x.Server)))
{
@@ -303,28 +323,13 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}
clusters.Add(cluster);
continue;
}
if (!string.IsNullOrWhiteSpace(location.ProxyPass))
{
destinations.Add(location.ProxyPass, new DestinationConfig()
{
Address = location.ProxyPass,
Host = new Uri(location.ProxyPass).Host,
});
clusters.Add(cluster);
continue;
}
if (!string.IsNullOrWhiteSpace(location.Root))
else if (location.Type == ApiServiceType.StaticProxy)
{
routes.Remove(route);
staticFiles.Add(location);
// 静态文件
continue;
}
}
builder.Services.AddSingleton<ICurrentContext>(new CurrentContext()
{
@@ -332,7 +337,16 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
}).AddSingleton<StatisticsMiddleware>();
builder.Services.AddReverseProxy()
.LoadFromMemory(routes, clusters);
.LoadFromMemory(routes, clusters)
// 删除所有代理的前缀
.AddTransforms(context =>
{
var prefix = context.Route.Match.Path?.Replace("/{**catch-all}", "");
if (!string.IsNullOrEmpty(prefix))
{
context.AddPathRemovePrefix(prefix);
}
});
var app = builder.Build();
@@ -340,7 +354,7 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
app.UseMiddleware<StatisticsMiddleware>();
foreach (var location in staticFiles)
foreach (var location in service.Locations.Where(x => x.Type == ApiServiceType.StaticProxy))
{
app.Map(location.Path.TrimEnd('/'), app =>
{
@@ -358,20 +372,25 @@ public class ApiServiceService() : ServiceBase("/api/v1/ApiService")
return;
}
if (location.TryFiles == null || location.TryFiles.Length == 0)
{
context.Response.StatusCode = 404;
return;
}
// 搜索 try_files
foreach (var tryFile in location.TryFiles)
{
var tryPath = Path.Combine(location.Root, tryFile);
if (File.Exists(tryPath))
{
defaultContentTypeProvider.TryGetContentType(tryPath, out var contentType);
context.Response.Headers.ContentType = contentType;
if (!File.Exists(tryPath)) continue;
await context.Response.SendFileAsync(tryPath);
defaultContentTypeProvider.TryGetContentType(tryPath, out var contentType);
context.Response.Headers.ContentType = contentType;
return;
}
await context.Response.SendFileAsync(tryPath);
return;
}
}));
});
+2 -2
View File
@@ -6,9 +6,9 @@ using Microsoft.IdentityModel.Tokens;
namespace FastGateway.Services;
public class AuthorizeService() : ServiceBase("/api/v1/Authorize")
public class AuthorizeService
{
public ResultDto CreateAsync(AuthorizeInput input)
public static ResultDto CreateAsync(AuthorizeInput input)
{
// 获取环境变量
var password = Environment.GetEnvironmentVariable("PASSWORD", EnvironmentVariableTarget.Machine) ?? "Aa123456";
+26 -12
View File
@@ -8,10 +8,22 @@ using Directory = System.IO.Directory;
namespace FastGateway.Services;
public class CertService() : ServiceBase("/api/v1/Cert")
public static class CertService
{
public static Dictionary<string, CertData> Certs { get; private set; } = [];
public static async Task LoadCerts(MasterDbContext masterDbContext)
{
var cert = (await masterDbContext.Certs
.Where(x => x.AutoRenew && x.Expired == false)
.Select(x => x.Certs)
.ToListAsync()).SelectMany(x => x);
Certs = cert.ToDictionary(x => x.Domain, x => x);
}
[Authorize]
public async Task<ResultDto<PageResultDto<Cert>>> GetListAsync(int page, int pageSize,
public static async Task<ResultDto<PageResultDto<Cert>>> GetListAsync(int page, int pageSize,
[FromServices] MasterDbContext masterDbContext)
{
var query = masterDbContext.Certs.AsQueryable();
@@ -24,7 +36,7 @@ public class CertService() : ServiceBase("/api/v1/Cert")
}
[Authorize]
public async Task<Cert> GetAsync(MasterDbContext masterDbContext, string id)
public static async Task<Cert> GetAsync(MasterDbContext masterDbContext, string id)
{
var cert = await masterDbContext.Certs.FirstOrDefaultAsync(x => x.Id == id);
@@ -32,7 +44,7 @@ public class CertService() : ServiceBase("/api/v1/Cert")
}
[Authorize]
public async Task<ResultDto> CreateAsync(MasterDbContext masterDbContext, CertInput input)
public static async Task<ResultDto> CreateAsync(MasterDbContext masterDbContext, CertInput input)
{
// 判断是否存在相同的域名
var certs = await masterDbContext.Certs.ToListAsync();
@@ -43,7 +55,6 @@ public class CertService() : ServiceBase("/api/v1/Cert")
return ResultDto.ErrorResult("存在相同的域名");
}
var cert = new Cert
{
Id = Guid.NewGuid().ToString(),
@@ -62,7 +73,7 @@ public class CertService() : ServiceBase("/api/v1/Cert")
}
[Authorize]
public async Task<Cert> UpdateAsync(MasterDbContext masterDbContext, Cert cert)
public static async Task<Cert> UpdateAsync(MasterDbContext masterDbContext, Cert cert)
{
masterDbContext.Certs.Update(cert);
@@ -72,7 +83,7 @@ public class CertService() : ServiceBase("/api/v1/Cert")
}
[Authorize]
public async Task DeleteAsync(MasterDbContext masterDbContext, string id)
public static async Task DeleteAsync(MasterDbContext masterDbContext, string id)
{
var cert = await masterDbContext.Certs.FirstOrDefaultAsync(x => x.Id == id);
@@ -85,7 +96,7 @@ public class CertService() : ServiceBase("/api/v1/Cert")
/// 申请证书
/// </summary>
[Authorize]
public async Task ApplyAsync(MasterDbContext masterDbContext, string id)
public static async Task ApplyAsync(MasterDbContext masterDbContext, string id)
{
var cert = await masterDbContext.Certs.FirstOrDefaultAsync(x => x.Id == id);
@@ -96,7 +107,10 @@ public class CertService() : ServiceBase("/api/v1/Cert")
var context = await RegisterWithLetsEncrypt(cert.Email);
await ApplyForCert(context, cert);
if (await ApplyForCert(context, cert))
{
await LoadCerts(masterDbContext);
}
masterDbContext.Certs.Update(cert);
@@ -104,8 +118,7 @@ public class CertService() : ServiceBase("/api/v1/Cert")
}
[IgnoreRoute]
public static async ValueTask ApplyForCert(AcmeContext context, Cert certItem)
public static async ValueTask<bool> ApplyForCert(AcmeContext context, Cert certItem)
{
certItem.ClearCerts();
@@ -176,9 +189,10 @@ public class CertService() : ServiceBase("/api/v1/Cert")
Password = certPassword
});
}
return true;
}
[IgnoreRoute]
public static async ValueTask<AcmeContext> RegisterWithLetsEncrypt(string email)
{
var path = Path.Combine(AppContext.BaseDirectory, "certs");
+3 -2
View File
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Mvc;
namespace FastGateway.Services;
@@ -108,9 +109,9 @@ public sealed class QpsService : IQpsService, IDisposable
}
}
public sealed class ApiQpsService(IQpsService qpsService) : ServiceBase("/api/v1/Qps")
public sealed class ApiQpsService
{
public async Task GetAsync(HttpContext context, string? serviceId = null)
public static async Task GetAsync([FromServices]IQpsService qpsService,HttpContext context, string? serviceId = null)
{
context.Response.Headers.ContentType = "text/event-stream";
qpsService.EnableQps(true);
@@ -1,11 +1,13 @@
namespace FastGateway.Services;
using FastGateway.BackgroundServices;
public sealed class StatisticRequestService : ServiceBase
namespace FastGateway.Services;
public static class StatisticRequestService
{
/// <summary>
/// 获取今天的统计数据
/// </summary>
public async Task<StatisticRequestCountDto> GetTodayStatisticAsync(MasterDbContext masterDbContext,
public static async Task<ResultDto<StatisticRequestCountDto>> GetDayStatisticAsync(MasterDbContext masterDbContext,
string? serviceId)
{
var now = DateTime.Now;
@@ -17,18 +19,46 @@ public sealed class StatisticRequestService : ServiceBase
x.Day == today.Day)
.FirstOrDefaultAsync();
var currentStatistic = StatisticsBackgroundService.GetCurrentRequestCount(serviceId);
if (statistic == null)
{
return new StatisticRequestCountDto();
return ResultDto<StatisticRequestCountDto>.SuccessResult(new StatisticRequestCountDto()
{
Error4xxCount = currentStatistic.Error4xxCount,
Error5xxCount = currentStatistic.Error5xxCount,
RequestCount = currentStatistic.RequestCount
});
}
return new StatisticRequestCountDto
return ResultDto<StatisticRequestCountDto>.SuccessResult(new StatisticRequestCountDto
{
RequestCount = statistic.RequestCount,
Error4xxCount = statistic.Error4xxCount,
Error5xxCount = statistic.Error5xxCount,
ServiceId = statistic.ServiceId,
CreatedTime = new DateTime(statistic.Year, statistic.Month, statistic.Day, 0, 0, 0)
};
RequestCount = statistic.RequestCount + currentStatistic.RequestCount,
Error4xxCount = statistic.Error4xxCount + currentStatistic.Error4xxCount,
Error5xxCount = statistic.Error5xxCount + currentStatistic.Error5xxCount,
});
}
public static async Task<ResultDto<StatisticRequestCountDto>> GetTotalStatisticAsync(
MasterDbContext masterDbContext,
string? serviceId)
{
var query = masterDbContext.StatisticRequestCounts.Where(x =>
string.IsNullOrEmpty(serviceId) || x.ServiceId == serviceId);
var currentStatistic = StatisticsBackgroundService.GetCurrentRequestCount(serviceId);
var requestCount = await query.SumAsync(x => x.RequestCount);
var error4XxCount = await query.SumAsync(x => x.Error4xxCount);
var error5XxCount = await query.SumAsync(x => x.Error5xxCount);
return ResultDto<StatisticRequestCountDto>.SuccessResult(new StatisticRequestCountDto
{
RequestCount = requestCount + currentStatistic.RequestCount,
Error4xxCount = error4XxCount + currentStatistic.Error4xxCount,
Error5xxCount = error5XxCount + currentStatistic.Error5xxCount,
});
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
<Directives>
<Application>
<Assembly Name="FastGateway" Dynamic="Required All">
</Assembly>
</Application>
</Directives>
+2
View File
@@ -3,6 +3,7 @@ export interface AuthorizeInput {
}
export interface ServiceInput {
id?: string;
serviceNames: string[];
listen: number;
enableHttp3: boolean;
@@ -17,6 +18,7 @@ export interface ServiceInput {
}
export interface LocationInput {
id?: string;
serviceId?: string;
path: string;
proxyPass?: string | null;
+31 -53
View File
@@ -2,13 +2,21 @@ import { Divider, Select, Tag } from '@douyinfe/semi-ui'
import './index.css'
import { useEffect, useState } from 'react';
import * as echarts from 'echarts';
import { GetSelectList } from '../../services/ApiServiceService';
import { getQpsChart } from '../../services/QpsService';
import { DayRequestCount, TotalRequestCount } from '../../services/StatisticRequestService';
export default function DataStatistics() {
const [qps, setQps] = useState(0)
const [serviceOptions, setServiceOptions] = useState([])
const [serviceId, setServiceId] = useState('')
const [dayRequestCount, setDayRequestCount] = useState({
requestCount: 0,
error4xxCount: 0,
error5xxCount: 0,
})
const [totalRequestCount, setTotalRequestCount] = useState({
requestCount: 0,
error4xxCount: 0,
error5xxCount: 0,
})
const [qps_chartData] = useState({
tooltip: {
trigger: 'axis',
@@ -60,7 +68,6 @@ export default function DataStatistics() {
}]
});
let qps_service: string;
var qps_chart: any;
useEffect(() => {
qps_chart = echarts.init(document.getElementById('qps_chart'));
@@ -79,33 +86,8 @@ export default function DataStatistics() {
};
window.addEventListener('resize', resizeHandler);
getSelectList();
return () => {
window.removeEventListener('resize', resizeHandler);
}
}, []);
function getSelectList() {
GetSelectList()
.then(res => {
if (res.success) {
res.data.push({
value: '',
label: '全部服务'
})
setServiceOptions(res.data);
}
})
}
useEffect(() => {
// 定时任务,10s一次
async function stream() {
const response = await getQpsChart(serviceId) as any;
const response = await getQpsChart('') as any;
for await (const data of response) {
qps_chartData.xAxis.data.shift();
qps_chartData.xAxis.data.push(data.now);
@@ -116,33 +98,29 @@ export default function DataStatistics() {
}
}
DayRequestCount().then((res) => {
setDayRequestCount(res.data)
})
TotalRequestCount().then((res) => {
setTotalRequestCount(res.data)
})
stream()
const intervalId = setInterval(stream, 10900);
return () => {
clearInterval(intervalId);
window.removeEventListener('resize', resizeHandler);
}
}, [serviceId]);
}, []);
return (
<div className="dashboard">
<div className="dashboard-header">
<Select
style={{
position: 'relative',
float: 'right',
marginRight: '20px',
width: '200px',
marginTop: '10px',
}}
defaultValue={serviceId}
onChange={(value) => {
qps_service = value as string;
setServiceId(value as string)
}}
value={serviceId}
optionList={serviceOptions}
/>
</div>
<div className="dashboard-content">
<div className="stat-block" style={{ backgroundColor: '#333' }}>
@@ -151,7 +129,7 @@ export default function DataStatistics() {
justifyContent: 'space-between',
lineHeight: '50px'
}}>
今天请求总量 <span>0</span>
今天请求总量 <span>{dayRequestCount.requestCount}</span>
</div>
<Divider />
<div style={{
@@ -159,7 +137,7 @@ export default function DataStatistics() {
justifyContent: 'space-between',
lineHeight: '50px'
}}>
今天4xx错误数量 <span>0 </span>
今天4xx错误数量 <span>{dayRequestCount.error4xxCount}</span>
</div>
<Divider />
<div style={{
@@ -167,7 +145,7 @@ export default function DataStatistics() {
justifyContent: 'space-between',
lineHeight: '50px'
}}>
今天5xx错误数量 <span>0</span>
今天5xx错误数量 <span>{dayRequestCount.error5xxCount}</span>
</div>
</div>
<div className="stat-block" style={{ backgroundColor: '#333' }}>
@@ -176,7 +154,7 @@ export default function DataStatistics() {
justifyContent: 'space-between',
lineHeight: '50px'
}}>
请求总量 <span>0</span>
请求总量 <span>{totalRequestCount.requestCount}</span>
</div>
<Divider />
<div style={{
@@ -184,7 +162,7 @@ export default function DataStatistics() {
justifyContent: 'space-between',
lineHeight: '50px'
}}>
4xx错误数量 <span>0 </span>
4xx错误数量 <span>{totalRequestCount.error4xxCount}</span>
</div>
<Divider />
<div style={{
@@ -192,7 +170,7 @@ export default function DataStatistics() {
justifyContent: 'space-between',
lineHeight: '50px'
}}>
5xx错误数量 <span>0</span>
5xx错误数量 <span>{totalRequestCount.error5xxCount}</span>
</div>
</div>
<div className="stat-block" style={{ backgroundColor: '#666', gridColumn: 'span 2' }}>
@@ -1,6 +1,6 @@
import { Button, Col, Collapse, Divider, Form, InputGroup, Modal, Notification, Row } from "@douyinfe/semi-ui";
import { LoadType, LocationInput, Service, ServiceInput } from "../../../module";
import { CheckDirecotryExistence, CreateApiService } from "../../../services/ApiServiceService";
import { CheckDirecotryExistence, CreateApiService, UpdateApiService } from "../../../services/ApiServiceService";
interface IUpdateHttpProxyProps {
visible: boolean;
@@ -35,11 +35,11 @@ export default function UpdateHttpProxy({
labelCol={{ span: 4 }}
initValues={values}
onSubmit={(values: ServiceInput) => {
CreateApiService(values)
UpdateApiService(values.id as string, values)
.then((res: any) => {
Notification.success({
title: '创建成功',
content: '创建成功',
title: '修改成功',
content: '修改成功',
});
onOk();
})
@@ -74,7 +74,6 @@ export default function UpdateHttpProxy({
allowCreate={true}
multiple={true}
filter={true}
onChange={v => console.log(v)}
defaultActiveFirstOption
></Select>
<Row>
@@ -184,6 +183,27 @@ export default function UpdateHttpProxy({
></Checkbox>
</Col>
</Row>
{
values.isHttps && (
<Row>
<Col span={12}>
<Checkbox
field="enableHttp3"
label="启用H3"
initValue={false}
onChange={(v: any) => {
values.enableHttp3 = v;
formApi.setValues(values);
}}
style={{
marginTop: '5px',
marginLeft: '10px',
}}
></Checkbox>
</Col>
</Row>
)
}
<Divider></Divider>
<div style={{
display: 'flex',
@@ -242,6 +262,7 @@ export default function UpdateHttpProxy({
<Select
label="代理类型"
field="type"
initValue={arrayField.type}
style={{
width: '100%',
borderRadius: '8px',
@@ -249,7 +270,6 @@ export default function UpdateHttpProxy({
border: '1px solid var(--semi-color-border)',
fontSize: '14px',
}}
initValue={arrayField.type}
onChange={(v: any) => {
arrayField.type = v;
formApi.setValues(values);
+1 -1
View File
@@ -9,7 +9,7 @@ export async function getQpsChart(serviceId?: string) {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer" + localStorage.getItem('token') || ''
'Authorization': "Bearer " + localStorage.getItem('token') || ''
}
});
@@ -0,0 +1,15 @@
import { get } from "../utils/fetch";
/**
* 获取今日请求数
*/
export function DayRequestCount(){
return get('/api/v1/Statistic/DayRequestCount');
}
/**
* 获取总请求数
*/
export function TotalRequestCount(){
return get('/api/v1/Statistic/TotalRequestCount');
}