diff --git a/.gitignore b/.gitignore index c7184451..e4ac8c90 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,4 @@ yarn-error.log* *.csproj.user # Go bin files -__debug_bin \ No newline at end of file +__debug_bin diff --git a/cppseminar/docker-compose.yml b/cppseminar/docker-compose.yml index f84685fd..681960a9 100644 --- a/cppseminar/docker-compose.yml +++ b/cppseminar/docker-compose.yml @@ -38,6 +38,10 @@ services: - ./presentation/Model:/src/Model - ./presentation/Pages:/src/Pages - ./presentation/Services:/src/Services + - ./presentation/wwwroot:/src/wwwroot + - ./presentation/Hubs:/src/Hubs + - ./presentation/Filters:/src/Filters + - ./presentation/appsettings.json:/src/appsettings.json networks: - sharednet ports: @@ -57,6 +61,7 @@ services: restart: always depends_on: - rabbitmq.local + - monitoringservice.local userservice.local: build: @@ -261,7 +266,46 @@ services: - ./envoy/envoy.yaml:/etc/envoy/envoy.yaml networks: - sharednet + + redis.local: + image: redis:7.2.0 + build: + target: dev + networks: + - sharednet + ports: + - 6379:6379 + restart: always + monitoringservice.local: + build: + context: ./monitoringservice + target: dev + volumes: + - ./monitoringservice/Program.cs:/src/Program.cs + - ./monitoringservice/Startup.cs:/src/Startup.cs + - ./monitoringservice/Model:/src/Model + - ./monitoringservice/Services:/src/Services + - ./monitoringservice/Controllers:/src/Controllers + - ./monitoringservice/monitoringservice.csproj:/src/monitoringservice.csproj + - ./monitoringservice/appsettings.json:/src/appsettings.json + networks: + - sharednet + ports: + - 8085:80 + environment: + DOTNET_USE_POLLING_FILE_WATCHER: 1 + DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM: 1 + DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER: 1 + DOTNET_WATCH_SUPPRESS_BROWSER_REFRESH: 1 + ASPNETCORE_ENVIRONMENT: Development + API_GATEWAY: http://gateway.local:5000/ + LOG_PRETTY: 1 + restart: always + depends_on: + redis.local: + condition: service_started + networks: sharednet: diff --git a/cppseminar/envoy/envoy.yaml b/cppseminar/envoy/envoy.yaml index 9f078355..19ec69cf 100644 --- a/cppseminar/envoy/envoy.yaml +++ b/cppseminar/envoy/envoy.yaml @@ -45,6 +45,10 @@ static_resources: path_separated_prefix: /test route: cluster: test-service + - match: + path_separated_prefix: /monitoring + route: + cluster: monitoring-service http_filters: - name: envoy.filters.http.router typed_config: @@ -88,4 +92,15 @@ static_resources: address: socket_address: address: submissions.local - port_value: 80 \ No newline at end of file + port_value: 80 + - name: monitoring-service + type: strict_dns + load_assignment: + cluster_name: monitoring-service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: monitoringservice.local + port_value: 80 diff --git a/cppseminar/monitoringservice/Controllers/MonitoringController.cs b/cppseminar/monitoringservice/Controllers/MonitoringController.cs new file mode 100644 index 00000000..b216c80c --- /dev/null +++ b/cppseminar/monitoringservice/Controllers/MonitoringController.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using monitoringservice.Model; +using monitoringservice.Services; + +namespace monitoringservice.Controllers; + +[Route("monitoring")] +[ApiController] +public class MonitoringController : ControllerBase +{ + private readonly ILogger _logger; + private readonly StorageService _service; + + public MonitoringController(ILogger logger, StorageService service) + { + _logger = logger; + _service = service; + } + + [HttpGet("get/recents")] + public async Task>> OnGetAsync() + { + try + { + return await _service.getConnectionLogsAsync(); + } + catch (Exception e) + { + _logger.LogError("Exception occured while retrieving all ConnectionLog records. " + e); + return StatusCode(500); + } + } + + [HttpPost("post/log")] + public async Task LogConnection([FromBody] ConnectionLog connectionLog) + { + if (connectionLog.UserEmail == null || connectionLog.Timestamp == null) + { + return BadRequest(); + } + else + { + try + { + await _service.setConnectionlogAsync(connectionLog); + return Ok(); + } + catch (Exception e) + { + _logger.LogError("Exception occured while logging user connection. " + e); + return StatusCode(500); + } + } + } +} diff --git a/cppseminar/monitoringservice/Dockerfile b/cppseminar/monitoringservice/Dockerfile new file mode 100644 index 00000000..92879da3 --- /dev/null +++ b/cppseminar/monitoringservice/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["monitoringservice.csproj", "./monitoringservice.csproj"] +RUN dotnet restore "monitoringservice.csproj" +COPY . . +RUN dotnet build "monitoringservice.csproj" -c Release -o /app/build + +FROM build as dev +CMD dotnet watch run --urls=http://0.0.0.0:80 + +FROM build AS publish +RUN dotnet publish "monitoringservice.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "monitoringservice.dll"] diff --git a/cppseminar/monitoringservice/Model/ConnectionLog.cs b/cppseminar/monitoringservice/Model/ConnectionLog.cs new file mode 100644 index 00000000..310e9e8b --- /dev/null +++ b/cppseminar/monitoringservice/Model/ConnectionLog.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + + +namespace monitoringservice.Model +{ + public class ConnectionLog + { + public ConnectionLog(string email, DateTime timestamp){ + UserEmail = email; + Timestamp = timestamp; + } + public ConnectionLog(){ + } + public string UserEmail { get; set; } + public DateTime Timestamp { get; set; } + } +} diff --git a/cppseminar/monitoringservice/Program.cs b/cppseminar/monitoringservice/Program.cs new file mode 100644 index 00000000..739b55cc --- /dev/null +++ b/cppseminar/monitoringservice/Program.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Compact; +using monitoringservice.Model; +using monitoringservice.Services; + +namespace monitoringservice; + +public class Program +{ + public static void Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console(new RenderedCompactJsonFormatter()) + .CreateLogger(); + + try + { + Log.Information("Starting web host"); + CreateHostBuilder(args).Build().Run(); + } + catch (Exception e) + { + Log.Fatal("Host terminated unexpectedly. {e}", e); + } + finally + { + Log.CloseAndFlush(); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + if (Environment.GetEnvironmentVariable("LOG_PRETTY") == "1") + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } + return Host.CreateDefaultBuilder(args) + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + diff --git a/cppseminar/monitoringservice/Properties/launchSettings.json b/cppseminar/monitoringservice/Properties/launchSettings.json new file mode 100644 index 00000000..06f09cec --- /dev/null +++ b/cppseminar/monitoringservice/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9271", + "sslPort": 44364 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5224", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7026;http://localhost:5224", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/cppseminar/monitoringservice/Services/StorageService.cs b/cppseminar/monitoringservice/Services/StorageService.cs new file mode 100644 index 00000000..053877ed --- /dev/null +++ b/cppseminar/monitoringservice/Services/StorageService.cs @@ -0,0 +1,47 @@ +using StackExchange.Redis; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using monitoringservice.Model; + +namespace monitoringservice.Services; +public class StorageService +{ + private readonly IDatabase _db; + private readonly IServer _server; + private readonly ILogger _logger; + + public StorageService(ILogger logger) + { + ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("redis.local"); + _db = redis.GetDatabase(); + _server = redis.GetServer("redis.local", 6379); + _logger = logger; + } + + public async Task setConnectionlogAsync(ConnectionLog connectionLog) + { + await _db.StringSetAsync(connectionLog.UserEmail, connectionLog.Timestamp.ToString()); + } + + public async Task getValueAsync(string Key) + { + string value = await _db.StringGetAsync(Key); + return value; + } + +/* This works only when key-value pairs of string-string are in redis */ + + public async Task> getConnectionLogsAsync() + { + List connectionLogsList = new List(); + + var emails = _server.Keys(); + foreach (var email in emails) + { + var timestamp = await _db.StringGetAsync(email); + connectionLogsList.Add(new ConnectionLog(email, DateTime.Parse(timestamp))); + } + return connectionLogsList; + } +} \ No newline at end of file diff --git a/cppseminar/monitoringservice/Startup.cs b/cppseminar/monitoringservice/Startup.cs new file mode 100644 index 00000000..dd64d144 --- /dev/null +++ b/cppseminar/monitoringservice/Startup.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using monitoringservice.Services; +using monitoringservice.Model; + +namespace monitoringservice; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSingleton(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} diff --git a/cppseminar/monitoringservice/appsettings.json b/cppseminar/monitoringservice/appsettings.json new file mode 100644 index 00000000..5784615a --- /dev/null +++ b/cppseminar/monitoringservice/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Trace", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/cppseminar/monitoringservice/monitoringservice.csproj b/cppseminar/monitoringservice/monitoringservice.csproj new file mode 100644 index 00000000..992fe1ff --- /dev/null +++ b/cppseminar/monitoringservice/monitoringservice.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/cppseminar/monitoringservice/monitoringservice.sln b/cppseminar/monitoringservice/monitoringservice.sln new file mode 100644 index 00000000..b91aac89 --- /dev/null +++ b/cppseminar/monitoringservice/monitoringservice.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "monitoringservice", "monitoringservice.csproj", "{6171F750-7572-4231-BBCB-E02126110C75}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6171F750-7572-4231-BBCB-E02126110C75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6171F750-7572-4231-BBCB-E02126110C75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6171F750-7572-4231-BBCB-E02126110C75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6171F750-7572-4231-BBCB-E02126110C75}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {872C4702-FDF5-4978-9622-6EB9E0F6CC23} + EndGlobalSection +EndGlobal diff --git a/cppseminar/presentation/Filters/GenericIPFilter.cs b/cppseminar/presentation/Filters/GenericIPFilter.cs new file mode 100644 index 00000000..b4265c3c --- /dev/null +++ b/cppseminar/presentation/Filters/GenericIPFilter.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Net; +using presentation.Services; +namespace presentation.Filters; + +public class GenericIPFilter +{ + private readonly byte[] _allowedLowerBytes; + private readonly byte[] _allowedUpperBytes; + + public GenericIPFilter(string allowedIPLowerStr, string allowedIPUpperStr) + { + IPAddress allowedIPLower; + IPAddress.TryParse(allowedIPLowerStr, out allowedIPLower); + _allowedLowerBytes = allowedIPLower.GetAddressBytes(); + + IPAddress allowedIPUpper; + IPAddress.TryParse(allowedIPUpperStr, out allowedIPUpper); + _allowedUpperBytes = allowedIPUpper.GetAddressBytes(); + + // Check if lower bound is indeed lower + for (int i = 0; i < _allowedLowerBytes.Length; i++) + { + if (_allowedLowerBytes[i] > _allowedUpperBytes[i]) + { + throw new ArgumentException("Invalid range of IP addresses."); + } + } + } + + public bool AddressWithinRange(IPAddress clientAddress) + { + byte[] clientAddressBytes = clientAddress.GetAddressBytes(); + for (int i = 0; i < _allowedLowerBytes.Length; i++) + { + if (clientAddressBytes[i] < _allowedLowerBytes[i] || clientAddressBytes[i] > _allowedUpperBytes[i]) + { + return false; + } + } + return true; + } + public IPAddress GetRemoteIP(string remoteIpAddressStr, string forwardedForHeaderStr) + { + IPAddress remoteIPAddress; + if(!string.IsNullOrEmpty(forwardedForHeaderStr)) + { + if(IPAddress.TryParse(forwardedForHeaderStr, out remoteIPAddress)) // check if the header is a valid IP address + { + return remoteIPAddress; + } + } + // if X-Forwarded-For is empty or not a valid IP address continue with remoteIPAddressStr + if (IPAddress.TryParse(remoteIpAddressStr, out remoteIPAddress)) + { + return remoteIPAddress; + } + return null; + } +} \ No newline at end of file diff --git a/cppseminar/presentation/Filters/IPHubFilter.cs b/cppseminar/presentation/Filters/IPHubFilter.cs new file mode 100644 index 00000000..87e4290b --- /dev/null +++ b/cppseminar/presentation/Filters/IPHubFilter.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.SignalR; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +namespace presentation.Filters; + +public class IPHubFilter : GenericIPFilter, IHubFilter +{ + public IPHubFilter(string IPLower, string IPUpper) : base(IPLower, IPUpper) + { + } + + public Task OnConnectedAsync(HubLifetimeContext context, Func next) + { + var remoteIpAddressStr = context.Context.GetHttpContext()?.Connection.RemoteIpAddress.ToString(); + var forwardedForHeaderStr = context.Context.GetHttpContext()?.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + IPAddress remoteIPAddress = GetRemoteIP(remoteIpAddressStr, forwardedForHeaderStr); + + if (remoteIPAddress == null || !AddressWithinRange(remoteIPAddress)) + { + System.Console.WriteLine($"IP Address failed to parse or not allowed in OnConnected. {remoteIpAddressStr} {forwardedForHeaderStr}"); + context.Context.Abort(); + } + return next(context); + } + + public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, Func> next) + { + var remoteIpAddressStr = invocationContext.Context.GetHttpContext()?.Connection.RemoteIpAddress.ToString(); + var forwardedForHeaderStr = invocationContext.Context.GetHttpContext()?.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + IPAddress remoteIPAddress = GetRemoteIP(remoteIpAddressStr, forwardedForHeaderStr); + if (remoteIPAddress == null || !AddressWithinRange(remoteIPAddress)) + { + System.Console.WriteLine($"IP Address failed to parse or not allowed in invoke method. {remoteIpAddressStr} {forwardedForHeaderStr}"); + invocationContext.Context.Abort(); + return null; + } + return await next(invocationContext); + } +} \ No newline at end of file diff --git a/cppseminar/presentation/Filters/PageIPFilter.cs b/cppseminar/presentation/Filters/PageIPFilter.cs new file mode 100644 index 00000000..a045e816 --- /dev/null +++ b/cppseminar/presentation/Filters/PageIPFilter.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Net; +using presentation.Services; +using System.Linq; +namespace presentation.Filters; + +public class PageIPFilter : GenericIPFilter, IResourceFilter +{ + public PageIPFilter(string IPLower, string IPUpper) : base(IPLower, IPUpper) + { + } + + public void OnResourceExecuting(ResourceExecutingContext context) + { + var remoteIpAddressStr = context.HttpContext.Connection.RemoteIpAddress.ToString(); + var forwardedForHeaderStr = context.HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + IPAddress clientIPAddress = GetRemoteIP(remoteIpAddressStr, forwardedForHeaderStr); + + if (clientIPAddress == null || !AddressWithinRange(clientIPAddress)) + { + System.Console.WriteLine($"IP Address failed to parse or not allowed on this page. {remoteIpAddressStr} {forwardedForHeaderStr}"); + context.Result = new ContentResult + { + StatusCode = (int)HttpStatusCode.Forbidden, + Content = "Access denied." + }; + } + } + + public void OnResourceExecuted(ResourceExecutedContext context) + { + } +} + + diff --git a/cppseminar/presentation/Hubs/MonitoringHub.cs b/cppseminar/presentation/Hubs/MonitoringHub.cs new file mode 100644 index 00000000..5a8099cd --- /dev/null +++ b/cppseminar/presentation/Hubs/MonitoringHub.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.AspNetCore.SignalR; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using presentation.Services; +using presentation.Model; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using System.Text.Json; +using System.Collections.Generic; +using presentation.Filters; + +namespace presentation.Hubs +{ + public class MonitoringHub: Hub + { + private MonitoringService _monitoringService; + public MonitoringHub(MonitoringService monitoringService){ + _monitoringService = monitoringService; + } + public override async Task OnConnectedAsync(){ + + } + + [Authorize] + public async Task LogConnection() + { + string userEmail = Context.User.FindFirst(claim => claim.Type == ClaimTypes.Email).Value; + var connectionLog = new ConnectionLog(userEmail, DateTime.UtcNow); + await _monitoringService.LogConnectionAsync(connectionLog); + } + + [Authorize(Policy="Administrator")] + public async Task GetConnectedUsersRecentAsync() + { + var responseData = await _monitoringService.GetConnectedUsersRecentAsync(); + if (responseData == null) + { + await Clients.Caller.SendAsync("ErrorGettingUsers", "Monitoring service responded with null"); + } + else + { + var connectionLogTimeDiffList = new List(); + foreach (var connectionLog in responseData) + { + connectionLogTimeDiffList.Add(new ConnectionLogTimeDiff(connectionLog)); + } + connectionLogTimeDiffList.Sort((log1, log2) => string.Compare(log1.UserEmail, log2.UserEmail)); + await Clients.Caller.SendAsync("ReceiveUsers", JsonSerializer.Serialize(connectionLogTimeDiffList)); + } + } + } +} \ No newline at end of file diff --git a/cppseminar/presentation/Model/ConnectionLog.cs b/cppseminar/presentation/Model/ConnectionLog.cs new file mode 100644 index 00000000..e24d8bc5 --- /dev/null +++ b/cppseminar/presentation/Model/ConnectionLog.cs @@ -0,0 +1,18 @@ +using System; +using System.Text.Json.Serialization; + + +namespace presentation.Model; + +public class ConnectionLog +{ + public string UserEmail { get; set; } + public DateTime Timestamp { get; set; } + + public ConnectionLog(string email, DateTime timestamp) + { + UserEmail = email; + Timestamp = timestamp; + } + +} \ No newline at end of file diff --git a/cppseminar/presentation/Model/ConnectionLogTimeDiff.cs b/cppseminar/presentation/Model/ConnectionLogTimeDiff.cs new file mode 100644 index 00000000..8304b0d4 --- /dev/null +++ b/cppseminar/presentation/Model/ConnectionLogTimeDiff.cs @@ -0,0 +1,15 @@ +using System; + +namespace presentation.Model; + +public class ConnectionLogTimeDiff +{ + public string UserEmail { get; set; } + public double Seconds { get; set; } + + public ConnectionLogTimeDiff(ConnectionLog connectionLog) + { + UserEmail = connectionLog.UserEmail; + Seconds = (double)(DateTime.UtcNow - connectionLog.Timestamp).TotalSeconds; + } +} \ No newline at end of file diff --git a/cppseminar/presentation/Pages/Admin/Monitoring/Index.cshtml b/cppseminar/presentation/Pages/Admin/Monitoring/Index.cshtml new file mode 100644 index 00000000..eb13b1c7 --- /dev/null +++ b/cppseminar/presentation/Pages/Admin/Monitoring/Index.cshtml @@ -0,0 +1,25 @@ +@page +@using System.Security.Claims +@model presentation.Pages.Monitoring.IndexModel +@{ +} +
+ +
+ + +
+ All connected users + + + + + +
+ User email + + Last message +
+
+ + \ No newline at end of file diff --git a/cppseminar/presentation/Pages/Admin/Monitoring/Index.cshtml.cs b/cppseminar/presentation/Pages/Admin/Monitoring/Index.cshtml.cs new file mode 100644 index 00000000..9ec8c6d3 --- /dev/null +++ b/cppseminar/presentation/Pages/Admin/Monitoring/Index.cshtml.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using presentation.Model; +using presentation.Services; +using System.Net.Mime; +using Microsoft.AspNetCore.Mvc; + + +namespace presentation.Pages.Monitoring +{ + public class IndexModel : PageModel + { + + } +} diff --git a/cppseminar/presentation/Pages/Admin/Submissions/List.cshtml.cs b/cppseminar/presentation/Pages/Admin/Submissions/List.cshtml.cs index 90b83cf8..0c5691f6 100644 --- a/cppseminar/presentation/Pages/Admin/Submissions/List.cshtml.cs +++ b/cppseminar/presentation/Pages/Admin/Submissions/List.cshtml.cs @@ -21,7 +21,7 @@ public ListModel(ILogger logger, SubmissionService submissionService, } public async Task OnGetAsync() - { + { try { if (SelectedUser == "") diff --git a/cppseminar/presentation/Pages/Admin/TestCase/Edit.cshtml.cs b/cppseminar/presentation/Pages/Admin/TestCase/Edit.cshtml.cs new file mode 100644 index 00000000..e983049c --- /dev/null +++ b/cppseminar/presentation/Pages/Admin/TestCase/Edit.cshtml.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using presentation.Model; +using presentation.Services; + +namespace presentation.Pages.Admin.TestCase +{ + public class EditModel : PageModel + { + private TestCaseService _testCaseService; + + [BindProperty] + public TestCaseRest TestCase { get; set; } + + private string testCaseId; + + public EditModel(TestCaseService testCaseService) + { + _testCaseService = testCaseService; + } + public async Task OnGetAsync([FromQuery] string caseId) + { + TestCase = await _testCaseService.GetById(caseId); + if (TestCase == null) // Error + { + ModelState.AddModelError(string.Empty, "Failed loading data"); + } + } + public async Task OnPostAsync([FromQuery] string caseId) + { + if (!ModelState.IsValid) + { + ModelState.AddModelError(string.Empty, "Model is not valid"); + return Page(); + } + try + { + // http post resets fields not in form and it ignores bindnever attribute, thats why we call updateTest with caseId from query await _testCaseService.UpdateTest(caseId, TestCase); + + return RedirectToPage("/Admin/TestCase/Index"); + } + catch (Exception) + { + ModelState.AddModelError(string.Empty, "Failed updating test case"); + return Page(); + } + } + } +} diff --git a/cppseminar/presentation/Pages/Admin/Users/Index.cshtml b/cppseminar/presentation/Pages/Admin/Users/Index.cshtml index c46f51ca..f9a2f16f 100644 --- a/cppseminar/presentation/Pages/Admin/Users/Index.cshtml +++ b/cppseminar/presentation/Pages/Admin/Users/Index.cshtml @@ -14,7 +14,7 @@ @foreach (var user in Model.AllUsers) { - @user + @user } diff --git a/cppseminar/presentation/Pages/Connection/Index.cshtml b/cppseminar/presentation/Pages/Connection/Index.cshtml new file mode 100644 index 00000000..7ad3db34 --- /dev/null +++ b/cppseminar/presentation/Pages/Connection/Index.cshtml @@ -0,0 +1,22 @@ +@page +@using System.Security.Claims +@model presentation.Pages.Connection.IndexModel +@{ +} + + +@section JavaScript +{ + + @if(User.Identity.IsAuthenticated) + { + + } +} + +
+

Connection checker

+

Connection status: Not started

+

+
+
\ No newline at end of file diff --git a/cppseminar/presentation/Pages/Connection/Index.cshtml.cs b/cppseminar/presentation/Pages/Connection/Index.cshtml.cs new file mode 100644 index 00000000..90ea5615 --- /dev/null +++ b/cppseminar/presentation/Pages/Connection/Index.cshtml.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using presentation.Filters; +using Microsoft.AspNetCore.Mvc; + +namespace presentation.Pages.Connection +{ + [ServiceFilter(typeof(PageIPFilter))] + public class IndexModel : PageModel + { + + private ILogger _logger; + public bool IsAdmin = false; + + public IndexModel(ILogger logger) + { + _logger = logger; + } + } +} diff --git a/cppseminar/presentation/Pages/Shared/_Layout.cshtml b/cppseminar/presentation/Pages/Shared/_Layout.cshtml index 4001bbc3..d9a847bd 100644 --- a/cppseminar/presentation/Pages/Shared/_Layout.cshtml +++ b/cppseminar/presentation/Pages/Shared/_Layout.cshtml @@ -31,10 +31,12 @@ + @if (User.IsAdmin()) { + } @@ -227,5 +229,6 @@ editor.setReadOnly(true) }) + @RenderSection("JavaScript", required: false) diff --git a/cppseminar/presentation/Pages/Shared/_UserPanel.cshtml b/cppseminar/presentation/Pages/Shared/_UserPanel.cshtml index c90aab54..93ca0be2 100644 --- a/cppseminar/presentation/Pages/Shared/_UserPanel.cshtml +++ b/cppseminar/presentation/Pages/Shared/_UserPanel.cshtml @@ -1,4 +1,4 @@ -@using System.Security.Claims; +@using System.Security.Claims; @if(Context.User.Identity.IsAuthenticated) { diff --git a/cppseminar/presentation/Pages/Submissions/Detail.cshtml b/cppseminar/presentation/Pages/Submissions/Detail.cshtml index ae2244a2..04bfe629 100644 --- a/cppseminar/presentation/Pages/Submissions/Detail.cshtml +++ b/cppseminar/presentation/Pages/Submissions/Detail.cshtml @@ -59,5 +59,5 @@ else { @await Component.InvokeAsync( "TestList", - new { userEmail = User.GetEmail(), submissionId = Model.MySubmission.Id }) + new { userEmail = User.GetEmail(), submissionId = Model.MySubmission.Id}) } diff --git a/cppseminar/presentation/Services/MonitoringService.cs b/cppseminar/presentation/Services/MonitoringService.cs new file mode 100644 index 00000000..b47510b4 --- /dev/null +++ b/cppseminar/presentation/Services/MonitoringService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Web; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using presentation.Model; + +namespace presentation.Services +{ + public class MonitoringService + { + private readonly HttpClient _client = new HttpClient(); + private readonly ILogger _logger = null; + + public MonitoringService(ILogger logger, IConfiguration config) + { + _client.BaseAddress = new Uri(config["API_GATEWAY"]); + _client.DefaultRequestHeaders.Accept.Clear(); + _client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + _logger = logger; + } + + public async Task LogConnectionAsync(ConnectionLog connectionLog) { + try + { + var response = await _client.PostAsJsonAsync("monitoring/post/log", connectionLog); + if (response.StatusCode != HttpStatusCode.OK) + { + _logger.LogError("LogConnectionAsync returned " + response.StatusCode); + } + } + catch (Exception e) + { + _logger.LogError("LogConnectionAsync failed. " + e); + } + } + + public async Task> GetConnectedUsersRecentAsync() + { + try + { + var response = await _client.GetAsync("monitoring/get/recents"); // monitoring/get/all + if (response.StatusCode != HttpStatusCode.OK) + { + _logger.LogError("GetConnectedUsersRecentAsync returned " + response.StatusCode); + return null; + } + else + { + return await response.Content.ReadAsAsync>(); + } + } + catch (Exception e) + { + _logger.LogError("GetConnectedUsersRecentAsync failed. " + e); + return null; + } + } + } + +} \ No newline at end of file diff --git a/cppseminar/presentation/Startup.cs b/cppseminar/presentation/Startup.cs index a53e9518..d2ccd890 100644 --- a/cppseminar/presentation/Startup.cs +++ b/cppseminar/presentation/Startup.cs @@ -13,6 +13,12 @@ using System; using presentation.Services; +using presentation.Hubs; +using presentation.Filters; +using Microsoft.AspNetCore.SignalR; +using System.Collections.Generic; +using System.Linq; + namespace presentation { @@ -30,6 +36,17 @@ public void ConfigureServices(IServiceCollection services) services.AddRazorPages(opts => { opts.Conventions.AuthorizeFolder("/Admin", "Administrator"); }); + // modified this + + IConfigurationSection allowedIpAddresses = Configuration.GetSection("ALLOWED_IP_RANGE"); + System.Console.WriteLine(allowedIpAddresses["Lower"]); + services.AddSignalR(hubOptions => { + hubOptions.AddFilter(new IPHubFilter(allowedIpAddresses["Lower"], allowedIpAddresses["Upper"])); + }); + services.AddSingleton(new PageIPFilter(allowedIpAddresses["Lower"], allowedIpAddresses["Upper"])); + + services.AddControllers(); + services.Configure(options => { options.AppendTrailingSlash = true; @@ -48,6 +65,8 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + // + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -82,25 +101,36 @@ public void ConfigureServices(IServiceCollection services) options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser() .Build(); options.AddPolicy("Administrator", policy => policy.RequireClaim("isAdmin", "true")); + options.AddPolicy("Student", policy => policy.RequireClaim("isStudent", "true")); }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + System.Console.WriteLine(env.ContentRootPath); app.UseForwardedHeaders(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } + + app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { + endpoints.MapControllers(); endpoints.MapRazorPages(); + endpoints.MapHub("/monitor"); + // endpoints.MapGet("/testingeverything", async context => + // { + // System.Console.WriteLine("Hello, World!"); + // }); }); + app.UseStaticFiles(); } } } diff --git a/cppseminar/presentation/appsettings.json b/cppseminar/presentation/appsettings.json index 6c4b5612..0312bbff 100644 --- a/cppseminar/presentation/appsettings.json +++ b/cppseminar/presentation/appsettings.json @@ -12,5 +12,9 @@ "ClientSecret": "" }, "API_GATEWAY": "http://gateway.local:5000/", - "STORAGE_CONNECTION_STRING": "" + "STORAGE_CONNECTION_STRING": "", + "ALLOWED_IP_RANGE": { + "Lower": "172.0.0.0", + "Upper": "172.255.255.255" + } } diff --git a/cppseminar/presentation/config.json b/cppseminar/presentation/config.json new file mode 100644 index 00000000..bad88a09 --- /dev/null +++ b/cppseminar/presentation/config.json @@ -0,0 +1,6 @@ +{ + "AllowedIpAddresses": [ + "162.18.0.1", + "172.18.0.1" + ] +} \ No newline at end of file diff --git a/cppseminar/presentation/presentation.sln b/cppseminar/presentation/presentation.sln new file mode 100644 index 00000000..c959c73d --- /dev/null +++ b/cppseminar/presentation/presentation.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "presentation", "presentation.csproj", "{C100687D-FF4D-4476-B381-B62D5AD7B541}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C100687D-FF4D-4476-B381-B62D5AD7B541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C100687D-FF4D-4476-B381-B62D5AD7B541}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C100687D-FF4D-4476-B381-B62D5AD7B541}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C100687D-FF4D-4476-B381-B62D5AD7B541}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {383FF98E-0479-4F28-AEB2-FB2182030969} + EndGlobalSection +EndGlobal diff --git a/cppseminar/presentation/wwwroot/js/connection_logging.js b/cppseminar/presentation/wwwroot/js/connection_logging.js new file mode 100644 index 00000000..f57aa28e --- /dev/null +++ b/cppseminar/presentation/wwwroot/js/connection_logging.js @@ -0,0 +1,49 @@ +const connection = new signalR.HubConnectionBuilder() + .withUrl("/monitor") + .configureLogging(signalR.LogLevel.Information) + .build(); + +connection.onclose(async () => { + console.log("Connection closed."); + await start(); +}); + +function setConnectionStatusDisplay(status, color) { + let elem = document.getElementById("connection-status"); + elem.style.color = color; + elem.innerText = "Connection status: " + status; +} + +function showLastLog() { + document.getElementById("last-timestamp").innerText = "Last timestamp sent at: " + new Date().toLocaleTimeString(); +} + +async function mainloop() { + while (true) { + try { + await connection.invoke("LogConnection"); + showLastLog(); + } catch (err) { + console.error(err); + setConnectionStatusDisplay("Error when invoking LogConnection", "red"); + break; + } + await new Promise(resolve => setTimeout(resolve, 2000)); + } +} + +async function start() { + try { + await connection.start(); + console.log("SignalR Connected."); + setConnectionStatusDisplay("Connected", "green"); + mainloop(); + } + catch (err) { + console.log(err); + setConnectionStatusDisplay("Unable to connect", "red"); + setTimeout(start, 5000); + } +} + +start(); \ No newline at end of file diff --git a/cppseminar/presentation/wwwroot/js/monitoring_admin.js b/cppseminar/presentation/wwwroot/js/monitoring_admin.js new file mode 100644 index 00000000..bf8fda1d --- /dev/null +++ b/cppseminar/presentation/wwwroot/js/monitoring_admin.js @@ -0,0 +1,89 @@ +const connection = new signalR.HubConnectionBuilder() + .withUrl("/monitor") + .configureLogging(signalR.LogLevel.Information) + .build(); + + +connection.onclose(async () => { + console.log("Connection closed."); + setAlert("Connection closed, trying to start a new connection..."); + await start(); +}); + +function setAlert(message){ + document.getElementById("alertBox").innerText = message; +} + +// Define the ReceiveMessage method so that it can be triggered from the Hub +connection.on("ReceiveUsers", (users) => { + try { + users = JSON.parse(users); + const tbl = document.getElementById("userLogs"); + tbl.innerHTML = ` + + User email + + + Last message + + `; + let time = 0; + let color = "black"; + users.forEach(user => { + time = (Math.round(user.Seconds * 100) / 100).toFixed(2); + if (time > 5 && time < 15){ + color = "orange"; + } + else if (time > 15) { + color = "red"; + } + else { + color = "green"; + } + tbl.innerHTML += `${user.UserEmail}${time} seconds ago`; + }) + } + catch (exception){ + console.log(exception); + } +}); +connection.on("ErrorGettingUsers", (message)=>{ + setAlert(message); +}) + +async function invokeGetConnectedUsersRecentAsync() { + // Invoke SendMessage on the Hub + try { + await connection.invoke("GetConnectedUsersRecentAsync"); + } catch (err) { + console.error(err); + } +} +async function start() { + try { + await connection.start(); + setAlert(""); + mainloop(); + } + catch (err) { + console.log(err); + setAlert("Unable to connect."); + setTimeout(start, 5000); + } +} + +async function mainloop() { + while (true){ + try { + await connection.invoke("GetConnectedUsersRecentAsync"); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + catch (err) { + setAlert("Error when invoking GetconnectedUsers."); + break; + } + } + +} + +start(); \ No newline at end of file diff --git a/cppseminar/userservice/Program.cs b/cppseminar/userservice/Program.cs index fd2bbb0b..f6715211 100644 --- a/cppseminar/userservice/Program.cs +++ b/cppseminar/userservice/Program.cs @@ -11,7 +11,7 @@ public class Program { public static void Main(string[] args) { - Log.Logger = new LoggerConfiguration() + Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .Enrich.FromLogContext() diff --git a/cppseminar/userservice/userservice.sln b/cppseminar/userservice/userservice.sln new file mode 100644 index 00000000..427b6848 --- /dev/null +++ b/cppseminar/userservice/userservice.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "userservice", "userservice.csproj", "{925E68F9-822C-42B7-8DCD-DE600F008A81}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {925E68F9-822C-42B7-8DCD-DE600F008A81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {925E68F9-822C-42B7-8DCD-DE600F008A81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {925E68F9-822C-42B7-8DCD-DE600F008A81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {925E68F9-822C-42B7-8DCD-DE600F008A81}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {067AAD9B-351B-47D6-B0FB-FB5266CE698F} + EndGlobalSection +EndGlobal