HttpLoggingMiddlewareTests.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861
  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. using System;
  4. using System.IO;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. using Microsoft.AspNetCore.Http;
  8. using Microsoft.AspNetCore.Testing;
  9. using Microsoft.Extensions.Options;
  10. using Microsoft.Net.Http.Headers;
  11. using Microsoft.Extensions.Logging;
  12. using Moq;
  13. using Xunit;
  14. namespace Microsoft.AspNetCore.HttpLogging
  15. {
  16. public class HttpLoggingMiddlewareTests : LoggedTest
  17. {
  18. public static TheoryData BodyData
  19. {
  20. get
  21. {
  22. var variations = new TheoryData<string>();
  23. variations.Add("Hello World");
  24. variations.Add(new string('a', 4097));
  25. variations.Add(new string('b', 10000));
  26. variations.Add(new string('あ', 10000));
  27. return variations;
  28. }
  29. }
  30. [Fact]
  31. public void Ctor_ThrowsExceptionsWhenNullArgs()
  32. {
  33. Assert.Throws<ArgumentNullException>(() => new HttpLoggingMiddleware(
  34. null,
  35. CreateOptionsAccessor(),
  36. LoggerFactory.CreateLogger<HttpLoggingMiddleware>()));
  37. Assert.Throws<ArgumentNullException>(() => new HttpLoggingMiddleware(c =>
  38. {
  39. return Task.CompletedTask;
  40. },
  41. null,
  42. LoggerFactory.CreateLogger<HttpLoggingMiddleware>()));
  43. Assert.Throws<ArgumentNullException>(() => new HttpLoggingMiddleware(c =>
  44. {
  45. return Task.CompletedTask;
  46. },
  47. CreateOptionsAccessor(),
  48. null));
  49. }
  50. [Fact]
  51. public async Task NoopWhenLoggingDisabled()
  52. {
  53. var options = CreateOptionsAccessor();
  54. options.CurrentValue.LoggingFields = HttpLoggingFields.None;
  55. var middleware = new HttpLoggingMiddleware(
  56. c =>
  57. {
  58. c.Response.StatusCode = 200;
  59. return Task.CompletedTask;
  60. },
  61. options,
  62. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  63. var httpContext = new DefaultHttpContext();
  64. httpContext.Request.Protocol = "HTTP/1.0";
  65. httpContext.Request.Method = "GET";
  66. httpContext.Request.Scheme = "http";
  67. httpContext.Request.Path = new PathString("/foo");
  68. httpContext.Request.PathBase = new PathString("/foo");
  69. httpContext.Request.QueryString = new QueryString("?foo");
  70. httpContext.Request.Headers["Connection"] = "keep-alive";
  71. httpContext.Request.ContentType = "text/plain";
  72. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
  73. await middleware.Invoke(httpContext);
  74. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
  75. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Method: GET"));
  76. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
  77. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
  78. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
  79. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("QueryString: ?foo"));
  80. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
  81. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  82. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
  83. }
  84. [Fact]
  85. public async Task DefaultRequestInfoOnlyHeadersAndRequestInfo()
  86. {
  87. var middleware = new HttpLoggingMiddleware(
  88. async c =>
  89. {
  90. var arr = new byte[4096];
  91. while (true)
  92. {
  93. var res = await c.Request.Body.ReadAsync(arr);
  94. if (res == 0)
  95. {
  96. break;
  97. }
  98. }
  99. },
  100. CreateOptionsAccessor(),
  101. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  102. var httpContext = new DefaultHttpContext();
  103. httpContext.Request.Protocol = "HTTP/1.0";
  104. httpContext.Request.Method = "GET";
  105. httpContext.Request.Scheme = "http";
  106. httpContext.Request.Path = new PathString("/foo");
  107. httpContext.Request.PathBase = new PathString("/foo");
  108. httpContext.Request.Headers["Connection"] = "keep-alive";
  109. httpContext.Request.ContentType = "text/plain";
  110. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
  111. await middleware.Invoke(httpContext);
  112. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
  113. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Method: GET"));
  114. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
  115. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
  116. Assert.Contains(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
  117. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
  118. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  119. }
  120. [Fact]
  121. public async Task RequestLogsAllRequestInfo()
  122. {
  123. var options = CreateOptionsAccessor();
  124. options.CurrentValue.LoggingFields = HttpLoggingFields.Request;
  125. var middleware = new HttpLoggingMiddleware(
  126. async c =>
  127. {
  128. var arr = new byte[4096];
  129. while (true)
  130. {
  131. var res = await c.Request.Body.ReadAsync(arr);
  132. if (res == 0)
  133. {
  134. break;
  135. }
  136. }
  137. },
  138. options,
  139. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  140. var httpContext = new DefaultHttpContext();
  141. httpContext.Request.Protocol = "HTTP/1.0";
  142. httpContext.Request.Method = "GET";
  143. httpContext.Request.Scheme = "http";
  144. httpContext.Request.Path = new PathString("/foo");
  145. httpContext.Request.PathBase = new PathString("/foo");
  146. httpContext.Request.Headers["Connection"] = "keep-alive";
  147. httpContext.Request.ContentType = "text/plain";
  148. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
  149. await middleware.Invoke(httpContext);
  150. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
  151. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Method: GET"));
  152. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
  153. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
  154. Assert.Contains(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
  155. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
  156. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Body: test"));
  157. }
  158. [Fact]
  159. public async Task RequestPropertiesLogs()
  160. {
  161. var options = CreateOptionsAccessor();
  162. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestProperties;
  163. var middleware = new HttpLoggingMiddleware(
  164. async c =>
  165. {
  166. var arr = new byte[4096];
  167. while (true)
  168. {
  169. var res = await c.Request.Body.ReadAsync(arr);
  170. if (res == 0)
  171. {
  172. break;
  173. }
  174. }
  175. },
  176. options,
  177. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  178. var httpContext = new DefaultHttpContext();
  179. httpContext.Request.Protocol = "HTTP/1.0";
  180. httpContext.Request.Method = "GET";
  181. httpContext.Request.Scheme = "http";
  182. httpContext.Request.Path = new PathString("/foo");
  183. httpContext.Request.PathBase = new PathString("/foo");
  184. httpContext.Request.Headers["Connection"] = "keep-alive";
  185. httpContext.Request.ContentType = "text/plain";
  186. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
  187. await middleware.Invoke(httpContext);
  188. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
  189. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Method: GET"));
  190. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
  191. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
  192. Assert.Contains(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
  193. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
  194. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  195. }
  196. [Fact]
  197. public async Task RequestHeadersLogs()
  198. {
  199. var options = CreateOptionsAccessor();
  200. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestHeaders;
  201. var middleware = new HttpLoggingMiddleware(
  202. async c =>
  203. {
  204. var arr = new byte[4096];
  205. while (true)
  206. {
  207. var res = await c.Request.Body.ReadAsync(arr);
  208. if (res == 0)
  209. {
  210. break;
  211. }
  212. }
  213. },
  214. options,
  215. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  216. var httpContext = new DefaultHttpContext();
  217. httpContext.Request.Protocol = "HTTP/1.0";
  218. httpContext.Request.Method = "GET";
  219. httpContext.Request.Scheme = "http";
  220. httpContext.Request.Path = new PathString("/foo");
  221. httpContext.Request.PathBase = new PathString("/foo");
  222. httpContext.Request.QueryString = new QueryString("?foo");
  223. httpContext.Request.Headers["Connection"] = "keep-alive";
  224. httpContext.Request.ContentType = "text/plain";
  225. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
  226. await middleware.Invoke(httpContext);
  227. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
  228. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Method: GET"));
  229. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
  230. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
  231. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
  232. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("QueryString: ?foo"));
  233. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
  234. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  235. }
  236. [Fact]
  237. public async Task UnknownRequestHeadersRedacted()
  238. {
  239. var middleware = new HttpLoggingMiddleware(
  240. async c =>
  241. {
  242. var arr = new byte[4096];
  243. while (true)
  244. {
  245. var res = await c.Request.Body.ReadAsync(arr);
  246. if (res == 0)
  247. {
  248. break;
  249. }
  250. }
  251. },
  252. CreateOptionsAccessor(),
  253. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  254. var httpContext = new DefaultHttpContext();
  255. httpContext.Request.Headers["foo"] = "bar";
  256. await middleware.Invoke(httpContext);
  257. Assert.Contains(TestSink.Writes, w => w.Message.Contains("foo: [Redacted]"));
  258. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("foo: bar"));
  259. }
  260. [Fact]
  261. public async Task CanConfigureRequestAllowList()
  262. {
  263. var options = CreateOptionsAccessor();
  264. options.CurrentValue.RequestHeaders.Clear();
  265. options.CurrentValue.RequestHeaders.Add("foo");
  266. var middleware = new HttpLoggingMiddleware(
  267. c =>
  268. {
  269. return Task.CompletedTask;
  270. },
  271. options,
  272. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  273. var httpContext = new DefaultHttpContext();
  274. // Header on the default allow list.
  275. httpContext.Request.Headers["Connection"] = "keep-alive";
  276. httpContext.Request.Headers["foo"] = "bar";
  277. await middleware.Invoke(httpContext);
  278. Assert.Contains(TestSink.Writes, w => w.Message.Contains("foo: bar"));
  279. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("foo: [Redacted]"));
  280. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: [Redacted]"));
  281. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
  282. }
  283. [Theory]
  284. [MemberData(nameof(BodyData))]
  285. public async Task RequestBodyReadingWorks(string expected)
  286. {
  287. var options = CreateOptionsAccessor();
  288. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
  289. var middleware = new HttpLoggingMiddleware(
  290. async c =>
  291. {
  292. var arr = new byte[4096];
  293. while (true)
  294. {
  295. var res = await c.Request.Body.ReadAsync(arr);
  296. if (res == 0)
  297. {
  298. break;
  299. }
  300. }
  301. },
  302. options,
  303. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  304. var httpContext = new DefaultHttpContext();
  305. httpContext.Request.ContentType = "text/plain";
  306. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(expected));
  307. await middleware.Invoke(httpContext);
  308. Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
  309. }
  310. [Fact]
  311. public async Task RequestBodyReadingLimitLongCharactersWorks()
  312. {
  313. var input = string.Concat(new string('あ', 5));
  314. var options = CreateOptionsAccessor();
  315. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
  316. options.CurrentValue.RequestBodyLogLimit = 4;
  317. var middleware = new HttpLoggingMiddleware(
  318. async c =>
  319. {
  320. var arr = new byte[4096];
  321. var count = 0;
  322. while (true)
  323. {
  324. var res = await c.Request.Body.ReadAsync(arr);
  325. if (res == 0)
  326. {
  327. break;
  328. }
  329. count += res;
  330. }
  331. Assert.Equal(15, count);
  332. },
  333. options,
  334. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  335. var httpContext = new DefaultHttpContext();
  336. httpContext.Request.ContentType = "text/plain";
  337. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
  338. await middleware.Invoke(httpContext);
  339. var expected = input.Substring(0, options.CurrentValue.RequestBodyLogLimit / 3);
  340. Assert.Contains(TestSink.Writes, w => w.Message.Equals("RequestBody: " + expected));
  341. }
  342. [Fact]
  343. public async Task RequestBodyReadingLimitWorks()
  344. {
  345. var input = string.Concat(new string('a', 60000), new string('b', 3000));
  346. var options = CreateOptionsAccessor();
  347. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
  348. var middleware = new HttpLoggingMiddleware(
  349. async c =>
  350. {
  351. var arr = new byte[4096];
  352. var count = 0;
  353. while (true)
  354. {
  355. var res = await c.Request.Body.ReadAsync(arr);
  356. if (res == 0)
  357. {
  358. break;
  359. }
  360. count += res;
  361. }
  362. Assert.Equal(63000, count);
  363. },
  364. options,
  365. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  366. var httpContext = new DefaultHttpContext();
  367. httpContext.Request.ContentType = "text/plain";
  368. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
  369. await middleware.Invoke(httpContext);
  370. var expected = input.Substring(0, options.CurrentValue.ResponseBodyLogLimit);
  371. Assert.Contains(TestSink.Writes, w => w.Message.Equals("RequestBody: " + expected));
  372. }
  373. [Fact]
  374. public async Task PartialReadBodyStillLogs()
  375. {
  376. var input = string.Concat(new string('a', 60000), new string('b', 3000));
  377. var options = CreateOptionsAccessor();
  378. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
  379. var middleware = new HttpLoggingMiddleware(
  380. async c =>
  381. {
  382. var arr = new byte[4096];
  383. var res = await c.Request.Body.ReadAsync(arr);
  384. },
  385. options,
  386. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  387. var httpContext = new DefaultHttpContext();
  388. httpContext.Request.ContentType = "text/plain";
  389. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
  390. await middleware.Invoke(httpContext);
  391. var expected = input.Substring(0, 4096);
  392. Assert.Contains(TestSink.Writes, w => w.Message.Equals("RequestBody: " + expected));
  393. }
  394. [Theory]
  395. [InlineData("text/plain")]
  396. [InlineData("text/html")]
  397. [InlineData("application/json")]
  398. [InlineData("application/xml")]
  399. [InlineData("application/entity+json")]
  400. [InlineData("application/entity+xml")]
  401. public async Task VerifyDefaultMediaTypeHeaders(string contentType)
  402. {
  403. // media headers that should work.
  404. var expected = new string('a', 1000);
  405. var options = CreateOptionsAccessor();
  406. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
  407. var middleware = new HttpLoggingMiddleware(
  408. async c =>
  409. {
  410. var arr = new byte[4096];
  411. while (true)
  412. {
  413. var res = await c.Request.Body.ReadAsync(arr);
  414. if (res == 0)
  415. {
  416. break;
  417. }
  418. }
  419. },
  420. options,
  421. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  422. var httpContext = new DefaultHttpContext();
  423. httpContext.Request.ContentType = contentType;
  424. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(expected));
  425. await middleware.Invoke(httpContext);
  426. Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
  427. }
  428. [Theory]
  429. [InlineData("application/invalid")]
  430. [InlineData("multipart/form-data")]
  431. public async Task RejectedContentTypes(string contentType)
  432. {
  433. // media headers that should work.
  434. var expected = new string('a', 1000);
  435. var options = CreateOptionsAccessor();
  436. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
  437. var middleware = new HttpLoggingMiddleware(
  438. async c =>
  439. {
  440. var arr = new byte[4096];
  441. var count = 0;
  442. while (true)
  443. {
  444. var res = await c.Request.Body.ReadAsync(arr);
  445. if (res == 0)
  446. {
  447. break;
  448. }
  449. count += res;
  450. }
  451. Assert.Equal(1000, count);
  452. },
  453. options,
  454. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  455. var httpContext = new DefaultHttpContext();
  456. httpContext.Request.ContentType = contentType;
  457. httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(expected));
  458. await middleware.Invoke(httpContext);
  459. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains(expected));
  460. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Unrecognized Content-Type for body."));
  461. }
  462. [Fact]
  463. public async Task DifferentEncodingsWork()
  464. {
  465. var encoding = Encoding.Unicode;
  466. var expected = new string('a', 1000);
  467. var options = CreateOptionsAccessor();
  468. options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
  469. options.CurrentValue.MediaTypeOptions.Clear();
  470. options.CurrentValue.MediaTypeOptions.AddText("text/plain", encoding);
  471. var middleware = new HttpLoggingMiddleware(
  472. async c =>
  473. {
  474. var arr = new byte[4096];
  475. var count = 0;
  476. while (true)
  477. {
  478. var res = await c.Request.Body.ReadAsync(arr);
  479. if (res == 0)
  480. {
  481. break;
  482. }
  483. count += res;
  484. }
  485. Assert.Equal(2000, count);
  486. },
  487. options,
  488. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  489. var httpContext = new DefaultHttpContext();
  490. httpContext.Request.ContentType = "text/plain";
  491. httpContext.Request.Body = new MemoryStream(encoding.GetBytes(expected));
  492. await middleware.Invoke(httpContext);
  493. Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
  494. }
  495. [Fact]
  496. public async Task DefaultResponseInfoOnlyHeadersAndRequestInfo()
  497. {
  498. var middleware = new HttpLoggingMiddleware(
  499. async c =>
  500. {
  501. c.Response.StatusCode = 200;
  502. c.Response.Headers[HeaderNames.TransferEncoding] = "test";
  503. c.Response.ContentType = "text/plain";
  504. await c.Response.WriteAsync("test");
  505. },
  506. CreateOptionsAccessor(),
  507. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  508. var httpContext = new DefaultHttpContext();
  509. await middleware.Invoke(httpContext);
  510. Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
  511. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
  512. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  513. }
  514. [Fact]
  515. public async Task ResponseInfoLogsAll()
  516. {
  517. var options = CreateOptionsAccessor();
  518. options.CurrentValue.LoggingFields = HttpLoggingFields.Response;
  519. var middleware = new HttpLoggingMiddleware(
  520. async c =>
  521. {
  522. c.Response.StatusCode = 200;
  523. c.Response.Headers[HeaderNames.TransferEncoding] = "test";
  524. c.Response.ContentType = "text/plain";
  525. await c.Response.WriteAsync("test");
  526. },
  527. options,
  528. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  529. var httpContext = new DefaultHttpContext();
  530. await middleware.Invoke(httpContext);
  531. Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
  532. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
  533. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Body: test"));
  534. }
  535. [Fact]
  536. public async Task StatusCodeLogs()
  537. {
  538. var options = CreateOptionsAccessor();
  539. options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseStatusCode;
  540. var middleware = new HttpLoggingMiddleware(
  541. async c =>
  542. {
  543. c.Response.StatusCode = 200;
  544. c.Response.Headers["Server"] = "Kestrel";
  545. c.Response.ContentType = "text/plain";
  546. await c.Response.WriteAsync("test");
  547. },
  548. options,
  549. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  550. var httpContext = new DefaultHttpContext();
  551. await middleware.Invoke(httpContext);
  552. Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
  553. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Server: Kestrel"));
  554. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  555. }
  556. [Fact]
  557. public async Task ResponseHeadersLogs()
  558. {
  559. var options = CreateOptionsAccessor();
  560. options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseHeaders;
  561. var middleware = new HttpLoggingMiddleware(
  562. async c =>
  563. {
  564. c.Response.StatusCode = 200;
  565. c.Response.Headers[HeaderNames.TransferEncoding] = "test";
  566. c.Response.ContentType = "text/plain";
  567. await c.Response.WriteAsync("test");
  568. },
  569. options,
  570. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  571. var httpContext = new DefaultHttpContext();
  572. await middleware.Invoke(httpContext);
  573. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
  574. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
  575. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  576. }
  577. [Fact]
  578. public async Task ResponseHeadersRedacted()
  579. {
  580. var options = CreateOptionsAccessor();
  581. options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseHeaders;
  582. var middleware = new HttpLoggingMiddleware(
  583. c =>
  584. {
  585. c.Response.Headers["Test"] = "Kestrel";
  586. return Task.CompletedTask;
  587. },
  588. options,
  589. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  590. var httpContext = new DefaultHttpContext();
  591. await middleware.Invoke(httpContext);
  592. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Test: [Redacted]"));
  593. }
  594. [Fact]
  595. public async Task AllowedResponseHeadersModify()
  596. {
  597. var options = CreateOptionsAccessor();
  598. options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseHeaders;
  599. options.CurrentValue.ResponseHeaders.Clear();
  600. options.CurrentValue.ResponseHeaders.Add("Test");
  601. var middleware = new HttpLoggingMiddleware(
  602. c =>
  603. {
  604. c.Response.Headers["Test"] = "Kestrel";
  605. c.Response.Headers["Server"] = "Kestrel";
  606. return Task.CompletedTask;
  607. },
  608. options,
  609. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  610. var httpContext = new DefaultHttpContext();
  611. await middleware.Invoke(httpContext);
  612. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Test: Kestrel"));
  613. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Server: [Redacted]"));
  614. }
  615. [Theory]
  616. [MemberData(nameof(BodyData))]
  617. public async Task ResponseBodyWritingWorks(string expected)
  618. {
  619. var options = CreateOptionsAccessor();
  620. options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseBody;
  621. var middleware = new HttpLoggingMiddleware(
  622. c =>
  623. {
  624. c.Response.ContentType = "text/plain";
  625. return c.Response.WriteAsync(expected);
  626. },
  627. options,
  628. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  629. var httpContext = new DefaultHttpContext();
  630. await middleware.Invoke(httpContext);
  631. Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
  632. }
  633. [Fact]
  634. public async Task ResponseBodyWritingLimitWorks()
  635. {
  636. var input = string.Concat(new string('a', 30000), new string('b', 3000));
  637. var options = CreateOptionsAccessor();
  638. options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseBody;
  639. var middleware = new HttpLoggingMiddleware(
  640. c =>
  641. {
  642. c.Response.ContentType = "text/plain";
  643. return c.Response.WriteAsync(input);
  644. },
  645. options,
  646. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  647. var httpContext = new DefaultHttpContext();
  648. await middleware.Invoke(httpContext);
  649. var expected = input.Substring(0, options.CurrentValue.ResponseBodyLogLimit);
  650. Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
  651. }
  652. [Fact]
  653. public async Task FirstWriteResponseHeadersLogged()
  654. {
  655. var options = CreateOptionsAccessor();
  656. options.CurrentValue.LoggingFields = HttpLoggingFields.Response;
  657. var writtenHeaders = new TaskCompletionSource<object>();
  658. var letBodyFinish = new TaskCompletionSource<object>();
  659. var middleware = new HttpLoggingMiddleware(
  660. async c =>
  661. {
  662. c.Response.StatusCode = 200;
  663. c.Response.Headers[HeaderNames.TransferEncoding] = "test";
  664. c.Response.ContentType = "text/plain";
  665. await c.Response.WriteAsync("test");
  666. writtenHeaders.SetResult(null);
  667. await letBodyFinish.Task;
  668. },
  669. options,
  670. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  671. var httpContext = new DefaultHttpContext();
  672. var middlewareTask = middleware.Invoke(httpContext);
  673. await writtenHeaders.Task;
  674. Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
  675. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
  676. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  677. letBodyFinish.SetResult(null);
  678. await middlewareTask;
  679. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Body: test"));
  680. }
  681. [Fact]
  682. public async Task StartAsyncResponseHeadersLogged()
  683. {
  684. var options = CreateOptionsAccessor();
  685. options.CurrentValue.LoggingFields = HttpLoggingFields.Response;
  686. var writtenHeaders = new TaskCompletionSource<object>();
  687. var letBodyFinish = new TaskCompletionSource<object>();
  688. var middleware = new HttpLoggingMiddleware(
  689. async c =>
  690. {
  691. c.Response.StatusCode = 200;
  692. c.Response.Headers[HeaderNames.TransferEncoding] = "test";
  693. c.Response.ContentType = "text/plain";
  694. await c.Response.StartAsync();
  695. writtenHeaders.SetResult(null);
  696. await letBodyFinish.Task;
  697. },
  698. options,
  699. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  700. var httpContext = new DefaultHttpContext();
  701. var middlewareTask = middleware.Invoke(httpContext);
  702. await writtenHeaders.Task;
  703. Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
  704. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
  705. Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
  706. letBodyFinish.SetResult(null);
  707. await middlewareTask;
  708. }
  709. [Fact]
  710. public async Task UnrecognizedMediaType()
  711. {
  712. var expected = "Hello world";
  713. var options = CreateOptionsAccessor();
  714. options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseBody;
  715. var middleware = new HttpLoggingMiddleware(
  716. c =>
  717. {
  718. c.Response.ContentType = "foo/*";
  719. return c.Response.WriteAsync(expected);
  720. },
  721. options,
  722. LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
  723. var httpContext = new DefaultHttpContext();
  724. await middleware.Invoke(httpContext);
  725. Assert.Contains(TestSink.Writes, w => w.Message.Contains("Unrecognized Content-Type for body."));
  726. }
  727. private IOptionsMonitor<HttpLoggingOptions> CreateOptionsAccessor()
  728. {
  729. var options = new HttpLoggingOptions();
  730. var optionsAccessor = Mock.Of<IOptionsMonitor<HttpLoggingOptions>>(o => o.CurrentValue == options);
  731. return optionsAccessor;
  732. }
  733. }
  734. }