mirror of
https://github.com/239573049/FastGateway.git
synced 2026-10-17 05:20:30 +08:00
发布v1.0.0
- 实现动态创建WebApplication - 实现自动申请Http证书 - 实现自动续期Http证书 - 实现QPS实时预览 - 实现每天请求数据统计
This commit is contained in:
+3
-1
@@ -103,7 +103,9 @@ StyleCopReport.xml
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
*.db*
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace FastGateway.Core;
|
||||
|
||||
public enum ApiServiceType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 静态代理
|
||||
/// </summary>
|
||||
StaticProxy = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 单个服务
|
||||
/// </summary>
|
||||
SingleService = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 负载均衡
|
||||
/// </summary>
|
||||
LoadBalance = 3,
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
// 判断数据库是否存在今年今月今日的数据
|
||||
|
||||
@@ -34,6 +34,8 @@ public class Location
|
||||
/// </summary>
|
||||
public string[]? TryFiles { get; set; }
|
||||
|
||||
public ApiServiceType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 负载均衡模型
|
||||
/// </summary>
|
||||
|
||||
@@ -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,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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -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");
|
||||
+1
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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.
@@ -0,0 +1,6 @@
|
||||
<Directives>
|
||||
<Application>
|
||||
<Assembly Name="FastGateway" Dynamic="Required All">
|
||||
</Assembly>
|
||||
</Application>
|
||||
</Directives>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user