WebRequestStream.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. //
  2. // WebRequestStream.cs
  3. //
  4. // Author:
  5. // Martin Baulig <[email protected]>
  6. //
  7. // Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com)
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. using System.IO;
  27. using System.Text;
  28. using System.Threading;
  29. using System.Threading.Tasks;
  30. using System.Runtime.ExceptionServices;
  31. using System.Net.Sockets;
  32. namespace System.Net
  33. {
  34. class WebRequestStream : WebConnectionStream
  35. {
  36. static byte[] crlf = new byte[] { 13, 10 };
  37. MemoryStream writeBuffer;
  38. bool requestWritten;
  39. bool allowBuffering;
  40. bool sendChunked;
  41. TaskCompletionSource<int> pendingWrite;
  42. long totalWritten;
  43. byte[] headers;
  44. bool headersSent;
  45. int completeRequestWritten;
  46. int chunkTrailerWritten;
  47. internal readonly string ME;
  48. public WebRequestStream (WebConnection connection, WebOperation operation,
  49. Stream stream, WebConnectionTunnel tunnel)
  50. : base (connection, operation, stream)
  51. {
  52. allowBuffering = operation.Request.InternalAllowBuffering;
  53. sendChunked = operation.Request.SendChunked && operation.WriteBuffer == null;
  54. if (!sendChunked && allowBuffering && operation.WriteBuffer == null)
  55. writeBuffer = new MemoryStream ();
  56. KeepAlive = Request.KeepAlive;
  57. if (tunnel?.ProxyVersion != null && tunnel?.ProxyVersion != HttpVersion.Version11)
  58. KeepAlive = false;
  59. #if MONO_WEB_DEBUG
  60. ME = $"WRQ(Cnc={Connection.ID}, Op={Operation.ID})";
  61. #endif
  62. }
  63. public bool KeepAlive {
  64. get;
  65. }
  66. public override long Length {
  67. get {
  68. throw new NotSupportedException ();
  69. }
  70. }
  71. public override bool CanRead => false;
  72. public override bool CanWrite => true;
  73. internal bool SendChunked {
  74. get { return sendChunked; }
  75. set { sendChunked = value; }
  76. }
  77. internal bool HasWriteBuffer {
  78. get {
  79. return Operation.WriteBuffer != null || writeBuffer != null;
  80. }
  81. }
  82. internal int WriteBufferLength {
  83. get {
  84. if (Operation.WriteBuffer != null)
  85. return Operation.WriteBuffer.Size;
  86. if (writeBuffer != null)
  87. return (int)writeBuffer.Length;
  88. return -1;
  89. }
  90. }
  91. internal BufferOffsetSize GetWriteBuffer ()
  92. {
  93. if (Operation.WriteBuffer != null)
  94. return Operation.WriteBuffer;
  95. if (writeBuffer == null || writeBuffer.Length == 0)
  96. return null;
  97. var buffer = writeBuffer.GetBuffer ();
  98. return new BufferOffsetSize (buffer, 0, (int)writeBuffer.Length, false);
  99. }
  100. async Task FinishWriting (CancellationToken cancellationToken)
  101. {
  102. if (Interlocked.CompareExchange (ref completeRequestWritten, 1, 0) != 0)
  103. return;
  104. WebConnection.Debug ($"{ME} FINISH WRITING: {sendChunked}");
  105. try {
  106. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  107. if (sendChunked)
  108. await WriteChunkTrailer_inner (cancellationToken).ConfigureAwait (false);
  109. } catch (Exception ex) {
  110. Operation.CompleteRequestWritten (this, ex);
  111. throw;
  112. } finally {
  113. WebConnection.Debug ($"{ME} FINISH WRITING DONE");
  114. }
  115. Operation.CompleteRequestWritten (this);
  116. }
  117. public override async Task WriteAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
  118. {
  119. WebConnection.Debug ($"{ME} WRITE ASYNC: {buffer.Length}/{offset}/{size}");
  120. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  121. if (Operation.WriteBuffer != null)
  122. throw new InvalidOperationException ();
  123. if (buffer == null)
  124. throw new ArgumentNullException (nameof (buffer));
  125. int length = buffer.Length;
  126. if (offset < 0 || length < offset)
  127. throw new ArgumentOutOfRangeException (nameof (offset));
  128. if (size < 0 || (length - offset) < size)
  129. throw new ArgumentOutOfRangeException (nameof (size));
  130. var myWriteTcs = new TaskCompletionSource<int> ();
  131. if (Interlocked.CompareExchange (ref pendingWrite, myWriteTcs, null) != null)
  132. throw new InvalidOperationException (SR.GetString (SR.net_repcall));
  133. try {
  134. await ProcessWrite (buffer, offset, size, cancellationToken).ConfigureAwait (false);
  135. WebConnection.Debug ($"{ME} WRITE ASYNC #1: {allowBuffering} {sendChunked} {Request.ContentLength} {totalWritten}");
  136. if (Request.ContentLength > 0 && totalWritten == Request.ContentLength)
  137. await FinishWriting (cancellationToken);
  138. pendingWrite = null;
  139. myWriteTcs.TrySetResult (0);
  140. } catch (Exception ex) {
  141. KillBuffer ();
  142. closed = true;
  143. WebConnection.Debug ($"{ME} WRITE ASYNC EX: {ex.Message}");
  144. if (ex is SocketException)
  145. ex = new IOException ("Error writing request", ex);
  146. Operation.CompleteRequestWritten (this, ex);
  147. pendingWrite = null;
  148. myWriteTcs.TrySetException (ex);
  149. throw;
  150. }
  151. }
  152. async Task ProcessWrite (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
  153. {
  154. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  155. if (sendChunked) {
  156. requestWritten = true;
  157. string cSize = String.Format ("{0:X}\r\n", size);
  158. byte[] head = Encoding.ASCII.GetBytes (cSize);
  159. int chunkSize = 2 + size + head.Length;
  160. byte[] newBuffer = new byte[chunkSize];
  161. Buffer.BlockCopy (head, 0, newBuffer, 0, head.Length);
  162. Buffer.BlockCopy (buffer, offset, newBuffer, head.Length, size);
  163. Buffer.BlockCopy (crlf, 0, newBuffer, head.Length + size, crlf.Length);
  164. if (allowBuffering) {
  165. if (writeBuffer == null)
  166. writeBuffer = new MemoryStream ();
  167. writeBuffer.Write (buffer, offset, size);
  168. }
  169. totalWritten += size;
  170. buffer = newBuffer;
  171. offset = 0;
  172. size = chunkSize;
  173. } else {
  174. CheckWriteOverflow (Request.ContentLength, totalWritten, size);
  175. if (allowBuffering) {
  176. if (writeBuffer == null)
  177. writeBuffer = new MemoryStream ();
  178. writeBuffer.Write (buffer, offset, size);
  179. totalWritten += size;
  180. if (Request.ContentLength <= 0 || totalWritten < Request.ContentLength)
  181. return;
  182. requestWritten = true;
  183. buffer = writeBuffer.GetBuffer ();
  184. offset = 0;
  185. size = (int)totalWritten;
  186. } else {
  187. totalWritten += size;
  188. }
  189. }
  190. try {
  191. await InnerStream.WriteAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false);
  192. } catch {
  193. if (!IgnoreIOErrors)
  194. throw;
  195. }
  196. }
  197. void CheckWriteOverflow (long contentLength, long totalWritten, long size)
  198. {
  199. if (contentLength == -1)
  200. return;
  201. long avail = contentLength - totalWritten;
  202. if (size > avail) {
  203. KillBuffer ();
  204. closed = true;
  205. var throwMe = new ProtocolViolationException (
  206. "The number of bytes to be written is greater than " +
  207. "the specified ContentLength.");
  208. Operation.CompleteRequestWritten (this, throwMe);
  209. throw throwMe;
  210. }
  211. }
  212. internal async Task Initialize (CancellationToken cancellationToken)
  213. {
  214. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  215. WebConnection.Debug ($"{ME} INIT: {Operation.WriteBuffer != null}");
  216. if (Operation.WriteBuffer != null) {
  217. if (Operation.IsNtlmChallenge)
  218. Request.InternalContentLength = 0;
  219. else
  220. Request.InternalContentLength = Operation.WriteBuffer.Size;
  221. }
  222. await SetHeadersAsync (false, cancellationToken).ConfigureAwait (false);
  223. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  224. if (Operation.WriteBuffer != null && !Operation.IsNtlmChallenge) {
  225. await WriteRequestAsync (cancellationToken);
  226. Close ();
  227. }
  228. }
  229. async Task SetHeadersAsync (bool setInternalLength, CancellationToken cancellationToken)
  230. {
  231. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  232. if (headersSent)
  233. return;
  234. string method = Request.Method;
  235. bool no_writestream = (method == "GET" || method == "CONNECT" || method == "HEAD" ||
  236. method == "TRACE");
  237. bool webdav = (method == "PROPFIND" || method == "PROPPATCH" || method == "MKCOL" ||
  238. method == "COPY" || method == "MOVE" || method == "LOCK" ||
  239. method == "UNLOCK");
  240. if (Operation.IsNtlmChallenge)
  241. no_writestream = true;
  242. if (setInternalLength && !no_writestream && HasWriteBuffer)
  243. Request.InternalContentLength = WriteBufferLength;
  244. bool has_content = !no_writestream && (!HasWriteBuffer || Request.ContentLength > -1);
  245. if (!(sendChunked || has_content || no_writestream || webdav))
  246. return;
  247. headersSent = true;
  248. headers = Request.GetRequestHeaders ();
  249. WebConnection.Debug ($"{ME} SET HEADERS: {Request.ContentLength}");
  250. try {
  251. await InnerStream.WriteAsync (headers, 0, headers.Length, cancellationToken).ConfigureAwait (false);
  252. var cl = Request.ContentLength;
  253. if (!sendChunked && cl == 0)
  254. requestWritten = true;
  255. } catch (Exception e) {
  256. if (e is WebException || e is OperationCanceledException)
  257. throw;
  258. throw new WebException ("Error writing headers", WebExceptionStatus.SendFailure, WebExceptionInternalStatus.RequestFatal, e);
  259. }
  260. }
  261. internal async Task WriteRequestAsync (CancellationToken cancellationToken)
  262. {
  263. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  264. WebConnection.Debug ($"{ME} WRITE REQUEST: {requestWritten} {sendChunked} {allowBuffering} {HasWriteBuffer}");
  265. if (requestWritten)
  266. return;
  267. requestWritten = true;
  268. if (sendChunked || !HasWriteBuffer)
  269. return;
  270. BufferOffsetSize buffer = GetWriteBuffer ();
  271. if (buffer != null && !Operation.IsNtlmChallenge && Request.ContentLength != -1 && Request.ContentLength < buffer.Size) {
  272. closed = true;
  273. var throwMe = new WebException ("Specified Content-Length is less than the number of bytes to write", null,
  274. WebExceptionStatus.ServerProtocolViolation, null);
  275. Operation.CompleteRequestWritten (this, throwMe);
  276. throw throwMe;
  277. }
  278. await SetHeadersAsync (true, cancellationToken).ConfigureAwait (false);
  279. WebConnection.Debug ($"{ME} WRITE REQUEST #1: {buffer != null}");
  280. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  281. if (buffer != null && buffer.Size > 0)
  282. await InnerStream.WriteAsync (buffer.Buffer, 0, buffer.Size, cancellationToken);
  283. await FinishWriting (cancellationToken);
  284. }
  285. async Task WriteChunkTrailer_inner (CancellationToken cancellationToken)
  286. {
  287. if (Interlocked.CompareExchange (ref chunkTrailerWritten, 1, 0) != 0)
  288. return;
  289. Operation.ThrowIfClosedOrDisposed (cancellationToken);
  290. byte[] chunk = Encoding.ASCII.GetBytes ("0\r\n\r\n");
  291. await InnerStream.WriteAsync (chunk, 0, chunk.Length, cancellationToken).ConfigureAwait (false);
  292. }
  293. async Task WriteChunkTrailer ()
  294. {
  295. using (var cts = new CancellationTokenSource ()) {
  296. cts.CancelAfter (WriteTimeout);
  297. var timeoutTask = Task.Delay (WriteTimeout);
  298. while (true) {
  299. var myWriteTcs = new TaskCompletionSource<int> ();
  300. var oldTcs = Interlocked.CompareExchange (ref pendingWrite, myWriteTcs, null);
  301. if (oldTcs == null)
  302. break;
  303. var ret = await Task.WhenAny (timeoutTask, oldTcs.Task).ConfigureAwait (false);
  304. if (ret == timeoutTask)
  305. throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout);
  306. }
  307. try {
  308. await WriteChunkTrailer_inner (cts.Token).ConfigureAwait (false);
  309. } catch {
  310. // Intentionally eating exceptions.
  311. } finally {
  312. pendingWrite = null;
  313. }
  314. }
  315. }
  316. internal void KillBuffer ()
  317. {
  318. writeBuffer = null;
  319. }
  320. public override Task<int> ReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
  321. {
  322. return Task.FromException<int> (new NotSupportedException (SR.net_writeonlystream));
  323. }
  324. protected override void Close_internal (ref bool disposed)
  325. {
  326. WebConnection.Debug ($"{ME} CLOSE: {disposed} {requestWritten} {allowBuffering}");
  327. if (disposed)
  328. return;
  329. disposed = true;
  330. if (sendChunked) {
  331. // Don't use FinishWriting() here, we need to block on 'pendingWrite' to ensure that
  332. // any pending WriteAsync() has been completed.
  333. //
  334. // FIXME: I belive .NET simply aborts if you call Close() or Dispose() while writing,
  335. // need to check this. 2017/07/17 Martin.
  336. WriteChunkTrailer ().Wait ();
  337. return;
  338. }
  339. if (!allowBuffering || requestWritten) {
  340. Operation.CompleteRequestWritten (this);
  341. return;
  342. }
  343. long length = Request.ContentLength;
  344. if (!sendChunked && !Operation.IsNtlmChallenge && length != -1 && totalWritten != length) {
  345. IOException io = new IOException ("Cannot close the stream until all bytes are written");
  346. closed = true;
  347. disposed = true;
  348. var throwMe = new WebException ("Request was cancelled.", WebExceptionStatus.RequestCanceled, WebExceptionInternalStatus.RequestFatal, io);
  349. Operation.CompleteRequestWritten (this, throwMe);
  350. throw throwMe;
  351. }
  352. // Commented out the next line to fix xamarin bug #1512
  353. //WriteRequest ();
  354. disposed = true;
  355. Operation.CompleteRequestWritten (this);
  356. }
  357. }
  358. }