@@ -0,0 +1,30 @@ | |||||
**/.classpath | |||||
**/.dockerignore | |||||
**/.env | |||||
**/.git | |||||
**/.gitignore | |||||
**/.project | |||||
**/.settings | |||||
**/.toolstarget | |||||
**/.vs | |||||
**/.vscode | |||||
**/*.*proj.user | |||||
**/*.dbmdl | |||||
**/*.jfm | |||||
**/azds.yaml | |||||
**/bin | |||||
**/charts | |||||
**/docker-compose* | |||||
**/Dockerfile* | |||||
**/node_modules | |||||
**/npm-debug.log | |||||
**/obj | |||||
**/secrets.dev.yaml | |||||
**/values.dev.yaml | |||||
LICENSE | |||||
README.md | |||||
!**/.gitignore | |||||
!.git/HEAD | |||||
!.git/config | |||||
!.git/packed-refs | |||||
!.git/refs/heads/** |
@@ -0,0 +1,37 @@ | |||||
using Microsoft.Extensions.Logging; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace TelpoKafkaConsole.Common | |||||
{ | |||||
public class CustomizeStopWatch : IDisposable | |||||
{ | |||||
private readonly Stopwatch _sw; | |||||
private readonly string _domain; | |||||
private readonly ILogger _logger; | |||||
public string Content { get; set; } = default!; | |||||
public CustomizeStopWatch(string domain, ILogger logger) | |||||
{ | |||||
_domain = domain; | |||||
_logger = logger; | |||||
_sw = new Stopwatch(); | |||||
_sw.Start(); | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
if (_sw != null) | |||||
{ | |||||
_logger.LogInformation($"统计时间[{_domain}],耗时 {_sw.Elapsed.TotalMilliseconds} 毫秒 {Content}"); | |||||
_sw.Stop(); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<Nullable>enable</Nullable> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.2" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,17 @@ | |||||
using Confluent.Kafka.Admin; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace TelpoKafkaConsole.Model | |||||
{ | |||||
public class ConsumerAcls:UserAcls | |||||
{ | |||||
public string Group { get; set; } = default!; | |||||
public override AclOperation Operation { get; set; } = AclOperation.Read; | |||||
public override AclPermissionType Permission { get; set; } = AclPermissionType.Allow; | |||||
} | |||||
} |
@@ -0,0 +1,16 @@ | |||||
using Confluent.Kafka.Admin; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace TelpoKafkaConsole.Model | |||||
{ | |||||
public class ProducerAcls:UserAcls | |||||
{ | |||||
public override AclOperation Operation { get; set; } = AclOperation.Write; | |||||
public override AclPermissionType Permission { get; set; } = AclPermissionType.Allow; | |||||
} | |||||
} |
@@ -0,0 +1,20 @@ | |||||
using Confluent.Kafka.Admin; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace TelpoKafkaConsole.Model | |||||
{ | |||||
public class ScramCredentialsUser | |||||
{ | |||||
public string Name { get; set; } = default!; | |||||
public string Password { get; set; } = string.Empty; | |||||
public ScramMechanism Mechanism { get; set; } = ScramMechanism.ScramSha256; | |||||
public int Iterations { get; set; } = 8192; | |||||
} | |||||
} |
@@ -0,0 +1,20 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace TelpoKafkaConsole.Model | |||||
{ | |||||
public class ServiceConfig | |||||
{ | |||||
/// <summary> | |||||
/// Kafka服务地址 | |||||
/// </summary> | |||||
public string KafkaServerAddress { get; set; } = default!; | |||||
public string KafkaServerPEMLocation { get; set;} = default!; | |||||
//public int TopicPartitionsNum { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<Nullable>enable</Nullable> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Confluent.Kafka" Version="2.4.0" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,22 @@ | |||||
using Confluent.Kafka.Admin; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace TelpoKafkaConsole.Model | |||||
{ | |||||
public class UserAcls | |||||
{ | |||||
public string UserName { get; set; } = default!; | |||||
public string Topic { get; set; } = default!; | |||||
//public string Group { get; set; } = default!; | |||||
public virtual AclOperation Operation { get; set; } = AclOperation.Any; | |||||
public virtual AclPermissionType Permission { get; set; } = AclPermissionType.Unknown; | |||||
} | |||||
} |
@@ -0,0 +1,387 @@ | |||||
using Confluent.Kafka; | |||||
using Confluent.Kafka.Admin; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using TelpoKafkaConsole.Model; | |||||
using static System.Net.Mime.MediaTypeNames; | |||||
namespace TelpoKafkaConsole.Service | |||||
{ | |||||
public class KafkaAdminService | |||||
{ | |||||
private readonly ILogger<KafkaAdminService> _logger; | |||||
private readonly ServiceConfig _configService; | |||||
public IAdminClient _adminClient; | |||||
public KafkaAdminService(ILogger<KafkaAdminService> logger, IOptions<ServiceConfig> _optConfigService) | |||||
{ | |||||
_logger = logger; | |||||
_configService = _optConfigService.Value; | |||||
_adminClient = new AdminClientBuilder(new AdminClientConfig | |||||
{ | |||||
BootstrapServers = _configService.KafkaServerAddress, | |||||
SecurityProtocol = SecurityProtocol.SaslSsl, | |||||
SaslMechanism = SaslMechanism.ScramSha256, | |||||
SaslUsername = "superuser", | |||||
SaslPassword = "password", | |||||
SslCaLocation = _configService.KafkaServerPEMLocation | |||||
// Add any other configuration options as needed | |||||
}).Build(); | |||||
} | |||||
public List<GroupInfo> ListGroups() | |||||
{ | |||||
try | |||||
{ | |||||
var groups = _adminClient.ListGroups(TimeSpan.FromSeconds(10)); | |||||
return groups; | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
throw new Exception(ex.Message); | |||||
} | |||||
} | |||||
#region UserScramCredentials | |||||
public async Task<List<UserScramCredentialsDescription>> DescribeUserScramCredentialsAsync(IEnumerable<string> users) | |||||
{ | |||||
try | |||||
{ | |||||
var timeout = TimeSpan.FromSeconds(10); | |||||
var descResult = await _adminClient.DescribeUserScramCredentialsAsync(users, new DescribeUserScramCredentialsOptions() { RequestTimeout = timeout }); | |||||
return descResult.UserScramCredentialsDescriptions; | |||||
//foreach (var description in descResult.UserScramCredentialsDescriptions) | |||||
//{ | |||||
// Console.WriteLine($" User: {description.User}"); | |||||
// foreach (var scramCredentialInfo in description.ScramCredentialInfos) | |||||
// { | |||||
// Console.WriteLine($" Mechanism: {scramCredentialInfo.Mechanism}"); | |||||
// Console.WriteLine($" Iterations: {scramCredentialInfo.Iterations}"); | |||||
// } | |||||
//} | |||||
} | |||||
catch (DescribeUserScramCredentialsException e) | |||||
{ | |||||
if (e.Error.Code.ToString().Equals("Local_Partial") | |||||
&& e.Results.UserScramCredentialsDescriptions.Count == 1 | |||||
&& e.Results.UserScramCredentialsDescriptions.First().ScramCredentialInfos.Count == 0 | |||||
) | |||||
{ | |||||
return new List<UserScramCredentialsDescription>(); | |||||
} | |||||
else | |||||
{ | |||||
var errMsg = $"An error occurred describing user SCRAM credentials for some users:\n"; | |||||
foreach (var description in e.Results.UserScramCredentialsDescriptions) | |||||
{ | |||||
errMsg += ($"User: {description.User} -- Error: {description.Error}\n"); | |||||
if (!description.Error.IsError) | |||||
{ | |||||
foreach (var scramCredentialInfo in description.ScramCredentialInfos) | |||||
{ | |||||
errMsg += ($"Mechanism: {scramCredentialInfo.Mechanism} -- Iterations: {scramCredentialInfo.Iterations}\n"); | |||||
} | |||||
} | |||||
} | |||||
throw new Exception(errMsg); | |||||
} | |||||
} | |||||
catch (KafkaException e) | |||||
{ | |||||
// _logger.LogError($"An error occurred describing user SCRAM credentials: {e}"); | |||||
throw new KafkaException(e.Error); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
throw new Exception(ex.Message); | |||||
} | |||||
} | |||||
public async Task AlterUserScramCredentialsAsync(ScramCredentialsUser scramUser, string Action = "UPSERT") | |||||
{ | |||||
var alterations = new List<UserScramCredentialAlteration>(); | |||||
string user = scramUser.Name; | |||||
var mechanism = scramUser.Mechanism; | |||||
var iterations = scramUser.Iterations; | |||||
var password = Encoding.UTF8.GetBytes(scramUser.Password); | |||||
if (Action.Equals("DELETE")) | |||||
{ | |||||
alterations.Add(new UserScramCredentialDeletion | |||||
{ | |||||
User = user, | |||||
Mechanism = mechanism, | |||||
} | |||||
); | |||||
} | |||||
else | |||||
{ | |||||
alterations.Add(new UserScramCredentialUpsertion | |||||
{ | |||||
User = user, | |||||
ScramCredentialInfo = new ScramCredentialInfo | |||||
{ | |||||
Mechanism = mechanism, | |||||
Iterations = iterations, | |||||
}, | |||||
Password = password, | |||||
// Salt = salt, | |||||
} | |||||
); | |||||
} | |||||
var timeout = TimeSpan.FromSeconds(30); | |||||
try | |||||
{ | |||||
await _adminClient.AlterUserScramCredentialsAsync(alterations,new AlterUserScramCredentialsOptions() { RequestTimeout = timeout }); | |||||
//_logger.LogError("All AlterUserScramCredentials operations completed successfully"); | |||||
} | |||||
catch (AlterUserScramCredentialsException e) | |||||
{ | |||||
var errMsg = ($"An error occurred altering user SCRAM credentials for some users:"); | |||||
foreach (var result in e.Results) | |||||
{ | |||||
errMsg += ($"User: {result.User} -- Error: {result.Error}\n"); | |||||
} | |||||
throw new Exception( errMsg ); | |||||
} | |||||
catch (KafkaException e) | |||||
{ | |||||
//_logger.LogError($"An error occurred altering user SCRAM credentials: {e}"); | |||||
throw new KafkaException( e.Error ); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
//_logger.LogError(ex.Message); | |||||
throw new Exception(ex.Message); | |||||
} | |||||
} | |||||
#endregion | |||||
#region ACLs | |||||
public async Task<List<AclBinding>> DescribeAclsAsync() | |||||
{ | |||||
//var name = "testtopic";//""; | |||||
//var principal = "demo-consumer"; | |||||
var host = "*"; | |||||
List<AclBinding> ParseAclBindings = new() | |||||
{ | |||||
new() { | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = Confluent.Kafka.Admin.ResourceType.Any,//resourceType, | |||||
//Name = "demo-orders", | |||||
ResourcePatternType = ResourcePatternType.Any//resourcePatternType | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
//Principal ="User:demo-consumer", | |||||
Host = host, | |||||
Operation = AclOperation.Any,//operation, | |||||
PermissionType = AclPermissionType.Any//permissionType | |||||
} | |||||
} | |||||
}; | |||||
List<AclBindingFilter> aclBindingFilters = ParseAclBindings.Select(aclBinding => aclBinding.ToFilter()).ToList(); | |||||
try | |||||
{ | |||||
var result = await _adminClient.DescribeAclsAsync(aclBindingFilters[0]); | |||||
return result.AclBindings; | |||||
} | |||||
catch (DescribeAclsException e) | |||||
{ | |||||
//_logger.LogError($"An error occurred in describe ACLs operation: Code: {e.Result.Error.Code}" + | |||||
// $", Reason: {e.Result.Error.Reason}"); | |||||
throw new Exception($"An error occurred in describe ACLs operation: Code: {e.Result.Error.Code}, Reason: {e.Result.Error.Reason}"); | |||||
} | |||||
catch (KafkaException e) | |||||
{ | |||||
throw new KafkaException(e.Error); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
//_logger.LogError(ex.Message); | |||||
throw new Exception(ex.Message); | |||||
} | |||||
} | |||||
public async Task CreateAclsAsync(List<AclBinding> aclBindings) | |||||
{ | |||||
try | |||||
{ | |||||
await _adminClient.CreateAclsAsync(aclBindings); | |||||
_logger.LogInformation("All create ACL operations completed successfully"); | |||||
} | |||||
catch (CreateAclsException e) | |||||
{ | |||||
var errMsg = ("One or more create ACL operations failed.\n"); | |||||
for (int i = 0; i < e.Results.Count; ++i) | |||||
{ | |||||
var result = e.Results[i]; | |||||
if (!result.Error.IsError) | |||||
{ | |||||
errMsg += ($"Create ACLs operation {i} completed successfully\n"); | |||||
} | |||||
else | |||||
{ | |||||
errMsg += ($"An error occurred in create ACL operation {i}: Code: {result.Error.Code}" + | |||||
$", Reason: {result.Error.Reason}\n"); | |||||
} | |||||
} | |||||
throw new Exception(errMsg); | |||||
} | |||||
catch (KafkaException e) | |||||
{ | |||||
//_logger.LogError($"An error occurred calling the CreateAcls operation: {e.Message}"); | |||||
throw new Exception($"An error occurred calling the CreateAcls operation: {e.Message}"); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
//_logger.LogError(ex.Message); | |||||
throw new Exception(ex.Message); | |||||
} | |||||
} | |||||
public async Task DeleteAclsAsync(List<AclBinding> aclBindings) | |||||
{ | |||||
List<AclBindingFilter> aclBindingFilters = aclBindings.Select(aclBinding => aclBinding.ToFilter()).ToList(); | |||||
try | |||||
{ | |||||
var results = await _adminClient.DeleteAclsAsync(aclBindingFilters); | |||||
int i = 0; | |||||
foreach (var result in results) | |||||
{ | |||||
_logger.LogInformation($"Deleted ACLs in operation {i}"); | |||||
// PrintAclBindings(result.AclBindings); | |||||
++i; | |||||
} | |||||
} | |||||
catch (DeleteAclsException e) | |||||
{ | |||||
var errMsg = ("One or more create ACL operations failed.\n"); | |||||
for (int i = 0; i < e.Results.Count; ++i) | |||||
{ | |||||
var result = e.Results[i]; | |||||
if (!result.Error.IsError) | |||||
{ | |||||
errMsg += ($"Deleted ACLs in operation {i}\n"); | |||||
// PrintAclBindings(result.AclBindings); | |||||
} | |||||
else | |||||
{ | |||||
errMsg += ($"An error occurred in delete ACL operation {i}: Code: {result.Error.Code}" + | |||||
$", Reason: {result.Error.Reason}\n"); | |||||
} | |||||
} | |||||
throw new Exception (errMsg); | |||||
} | |||||
catch (KafkaException e) | |||||
{ | |||||
throw new KafkaException(e.Error); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
//_logger.LogError(ex.Message); | |||||
throw new Exception(ex.Message); | |||||
} | |||||
} | |||||
#endregion | |||||
#region Topics | |||||
public async Task CreateTopic(string topicName, TimeSpan retentionTime,int numPartitions=1) | |||||
{ | |||||
try | |||||
{ | |||||
var configEntries = new Dictionary<string, string> | |||||
{ | |||||
{ "retention.ms", ((int)retentionTime.TotalMilliseconds).ToString() } | |||||
}; | |||||
await _adminClient.CreateTopicsAsync( | |||||
new TopicSpecification[] { | |||||
new() { | |||||
Name = topicName, | |||||
ReplicationFactor = 1, | |||||
NumPartitions = numPartitions, | |||||
Configs = configEntries | |||||
} | |||||
}); | |||||
} | |||||
catch (CreateTopicsException e) | |||||
{ | |||||
throw new Exception($"An error occurred creating topic {e.Results[0].Topic}: {e.Results[0].Error.Reason}"); | |||||
} | |||||
} | |||||
public async Task DeleteTopics(IEnumerable<string> topicNames) | |||||
{ | |||||
try | |||||
{ | |||||
await _adminClient.DeleteTopicsAsync(topicNames); | |||||
} | |||||
catch (DeleteTopicsException e) | |||||
{ | |||||
throw new Exception($"An error occurred deleting topic {e.Results[0].Topic}: {e.Results[0].Error.Reason}"); | |||||
} | |||||
} | |||||
public async Task<List<TopicDescription>> DescribeTopicsAsync(IEnumerable<string> topicNames) | |||||
{ | |||||
try | |||||
{ | |||||
// var topicCollection = TopicCollection.OfTopicNames(new[] { topicName }); | |||||
var topicCollection = TopicCollection.OfTopicNames(topicNames); | |||||
var topicDescriptions = await _adminClient.DescribeTopicsAsync(topicCollection); | |||||
return topicDescriptions.TopicDescriptions; | |||||
//foreach (var topicDescription in topicDescriptions.TopicDescriptions) | |||||
//{ | |||||
// Console.WriteLine($"Topic: {topicDescription.Name}"); | |||||
// foreach (var partition in topicDescription.Partitions) | |||||
// { | |||||
// Console.WriteLine($"Partition: {partition.Partition}, Leader: {partition.Leader}, Replicas: {string.Join(",", partition.Replicas)}, Isr: {string.Join(",", partition.ISR)}"); | |||||
// } | |||||
//} | |||||
} | |||||
catch (DescribeTopicsException e) | |||||
{ | |||||
if (e.Error.Code.ToString().Equals("Local_Partial") | |||||
&& e.Results.TopicDescriptions.First().Error.Code.ToString().Equals("UnknownTopicOrPart") | |||||
&& e.Results.TopicDescriptions.First().Name.Equals(topicNames.First()) | |||||
) | |||||
{ | |||||
return new List<TopicDescription>(); | |||||
} | |||||
else | |||||
{ | |||||
throw new Exception($"An error occurred describing topic {e.Message}"); | |||||
} | |||||
// throw new Exception($"An error occurred describing topic {e.Message}"); | |||||
} | |||||
} | |||||
#endregion | |||||
} | |||||
} |
@@ -0,0 +1,19 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<Nullable>enable</Nullable> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.2" /> | |||||
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.2" /> | |||||
<PackageReference Include="Confluent.Kafka" Version="2.4.0" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\TelpoKafkaConsole.Model\TelpoKafkaConsole.Model.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,26 @@ | |||||
using Serilog.Configuration; | |||||
using Serilog.Core; | |||||
using Serilog.Events; | |||||
using Serilog; | |||||
namespace TelpoKafkaConsole.WebApi.Configs | |||||
{ | |||||
public class ThreadInfoEnricher : ILogEventEnricher | |||||
{ | |||||
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) | |||||
{ | |||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadId", Thread.CurrentThread.ManagedThreadId)); | |||||
} | |||||
} | |||||
public static class EnricherExtensions | |||||
{ | |||||
public static LoggerConfiguration WithThreadInfo(this LoggerEnrichmentConfiguration enrich) | |||||
{ | |||||
if (enrich == null) | |||||
throw new ArgumentNullException(nameof(enrich)); | |||||
return enrich.With<ThreadInfoEnricher>(); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,179 @@ | |||||
using Confluent.Kafka.Admin; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using TelpoKafkaConsole.Service; | |||||
using TelpoKafkaConsole.WebApi.Controllers.Api; | |||||
using TelpoKafkaConsole.WebApi.Model.Request; | |||||
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 | |||||
namespace TelpoKafkaConsole.WebApi.Controllers | |||||
{ | |||||
[Route("api/[controller]/[action]")] | |||||
[ApiController] | |||||
public class AclsController : ControllerBase | |||||
{ | |||||
private readonly KafkaAdminService _servicekafkaAdmin; | |||||
public AclsController(KafkaAdminService kafkaAdminService) | |||||
{ | |||||
_servicekafkaAdmin = kafkaAdminService; | |||||
} | |||||
// GET: api/<AclsController> | |||||
[HttpGet] | |||||
public async Task<ApiResponse<List<AclBinding>>> Get() | |||||
{ | |||||
var acls = await _servicekafkaAdmin.DescribeAclsAsync(); | |||||
return ApiResponse<List<AclBinding>>.Success(acls); | |||||
} | |||||
// GET api/<AclsController>/5 | |||||
[HttpGet("{username}")] | |||||
public async Task<ApiResponse<IEnumerable<AclBinding>>> Get(string username) | |||||
{ | |||||
var acls = await _servicekafkaAdmin.DescribeAclsAsync(); | |||||
return ApiResponse<IEnumerable<AclBinding>>.Success(acls.Where(i => i.Entry.Principal.EndsWith(username))); | |||||
} | |||||
// POST api/<AclsController> | |||||
[HttpPost] | |||||
public async Task<ApiResponse<string>> Post([FromBody] AclsReq aclsReq) | |||||
{ | |||||
List<AclBinding> aclBindings = new(); | |||||
// 生产者 | |||||
if (string.IsNullOrEmpty(aclsReq.Group)) | |||||
{ | |||||
aclBindings.Add(new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Topic, | |||||
Name = aclsReq.Topic, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{aclsReq.UserName}", | |||||
Host = "*", | |||||
Operation = AclOperation.Write, | |||||
PermissionType = AclPermissionType.Allow | |||||
} | |||||
}); | |||||
} | |||||
// 消费者 | |||||
else | |||||
{ | |||||
aclBindings.Add(new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Group, | |||||
Name = aclsReq.Group, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{aclsReq.UserName}", | |||||
Host = "*", | |||||
Operation = AclOperation.Read, | |||||
PermissionType = AclPermissionType.Allow | |||||
} | |||||
}); | |||||
aclBindings.Add(new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Topic, | |||||
Name = aclsReq.Topic, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{aclsReq.UserName}", | |||||
Host = "*", | |||||
Operation = AclOperation.Read, | |||||
PermissionType = AclPermissionType.Allow | |||||
} | |||||
}); | |||||
} | |||||
// - Group: {aclsReq.Group} | |||||
await _servicekafkaAdmin.CreateAclsAsync(aclBindings); | |||||
var operation = string.IsNullOrEmpty(aclsReq.Group) ? "写" : "读"; | |||||
var group = string.IsNullOrEmpty(aclsReq.Group) ? "" : $"Group:{aclsReq.Group} - "; | |||||
return ApiResponse<string>.Success($"创建 ACLs 规则 用户:{aclsReq.UserName} - Topic:{aclsReq.Topic} - {group}{operation}权限成功"); | |||||
} | |||||
// DELETE api/<AclsController>/5 | |||||
[HttpDelete] | |||||
public async Task<ApiResponse<string>> DeleteAsync([FromBody] AclsReq aclsReq) | |||||
{ | |||||
List<AclBinding> aclBindings = new(); | |||||
// 生产者 | |||||
if (string.IsNullOrEmpty(aclsReq.Group)) | |||||
{ | |||||
aclBindings.Add(new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Topic, | |||||
Name = aclsReq.Topic, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{aclsReq.UserName}", | |||||
Host = "*", | |||||
Operation = AclOperation.Any, | |||||
PermissionType = AclPermissionType.Any | |||||
} | |||||
}); | |||||
} | |||||
// 消费者 | |||||
else | |||||
{ | |||||
aclBindings.Add(new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Group, | |||||
Name = aclsReq.Group, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{aclsReq.UserName}", | |||||
Host = "*", | |||||
Operation = AclOperation.Any, | |||||
PermissionType = AclPermissionType.Any | |||||
} | |||||
}); | |||||
aclBindings.Add(new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Topic, | |||||
Name = aclsReq.Topic, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{aclsReq.UserName}", | |||||
Host = "*", | |||||
Operation = AclOperation.Read, | |||||
PermissionType = AclPermissionType.Allow | |||||
} | |||||
}); | |||||
} | |||||
await _servicekafkaAdmin.DeleteAclsAsync(aclBindings); | |||||
// var operation = string.IsNullOrEmpty(aclsReq.Group) ? "写" : "读"; | |||||
var group = string.IsNullOrEmpty(aclsReq.Group) ? "" : $"Group:{aclsReq.Group} - "; | |||||
return ApiResponse<string>.Success($"删除 ACLs 规则 用户:{aclsReq.UserName} - Topic:{aclsReq.Topic} - {group}所有权限成功"); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,74 @@ | |||||
using Newtonsoft.Json.Serialization; | |||||
using Newtonsoft.Json; | |||||
namespace TelpoKafkaConsole.WebApi.Controllers.Api | |||||
{ | |||||
public class ApiResponse<T> | |||||
{ | |||||
public string Timestamp { get; set; } = default!; | |||||
public T Data { get; set; } = default!; | |||||
public Result Result { get; set; } = new Result(); | |||||
//public bool Succeeded { get; set; } | |||||
// public string Message { get; set; } = String.Empty; | |||||
public static ApiResponse<T> Fail(int code, string errorMessage) => new() | |||||
{ | |||||
//MsgType = msgType, | |||||
//Signature = signature, | |||||
Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), | |||||
Result = new() | |||||
{ | |||||
Status = "failed", | |||||
Code = code, | |||||
Message = errorMessage, | |||||
}, | |||||
}; | |||||
//public static ApiResponse<T> Success(string msgType,string signature,T data) => new() | |||||
//{ | |||||
// MsgType= msgType, | |||||
// Signature = signature, | |||||
// Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), | |||||
// Data = data, | |||||
// Result = new() | |||||
// { | |||||
// Status = "succeed", | |||||
// Code = 200, | |||||
// Message = "请求成功!", | |||||
// }, | |||||
//}; | |||||
public static ApiResponse<T> Success(T data) => new() | |||||
{ | |||||
Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), | |||||
Data = data, | |||||
Result = new() | |||||
{ | |||||
Status = "succeed", | |||||
Code = 200, | |||||
Message = "请求成功!", | |||||
}, | |||||
}; | |||||
public string ToJsonString() | |||||
{ | |||||
var settings = new JsonSerializerSettings | |||||
{ | |||||
DateFormatString = "yyyy-MM-dd HH:mm:ss.fff", // 设置日期格式 | |||||
ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } | |||||
}; | |||||
return JsonConvert.SerializeObject(this, settings); | |||||
} | |||||
} | |||||
public class Result | |||||
{ | |||||
public string Status { get; set; } = default!; | |||||
public int Code { get; set; } | |||||
public string Message { get; set; } = default!; | |||||
} | |||||
} |
@@ -0,0 +1,149 @@ | |||||
using Confluent.Kafka.Admin; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using TelpoKafkaConsole.Model; | |||||
using TelpoKafkaConsole.Service; | |||||
using TelpoKafkaConsole.WebApi.Controllers.Api; | |||||
using TelpoKafkaConsole.WebApi.Model.Request; | |||||
using static Confluent.Kafka.ConfigPropertyNames; | |||||
namespace TelpoKafkaConsole.WebApi.Controllers | |||||
{ | |||||
[Route("api/[controller]")] | |||||
[ApiController] | |||||
public class ScramAclsController : ControllerBase | |||||
{ | |||||
private readonly KafkaAdminService _servicekafkaAdmin; | |||||
public ScramAclsController(KafkaAdminService kafkaAdminService) { _servicekafkaAdmin = kafkaAdminService; } | |||||
// POST api/<ScramAclsController>/Consumer | |||||
[HttpPost("Consumer")] // 添加了路由 | |||||
public async Task<ApiResponse<string>> Consumer([FromBody] ScramAclsConsumerReq consumer) | |||||
{ | |||||
// 创建用户 | |||||
ScramCredentialsUser scramUser = new() | |||||
{ | |||||
Name = consumer.Name, | |||||
Password = consumer.Password, | |||||
}; | |||||
await _servicekafkaAdmin.AlterUserScramCredentialsAsync(scramUser); | |||||
// 创建 topic | |||||
var topics = await _servicekafkaAdmin.DescribeTopicsAsync(new List<string> { consumer.Topic }); | |||||
if (topics.Count.Equals(0)) | |||||
{ | |||||
await _servicekafkaAdmin.CreateTopic(consumer.Topic, TimeSpan.FromDays(3), consumer.NumPartitions); | |||||
} | |||||
// 创建 alcs | |||||
List<AclBinding> aclBindings = new() | |||||
{ | |||||
new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Group, | |||||
Name = consumer.Group, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{consumer.Name}", | |||||
Host = "*", | |||||
Operation = AclOperation.Read, | |||||
PermissionType = AclPermissionType.Allow | |||||
} | |||||
}, | |||||
new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Topic, | |||||
Name = consumer.Topic, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{consumer.Name}", | |||||
Host = "*", | |||||
Operation = AclOperation.Read, | |||||
PermissionType = AclPermissionType.Allow | |||||
} | |||||
} | |||||
}; | |||||
await _servicekafkaAdmin.CreateAclsAsync(aclBindings); | |||||
return ApiResponse<string>.Success($"创建 消费者用户 {consumer.Name} Acls 规则成功"); | |||||
} | |||||
// POST api/<ScramAclsController>/Producer | |||||
[HttpPost("Producer")] // 添加了路由 | |||||
public async Task<ApiResponse<string>> Producer([FromBody] ScramAclsProducerReq producer) | |||||
{ | |||||
// 创建用户 | |||||
ScramCredentialsUser scramUser = new() | |||||
{ | |||||
Name = producer.Name, | |||||
Password = producer.Password, | |||||
}; | |||||
await _servicekafkaAdmin.AlterUserScramCredentialsAsync(scramUser); | |||||
// 创建 topic | |||||
var topics = await _servicekafkaAdmin.DescribeTopicsAsync(new List<string> { producer.Topic }); | |||||
if (topics.Count.Equals(0)) | |||||
{ | |||||
await _servicekafkaAdmin.CreateTopic(producer.Topic, TimeSpan.FromDays(3), producer.NumPartitions); | |||||
} | |||||
// 创建 alcs | |||||
List<AclBinding> aclBindings = new() | |||||
{ | |||||
new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Topic, | |||||
Name = producer.Topic, | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:{producer.Name}", | |||||
Host = "*", | |||||
Operation = AclOperation.Write, | |||||
PermissionType = AclPermissionType.Allow | |||||
} | |||||
} | |||||
}; | |||||
await _servicekafkaAdmin.CreateAclsAsync(aclBindings); | |||||
return ApiResponse<string>.Success($"创建 生产者用户 {producer.Name} Acls 规则成功"); | |||||
} | |||||
// DELETE api/<ScramAclsController>/{username} | |||||
[HttpDelete("{username}")] | |||||
public async Task<ApiResponse<string>> Delete(string username) | |||||
{ | |||||
// 删除用户 | |||||
var scramUsers = await _servicekafkaAdmin.DescribeUserScramCredentialsAsync(new List<string> | |||||
{ | |||||
username | |||||
}); | |||||
if (scramUsers.Count==1) | |||||
{ | |||||
ScramCredentialsUser scramUser = new() | |||||
{ | |||||
Name = username | |||||
}; | |||||
await _servicekafkaAdmin.AlterUserScramCredentialsAsync(scramUser, "DELETE"); | |||||
} | |||||
// 删除alcs | |||||
var acls = await _servicekafkaAdmin.DescribeAclsAsync(); | |||||
var userAclsBinding = acls.Where(i => i.Entry.Principal.EndsWith(username)).ToList(); | |||||
if (userAclsBinding.Count>0) | |||||
{ | |||||
await _servicekafkaAdmin.DeleteAclsAsync(userAclsBinding); | |||||
} | |||||
return ApiResponse<string>.Success($"删除用户 {username} 和 Acls 规则成功"); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,89 @@ | |||||
using Confluent.Kafka; | |||||
using Confluent.Kafka.Admin; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using Microsoft.OpenApi.Extensions; | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json.Linq; | |||||
using TelpoKafkaConsole.Model; | |||||
using TelpoKafkaConsole.Service; | |||||
using TelpoKafkaConsole.WebApi.Controllers.Api; | |||||
using TelpoKafkaConsole.WebApi.Model.Request; | |||||
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 | |||||
namespace TelpoKafkaConsole.WebApi.Controllers | |||||
{ | |||||
[Route("api/[controller]")] | |||||
[ApiController] | |||||
public class ScramCredentialsUserController : ControllerBase | |||||
{ | |||||
private readonly KafkaAdminService _servicekafkaAdmin; | |||||
public ScramCredentialsUserController(KafkaAdminService kafkaAdminService) | |||||
{ | |||||
_servicekafkaAdmin = kafkaAdminService; | |||||
} | |||||
// GET: api/<ScramCredentialsUserController> | |||||
[HttpGet] | |||||
public async Task<ApiResponse<object>> Get() | |||||
{ | |||||
var usersScram = await _servicekafkaAdmin.DescribeUserScramCredentialsAsync(new List<string>()); | |||||
var users = usersScram | |||||
.Select(i => new {i.User, ScramCredentialInfos=i.ScramCredentialInfos | |||||
.Select(s=>new { Mechanism=s.Mechanism.GetDisplayName(),s.Iterations }) }) | |||||
.Where(i=>!i.User.Equals("superuser")); | |||||
return ApiResponse<object>.Success(users); | |||||
} | |||||
// GET api/<ScramCredentialsUserController>/5 | |||||
[HttpGet("{username}")] | |||||
public async Task<ApiResponse<object>> GetAsync(string username) | |||||
{ | |||||
var users = new List<string> | |||||
{ | |||||
username | |||||
}; | |||||
var usersScram = await _servicekafkaAdmin.DescribeUserScramCredentialsAsync(users); | |||||
var firstUserScram = usersScram.FirstOrDefault(); | |||||
var user = new | |||||
{ | |||||
firstUserScram?.User, | |||||
ScramCredentialInfos = firstUserScram?.ScramCredentialInfos | |||||
.Select(i => new | |||||
{ | |||||
Mechanism = i.Mechanism.GetDisplayName(), | |||||
i.Iterations | |||||
}) | |||||
}; | |||||
return ApiResponse<object>.Success(user); | |||||
} | |||||
// POST api/<ScramCredentialsUserController> | |||||
[HttpPost] | |||||
public async Task<ApiResponse<string>> PostAsync([FromBody] UserReq user) | |||||
{ | |||||
ScramCredentialsUser scramUser = new() | |||||
{ | |||||
Name = user.Name, | |||||
Password = user.Password, | |||||
}; | |||||
await _servicekafkaAdmin.AlterUserScramCredentialsAsync(scramUser); | |||||
return ApiResponse<string>.Success($"创建 Scram 用户{user.Name}成功"); | |||||
} | |||||
[HttpDelete("{username}")] | |||||
public async Task<ApiResponse<string>> Delete(string username) | |||||
{ | |||||
ScramCredentialsUser scramUser = new() | |||||
{ | |||||
Name = username | |||||
}; | |||||
await _servicekafkaAdmin.AlterUserScramCredentialsAsync(scramUser,"DELETE"); | |||||
return ApiResponse<string>.Success($"删除 Scram 用户{username} 成功"); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,40 @@ | |||||
using Confluent.Kafka.Admin; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using TelpoKafkaConsole.Service; | |||||
using TelpoKafkaConsole.WebApi.Controllers.Api; | |||||
using TelpoKafkaConsole.WebApi.Model.Request; | |||||
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 | |||||
namespace TelpoKafkaConsole.WebApi.Controllers | |||||
{ | |||||
[Route("api/[controller]")] | |||||
[ApiController] | |||||
public class TopicController : ControllerBase | |||||
{ | |||||
private readonly KafkaAdminService _servicekafkaAdmin; | |||||
public TopicController(KafkaAdminService kafkaAdminService) { _servicekafkaAdmin = kafkaAdminService; } | |||||
[HttpGet("{topic}")] | |||||
public async Task<ApiResponse<List<TopicDescription>>> Get(string topic) | |||||
{ | |||||
var topicInfo = await _servicekafkaAdmin.DescribeTopicsAsync(new List<string>() { topic }); | |||||
return ApiResponse<List<TopicDescription>>.Success(topicInfo); | |||||
} | |||||
[HttpPost] | |||||
public async Task<ApiResponse<string>> Post([FromBody] TopicReq topic) | |||||
{ | |||||
await _servicekafkaAdmin.CreateTopic(topic.TopicName,TimeSpan.FromDays(3)); | |||||
return ApiResponse<string>.Success($"创建 Topic {topic} 成功"); | |||||
} | |||||
[HttpDelete("{topic}")] | |||||
public async Task<ApiResponse<string>> Delete(string topic) | |||||
{ | |||||
await _servicekafkaAdmin.DeleteTopics(new List<string>() {topic}); | |||||
return ApiResponse<string>.Success($"删除 Topic {topic} 成功"); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,74 @@ | |||||
using Confluent.Kafka; | |||||
using Confluent.Kafka.Admin; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using TelpoKafkaConsole.Service; | |||||
using TelpoKafkaConsole.WebApi.Controllers.Api; | |||||
namespace TelpoKafkaConsole.WebApi.Controllers | |||||
{ | |||||
[ApiController] | |||||
[Route("[controller]")] | |||||
public class WeatherForecastController : ControllerBase | |||||
{ | |||||
private readonly KafkaAdminService _servicekafkaAdmin; | |||||
private static readonly string[] Summaries = new[] | |||||
{ | |||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" | |||||
}; | |||||
private readonly ILogger<WeatherForecastController> _logger; | |||||
public WeatherForecastController(ILogger<WeatherForecastController> logger, KafkaAdminService kafkaAdminService) | |||||
{ | |||||
_logger = logger; | |||||
_servicekafkaAdmin = kafkaAdminService; | |||||
} | |||||
//[HttpGet(Name = "GetWeatherForecast")] | |||||
//public IEnumerable<WeatherForecast> Get() | |||||
//{ | |||||
// return Enumerable.Range(1, 5).Select(index => new WeatherForecast | |||||
// { | |||||
// Date = DateTime.Now.AddDays(index), | |||||
// TemperatureC = Random.Shared.Next(-20, 55), | |||||
// Summary = Summaries[Random.Shared.Next(Summaries.Length)] | |||||
// }) | |||||
// .ToArray(); | |||||
//} | |||||
//[HttpGet(Name = "GetGroup")] | |||||
//public ApiResponse<List<GroupInfo>> GetGroup() | |||||
//{ | |||||
// var group = _servicekafkaAdmin.ListGroups(); | |||||
// return ApiResponse<List<GroupInfo>>.Success(group); | |||||
//} | |||||
[HttpGet(Name = "GetGroup")] | |||||
public async Task<ApiResponse<string>> GetGroupAsync() | |||||
{ | |||||
// var group = _servicekafkaAdmin.ListGroups(); | |||||
List<AclBinding> aclBindings = new() | |||||
{ | |||||
new AclBinding() | |||||
{ | |||||
Pattern = new ResourcePattern | |||||
{ | |||||
Type = ResourceType.Broker, | |||||
Name = "kafka-cluster", | |||||
ResourcePatternType = ResourcePatternType.Literal | |||||
}, | |||||
Entry = new AccessControlEntry | |||||
{ | |||||
Principal = $"User:telpo-consumer", | |||||
Host = "*", | |||||
Operation = AclOperation.All, | |||||
PermissionType = AclPermissionType.Deny | |||||
} | |||||
} | |||||
}; | |||||
await _servicekafkaAdmin.CreateAclsAsync(aclBindings); | |||||
return ApiResponse<string>.Success("ok"); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,36 @@ | |||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. | |||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base | |||||
WORKDIR /app | |||||
EXPOSE 80 | |||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build | |||||
ARG BUILD_CONFIGURATION=Release | |||||
WORKDIR /src | |||||
COPY ["TelpoKafkaConsole.WebApi/TelpoKafkaConsole.WebApi.csproj", "TelpoKafkaConsole.WebApi/"] | |||||
COPY ["TelpoKafkaConsole.Common/TelpoKafkaConsole.Common.csproj", "TelpoKafkaConsole.Common/"] | |||||
COPY ["TelpoKafkaConsole.Service/TelpoKafkaConsole.Service.csproj", "TelpoKafkaConsole.Service/"] | |||||
COPY ["TelpoKafkaConsole.Model/TelpoKafkaConsole.Model.csproj", "TelpoKafkaConsole.Model/"] | |||||
RUN dotnet restore "./TelpoKafkaConsole.WebApi/./TelpoKafkaConsole.WebApi.csproj" | |||||
COPY . . | |||||
WORKDIR "/src/TelpoKafkaConsole.WebApi" | |||||
RUN dotnet build "./TelpoKafkaConsole.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/build | |||||
FROM build AS publish | |||||
ARG BUILD_CONFIGURATION=Release | |||||
RUN dotnet publish "./TelpoKafkaConsole.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | |||||
FROM base AS final | |||||
WORKDIR /app | |||||
COPY --from=publish /app/publish . | |||||
COPY pem /app/pem | |||||
ENV environment=Development | |||||
ENV TimeZone=Asia/Shanghai | |||||
ENV LANG C.UTF-8 | |||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone | |||||
#ENTRYPOINT ["dotnet", "TelpoKafkaConsole.WebApi.dll"] | |||||
ENTRYPOINT ["sh", "-c", "dotnet TelpoKafkaConsole.App.dll --environment=$environment"] |
@@ -0,0 +1,172 @@ | |||||
using Confluent.Kafka; | |||||
using Microsoft.AspNetCore.Http; | |||||
using Newtonsoft.Json; | |||||
using System.Net; | |||||
using System.Text; | |||||
using TelpoKafkaConsole.Common; | |||||
using TelpoKafkaConsole.WebApi.Controllers.Api; | |||||
namespace TelpoKafkaConsole.WebApi.Middleware | |||||
{ | |||||
public class LoggingMiddleware | |||||
{ | |||||
private readonly RequestDelegate _next; | |||||
private readonly ILogger<LoggingMiddleware> _logger; | |||||
public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger) | |||||
{ | |||||
_next = next; | |||||
_logger = logger; | |||||
} | |||||
//public async Task InvokeAsync(HttpContext context) | |||||
//{ | |||||
// //// 在请求处理之前记录日志 | |||||
// //using (_logger.BeginScope(new Dictionary<string, object> { ["RequestId"] = "" })) | |||||
// using (new CustomizeStopWatch(nameof(LoggingMiddleware), _logger)) | |||||
// { | |||||
// var request = await FormatRequest(context.Request); | |||||
// _logger.LogInformation(request); | |||||
// var originalBodyStream = context.Response.Body; | |||||
// using var responseBody = new MemoryStream(); | |||||
// context.Response.Body = responseBody; | |||||
// await _next(context); | |||||
// var response = await FormatResponse(context.Response); | |||||
// _logger.LogInformation(response); | |||||
// await responseBody.CopyToAsync(originalBodyStream); | |||||
// } | |||||
//} | |||||
public async Task InvokeAsync(HttpContext context) | |||||
{ | |||||
//// 在请求处理之前记录日志 | |||||
//using (_logger.BeginScope(new Dictionary<string, object> { ["RequestId"] = "" })) | |||||
using (new CustomizeStopWatch(nameof(LoggingMiddleware), _logger)) | |||||
{ | |||||
using var responseBody = new MemoryStream(); | |||||
var originalBodyStream = context.Response.Body; | |||||
try | |||||
{ | |||||
var request = await FormatRequest(context.Request); | |||||
_logger.LogInformation(request); | |||||
context.Response.Body = responseBody; | |||||
await _next(context); | |||||
var response = await FormatResponse(context.Response); | |||||
_logger.LogInformation(response); | |||||
// await responseBody.CopyToAsync(originalBodyStream); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
await HandleExceptionAsync(context, ex); // 捕获异常了 在HandleExceptionAsync中处理 | |||||
} | |||||
//var response = await FormatResponse(context.Response); | |||||
//_logger.LogInformation(response); | |||||
await responseBody.CopyToAsync(originalBodyStream); | |||||
} | |||||
} | |||||
private async Task<string> FormatRequest(HttpRequest request) | |||||
{ | |||||
request.EnableBuffering(); | |||||
var body = await new StreamReader(request.Body).ReadToEndAsync(); | |||||
var formattedBody = FormatJson(body); | |||||
request.Body.Position = 0; | |||||
return $"请求: {request.Scheme} {request.Host}{request.Path} {request.QueryString} {formattedBody}"; | |||||
} | |||||
private async Task<string> FormatResponse(HttpResponse response) | |||||
{ | |||||
response.Body.Seek(0, SeekOrigin.Begin); | |||||
var body = await new StreamReader(response.Body).ReadToEndAsync(); | |||||
var formattedBody = FormatJson(body); | |||||
response.Body.Seek(0, SeekOrigin.Begin); | |||||
return $"响应: {response.StatusCode}: {formattedBody}"; | |||||
} | |||||
private static string FormatJson(string json) | |||||
{ | |||||
if (string.IsNullOrEmpty(json)) | |||||
{ | |||||
return string.Empty; | |||||
} | |||||
try | |||||
{ | |||||
var obj = JsonConvert.DeserializeObject(json); | |||||
// return JsonConvert.SerializeObject(obj, Formatting.Indented); | |||||
return JsonConvert.SerializeObject(obj); | |||||
} | |||||
catch | |||||
{ | |||||
return json; | |||||
} | |||||
} | |||||
private async Task HandleExceptionAsync(HttpContext context, Exception exception) | |||||
{ | |||||
context.Response.ContentType = "application/json"; // 返回json 类型 | |||||
var response = context.Response; | |||||
var errorResponse = new ErrorResponse | |||||
{ | |||||
Success = false | |||||
}; // 自定义的异常错误信息类型 | |||||
switch (exception) | |||||
{ | |||||
case ApplicationException ex: | |||||
if (ex.Message.Contains("Invalid token")) | |||||
{ | |||||
response.StatusCode = (int)HttpStatusCode.Forbidden; | |||||
errorResponse.Message = ex.Message; | |||||
break; | |||||
} | |||||
response.StatusCode = (int)HttpStatusCode.BadRequest; | |||||
errorResponse.Message = ex.Message; | |||||
break; | |||||
case KeyNotFoundException ex: | |||||
response.StatusCode = (int)HttpStatusCode.NotFound; | |||||
errorResponse.Message = ex.Message; | |||||
break; | |||||
default: | |||||
response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||||
errorResponse.Message = "Internal Server errors. Check Logs!"; | |||||
break; | |||||
} | |||||
//var apiResponse = ApiResponse<object>.Fail(response.StatusCode, $"{exception.Message}\n{exception.InnerException}\n{exception.StackTrace}"); | |||||
var apiResponse = ApiResponse<object>.Fail(response.StatusCode, $"{exception.Message}"); | |||||
var resultJson = JsonConvert.SerializeObject(apiResponse); | |||||
var resultBytes = Encoding.UTF8.GetBytes(resultJson); | |||||
await response.Body.WriteAsync(resultBytes, 0, resultBytes.Length); | |||||
var responseStr = await FormatResponse(context.Response); | |||||
_logger.LogError(responseStr); | |||||
} | |||||
internal class ErrorResponse | |||||
{ | |||||
public bool Success { get; set; } | |||||
public string Message { get; set; } = String.Empty; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
namespace TelpoKafkaConsole.WebApi.Model.Request | |||||
{ | |||||
public class AclsReq | |||||
{ | |||||
public string UserName { get; set; } = default!; | |||||
public string Topic { get; set; } = default!; | |||||
public string Group { get; set; } = default!; | |||||
} | |||||
} |
@@ -0,0 +1,15 @@ | |||||
namespace TelpoKafkaConsole.WebApi.Model.Request | |||||
{ | |||||
public class ScramAclsConsumerReq | |||||
{ | |||||
public string Name { get; set; } = default!; | |||||
public string Password { get; set; } = default!; | |||||
public string Topic { get; set; } = default!; | |||||
public int NumPartitions { get; set; } = 1; | |||||
public string Group { get; set; } = default!; | |||||
} | |||||
} |
@@ -0,0 +1,14 @@ | |||||
namespace TelpoKafkaConsole.WebApi.Model.Request | |||||
{ | |||||
public class ScramAclsProducerReq | |||||
{ | |||||
public string Name { get; set; } = default!; | |||||
public string Password { get; set; } = default!; | |||||
public string Topic { get; set; } = default!; | |||||
public int NumPartitions { get; set; } = 1; | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
namespace TelpoKafkaConsole.WebApi.Model.Request | |||||
{ | |||||
public class TopicReq | |||||
{ | |||||
public string TopicName { get; set; } = default!; | |||||
public int NumPartitions { get; set; } = 1; | |||||
//public int RetentionTime { get; set; } = 1000 * 24 * 3600; | |||||
} | |||||
} |
@@ -0,0 +1,9 @@ | |||||
namespace TelpoKafkaConsole.WebApi.Model.Request | |||||
{ | |||||
public class UserReq | |||||
{ | |||||
public string Name { get; set; } = default!; | |||||
public string Password { get; set; } = default!; | |||||
} | |||||
} |
@@ -0,0 +1,65 @@ | |||||
using Microsoft.Extensions.Configuration; | |||||
using Serilog; | |||||
using TelpoKafkaConsole.Model; | |||||
using TelpoKafkaConsole.Service; | |||||
using TelpoKafkaConsole.WebApi.Configs; | |||||
using TelpoKafkaConsole.WebApi.Middleware; | |||||
namespace TelpoKafkaConsole.WebApi | |||||
{ | |||||
public class Program | |||||
{ | |||||
public static void Main(string[] args) | |||||
{ | |||||
//Ñ¡ÔñÅäÖÃÎļþappsetting.json | |||||
var config = new ConfigurationBuilder() | |||||
.SetBasePath(Directory.GetCurrentDirectory()) | |||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) | |||||
.Build(); | |||||
Log.Logger = new LoggerConfiguration() | |||||
.ReadFrom.Configuration(config).Enrich.WithThreadInfo() | |||||
.CreateLogger(); | |||||
var builder = WebApplication.CreateBuilder(args); | |||||
// Add services to the container. | |||||
builder.Services.AddControllers(); | |||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle | |||||
builder.Services.AddEndpointsApiExplorer(); | |||||
builder.Services.AddSwaggerGen(); | |||||
#region ÅäÖÃÐÅÏ¢ | |||||
builder.Services | |||||
.Configure<ServiceConfig>(builder.Configuration.GetSection("ServiceConfig")); | |||||
#endregion | |||||
builder.Services.AddSingleton<KafkaAdminService>(); | |||||
builder.Host.UseSerilog(); | |||||
var app = builder.Build(); | |||||
// Configure the HTTP request pipeline. | |||||
if (app.Environment.IsDevelopment()) | |||||
{ | |||||
app.UseSwagger(); | |||||
app.UseSwaggerUI(); | |||||
} | |||||
app.UseAuthorization(); | |||||
app.UseMiddleware<LoggingMiddleware>(); | |||||
app.MapControllers(); | |||||
app.Run(); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,40 @@ | |||||
{ | |||||
"profiles": { | |||||
"TelpoKafkaConsole.WebApi": { | |||||
"commandName": "Project", | |||||
"launchBrowser": true, | |||||
"launchUrl": "swagger", | |||||
"environmentVariables": { | |||||
"ASPNETCORE_ENVIRONMENT": "Development" | |||||
}, | |||||
"dotnetRunMessages": true, | |||||
"applicationUrl": "http://localhost:5046" | |||||
}, | |||||
"IIS Express": { | |||||
"commandName": "IISExpress", | |||||
"launchBrowser": true, | |||||
"launchUrl": "swagger", | |||||
"environmentVariables": { | |||||
"ASPNETCORE_ENVIRONMENT": "Development" | |||||
} | |||||
}, | |||||
"Docker": { | |||||
"commandName": "Docker", | |||||
"launchBrowser": true, | |||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", | |||||
"environmentVariables": { | |||||
"ASPNETCORE_URLS": "http://+:80" | |||||
}, | |||||
"publishAllPorts": true | |||||
} | |||||
}, | |||||
"$schema": "https://json.schemastore.org/launchsettings.json", | |||||
"iisSettings": { | |||||
"windowsAuthentication": false, | |||||
"anonymousAuthentication": true, | |||||
"iisExpress": { | |||||
"applicationUrl": "http://localhost:10584", | |||||
"sslPort": 0 | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,25 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<Nullable>enable</Nullable> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" /> | |||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> | |||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> | |||||
<PackageReference Include="Serilog.Expressions" Version="3.4.0" /> | |||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\TelpoKafkaConsole.Common\TelpoKafkaConsole.Common.csproj" /> | |||||
<ProjectReference Include="..\TelpoKafkaConsole.Service\TelpoKafkaConsole.Service.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,13 @@ | |||||
namespace TelpoKafkaConsole.WebApi | |||||
{ | |||||
public class WeatherForecast | |||||
{ | |||||
public DateTime Date { get; set; } | |||||
public int TemperatureC { get; set; } | |||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); | |||||
public string? Summary { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,14 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft.AspNetCore": "Warning" | |||||
} | |||||
}, | |||||
"ServiceConfig": { | |||||
"KafkaServerAddress": "k0.id.gdssjl.com:9094", | |||||
"KafkaServerPEMLocation": "C:\\Users\\vsoni\\source\\repos\\TelpoKafkaConsole\\pem\\ca-root-test.pem", | |||||
//"TopicPartitionsNum": 1 | |||||
} | |||||
} |
@@ -0,0 +1,118 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft.AspNetCore": "Warning" | |||||
} | |||||
}, | |||||
"Serilog": { | |||||
"Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Async", "Serilog.Sinks.Console", "Serilog.Expressions" ], | |||||
"MinimumLevel": { | |||||
"Default": "Verbose", | |||||
"Override": { | |||||
"Microsoft": "Warning", | |||||
"Microsoft.Hosting.Lifetime": "Information", | |||||
"HttpClient": "Information" | |||||
} | |||||
}, | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "Console", | |||||
"Args": { | |||||
"restrictedToMinimumLevel": "Verbose", | |||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}{Exception}", | |||||
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console" | |||||
} | |||||
}, | |||||
{ | |||||
"Name": "Logger", | |||||
"Args": { | |||||
"ConfigureLogger": { | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "File", | |||||
"Args": { | |||||
"RestrictedToMinimumLevel": "Information", | |||||
"RollingInterval": "Day", | |||||
"RollOnFileSizeLimit": "true", | |||||
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}{Exception}", | |||||
"Path": "/var/telpo_kafka_console/logs/infos/info.log", | |||||
"RetainedFileCountLimit": 10 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null | |||||
//"FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G | |||||
//"RollOnFileSizeLimit": true //超过文件大小后创建新的 | |||||
} | |||||
} | |||||
], | |||||
"Filter": [ | |||||
{ | |||||
"Name": "ByIncludingOnly", | |||||
"Args": { | |||||
"Expression": "@l = 'Information'" | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} | |||||
}, | |||||
{ | |||||
"Name": "Logger", | |||||
"Args": { | |||||
"ConfigureLogger": { | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "File", | |||||
"Args": { | |||||
"RestrictedToMinimumLevel": "Warning", | |||||
"RollingInterval": "Day", | |||||
"RollOnFileSizeLimit": "true", | |||||
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}", | |||||
"Path": "/var/telpo_kafka_console/logs/warnings/warn.log", | |||||
"RetainedFileCountLimit": 10 | |||||
} | |||||
} | |||||
], | |||||
"Filter": [ | |||||
{ | |||||
"Name": "ByIncludingOnly", | |||||
"Args": { | |||||
"Expression": "@l = 'Warning'" | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} | |||||
}, | |||||
{ | |||||
"Name": "Logger", | |||||
"Args": { | |||||
"ConfigureLogger": { | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "File", | |||||
"Args": { | |||||
"RestrictedToMinimumLevel": "Error", | |||||
"RollingInterval": "Day", | |||||
"RollOnFileSizeLimit": "true", | |||||
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}][{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}{Exception}", | |||||
"Path": "/var/telpo_kafka_console/logs/errors/error.log", | |||||
"RetainedFileCountLimit": 15 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null | |||||
//"FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G | |||||
//"RollOnFileSizeLimit": true //超过文件大小后创建新的 | |||||
} | |||||
} | |||||
], | |||||
"Filter": [ | |||||
{ | |||||
"Name": "ByIncludingOnly", | |||||
"Args": { | |||||
"Expression": "@l = 'Error'" | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft.AspNetCore": "Warning" | |||||
} | |||||
}, | |||||
"ServiceConfig": { | |||||
"KafkaServerAddress": "k0.id.gdssjl.com:9094", | |||||
"KafkaServerLocation": "pem/ca-root-test.pem" | |||||
} | |||||
} |
@@ -0,0 +1,43 @@ | |||||
| |||||
Microsoft Visual Studio Solution File, Format Version 12.00 | |||||
# Visual Studio Version 17 | |||||
VisualStudioVersion = 17.8.34330.188 | |||||
MinimumVisualStudioVersion = 10.0.40219.1 | |||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelpoKafkaConsole.Model", "TelpoKafkaConsole.Model\TelpoKafkaConsole.Model.csproj", "{5E43DF79-9F68-4108-B90D-C5175C680B5F}" | |||||
EndProject | |||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelpoKafkaConsole.WebApi", "TelpoKafkaConsole.WebApi\TelpoKafkaConsole.WebApi.csproj", "{D90BBA33-02B6-4C25-98CC-7E5881910085}" | |||||
EndProject | |||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelpoKafkaConsole.Common", "TelpoKafkaConsole.Common\TelpoKafkaConsole.Common.csproj", "{C6DCEB60-58AB-44A9-9C05-43B88623EE4A}" | |||||
EndProject | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelpoKafkaConsole.Service", "TelpoKafkaConsole.Service\TelpoKafkaConsole.Service.csproj", "{4B403911-CD11-47C7-9C01-E89C150880D5}" | |||||
EndProject | |||||
Global | |||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||||
Debug|Any CPU = Debug|Any CPU | |||||
Release|Any CPU = Release|Any CPU | |||||
EndGlobalSection | |||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | |||||
{5E43DF79-9F68-4108-B90D-C5175C680B5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{5E43DF79-9F68-4108-B90D-C5175C680B5F}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{5E43DF79-9F68-4108-B90D-C5175C680B5F}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{5E43DF79-9F68-4108-B90D-C5175C680B5F}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{D90BBA33-02B6-4C25-98CC-7E5881910085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{D90BBA33-02B6-4C25-98CC-7E5881910085}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{D90BBA33-02B6-4C25-98CC-7E5881910085}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{D90BBA33-02B6-4C25-98CC-7E5881910085}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{C6DCEB60-58AB-44A9-9C05-43B88623EE4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{C6DCEB60-58AB-44A9-9C05-43B88623EE4A}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{C6DCEB60-58AB-44A9-9C05-43B88623EE4A}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{C6DCEB60-58AB-44A9-9C05-43B88623EE4A}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{4B403911-CD11-47C7-9C01-E89C150880D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{4B403911-CD11-47C7-9C01-E89C150880D5}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{4B403911-CD11-47C7-9C01-E89C150880D5}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{4B403911-CD11-47C7-9C01-E89C150880D5}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
EndGlobalSection | |||||
GlobalSection(SolutionProperties) = preSolution | |||||
HideSolutionNode = FALSE | |||||
EndGlobalSection | |||||
GlobalSection(ExtensibilityGlobals) = postSolution | |||||
SolutionGuid = {431CD79F-C549-49D3-9969-1F3804DEAB64} | |||||
EndGlobalSection | |||||
EndGlobal |
@@ -0,0 +1,29 @@ | |||||
-----BEGIN CERTIFICATE----- | |||||
MIIE/zCCAuegAwIBAgIJAIgzt1mx6ZClMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV | |||||
BAMMC2thZmthLWFkbWluMB4XDTI0MDUwNjA2NDE1NloXDTM0MDUwNDA2NDE1Nlow | |||||
FjEUMBIGA1UEAwwLa2Fma2EtYWRtaW4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw | |||||
ggIKAoICAQDUCuuolDbpE6O8IvAK0YDvK+p96hjQ4bqD7UWKzie60q2OWHOfrcr3 | |||||
ra4FYiEtpZMSa52Y9K+VmXnP/Ol1kJHqcJPyIEkgqnC3nMDLNtO4BTqbXb7/eL7n | |||||
ln+Sn4v96Qc6+248yV87Of7NhW0Ou7Q7Qa7Y8Podvox6CANwQcVDAuAp1segHtsl | |||||
3w3Tmz4Ty9A919lGLfSRr1XQUGGt2DLrU4GajBEs8FJoWrCWnOBH251oSwQrisLw | |||||
+/PNRR8f8NE9uxE1/qmWwwtPg0a978WaUpqaLLUd7dU+TrbbptkAAcTdV0+JPrGJ | |||||
7JCleO9akmquRrSVQfKXKCq0ardQpndGEGhfAxoqPi5BuFyn0Sx2d+NsDIQKyW7U | |||||
e7lwLafrXFwnNmKPCfigE4IvPonz6HcJORRtJDzORbjSCB347MMlkmKY+ANB5fKc | |||||
UiqEaV+/NOw99/qNICEmBUa24aU3O5ASr5hPGNz+vAtGDGKokf0K9CLcPE/FKkvY | |||||
AYzM4O/kyqJhWgs8qoHITnv0BTb9psJEZp5VLJP6iPQhJXksnCQRX9+e0Rd+4d5B | |||||
d6JZ++KfmgxMy0YhTqz58IZe0yHolxTE6S0S5IULKkPk8GnIHPgw7/zfvsOM/eFs | |||||
676401tafc/Y7gC3H/GEPHo/T8/tuHLGz7GKwcytUFQdI2YgBl0kMwIDAQABo1Aw | |||||
TjAdBgNVHQ4EFgQULe3CAIvXjKYJOGF+zt1Lm87u6p0wHwYDVR0jBBgwFoAULe3C | |||||
AIvXjKYJOGF+zt1Lm87u6p0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC | |||||
AgEAfktPTwfBKTONVi5ILH6/70bvIxNRQljFdRP6Ktj76FlmkGXetYQMh0Fc+Wwd | |||||
meaU7z3JFV3BJOlQkh7UPKTbmXfIrk+Kvf14sSKYf4O47GatoSY/ifZrocs+owr2 | |||||
FP/6Oh3l70+vcUbLmU/328VHHumAuZjQ/199CnKgvgarEPRsekHDg0erzCu7fwVk | |||||
9ZIpRW5Kiyav+VBbg2SUVxaOHeGzy1eHxWHYeUpBpryLh4VDU1Va9LPR6nhmKPQI | |||||
GzJUgDjx9OK7Lhr6IHDWGbbOjKmHhL5nPtVOi9JPqTdJ0DXHhj94RlQdw3OxCa47 | |||||
ZhvCE5re8fJGtDpExQBbaPTXR0SGJ/q8td9bcJ+zHLcCALKXwHW7cf21YiPoiQ9J | |||||
I1DmYIwxEUqbngpCA6guDoL0HONVPOwG1SG/yLHy+jaKaMHUHTptbiRpPf2znxSm | |||||
UxUxEUEg+bhtoHd3Areky4RsuPQUFbDyYEDtduSjV+00vAicxcC/sLeZRWoKsPfb | |||||
Bp4xf7Jn5OO+jNcTfjoXnlVpyBI7zdwkbyMdF/xxcaj+POIgaljTQHoGxc31taJo | |||||
vwlNqD7oLHysuL06Qzos8nEbJMT/MJzDOSIRZ3FL3aTtdnP92j/G6hVoN5XnAAXS | |||||
O31Nvpo+6guwJmFg4SuBGwT8x2wlM35nHs84vR36ScNBvq8= | |||||
-----END CERTIFICATE----- |