Skip to content

Commit 1cf2fa9

Browse files
committed
v1.0.0
1 parent 2f4e453 commit 1cf2fa9

File tree

14 files changed

+469
-0
lines changed

14 files changed

+469
-0
lines changed

.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.vscode
2+
.git
3+
.cache
4+
**/obj/
5+
**/bin/
6+
appsettings.Development.json

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.vscode
2+
.vscode/settings.json
3+
obj
4+
bin
5+
out
6+
**/obj/
7+
**/bin/
8+
*.bak

BasicAuth/BasicAuthAttribute.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace Json2Kafka.BasicAuth
5+
{
6+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
7+
public class BasicAuthAttribute : TypeFilterAttribute
8+
{
9+
public BasicAuthAttribute(string realm = @"My Realm") : base(typeof(BasicAuthFilter))
10+
{
11+
Arguments = new object[] { realm };
12+
}
13+
}
14+
}

BasicAuth/BasicAuthFilter.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http.Headers;
4+
using System.Text;
5+
using Json2Kafka.Services;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.AspNetCore.Mvc.Filters;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
namespace Json2Kafka.BasicAuth
11+
{
12+
public class BasicAuthFilter : IAuthorizationFilter
13+
{
14+
private readonly string _realm;
15+
public BasicAuthFilter(string realm)
16+
{
17+
_realm = realm;
18+
if (string.IsNullOrWhiteSpace(_realm))
19+
{
20+
throw new ArgumentNullException(nameof(realm), @"Please provide a non-empty realm value.");
21+
}
22+
}
23+
public void OnAuthorization(AuthorizationFilterContext context)
24+
{
25+
try
26+
{
27+
string authHeader = context.HttpContext.Request.Headers["Authorization"];
28+
if (authHeader != null)
29+
{
30+
var authHeaderValue = AuthenticationHeaderValue.Parse(authHeader);
31+
if (authHeaderValue.Scheme.Equals(AuthenticationSchemes.Basic.ToString(), StringComparison.OrdinalIgnoreCase))
32+
{
33+
var credentials = Encoding.UTF8
34+
.GetString(Convert.FromBase64String(authHeaderValue.Parameter ?? string.Empty))
35+
.Split(':', 2);
36+
if (credentials.Length == 2)
37+
{
38+
if (IsAuthorized(context, credentials[0], credentials[1]))
39+
{
40+
return;
41+
}
42+
}
43+
}
44+
}
45+
46+
ReturnUnauthorizedResult(context);
47+
}
48+
catch (FormatException)
49+
{
50+
ReturnUnauthorizedResult(context);
51+
}
52+
}
53+
54+
public bool IsAuthorized(AuthorizationFilterContext context, string username, string password)
55+
{
56+
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
57+
return userService.IsValidUser(username, password);
58+
}
59+
60+
private void ReturnUnauthorizedResult(AuthorizationFilterContext context)
61+
{
62+
// Return 401 and a basic authentication challenge (causes browser to show login dialog)
63+
context.HttpContext.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{_realm}\"";
64+
context.Result = new UnauthorizedResult();
65+
}
66+
}
67+
}

Controllers/MsgController.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.Logging;
6+
using Json2Kafka.Services;
7+
using Json2Kafka.BasicAuth;
8+
9+
namespace Json2Kafka.Controllers
10+
{
11+
// Ajoute une route vide pour afficher l'aide de la méthode GET par défaut en cas d'affichage de la racine sur un navigateur
12+
[Route("")]
13+
[Route("api/[controller]")]
14+
[ApiController]
15+
public class MsgController : ControllerBase
16+
{
17+
18+
private ProducerService _Producer;
19+
20+
// Constructeur du controller de l'API, instanciation du producer Kafka
21+
public MsgController(IConfiguration Configuration) {
22+
_Producer = ProducerService.GetInstance(Configuration);
23+
}
24+
25+
// GET: / or /api/[controller]
26+
// Ne fait rien à part afficher une info sur la méthode POST à utiliser
27+
[HttpGet]
28+
public ActionResult<Object> GetMessage() => Content("Please use POST method on /api/msg - UTF8 JSON object mandatory");
29+
30+
31+
32+
// POST: /api/[controller]
33+
// Méthode asynchrone
34+
// le message vient du body de la request POST http.
35+
// le message doit être du JSON UTF8 : {"timestamp": 123,"info": "myData","myKey":"myValue"}
36+
// si message est différent d'un json la méthode retourne une erreur http "415 Unsupported Media Type"
37+
// si la requête est incorrecte la méthode retourne "400 Bad Request"
38+
// si BasicAuth est activé dans la configuration et que le login ou mot de passe sont incorects la méthode retourne une erreur "401 Unauthorized"
39+
// si la requête est correcte la réponse de kafka est envoyée
40+
// si kafka est injoignable le producer stock et attend le retour de Kafka (time out kafka par défaut 5 minutes / 300000ms)
41+
[HttpPost]
42+
[Route("api/[controller]")] // oblige à utiliser cette route : /api/msg
43+
[HttpGet("basic")]
44+
[BasicAuth("thisServiceNotKafka")]
45+
public async Task<ActionResult<Object>> PostMessage(Object message)
46+
{
47+
var deliveryResult = await _Producer.ProduceAwait(message); // send the message to kafka
48+
return Created("Kafka", deliveryResult); // return http 201 created to the requester with a DeliveryResult or ProduceException in case of problem
49+
}
50+
}
51+
}

Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# https://hub.docker.com/_/microsoft-dotnet-core
2+
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build
3+
WORKDIR /source
4+
5+
# copy csproj and restore as distinct layers
6+
COPY *.csproj .
7+
RUN dotnet restore -r linux-musl-x64
8+
9+
# copy everything else and build app
10+
COPY . .
11+
RUN dotnet publish -c release -o /app -r linux-musl-x64 --self-contained true /p:PublishTrimmed=true /p:PublishReadyToRun=true /p:PublishReadyToRunShowWarnings=true
12+
#RUN dotnet publish -c release -o /app -r linux-musl-x64 --self-contained false
13+
14+
# final stage/image
15+
FROM mcr.microsoft.com/dotnet/core/runtime-deps:3.1-alpine
16+
#FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine
17+
LABEL author="Lefebsy" \
18+
info="DotnetCore3.1 - Simple REST Json webservice feeding a kafka producer"
19+
WORKDIR /app
20+
COPY --from=build /app ./
21+
22+
EXPOSE 80/tcp
23+
#name of the csproj
24+
ENTRYPOINT ["./Json2Kafka"]

Json2Kafka.csproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Confluent.Kafka" Version="1.4.2" />
9+
</ItemGroup>
10+
11+
</Project>

Program.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.Extensions.Hosting;
3+
4+
namespace Json2Kafka
5+
{
6+
public class Program
7+
{
8+
public static void Main(string[] args)
9+
{
10+
CreateHostBuilder(args).Build().Run();
11+
}
12+
13+
14+
15+
public static IHostBuilder CreateHostBuilder(string[] args) =>
16+
Host.CreateDefaultBuilder(args)
17+
.ConfigureWebHostDefaults(webBuilder =>
18+
{
19+
webBuilder.UseStartup<Startup>();
20+
});
21+
22+
}
23+
24+
25+
}

Properties/launchSettings.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"iisSettings": {
4+
"windowsAuthentication": false,
5+
"anonymousAuthentication": true,
6+
"iisExpress": {
7+
"applicationUrl": "http://localhost:55302",
8+
"sslPort": 44304
9+
}
10+
},
11+
"profiles": {
12+
"IIS Express": {
13+
"commandName": "IISExpress",
14+
"launchBrowser": true,
15+
"launchUrl": "message",
16+
"environmentVariables": {
17+
"ASPNETCORE_ENVIRONMENT": "Development"
18+
}
19+
},
20+
"Json2Kafka": {
21+
"commandName": "Project",
22+
"launchBrowser": true,
23+
"launchUrl": "message",
24+
"applicationUrl": "https://localhost:5001;http://localhost:5000",
25+
"environmentVariables": {
26+
"ASPNETCORE_ENVIRONMENT": "Development"
27+
}
28+
}
29+
}
30+
}

Services/ProducerService.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Confluent.Kafka;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Json2Kafka.Services
8+
{
9+
class ProducerService
10+
{
11+
private static IProducer<Null, string> _Producer; // singleton du producer
12+
private static Boolean _SetupDone = false; //pour ne pas rejouer x fois la config
13+
private static ProducerConfig _ProducerConfig; // configuration du producer (depuis les variables d'environnement)
14+
private static string _Topic; // (depuis les variables d'environnement)
15+
private static ILogger _logger;
16+
17+
18+
19+
// Constructeur du singleton et de la gestion de l'obtention de son instance (paramètres interdits dans le constructeur, donc Setup depuis une méthode séparée)
20+
private static readonly ProducerService _ProducerServiceInstance = new ProducerService();
21+
public static ProducerService GetInstance(IConfiguration Configuration) {
22+
if (!_SetupDone)
23+
{
24+
// création de la configuration Kafka à partir des infos récupèrées
25+
// dans appsettings.json qui peuvent être surchargées par
26+
// les variables d'environements pour tourner en container
27+
28+
// configs obligatoires
29+
_Topic = Configuration["Topic"];
30+
_ProducerConfig = new ProducerConfig {
31+
ClientId = Configuration["ClientId"],
32+
CompressionType = CompressionType.Snappy, //Compression active par défaut, faible charge CPU et 0.5x de traffic et empreinte stockage sur kafka
33+
BootstrapServers = Configuration["BootstrapServers"],
34+
EnableSslCertificateVerification = bool.Parse( Configuration["EnableSslCertificateVerification"] ),
35+
EnableIdempotence = bool.Parse( Configuration["EnableIdempotence"] )
36+
};
37+
// configs optionnelles
38+
if ("" != Configuration["SaslPassword"]) _ProducerConfig.SaslPassword = Configuration["SaslPassword"];
39+
if ("" != Configuration["SaslUsername"]) _ProducerConfig.SaslUsername = Configuration["SaslUsername"];
40+
if ("" != Configuration["SslCaLocation"]) _ProducerConfig.SslCaLocation = Configuration["SslCaLocation"];
41+
if ("" != Configuration["SaslMechanism"]) _ProducerConfig.SaslMechanism = (SaslMechanism)int.Parse(Configuration["SaslMechanism"]);
42+
if ("" != Configuration["SecurityProtocol"]) _ProducerConfig.SecurityProtocol = (SecurityProtocol)int.Parse(Configuration["SecurityProtocol"]);
43+
44+
_Producer = new ProducerBuilder<Null, string>(_ProducerConfig).Build();
45+
_logger = LoggerFactory.Create(builder => builder.AddConsole().AddConfiguration(Configuration.GetSection("Logging"))).CreateLogger("Json2Kafka.services.ProducerService");
46+
_SetupDone = true;
47+
}
48+
49+
return _ProducerServiceInstance;
50+
}
51+
52+
// méthode principale pour écrire dans Kafka
53+
public async Task<object> ProduceAwait(object msg)
54+
{
55+
try
56+
{
57+
_logger.LogInformation($"Sending message to producer [{msg}]");
58+
return await _Producer.ProduceAsync(_Topic, new Message<Null, string> { Value=msg.ToString() });
59+
}
60+
catch (ProduceException<Null, string> e)
61+
{
62+
//Console.WriteLine($"Delivery failed: {e.Error.Reason}");
63+
_logger.LogError($"Delivery failed: {e.Error.Reason}");
64+
return e;
65+
}
66+
}
67+
// méthode en cours de test
68+
private void Produce(object msg)
69+
{
70+
Action<DeliveryReport<Null, string>> handler = r =>
71+
Console.WriteLine(!r.Error.IsError
72+
? $"Delivered message to {r.TopicPartitionOffset}"
73+
: $"Delivery Error: {r.Error.Reason}");
74+
75+
_Producer.Produce(_Topic, new Message<Null, string> { Value = msg.ToString() }, handler);
76+
77+
// wait for up to 10 seconds for any inflight messages to be delivered.
78+
_Producer.Flush(TimeSpan.FromSeconds(10));
79+
80+
}
81+
// méthode en cours de test
82+
private async Task ProduceAsync(object msg)
83+
{
84+
try
85+
{
86+
var dr = await _Producer.ProduceAsync(_Topic, new Message<Null, string> { Value=msg.ToString() });
87+
Console.WriteLine($"Delivered '{dr.Value}' to '{dr.TopicPartitionOffset}'");
88+
}
89+
catch (ProduceException<Null, string> e)
90+
{
91+
Console.WriteLine($"Delivery failed: {e.Error.Reason}");
92+
}
93+
}
94+
95+
}
96+
97+
}

0 commit comments

Comments
 (0)