From d789b5d48ab5812bd44471da9a83c604046dadf0 Mon Sep 17 00:00:00 2001 From: cKey <35512826+colinin@users.noreply.github.com> Date: Fri, 21 May 2021 10:10:46 +0800 Subject: [PATCH] Add an aggregation api-gateway based on the /api/abp/api-definition endpoint --- aspnet-core/LINGYUN.MicroService.All.sln | 7 + .../LINGYUN.MicroService.ApiGateway.sln | 7 + .../DotNetCore/CAP/ConsumerServiceSelector.cs | 15 +- .../AbpApiGatewayHostModule.cs | 168 ++++++++++++++++ .../AbpApiGatewayOptions.cs | 11 ++ .../LINGYUN.Abp.ApiGateway.Host/Dockerfile | 13 ++ .../LINGYUN.Abp.ApiGateway.Host.csproj | 28 +++ .../LINGYUN.Abp.ApiGateway.Host.csproj.user | 6 + ...iDescriptionFileConfigurationRepository.cs | 185 ++++++++++++++++++ .../Extenssions/OcelotMiddlewareExtensions.cs | 123 ++++++++++++ .../Multiplexer/AbpResponseMergeAggregator.cs | 51 +++++ .../LINGYUN.Abp.ApiGateway.Host/Program.cs | 47 +++++ .../Properties/launchSettings.json | 20 ++ .../LINGYUN.Abp.ApiGateway.Host/Startup.cs | 18 ++ .../LINGYUN.ApiGateway.Host/Program.cs | 22 +-- vueJs/package-lock.json | 137 +++++++++++-- 16 files changed, 825 insertions(+), 33 deletions(-) create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayHostModule.cs create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayOptions.cs create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Dockerfile create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj.user create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Configuration/Repository/AbpApiDescriptionFileConfigurationRepository.cs create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Extenssions/OcelotMiddlewareExtensions.cs create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Middleware/Multiplexer/AbpResponseMergeAggregator.cs create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Program.cs create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Properties/launchSettings.json create mode 100644 aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Startup.cs diff --git a/aspnet-core/LINGYUN.MicroService.All.sln b/aspnet-core/LINGYUN.MicroService.All.sln index 663fd24cf..5db746f39 100644 --- a/aspnet-core/LINGYUN.MicroService.All.sln +++ b/aspnet-core/LINGYUN.MicroService.All.sln @@ -331,6 +331,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Rules.RulesEngi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.AspNetCore.HttpOverrides", "modules\common\LINGYUN.Abp.AspNetCore.HttpOverrides\LINGYUN.Abp.AspNetCore.HttpOverrides.csproj", "{13219C1C-23E1-4EBA-93FB-86830C93A800}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.ApiGateway.Host", "services\apigateway\LINGYUN.Abp.ApiGateway.Host\LINGYUN.Abp.ApiGateway.Host.csproj", "{A320E23E-792D-4736-B963-381F9D7AF605}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -865,6 +867,10 @@ Global {13219C1C-23E1-4EBA-93FB-86830C93A800}.Debug|Any CPU.Build.0 = Debug|Any CPU {13219C1C-23E1-4EBA-93FB-86830C93A800}.Release|Any CPU.ActiveCfg = Release|Any CPU {13219C1C-23E1-4EBA-93FB-86830C93A800}.Release|Any CPU.Build.0 = Release|Any CPU + {A320E23E-792D-4736-B963-381F9D7AF605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A320E23E-792D-4736-B963-381F9D7AF605}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A320E23E-792D-4736-B963-381F9D7AF605}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A320E23E-792D-4736-B963-381F9D7AF605}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1028,6 +1034,7 @@ Global {4D83BDA7-2059-41C7-85AE-FEFAD5CD9498} = {6084D52D-775B-4A39-8CD5-AA2F362B5A61} {8EF31071-3521-409D-9740-BBFBFC04C50E} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F} {13219C1C-23E1-4EBA-93FB-86830C93A800} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E} + {A320E23E-792D-4736-B963-381F9D7AF605} = {19E6BD61-062B-4FAD-A51A-B55F5CE88B7A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718} diff --git a/aspnet-core/LINGYUN.MicroService.ApiGateway.sln b/aspnet-core/LINGYUN.MicroService.ApiGateway.sln index 58554c708..101471af6 100644 --- a/aspnet-core/LINGYUN.MicroService.ApiGateway.sln +++ b/aspnet-core/LINGYUN.MicroService.ApiGateway.sln @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.EventBus.CAP", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.AspNetCore.HttpOverrides", "modules\common\LINGYUN.Abp.AspNetCore.HttpOverrides\LINGYUN.Abp.AspNetCore.HttpOverrides.csproj", "{7588F35B-7C0E-4D80-B43A-8A5C9AC6FE03}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.ApiGateway.Host", "services\apigateway\LINGYUN.Abp.ApiGateway.Host\LINGYUN.Abp.ApiGateway.Host.csproj", "{A38139F5-A856-4CA2-89F8-51798ED557CC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,10 @@ Global {7588F35B-7C0E-4D80-B43A-8A5C9AC6FE03}.Debug|Any CPU.Build.0 = Debug|Any CPU {7588F35B-7C0E-4D80-B43A-8A5C9AC6FE03}.Release|Any CPU.ActiveCfg = Release|Any CPU {7588F35B-7C0E-4D80-B43A-8A5C9AC6FE03}.Release|Any CPU.Build.0 = Release|Any CPU + {A38139F5-A856-4CA2-89F8-51798ED557CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A38139F5-A856-4CA2-89F8-51798ED557CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A38139F5-A856-4CA2-89F8-51798ED557CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A38139F5-A856-4CA2-89F8-51798ED557CC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -101,6 +107,7 @@ Global {D6629DD3-BA0F-44B5-A97F-3B992ECAB85D} = {630FB448-8C5C-438F-930D-B0209407DE6A} {1DA1835B-9EA4-4095-A8CF-10E2778206D3} = {D6629DD3-BA0F-44B5-A97F-3B992ECAB85D} {7588F35B-7C0E-4D80-B43A-8A5C9AC6FE03} = {D6629DD3-BA0F-44B5-A97F-3B992ECAB85D} + {A38139F5-A856-4CA2-89F8-51798ED557CC} = {F3B1B755-37B6-420B-9E82-A5BDFF2BF647} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B327C65A-BD15-480D-825E-9D5A870C521A} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.EventBus.CAP/DotNetCore/CAP/ConsumerServiceSelector.cs b/aspnet-core/modules/common/LINGYUN.Abp.EventBus.CAP/DotNetCore/CAP/ConsumerServiceSelector.cs index 36117ae68..f3df9280c 100644 --- a/aspnet-core/modules/common/LINGYUN.Abp.EventBus.CAP/DotNetCore/CAP/ConsumerServiceSelector.cs +++ b/aspnet-core/modules/common/LINGYUN.Abp.EventBus.CAP/DotNetCore/CAP/ConsumerServiceSelector.cs @@ -20,6 +20,10 @@ namespace DotNetCore.CAP public class ConsumerServiceSelector : Internal.ConsumerServiceSelector { + /// + /// CAP配置 + /// + protected CapOptions CapOptions { get; } /// /// Abp分布式事件配置 /// @@ -34,8 +38,10 @@ namespace DotNetCore.CAP /// public ConsumerServiceSelector( IServiceProvider serviceProvider, + IOptions capOptions, IOptions distributedEventBusOptions) : base(serviceProvider) { + CapOptions = capOptions.Value; ServiceProvider = serviceProvider; AbpDistributedEventBusOptions = distributedEventBusOptions.Value; } @@ -70,9 +76,12 @@ namespace DotNetCore.CAP if (executorDescriptorList.Any(x => new ConsumerExecutorDescriptorComparer().Equals(x, consumerExecutorDescriptor))) { // 如果存在多个消费者,后续的消费者需要重新定义分组才能不被 CAP 框架过滤掉 - consumerExecutorDescriptor.Attribute.Group = handler.IsGenericType - ? handler.GetGenericTypeDefinition().FullName - : handler.FullName; + var groupAliaName = handler.IsGenericType + ? handler.GetGenericTypeDefinition().Name + : handler.Name; + + // TODO: 2021-05-21 直接使用类型全名作为GroupName会引起用户困惑,加上组别名称在前 + consumerExecutorDescriptor.Attribute.Group = $"{CapOptions.DefaultGroupName}.{groupAliaName}"; SetSubscribeAttribute(consumerExecutorDescriptor.Attribute); } diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayHostModule.cs b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayHostModule.cs new file mode 100644 index 000000000..495fdfed7 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayHostModule.cs @@ -0,0 +1,168 @@ +using LINGYUN.Abp.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Caching.StackExchangeRedis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Ocelot.Configuration.Repository; +using Ocelot.DependencyInjection; +using Ocelot.Extenssions; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Provider.Polly; +using StackExchange.Redis; +using System; +using System.Text; +using Volo.Abp; +using Volo.Abp.AspNetCore; +using Volo.Abp.Autofac; +using Volo.Abp.Caching; +using Volo.Abp.Caching.StackExchangeRedis; +using Volo.Abp.Http.Client; +using Volo.Abp.Json; +using Volo.Abp.Modularity; +using Volo.Abp.Security.Encryption; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.ApiGateway +{ + [DependsOn( + typeof(AbpAutofacModule), + typeof(AbpHttpClientModule), + typeof(AbpCachingStackExchangeRedisModule), + typeof(AbpAspNetCoreModule), + typeof(AbpAspNetCoreHttpOverridesModule) + )] + public class AbpApiGatewayHostModule : AbpModule + { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddHttpClient("_AbpApiDefinitionClient"); + context.Services.AddSingleton(); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + // 解决某些不支持类型的序列化 + Configure(options => + { + // See: https://docs.abp.io/en/abp/4.0/Migration-Guides/Abp-4_0#always-use-the-newtonsoft-json + options.UseHybridSerializer = false; + }); + // 中文序列化的编码问题 + //Configure(options => + //{ + // options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); + //}); + + Configure(configuration.GetSection("ApiGateway")); + + context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.Audience = configuration["AuthServer:ApiName"]; + }); + + Configure(options => + { + // 最好统一命名,不然某个缓存变动其他应用服务有例外发生 + options.KeyPrefix = "LINGYUN.Abp.Application"; + // 滑动过期30天 + options.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromDays(30); + // 绝对过期60天 + options.GlobalCacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60); + }); + + Configure(options => + { + var redisConfig = ConfigurationOptions.Parse(options.Configuration); + options.ConfigurationOptions = redisConfig; + options.InstanceName = configuration["Redis:InstanceName"]; + }); + + // 加解密 + Configure(options => + { + var encryptionConfiguration = configuration.GetSection("Encryption"); + if (encryptionConfiguration.Exists()) + { + options.DefaultPassPhrase = encryptionConfiguration["PassPhrase"] ?? options.DefaultPassPhrase; + options.DefaultSalt = encryptionConfiguration.GetSection("Salt").Exists() + ? Encoding.ASCII.GetBytes(encryptionConfiguration["Salt"]) + : options.DefaultSalt; + options.InitVectorBytes = encryptionConfiguration.GetSection("InitVector").Exists() + ? Encoding.ASCII.GetBytes(encryptionConfiguration["InitVector"]) + : options.InitVectorBytes; + } + }); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + var mvcBuilder = context.Services.AddMvc(); + mvcBuilder.AddApplicationPart(typeof(AbpApiGatewayHostModule).Assembly); + + Configure(options => + { + options.EndpointConfigureActions.Add(endpointContext => + { + endpointContext.Endpoints.MapControllerRoute("defaultWithArea", "{area}/{controller=Home}/{action=Index}/{id?}"); + endpointContext.Endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); + }); + }); + + if (!hostingEnvironment.IsDevelopment()) + { + // Ssl证书 + var sslOptions = configuration.GetSection("App:SslOptions"); + if (sslOptions.Exists()) + { + var fileName = sslOptions["FileName"]; + var password = sslOptions["Password"]; + Configure(options => + { + options.ConfigureEndpointDefaults(cfg => + { + cfg.UseHttps(fileName, password); + }); + }); + } + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services + .AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "ApiGatewayHost-Protection-Keys"); + } + + context.Services + .AddOcelot() + .AddPolly() + .AddSingletonDefinedAggregator(); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + app.UseForwardedHeaders(); + app.UseAuditing(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseAuthentication(); + // 启用ws协议 + app.UseWebSockets(); + app.UseOcelot().Wait(); + } + } +} diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayOptions.cs b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayOptions.cs new file mode 100644 index 000000000..13eb7d228 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/AbpApiGatewayOptions.cs @@ -0,0 +1,11 @@ +using Ocelot.Configuration.File; +using System.Collections.Generic; + +namespace LINGYUN.Abp.ApiGateway +{ + public class AbpApiGatewayOptions + { + public FileGlobalConfiguration GlobalConfiguration { get; set; } = new FileGlobalConfiguration(); + public List AggrageRouteUrls { get; set; } = new List(); + } +} diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Dockerfile b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Dockerfile new file mode 100644 index 000000000..c94c5a31c --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0 +LABEL maintainer="colin.in@foxmail.com" +WORKDIR /app + +COPY . /app + +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone + +EXPOSE 80/tcp +VOLUME [ "./app/Logs" ] + +ENTRYPOINT ["dotnet", "LINGYUN.ApiGateway.Host.dll"] \ No newline at end of file diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj new file mode 100644 index 000000000..036fe7ce6 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj @@ -0,0 +1,28 @@ + + + + net5.0 + LINGYUN.Abp.ApiGateway + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj.user b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj.user new file mode 100644 index 000000000..cff74a90e --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/LINGYUN.Abp.ApiGateway.Host.csproj.user @@ -0,0 +1,6 @@ + + + + IIS Express + + \ No newline at end of file diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Configuration/Repository/AbpApiDescriptionFileConfigurationRepository.cs b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Configuration/Repository/AbpApiDescriptionFileConfigurationRepository.cs new file mode 100644 index 000000000..f524033b1 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Configuration/Repository/AbpApiDescriptionFileConfigurationRepository.cs @@ -0,0 +1,185 @@ +using LINGYUN.Abp.ApiGateway; +using Microsoft.Extensions.Options; +using Ocelot.Configuration.File; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Responses; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Volo.Abp.Http.Client; +using Volo.Abp.Http.Client.DynamicProxying; +using Volo.Abp.Http.Modeling; + +namespace Ocelot.Configuration.Repository +{ + /// + /// 通过定义的RemoteServices来寻找abp端点定义的服务列表 + /// 在AbpApiGatewayOptions.AggrageRouteUrls中定义聚合的url + /// 可以在发现相同端点后加入到聚合路由,聚合路由下游端点会自动加入聚合标识,以发现的聚合数量作为前缀 + /// 例如/api/abp/api-definition、/api/abp/application-configuration + /// 下游端点将变成/aggregate1/api/abp/api-definition、/aggregate1/api/abp/application-configuration + /// 上游服务直接调用/api/abp/api-definition、/api/abp/application-configuration即可唤起路由聚合 + /// + public class AbpApiDescriptionFileConfigurationRepository : IFileConfigurationRepository + { + private readonly AbpRemoteServiceOptions _remoteServiceOptions; + private readonly AbpApiGatewayOptions _apiGatewayOptions; + + private readonly IApiDescriptionFinder _apiDescriptionFinder; + private readonly IHttpClientFactory _httpClientFactory; + public AbpApiDescriptionFileConfigurationRepository( + IHttpClientFactory httpClientFactory, + IApiDescriptionFinder apiDescriptionFinder, + IOptions apiGatewayOptions, + IOptions remoteServiceOptions + ) + { + _httpClientFactory = httpClientFactory; + _apiDescriptionFinder = apiDescriptionFinder; + + _apiGatewayOptions = apiGatewayOptions.Value; + _remoteServiceOptions = remoteServiceOptions.Value; + } + public async Task> Get() + { + var fileConfiguration = new FileConfiguration + { + GlobalConfiguration = _apiGatewayOptions.GlobalConfiguration + }; + var apiDescriptionModels = await GetApiDescriptionsAsync(); + int foundAggrageRouteCount = 0; + foreach (var apiDescriptionModel in apiDescriptionModels) + { + foreach (var moduleModel in apiDescriptionModel.Value.Modules) + { + foreach (var controllerModel in moduleModel.Value.Controllers) + { + foreach (var actionModel in controllerModel.Value.Actions) + { + var action = actionModel.Value; + var downstreamUrl = action.Url.EnsureStartsWith('/'); + + // TODO: 多个相同的下游路由地址应组合为聚合路由 + + // TODO: 下游路由地址已添加 + var route = fileConfiguration.Routes + .Where(route => HasBeenAddedRoute(route, downstreamUrl)) + .LastOrDefault(); + string aggregateKey = ""; + + if (route != null) + { + // TODO: 下游方法已添加 + if (route.UpstreamHttpMethod.Any(method => method.Equals(action.HttpMethod, StringComparison.CurrentCultureIgnoreCase))) + { + if (_apiGatewayOptions.AggrageRouteUrls.Any(url => url.Equals(downstreamUrl))) + { + foundAggrageRouteCount++; + var aggregateRoute = fileConfiguration.Aggregates + .Where(route => HasBeenAddedAggregateRoute(route, downstreamUrl)) + .FirstOrDefault(); + if (aggregateRoute == null) + { + aggregateRoute = new FileAggregateRoute + { + RouteIsCaseSensitive = false, + // TODO: 可实现自定义的聚合提供装置 + Aggregator = nameof(AbpResponseMergeAggregator), + UpstreamPathTemplate = downstreamUrl, + RouteKeys = new List() + }; + fileConfiguration.Aggregates.Add(aggregateRoute); + } + if (route.Key.IsNullOrWhiteSpace()) + { + route.Key = $"aggregate{foundAggrageRouteCount}"; + route.UpstreamPathTemplate = $"/{route.Key}{route.UpstreamPathTemplate}"; + aggregateRoute.RouteKeys.Add(route.Key); + foundAggrageRouteCount++; + } + aggregateKey = $"aggregate{foundAggrageRouteCount}"; + aggregateRoute.RouteKeys.Add(aggregateKey); + } + else + { + continue; + } + } + else + { + route.UpstreamHttpMethod.Add(action.HttpMethod); + continue; + } + } + + var newRoute = new FileRoute + { + Key = aggregateKey, + UpstreamPathTemplate = (aggregateKey + downstreamUrl).EnsureStartsWith('/'), + DownstreamPathTemplate = downstreamUrl, + DangerousAcceptAnyServerCertificateValidator = false + }; + + var baseUrl = apiDescriptionModel.Key; + baseUrl = baseUrl.StartsWith("http://") ? baseUrl[7..] : baseUrl; + baseUrl = baseUrl.StartsWith("https://") ? baseUrl[8..] : baseUrl; + baseUrl = baseUrl.EndsWith("/") ? baseUrl[0..^1] : baseUrl; + + var addresses = baseUrl.Split(":"); + var hostAndPort = new FileHostAndPort + { + Host = addresses[0] + }; + if (addresses.Length == 2) + { + hostAndPort.Port = int.Parse(addresses[1]); + } + newRoute.DownstreamHostAndPorts.Add(hostAndPort); + newRoute.UpstreamHttpMethod.Add(action.HttpMethod); + + // abp api版本号支持通过query发送 + //newRoute.DownstreamHttpVersion = action.SupportedVersions.Last(); + + fileConfiguration.Routes.Add(newRoute); + } + } + } + } + + return new OkResponse(fileConfiguration); + } + + private static bool HasBeenAddedRoute(FileRoute route, string downstreamUrl) + { + return route.DownstreamPathTemplate.Equals(downstreamUrl, StringComparison.CurrentCultureIgnoreCase); + } + + private static bool HasBeenAddedAggregateRoute(FileAggregateRoute route, string upstreamUrl) + { + return route.UpstreamPathTemplate.Equals(upstreamUrl, StringComparison.CurrentCultureIgnoreCase); + } + + public async Task Set(FileConfiguration fileConfiguration) + { + return await Task.FromResult(new OkResponse(fileConfiguration)); + } + + protected virtual async Task> GetApiDescriptionsAsync() + { + var client = _httpClientFactory.CreateClient("_AbpApiDefinitionClient"); + + var apiDescriptionModels = new Dictionary(); + + foreach (var remoteService in _remoteServiceOptions.RemoteServices) + { + var model = await _apiDescriptionFinder.GetApiDescriptionAsync(client, remoteService.Value.BaseUrl); + + apiDescriptionModels.Add(remoteService.Value.BaseUrl, model); + } + + return apiDescriptionModels; + } + } +} diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Extenssions/OcelotMiddlewareExtensions.cs b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Extenssions/OcelotMiddlewareExtensions.cs new file mode 100644 index 000000000..88765bd26 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Extenssions/OcelotMiddlewareExtensions.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.Repository; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Responses; +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Extenssions +{ + public static class OcelotMiddlewareExtensions + { + public static async Task UseOcelot(this IApplicationBuilder builder) + { + await builder.UseOcelot(new OcelotPipelineConfiguration()); + return builder; + } + + public static async Task UseOcelot(this IApplicationBuilder builder, Action pipelineConfiguration) + { + var config = new OcelotPipelineConfiguration(); + pipelineConfiguration?.Invoke(config); + return await builder.UseOcelot(config); + } + + public static async Task UseOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) + { + var configuration = await CreateConfiguration(builder); + + ConfigureDiagnosticListener(builder); + + return CreateOcelotPipeline(builder, pipelineConfiguration); + } + + public static Task UseOcelot(this IApplicationBuilder app, Action builderAction) + => UseOcelot(app, builderAction, new OcelotPipelineConfiguration()); + + public static async Task UseOcelot(this IApplicationBuilder app, Action builderAction, OcelotPipelineConfiguration configuration) + { + await CreateConfiguration(app); + + ConfigureDiagnosticListener(app); + + builderAction?.Invoke(app, configuration ?? new OcelotPipelineConfiguration()); + + app.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; + + return app; + } + + private static IApplicationBuilder CreateOcelotPipeline(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) + { + builder.BuildOcelotPipeline(pipelineConfiguration); + + /* + inject first delegate into first piece of asp.net middleware..maybe not like this + then because we are updating the http context in ocelot it comes out correct for + rest of asp.net.. + */ + + builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; + + return builder; + } + private static async Task CreateConfiguration(IApplicationBuilder builder) + { + var fileConfigRepo = builder.ApplicationServices.GetRequiredService(); + var fileConfig = await fileConfigRepo.Get(); + // IOptionsMonitor fileConfig = builder.ApplicationServices.GetService>(); + IInternalConfigurationCreator internalConfigCreator = builder.ApplicationServices.GetService(); + Response response = await internalConfigCreator.Create(fileConfig.Data); + if (response.IsError) + { + ThrowToStopOcelotStarting(response); + } + IInternalConfigurationRepository internalConfigRepo = builder.ApplicationServices.GetService(); + internalConfigRepo.AddOrReplace(response.Data); + return GetOcelotConfigAndReturn(internalConfigRepo); + } + + private static bool AdministrationApiInUse(IAdministrationPath adminPath) + { + return adminPath != null; + } + + private static bool IsError(Response response) + { + return response == null || response.IsError; + } + + private static IInternalConfiguration GetOcelotConfigAndReturn(IInternalConfigurationRepository provider) + { + var ocelotConfiguration = provider.Get(); + + if (ocelotConfiguration?.Data == null || ocelotConfiguration.IsError) + { + ThrowToStopOcelotStarting(ocelotConfiguration); + } + + return ocelotConfiguration.Data; + } + + private static void ThrowToStopOcelotStarting(Response config) + { + throw new Exception($"Unable to start Ocelot, errors are: {string.Join(",", config.Errors.Select(x => x.ToString()))}"); + } + + private static void ConfigureDiagnosticListener(IApplicationBuilder builder) + { + var env = builder.ApplicationServices.GetService(); + var listener = builder.ApplicationServices.GetService(); + var diagnosticListener = builder.ApplicationServices.GetService(); + diagnosticListener.SubscribeWithAdapter(listener); + } + } +} \ No newline at end of file diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Middleware/Multiplexer/AbpResponseMergeAggregator.cs b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Middleware/Multiplexer/AbpResponseMergeAggregator.cs new file mode 100644 index 000000000..34c351415 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Ocelot/Middleware/Multiplexer/AbpResponseMergeAggregator.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Ocelot.Multiplexer; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Ocelot.Middleware.Multiplexer +{ + public class AbpResponseMergeAggregator : IDefinedAggregator + { + public async Task Aggregate(List responses) + { + return await MapAbpApiDefinitionAggregateContentAsync(responses); + } + + protected virtual async Task MapAbpApiDefinitionAggregateContentAsync(List responses) + { + JObject responseObject = null; + JsonMergeSettings mergeSetting = new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Union, + PropertyNameComparison = System.StringComparison.CurrentCultureIgnoreCase + }; + foreach (var httpResponse in responses) + { + var content = await httpResponse.Items.DownstreamResponse().Content.ReadAsStringAsync(); + var contentObject = JsonConvert.DeserializeObject(content); + if (responseObject == null) + { + responseObject = JObject.FromObject(contentObject); + } + else + { + responseObject.Merge(contentObject, mergeSetting); + } + } + var stringContent = new StringContent(responseObject.ToString()) + { + Headers = { ContentType = new MediaTypeHeaderValue("application/json") } + }; + stringContent.Headers.Add("_AbpErrorFormat", "true"); + return new DownstreamResponse(stringContent, HttpStatusCode.OK, + new List>>(), "OK"); + } + + } +} diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Program.cs b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Program.cs new file mode 100644 index 000000000..47f651df7 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Program.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; +using System; +using System.IO; + +namespace LINGYUN.Abp.ApiGateway +{ + public class Program + { + public static int Main(string[] args) + { + try + { + var hostBuilder = CreateHostBuilder(args).Build(); + Log.Information("Starting web host."); + hostBuilder.Run(); + + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly!"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + internal static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .UseKestrel() + .UseStartup(); + }) + .UseSerilog((context, provider, config) => + { + config.ReadFrom.Configuration(context.Configuration); + }) + .UseAutofac(); + } +} diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Properties/launchSettings.json b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Properties/launchSettings.json new file mode 100644 index 000000000..ca554589e --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Properties/launchSettings.json @@ -0,0 +1,20 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54450/", + "sslPort": 0 + } + }, + "profiles": { + "LINGYUN.Abp.ApiGateway.Host": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:30000" + } + } +} \ No newline at end of file diff --git a/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Startup.cs b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Startup.cs new file mode 100644 index 000000000..8dd037ee2 --- /dev/null +++ b/aspnet-core/services/apigateway/LINGYUN.Abp.ApiGateway.Host/Startup.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace LINGYUN.Abp.ApiGateway +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddApplication(); + } + + public void Configure(IApplicationBuilder app) + { + app.InitializeApplication(); + } + } +} diff --git a/aspnet-core/services/apigateway/LINGYUN.ApiGateway.Host/Program.cs b/aspnet-core/services/apigateway/LINGYUN.ApiGateway.Host/Program.cs index 05f076f50..52812b9e4 100644 --- a/aspnet-core/services/apigateway/LINGYUN.ApiGateway.Host/Program.cs +++ b/aspnet-core/services/apigateway/LINGYUN.ApiGateway.Host/Program.cs @@ -11,19 +11,12 @@ namespace LINGYUN.ApiGateway { public static int Main(string[] args) { - var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: true) - .AddEnvironmentVariables() - .Build(); - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .CreateLogger(); try { + var hostBuilder = CreateHostBuilder(args).Build(); Log.Information("Starting web host."); - CreateHostBuilder(args).Build().Run(); + hostBuilder.Run(); + return 0; } catch (Exception ex) @@ -41,9 +34,14 @@ namespace LINGYUN.ApiGateway Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); + webBuilder + .UseKestrel() + .UseStartup(); + }) + .UseSerilog((context, provider, config) => + { + config.ReadFrom.Configuration(context.Configuration); }) - .UseSerilog() .UseAutofac(); } } diff --git a/vueJs/package-lock.json b/vueJs/package-lock.json index 8617a18b2..f1fe61585 100644 --- a/vueJs/package-lock.json +++ b/vueJs/package-lock.json @@ -5949,9 +5949,9 @@ } }, "concurrently": { - "version": "5.2.0", - "resolved": "https://registry.npm.taobao.org/concurrently/download/concurrently-5.2.0.tgz", - "integrity": "sha1-6tVRIdCKD8gXCFWEwSPO3sLgiXU=", + "version": "5.3.0", + "resolved": "https://registry.nlark.com/concurrently/download/concurrently-5.3.0.tgz?cache=0&sync_timestamp=1620443100193&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fconcurrently%2Fdownload%2Fconcurrently-5.3.0.tgz", + "integrity": "sha1-dQDeZBDQQ8kSston3jICy0ibHns=", "dev": true, "requires": { "chalk": "^2.4.2", @@ -8338,8 +8338,8 @@ "dev": true }, "eve": { - "version": "git://github.com/adobe-webplatform/eve.git#eef80ed8d188423c2272746fb8ae5cc8dad84cb1", - "from": "git://github.com/adobe-webplatform/eve.git#eef80ed" + "version": "git+ssh://git@github.com/adobe-webplatform/eve.git#eef80ed8d188423c2272746fb8ae5cc8dad84cb1", + "from": "eve@git://github.com/adobe-webplatform/eve.git#eef80ed" }, "event-pubsub": { "version": "4.3.0", @@ -18346,10 +18346,10 @@ "dev": true }, "raphael": { - "version": "git+https://github.com/nhn/raphael.git#78a6ed3ec269f33b6457b0ec66f8c3d1f2ed70e0", - "from": "git+https://github.com/nhn/raphael.git#2.2.0-c", + "version": "git+ssh://git@github.com/nhn/raphael.git#78a6ed3ec269f33b6457b0ec66f8c3d1f2ed70e0", + "from": "raphael@git+https://github.com/nhn/raphael.git#2.2.0-c", "requires": { - "eve": "git://github.com/adobe-webplatform/eve.git#eef80ed" + "eve": "eve@git://github.com/adobe-webplatform/eve.git#eef80ed" } }, "raw-body": { @@ -18878,17 +18878,118 @@ } }, "sass": { - "version": "1.26.5", - "resolved": "https://registry.npm.taobao.org/sass/download/sass-1.26.5.tgz", - "integrity": "sha1-LXrs+7q/ophWfI8GYVtuJNLWgJk=", + "version": "1.32.13", + "resolved": "https://registry.nlark.com/sass/download/sass-1.32.13.tgz?cache=0&sync_timestamp=1620837185067&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsass%2Fdownload%2Fsass-1.32.13.tgz", + "integrity": "sha1-jSnISeYlpBW85xYJx8+V4V907QA=", "dev": true, "requires": { - "chokidar": ">=2.0.0 <4.0.0" + "chokidar": ">=3.0.0 <4.0.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.nlark.com/anymatch/download/anymatch-3.1.2.tgz", + "integrity": "sha1-wFV8CWrzLxBhmPT04qODU343hxY=", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-2.2.0.tgz?cache=0&sync_timestamp=1610299285874&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbinary-extensions%2Fdownload%2Fbinary-extensions-2.2.0.tgz", + "integrity": "sha1-dfUC7q+f/eQvyYgpZFvk6na9ni0=", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npm.taobao.org/braces/download/braces-3.0.2.tgz", + "integrity": "sha1-NFThpGLujVmeI23zNs2epPiv4Qc=", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npm.taobao.org/chokidar/download/chokidar-3.5.1.tgz?cache=0&sync_timestamp=1610719499558&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchokidar%2Fdownload%2Fchokidar-3.5.1.tgz", + "integrity": "sha1-7pznu+vSt59J8wR5nVRo4x4U5oo=", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npm.taobao.org/fill-range/download/fill-range-7.0.1.tgz", + "integrity": "sha1-GRmmp8df44ssfHflGYU12prN2kA=", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-2.3.2.tgz?cache=0&sync_timestamp=1612536512306&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffsevents%2Fdownload%2Ffsevents-2.3.2.tgz", + "integrity": "sha1-ilJveLj99GI7cJ4Ll1xSwkwC/Ro=", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.nlark.com/glob-parent/download/glob-parent-5.1.2.tgz?cache=0&sync_timestamp=1620073321855&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fglob-parent%2Fdownload%2Fglob-parent-5.1.2.tgz", + "integrity": "sha1-hpgyxYA0/mikCTwX3BXoNA2EAcQ=", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/is-binary-path/download/is-binary-path-2.1.0.tgz", + "integrity": "sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npm.taobao.org/is-number/download/is-number-7.0.0.tgz", + "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=", + "dev": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npm.taobao.org/readdirp/download/readdirp-3.5.0.tgz?cache=0&sync_timestamp=1615717425931&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freaddirp%2Fdownload%2Freaddirp-3.5.0.tgz", + "integrity": "sha1-m6dMAZsV02UnjS6Ru4xI17TULJ4=", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npm.taobao.org/to-regex-range/download/to-regex-range-5.0.1.tgz", + "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } } }, "sass-loader": { "version": "8.0.2", - "resolved": "https://registry.npm.taobao.org/sass-loader/download/sass-loader-8.0.2.tgz", + "resolved": "https://registry.nlark.com/sass-loader/download/sass-loader-8.0.2.tgz", "integrity": "sha1-3r7NjDziQ8dkVPLoKQSCFQOACQ0=", "dev": true, "requires": { @@ -18901,7 +19002,7 @@ "dependencies": { "semver": { "version": "6.3.0", - "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1586886301819&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz", + "resolved": "https://registry.nlark.com/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1618846828850&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz", "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=", "dev": true } @@ -19628,8 +19729,8 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "squire-rte": { - "version": "github:seonim-ryu/Squire#fd40b4e3020845825701e9689f190bab3f4775d4", - "from": "github:seonim-ryu/Squire#fd40b4e3020845825701e9689f190bab3f4775d4" + "version": "git+ssh://git@github.com/seonim-ryu/Squire.git#fd40b4e3020845825701e9689f190bab3f4775d4", + "from": "squire-rte@github:seonim-ryu/Squire#fd40b4e3020845825701e9689f190bab3f4775d4" }, "ssf": { "version": "0.10.3", @@ -20751,7 +20852,7 @@ "integrity": "sha1-LRjs3+JlSL+ELoG2woscfiIvs8w=", "requires": { "core-js": "^3.6.4", - "raphael": "git+https://github.com/nhn/raphael.git#2.2.0-c", + "raphael": "raphael@git+https://github.com/nhn/raphael.git#2.2.0-c", "tui-code-snippet": "^2.3.1" }, "dependencies": { @@ -20796,7 +20897,7 @@ "markdown-it": "^9.0.0", "plantuml-encoder": "^1.2.5", "resize-observer-polyfill": "^1.5.0", - "squire-rte": "github:seonim-ryu/Squire#fd40b4e3020845825701e9689f190bab3f4775d4", + "squire-rte": "squire-rte@github:seonim-ryu/Squire#fd40b4e3020845825701e9689f190bab3f4775d4", "to-mark": "^1.1.9", "tui-chart": "^3.7.0", "tui-code-snippet": "^1.5.0",