// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Events; using Microsoft.Teams.Apps.Plugins; using Microsoft.Teams.Common.Logging; using Microsoft.Teams.Common.Text; using Microsoft.Teams.Plugins.AspNetCore.DevTools.Extensions; using Microsoft.Teams.Plugins.AspNetCore.DevTools.Models; namespace Microsoft.Teams.Plugins.AspNetCore.DevTools; [Plugin] [Obsolete("DevTools is deprecated and will be removed in a later version. Use Microsoft 365 Agents Playground instead.")] public class DevToolsPlugin : IAspNetCorePlugin { [AllowNull] [Dependency] public ILogger Logger { get; set; } [Dependency("AppId", optional: true)] public string? AppId { get; set; } [Dependency("AppName", optional: true)] public string? AppName { get; set; } public event EventFunction Events; internal MetaData MetaData => new() { Id = AppId, Name = AppName, Pages = _pages }; internal readonly WebSocketCollection Sockets = []; private readonly ISenderPlugin _sender; private readonly IServiceProvider _services; private readonly IList _pages = []; private readonly TeamsDevToolsSettings _settings; public DevToolsPlugin(AspNetCorePlugin sender, IServiceProvider provider) { _sender = sender; _services = provider; _settings = provider.GetRequiredService(); } public IApplicationBuilder Configure(IApplicationBuilder builder) { // Reject cross-origin WebSocket upgrades. The DevTools UI is always // loaded same-origin, so Origin must equal request Host; absent // Origin is rejected. Headers are inspected directly so this // middleware does not require UseWebSockets to run first. builder.Use(async (context, next) => { var upgrade = context.Request.Headers["Upgrade"].ToString(); if (string.Equals(upgrade, "websocket", StringComparison.OrdinalIgnoreCase)) { var origin = context.Request.Headers["Origin"].ToString(); var expectedOrigin = $"{context.Request.Scheme}://{context.Request.Host}"; if (string.IsNullOrEmpty(origin) || !string.Equals(origin, expectedOrigin, StringComparison.OrdinalIgnoreCase)) { context.Response.StatusCode = StatusCodes.Status403Forbidden; return; } } await next(context); }); builder.UseWebSockets(); builder.UseStaticFiles(new StaticFileOptions() { FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "web"), ServeUnknownFileTypes = true, RequestPath = "/devtools" }); builder.Use(async (context, next) => { try { await next(context).ConfigureAwait(false); } catch (Exception ex) { Logger.Error(ex, "http error"); throw new Exception(ex.Message, innerException: ex); } }); return builder; } public DevToolsPlugin AddPage(Page page) { _pages.Add(page); Logger.Debug($"page '{page.Name}' added at '{page.Url}'"); return this; } public Task OnInit(App app, CancellationToken cancellationToken = default) { var hostEnvironment = _services.GetService(); var isProduction = hostEnvironment?.IsProduction() ?? string.Equals(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"), "Production", StringComparison.OrdinalIgnoreCase); if (isProduction) { throw new InvalidOperationException( "Devtools plugin cannot be used in production environments. " + "Remove the devtools plugin from your app configuration."); } foreach (var page in _settings.Pages) { AddPage(page); } Logger.Warn( new StringBuilder() .Bold( new StringBuilder() .Yellow("⚠️ Devtools are not secure and should not be used production environments ⚠️") .ToString() ) ); return Task.CompletedTask; } public Task OnStart(App app, CancellationToken cancellationToken = default) { var server = _services.GetRequiredService(); var addresses = server.Features.GetRequiredFeature().Addresses; foreach (var address in addresses) { Logger.Info($"Available at {address}/devtools"); } Logger.Debug("OnStart"); return Task.CompletedTask; } public Task OnError(App app, IPlugin plugin, ErrorEvent @event, CancellationToken cancellationToken = default) { Logger.Debug("OnError"); return Task.CompletedTask; } public async Task OnActivity(App app, ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default) { Logger.Debug("OnActivity"); await Sockets.Emit( DevTools.Events.ActivityEvent.Received( @event.Activity, @event.Activity.Conversation ), cancellationToken ).ConfigureAwait(false); } public async Task OnActivitySent(App app, ISenderPlugin sender, ActivitySentEvent @event, CancellationToken cancellationToken = default) { Logger.Debug("OnActivitySent"); await Sockets.Emit( DevTools.Events.ActivityEvent.Sent( @event.Activity, @event.Activity.Conversation ), cancellationToken ).ConfigureAwait(false); } public Task OnActivityResponse(App app, ISenderPlugin sender, ActivityResponseEvent @event, CancellationToken cancellationToken = default) { Logger.Debug("OnActivityResponse"); return Task.CompletedTask; } public Task Do(ActivityEvent @event, CancellationToken cancellationToken = default) { return _sender.Do(@event, cancellationToken); } }