• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 363877

10 Aug 2023 08:52PM UTC coverage: 79.092% (+0.1%) from 78.979%
363877

Pull #6655

CI-PR build

web-flow
Merge 94ad1d11f into fdaed8b69
Pull Request #6655: Implementation of Teams batch APIs

26094 of 32992 relevant lines covered (79.09%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.4
/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/BotFrameworkHttpAdapter.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Generic;
6
using System.Linq;
7
using System.Net;
8
using System.Net.Http;
9
using System.Net.WebSockets;
10
using System.Security.Claims;
11
using System.Threading;
12
using System.Threading.Tasks;
13
using Microsoft.AspNetCore.Http;
14
using Microsoft.Bot.Builder.BotFramework;
15
using Microsoft.Bot.Builder.Streaming;
16
using Microsoft.Bot.Connector.Authentication;
17
using Microsoft.Bot.Schema;
18
using Microsoft.Extensions.Configuration;
19
using Microsoft.Extensions.Logging;
20
using Microsoft.Rest.TransientFaultHandling;
21

22
namespace Microsoft.Bot.Builder.Integration.AspNet.Core
23
{
24
    /// <summary>
25
    /// A Bot Builder Adapter implementation used to handled bot Framework HTTP requests.
26
    /// </summary>
27
    /// <remarks>
28
    /// BotFrameworkAdapter is still supported but the recommended adapter is `CloudAdapter`.
29
    /// </remarks>
30
    public class BotFrameworkHttpAdapter : BotFrameworkHttpAdapterBase, IBotFrameworkHttpAdapter
31
    {
32
        private const string AuthHeaderName = "authorization";
33
        private const string ChannelIdHeaderName = "channelid";
34

35
        /// <summary>
36
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class,
37
        /// using a credential provider.
38
        /// </summary>
39
        /// <param name="credentialProvider">The credential provider.</param>
40
        /// <param name="authConfig">The authentication configuration.</param>
41
        /// <param name="channelProvider">The channel provider.</param>
42
        /// <param name="connectorClientRetryPolicy">Retry policy for retrying HTTP operations.</param>
43
        /// <param name="customHttpClient">The HTTP client.</param>
44
        /// <param name="middleware">The middleware to initially add to the adapter.</param>
45
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
46
        /// <exception cref="ArgumentNullException">
47
        /// <paramref name="credentialProvider"/> is <c>null</c>.</exception>
48
        /// <remarks>Use a <see cref="MiddlewareSet"/> object to add multiple middleware
49
        /// components in the constructor. Use the IMiddleware method to
50
        /// add additional middleware to the adapter after construction.
51
        /// </remarks>
52
        public BotFrameworkHttpAdapter(
53
            ICredentialProvider credentialProvider,
54
            AuthenticationConfiguration authConfig,
55
            IChannelProvider channelProvider = null,
56
            RetryPolicy connectorClientRetryPolicy = null,
57
            HttpClient customHttpClient = null,
58
            IMiddleware middleware = null,
59
            ILogger logger = null)
60
            : base(credentialProvider, authConfig ?? new AuthenticationConfiguration(), channelProvider, connectorClientRetryPolicy, customHttpClient, middleware, logger)
×
61
        {
62
        }
1✔
63

64
        /// <summary>
65
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class,
66
        /// using a credential provider.
67
        /// </summary>
68
        /// <param name="credentialProvider">The credential provider.</param>
69
        /// <param name="channelProvider">The channel provider.</param>
70
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
71
        public BotFrameworkHttpAdapter(ICredentialProvider credentialProvider = null, IChannelProvider channelProvider = null, ILogger<BotFrameworkHttpAdapter> logger = null)
72
            : this(credentialProvider ?? new SimpleCredentialProvider(), new AuthenticationConfiguration(), channelProvider, null, null, null, logger)
1✔
73
        {
74
        }
1✔
75

76
        /// <summary>
77
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class,
78
        /// using a credential provider.
79
        /// </summary>
80
        /// <param name="credentialProvider">The credential provider.</param>
81
        /// <param name="channelProvider">The channel provider.</param>
82
        /// <param name="httpClient">The <see cref="HttpClient"/> used.</param>
83
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
84
        public BotFrameworkHttpAdapter(ICredentialProvider credentialProvider, IChannelProvider channelProvider, HttpClient httpClient, ILogger<BotFrameworkHttpAdapter> logger)
85
            : this(credentialProvider ?? new SimpleCredentialProvider(), new AuthenticationConfiguration(), channelProvider, null, httpClient, null, logger)
1✔
86
        {
87
        }
1✔
88

89
        /// <summary>
90
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class.
91
        /// </summary>
92
        /// <param name="configuration">An <see cref="IConfiguration"/> instance.</param>
93
        /// <param name="credentialProvider">The credential provider.</param>
94
        /// <param name="authConfig">The authentication configuration.</param>
95
        /// <param name="channelProvider">The channel provider.</param>
96
        /// <param name="connectorClientRetryPolicy">Retry policy for retrying HTTP operations.</param>
97
        /// <param name="customHttpClient">The HTTP client.</param>
98
        /// <param name="middleware">The middleware to initially add to the adapter.</param>
99
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
100
        protected BotFrameworkHttpAdapter(
101
            IConfiguration configuration,
102
            ICredentialProvider credentialProvider,
103
            AuthenticationConfiguration authConfig = null,
104
            IChannelProvider channelProvider = null,
105
            RetryPolicy connectorClientRetryPolicy = null,
106
            HttpClient customHttpClient = null,
107
            IMiddleware middleware = null,
108
            ILogger logger = null)
109
            : this(credentialProvider ?? new ConfigurationCredentialProvider(configuration), authConfig ?? new AuthenticationConfiguration(), channelProvider ?? new ConfigurationChannelProvider(configuration), connectorClientRetryPolicy, customHttpClient, middleware, logger)
×
110
        {
111
            var openIdEndpoint = configuration.GetSection(AuthenticationConstants.BotOpenIdMetadataKey)?.Value;
×
112

113
            if (!string.IsNullOrEmpty(openIdEndpoint))
1✔
114
            {
115
                // Indicate which Cloud we are using, for example, Public or Sovereign.
116
                ChannelValidation.OpenIdMetadataUrl = openIdEndpoint;
1✔
117
                GovernmentChannelValidation.OpenIdMetadataUrl = openIdEndpoint;
1✔
118
            }
119
        }
1✔
120

121
        /// <summary>
122
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class.
123
        /// </summary>
124
        /// <param name="configuration">An <see cref="IConfiguration"/> instance.</param>
125
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
126
        protected BotFrameworkHttpAdapter(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger = null)
127
            : this(configuration, new ConfigurationCredentialProvider(configuration), new AuthenticationConfiguration(), new ConfigurationChannelProvider(configuration), logger: logger)
1✔
128
        {
129
        }
1✔
130

131
        /// <summary>
132
        /// This method can be called from inside a POST method on any Controller implementation.
133
        /// </summary>
134
        /// <param name="httpRequest">The HTTP request object, typically in a POST handler by a Controller.</param>
135
        /// <param name="httpResponse">The HTTP response object.</param>
136
        /// <param name="bot">The bot implementation.</param>
137
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
138
        /// or threads to receive notice of cancellation.</param>
139
        /// <returns>A task that represents the work queued to execute.</returns>
140
        public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpResponse, IBot bot, CancellationToken cancellationToken = default)
141
        {
142
            if (httpRequest == null)
1✔
143
            {
144
                throw new ArgumentNullException(nameof(httpRequest));
×
145
            }
146

147
            if (httpResponse == null)
1✔
148
            {
149
                throw new ArgumentNullException(nameof(httpResponse));
×
150
            }
151

152
            if (bot == null)
1✔
153
            {
154
                throw new ArgumentNullException(nameof(bot));
×
155
            }
156

157
            if (httpRequest.Method == HttpMethods.Get)
1✔
158
            {
159
                await ConnectWebSocketAsync(bot, httpRequest, httpResponse, cancellationToken).ConfigureAwait(false);
1✔
160
            }
161
            else
162
            {
163
                // Deserialize the incoming Activity
164
                var activity = await HttpHelper.ReadRequestAsync<Activity>(httpRequest).ConfigureAwait(false);
1✔
165

166
                if (string.IsNullOrEmpty(activity?.Type))
1✔
167
                {
168
                    httpResponse.StatusCode = (int)HttpStatusCode.BadRequest;
1✔
169
                    Logger.LogWarning("BadRequest: Missing activity or activity type.");
1✔
170
                    return;
1✔
171
                }
172

173
                // Grab the auth header from the inbound http request
174
                var authHeader = httpRequest.Headers["Authorization"];
1✔
175

176
                try
177
                {
178
                    // Process the inbound activity with the bot
179
                    var invokeResponse = await ProcessActivityAsync(authHeader, activity, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false);
1✔
180

181
                    // write the response, potentially serializing the InvokeResponse
182
                    await HttpHelper.WriteResponseAsync(httpResponse, invokeResponse).ConfigureAwait(false);
1✔
183
                }
1✔
184
                catch (UnauthorizedAccessException)
×
185
                {
186
                    // handle unauthorized here as this layer creates the http response
187
                    httpResponse.StatusCode = (int)HttpStatusCode.Unauthorized;
×
188
                }
×
189
            }
190
        }
1✔
191

192
        /// <summary>
193
        /// Create the <see cref="StreamingRequestHandler"/> for processing for a new Web Socket connection request.
194
        /// </summary>
195
        /// <param name="bot">The <see cref="IBot"/> implementation which will process the request.</param>
196
        /// <param name="socket">The <see cref="WebSocket"/> which the request will be received on.</param>
197
        /// <param name="audience">The authorized audience of the incoming connection request.</param>
198
        /// <returns>Returns a new <see cref="StreamingRequestHandler"/> implementation.</returns>
199
        public virtual StreamingRequestHandler CreateStreamingRequestHandler(IBot bot, WebSocket socket, string audience)
200
        {
201
            if (bot == null)
1✔
202
            {
203
                throw new ArgumentNullException(nameof(bot));
×
204
            }
205

206
            if (socket == null)
1✔
207
            {
208
                throw new ArgumentNullException(nameof(socket));
×
209
            }
210

211
            return new StreamingRequestHandler(bot, this, socket, audience, Logger);
1✔
212
        }
213

214
        private static async Task WriteUnauthorizedResponseAsync(string headerName, HttpRequest httpRequest)
215
        {
216
            httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
×
217
            await httpRequest.HttpContext.Response.WriteAsync($"Unable to authenticate. Missing header: {headerName}").ConfigureAwait(false);
×
218
        }
×
219

220
        /// <summary>
221
        /// Process the initial request to establish a long lived connection via a streaming server.
222
        /// </summary>
223
        /// <param name="bot">The <see cref="IBot"/> instance.</param>
224
        /// <param name="httpRequest">The connection request.</param>
225
        /// <param name="httpResponse">The response sent on error or connection termination.</param>
226
        /// <param name="cancellationToken">The cancellation token.</param>
227
        /// <returns>Returns on task completion.</returns>
228
        private async Task ConnectWebSocketAsync(IBot bot, HttpRequest httpRequest, HttpResponse httpResponse, CancellationToken cancellationToken = default)
229
        {
230
            if (httpRequest == null)
1✔
231
            {
232
                throw new ArgumentNullException(nameof(httpRequest));
×
233
            }
234

235
            if (httpResponse == null)
1✔
236
            {
237
                throw new ArgumentNullException(nameof(httpResponse));
×
238
            }
239

240
            ConnectedBot = bot ?? throw new ArgumentNullException(nameof(bot));
×
241

242
            if (!httpRequest.HttpContext.WebSockets.IsWebSocketRequest)
1✔
243
            {
244
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
×
245
                await httpRequest.HttpContext.Response.WriteAsync("Upgrade to WebSocket is required.").ConfigureAwait(false);
×
246

247
                return;
×
248
            }
249

250
            var claimsIdentity = await AuthenticateRequestAsync(httpRequest).ConfigureAwait(false);
1✔
251
            if (claimsIdentity == null)
1✔
252
            {
253
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
×
254
                await httpRequest.HttpContext.Response.WriteAsync("Request authentication failed.").ConfigureAwait(false);
×
255

256
                return;
×
257
            }
258

259
            try
260
            {
261
                // Set ClaimsIdentity on Adapter to enable Skills and User OAuth in WebSocket-based streaming scenarios.
262
                var audience = GetAudience(claimsIdentity);
1✔
263

264
                var socket = await httpRequest.HttpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
1✔
265
                var requestHandler = CreateStreamingRequestHandler(bot, socket, audience);
1✔
266

267
                if (RequestHandlers == null)
1✔
268
                {
269
                    RequestHandlers = new List<StreamingRequestHandler>();
×
270
                }
271

272
                RequestHandlers.Add(requestHandler);
1✔
273

274
                Log.WebSocketConnectionStarted(Logger);
1✔
275
                await requestHandler.ListenAsync(cancellationToken).ConfigureAwait(false);
1✔
276
                Log.WebSocketConnectionCompleted(Logger);
1✔
277
            }
1✔
278
            catch (Exception ex)
×
279
            {
280
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
×
281
                await httpRequest.HttpContext.Response.WriteAsync($"Unable to create transport server. Error: {ex.ToString()}").ConfigureAwait(false);
×
282

283
                throw;
×
284
            }
285
        }
1✔
286

287
        /// <summary>
288
        /// Validates the auth header for WebSocket upgrade requests.
289
        /// </summary>
290
        /// <remarks>
291
        /// Returns a ClaimsIdentity for successful auth and when auth is disabled. Returns null for failed auth.
292
        /// </remarks>
293
        /// <param name="httpRequest">The connection request.</param>
294
        private async Task<ClaimsIdentity> AuthenticateRequestAsync(HttpRequest httpRequest)
295
        {
296
            try
297
            {
298
                if (!await CredentialProvider.IsAuthenticationDisabledAsync().ConfigureAwait(false))
1✔
299
                {
300
                    var authHeader = httpRequest.Headers.First(x => string.Equals(x.Key, AuthHeaderName, StringComparison.OrdinalIgnoreCase)).Value.FirstOrDefault();
×
301
                    var channelId = httpRequest.Headers.First(x => string.Equals(x.Key, ChannelIdHeaderName, StringComparison.OrdinalIgnoreCase)).Value.FirstOrDefault();
×
302

303
                    if (string.IsNullOrWhiteSpace(authHeader))
×
304
                    {
305
                        await WriteUnauthorizedResponseAsync(AuthHeaderName, httpRequest).ConfigureAwait(false);
×
306
                        return null;
×
307
                    }
308

309
                    if (string.IsNullOrWhiteSpace(channelId))
×
310
                    {
311
                        await WriteUnauthorizedResponseAsync(ChannelIdHeaderName, httpRequest).ConfigureAwait(false);
×
312
                        return null;
×
313
                    }
314

315
                    var claimsIdentity = await JwtTokenValidation.ValidateAuthHeader(authHeader, CredentialProvider, ChannelProvider, channelId).ConfigureAwait(false);
×
316
                    if (!claimsIdentity.IsAuthenticated)
×
317
                    {
318
                        httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
×
319
                        return null;
×
320
                    }
321
                    
322
                    ClaimsIdentity = claimsIdentity;
×
323
                    return claimsIdentity;
×
324
                }
325

326
                // Authentication is not enabled, therefore return an anonymous ClaimsIdentity.
327
                return new ClaimsIdentity(new List<Claim>(), "anonymous");
1✔
328
            }
329
            catch (Exception)
×
330
            {
331
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
×
332
                await httpRequest.HttpContext.Response.WriteAsync("Error while attempting to authorize connection.").ConfigureAwait(false);
×
333

334
                throw;
×
335
            }
336
        }
1✔
337

338
        /// <summary>
339
        /// Get the audience for the WebSocket connection from the authenticated ClaimsIdentity.
340
        /// </summary>
341
        /// <remarks>
342
        /// Setting the Audience on the StreamingRequestHandler enables the bot to call skills and correctly forward responses from the skill to the next recipient.
343
        /// i.e. the participant at the other end of the WebSocket connection.
344
        /// </remarks>
345
        /// <param name="claimsIdentity">ClaimsIdentity for authenticated caller.</param>
346
        private string GetAudience(ClaimsIdentity claimsIdentity)
347
        {
348
            if (claimsIdentity.AuthenticationType != AuthenticationConstants.AnonymousAuthType)
1✔
349
            {
350
                var audience = ChannelProvider != null && ChannelProvider.IsGovernment() ?
×
351
    GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope :
×
352
    AuthenticationConstants.ToChannelFromBotOAuthScope;
×
353

354
                if (SkillValidation.IsSkillClaim(claimsIdentity.Claims))
×
355
                {
356
                    audience = JwtTokenValidation.GetAppIdFromClaims(claimsIdentity.Claims);
×
357
                }
358

359
                return audience;
×
360
            }
361

362
            return null;
1✔
363
        }
364

365
        private class Log
366
        {
367
            private static readonly Action<ILogger, Exception> _webSocketConnectionStarted =
1✔
368
                LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(WebSocketConnectionStarted)), "WebSocket connection started.");
1✔
369

370
            private static readonly Action<ILogger, Exception> _webSocketConnectionCompleted =
1✔
371
                LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(WebSocketConnectionCompleted)), "WebSocket connection completed.");
1✔
372

373
            public static void WebSocketConnectionStarted(ILogger logger) => _webSocketConnectionStarted(logger, null);
1✔
374

375
            public static void WebSocketConnectionCompleted(ILogger logger) => _webSocketConnectionCompleted(logger, null);
1✔
376
        }
377
    }
378
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc