Xamarin Public Jenkins (auto-signing) 01c08d50e8 Imported Upstream version 5.14.0.148
Former-commit-id: ccfce85f9487e4135d045a812192413d573f33be
2018-06-28 08:19:18 +00:00

740 lines
22 KiB
C#

//
// WebResponseStream.cs
//
// Author:
// Martin Baulig <mabaul@microsoft.com>
//
// Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.ExceptionServices;
using System.Net.Sockets;
namespace System.Net
{
class WebResponseStream : WebConnectionStream
{
BufferOffsetSize readBuffer;
long contentLength;
long totalRead;
bool nextReadCalled;
int stream_length; // -1 when CL not present
WebCompletionSource pendingRead;
object locker = new object ();
int nestedRead;
bool read_eof;
public WebRequestStream RequestStream {
get;
}
public WebHeaderCollection Headers {
get;
private set;
}
public HttpStatusCode StatusCode {
get;
private set;
}
public string StatusDescription {
get;
private set;
}
public Version Version {
get;
private set;
}
public bool KeepAlive {
get;
private set;
}
internal readonly string ME;
public WebResponseStream (WebRequestStream request)
: base (request.Connection, request.Operation, request.InnerStream)
{
RequestStream = request;
#if MONO_WEB_DEBUG
ME = $"WRP(Cnc={Connection.ID}, Op={Operation.ID})";
#endif
}
public override long Length {
get {
return stream_length;
}
}
public override bool CanRead => true;
public override bool CanWrite => false;
protected bool ChunkedRead {
get;
private set;
}
protected MonoChunkStream ChunkStream {
get;
private set;
}
public override async Task<int> ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
WebConnection.Debug ($"{ME} READ ASYNC");
cancellationToken.ThrowIfCancellationRequested ();
if (buffer == null)
throw new ArgumentNullException (nameof (buffer));
int length = buffer.Length;
if (offset < 0 || length < offset)
throw new ArgumentOutOfRangeException (nameof (offset));
if (count < 0 || (length - offset) < count)
throw new ArgumentOutOfRangeException (nameof (count));
if (Interlocked.CompareExchange (ref nestedRead, 1, 0) != 0)
throw new InvalidOperationException ("Invalid nested call.");
var completion = new WebCompletionSource ();
while (!cancellationToken.IsCancellationRequested) {
/*
* 'currentRead' is set by ReadAllAsync().
*/
var oldCompletion = Interlocked.CompareExchange (ref pendingRead, completion, null);
WebConnection.Debug ($"{ME} READ ASYNC #1: {oldCompletion != null}");
if (oldCompletion == null)
break;
await oldCompletion.WaitForCompletion ().ConfigureAwait (false);
}
WebConnection.Debug ($"{ME} READ ASYNC #2: {totalRead} {contentLength}");
int oldBytes = 0, nbytes = 0;
Exception throwMe = null;
try {
// FIXME: NetworkStream.ReadAsync() does not support cancellation.
(oldBytes, nbytes) = await HttpWebRequest.RunWithTimeout (
ct => ProcessRead (buffer, offset, count, ct),
ReadTimeout, () => {
Operation.Abort ();
InnerStream.Dispose ();
}, () => Operation.Aborted, cancellationToken).ConfigureAwait (false);
} catch (Exception e) {
throwMe = GetReadException (WebExceptionStatus.ReceiveFailure, e, "ReadAsync");
}
WebConnection.Debug ($"{ME} READ ASYNC #3: {totalRead} {contentLength} - {oldBytes} {nbytes} {throwMe?.Message}");
if (throwMe != null) {
lock (locker) {
completion.TrySetException (throwMe);
pendingRead = null;
nestedRead = 0;
}
closed = true;
Operation.Finish (false, throwMe);
throw throwMe;
}
lock (locker) {
pendingRead.TrySetCompleted ();
pendingRead = null;
nestedRead = 0;
}
if (totalRead >= contentLength && !nextReadCalled) {
WebConnection.Debug ($"{ME} READ ASYNC - READ COMPLETE: {oldBytes} {nbytes} - {totalRead} {contentLength} {nextReadCalled}");
if (!nextReadCalled) {
nextReadCalled = true;
Operation.Finish (true);
}
}
return oldBytes + nbytes;
}
async Task<(int, int)> ProcessRead (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
{
WebConnection.Debug ($"{ME} PROCESS READ: {totalRead} {contentLength}");
cancellationToken.ThrowIfCancellationRequested ();
if (totalRead >= contentLength) {
read_eof = true;
contentLength = totalRead;
return (0, 0);
}
int oldBytes = 0;
int remaining = readBuffer?.Size ?? 0;
if (remaining > 0) {
int copy = (remaining > size) ? size : remaining;
Buffer.BlockCopy (readBuffer.Buffer, readBuffer.Offset, buffer, offset, copy);
readBuffer.Offset += copy;
readBuffer.Size -= copy;
offset += copy;
size -= copy;
totalRead += copy;
if (totalRead >= contentLength) {
contentLength = totalRead;
read_eof = true;
}
if (size == 0 || totalRead >= contentLength)
return (0, copy);
oldBytes = copy;
}
if (contentLength != Int64.MaxValue && contentLength - totalRead < size)
size = (int)(contentLength - totalRead);
WebConnection.Debug ($"{ME} PROCESS READ #1: {oldBytes} {size} {read_eof}");
if (read_eof) {
contentLength = totalRead;
return (oldBytes, 0);
}
var ret = await InnerReadAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false);
if (ret <= 0) {
read_eof = true;
contentLength = totalRead;
return (oldBytes, 0);
}
totalRead += ret;
return (oldBytes, ret);
}
internal async Task<int> InnerReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
{
WebConnection.Debug ($"{ME} INNER READ ASYNC");
Operation.ThrowIfDisposed (cancellationToken);
int nbytes = 0;
bool done = false;
if (!ChunkedRead || (!ChunkStream.DataAvailable && ChunkStream.WantMore)) {
nbytes = await InnerStream.ReadAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false);
WebConnection.Debug ($"{ME} INNER READ ASYNC #1: {nbytes} {ChunkedRead}");
if (!ChunkedRead)
return nbytes;
done = nbytes == 0;
}
try {
ChunkStream.WriteAndReadBack (buffer, offset, size, ref nbytes);
WebConnection.Debug ($"{ME} INNER READ ASYNC #1: {done} {nbytes} {ChunkStream.WantMore}");
if (!done && nbytes == 0 && ChunkStream.WantMore)
nbytes = await EnsureReadAsync (buffer, offset, size, cancellationToken).ConfigureAwait (false);
} catch (Exception e) {
if (e is WebException || e is OperationCanceledException)
throw;
throw new WebException ("Invalid chunked data.", e, WebExceptionStatus.ServerProtocolViolation, null);
}
if ((done || nbytes == 0) && ChunkStream.ChunkLeft != 0) {
// HandleError (WebExceptionStatus.ReceiveFailure, null, "chunked EndRead");
throw new WebException ("Read error", null, WebExceptionStatus.ReceiveFailure, null);
}
return nbytes;
}
async Task<int> EnsureReadAsync (byte[] buffer, int offset, int size, CancellationToken cancellationToken)
{
byte[] morebytes = null;
int nbytes = 0;
while (nbytes == 0 && ChunkStream.WantMore && !cancellationToken.IsCancellationRequested) {
int localsize = ChunkStream.ChunkLeft;
if (localsize <= 0) // not read chunk size yet
localsize = 1024;
else if (localsize > 16384)
localsize = 16384;
if (morebytes == null || morebytes.Length < localsize)
morebytes = new byte[localsize];
int nread = await InnerStream.ReadAsync (morebytes, 0, localsize, cancellationToken).ConfigureAwait (false);
if (nread <= 0)
return 0; // Error
ChunkStream.Write (morebytes, 0, nread);
nbytes += ChunkStream.Read (buffer, offset + nbytes, size - nbytes);
}
return nbytes;
}
bool CheckAuthHeader (string headerName)
{
var authHeader = Headers[headerName];
return (authHeader != null && authHeader.IndexOf ("NTLM", StringComparison.Ordinal) != -1);
}
bool IsNtlmAuth ()
{
bool isProxy = (Request.Proxy != null && !Request.Proxy.IsBypassed (Request.Address));
if (isProxy && CheckAuthHeader ("Proxy-Authenticate"))
return true;
return CheckAuthHeader ("WWW-Authenticate");
}
bool ExpectContent {
get {
if (Request.Method == "HEAD")
return false;
return ((int)StatusCode >= 200 && (int)StatusCode != 204 && (int)StatusCode != 304);
}
}
async Task Initialize (BufferOffsetSize buffer, CancellationToken cancellationToken)
{
WebConnection.Debug ($"{ME} INIT: status={(int)StatusCode} bos={buffer.Offset}/{buffer.Size}");
string contentType = Headers["Transfer-Encoding"];
bool chunkedRead = (contentType != null && contentType.IndexOf ("chunked", StringComparison.OrdinalIgnoreCase) != -1);
string clength = Headers["Content-Length"];
if (!chunkedRead && !string.IsNullOrEmpty (clength)) {
if (!long.TryParse (clength, out contentLength))
contentLength = Int64.MaxValue;
} else {
contentLength = Int64.MaxValue;
}
if (Version == HttpVersion.Version11 && RequestStream.KeepAlive) {
KeepAlive = true;
var cncHeader = Headers[ServicePoint.UsesProxy ? "Proxy-Connection" : "Connection"];
if (cncHeader != null) {
cncHeader = cncHeader.ToLower ();
KeepAlive = cncHeader.IndexOf ("keep-alive", StringComparison.Ordinal) != -1;
if (cncHeader.IndexOf ("close", StringComparison.Ordinal) != -1)
KeepAlive = false;
}
}
// Negative numbers?
if (!Int32.TryParse (clength, out stream_length))
stream_length = -1;
string me = "WebResponseStream.Initialize()";
string tencoding = null;
if (ExpectContent)
tencoding = Headers["Transfer-Encoding"];
ChunkedRead = (tencoding != null && tencoding.IndexOf ("chunked", StringComparison.OrdinalIgnoreCase) != -1);
if (!ChunkedRead) {
readBuffer = buffer;
try {
if (contentLength > 0 && readBuffer.Size >= contentLength) {
if (!IsNtlmAuth ())
await ReadAllAsync (false, cancellationToken).ConfigureAwait (false);
}
} catch (Exception e) {
throw GetReadException (WebExceptionStatus.ReceiveFailure, e, me);
}
} else if (ChunkStream == null) {
try {
ChunkStream = new MonoChunkStream (buffer.Buffer, buffer.Offset, buffer.Offset + buffer.Size, Headers);
} catch (Exception e) {
throw GetReadException (WebExceptionStatus.ServerProtocolViolation, e, me);
}
} else {
ChunkStream.ResetBuffer ();
try {
ChunkStream.Write (buffer.Buffer, buffer.Offset, buffer.Size);
} catch (Exception e) {
throw GetReadException (WebExceptionStatus.ServerProtocolViolation, e, me);
}
}
WebConnection.Debug ($"{ME} INIT #1: - {ExpectContent} {closed} {nextReadCalled}");
if (!ExpectContent) {
if (!closed && !nextReadCalled) {
if (contentLength == Int64.MaxValue)
contentLength = 0;
nextReadCalled = true;
}
Operation.Finish (true);
}
}
internal async Task ReadAllAsync (bool resending, CancellationToken cancellationToken)
{
WebConnection.Debug ($"{ME} READ ALL ASYNC: resending={resending} eof={read_eof} total={totalRead} " +
"length={contentLength} nextReadCalled={nextReadCalled}");
if (read_eof || totalRead >= contentLength || nextReadCalled) {
if (!nextReadCalled) {
nextReadCalled = true;
Operation.Finish (true);
}
return;
}
var completion = new WebCompletionSource ();
var timeoutCts = new CancellationTokenSource ();
try {
var timeoutTask = Task.Delay (ReadTimeout, timeoutCts.Token);
while (true) {
/*
* 'currentRead' is set by ReadAsync().
*/
cancellationToken.ThrowIfCancellationRequested ();
var oldCompletion = Interlocked.CompareExchange (ref pendingRead, completion, null);
if (oldCompletion == null)
break;
// ReadAsync() is in progress.
var oldReadTask = oldCompletion.WaitForCompletion ();
var anyTask = await Task.WhenAny (oldReadTask, timeoutTask).ConfigureAwait (false);
if (anyTask == timeoutTask)
throw new WebException ("The operation has timed out.", WebExceptionStatus.Timeout);
}
} finally {
timeoutCts.Cancel ();
timeoutCts.Dispose ();
}
WebConnection.Debug ($"{ME} READ ALL ASYNC #1");
cancellationToken.ThrowIfCancellationRequested ();
try {
if (totalRead >= contentLength)
return;
byte[] b = null;
int new_size;
if (contentLength == Int64.MaxValue && !ChunkedRead) {
WebConnection.Debug ($"{ME} READ ALL ASYNC - NEITHER LENGTH NOR CHUNKED");
/*
* This is a violation of the HTTP Spec - the server neither send a
* "Content-Length:" nor a "Transfer-Encoding: chunked" header.
*
* When we're redirecting or resending for NTLM, then we can simply close
* the connection here.
*
* However, if it's the final reply, then we need to try our best to read it.
*/
if (resending) {
Close ();
return;
}
KeepAlive = false;
}
if (contentLength == Int64.MaxValue) {
MemoryStream ms = new MemoryStream ();
BufferOffsetSize buffer = null;
if (readBuffer != null && readBuffer.Size > 0) {
ms.Write (readBuffer.Buffer, readBuffer.Offset, readBuffer.Size);
readBuffer.Offset = 0;
readBuffer.Size = readBuffer.Buffer.Length;
if (readBuffer.Buffer.Length >= 8192)
buffer = readBuffer;
}
if (buffer == null)
buffer = new BufferOffsetSize (new byte[8192], false);
int read;
while ((read = await InnerReadAsync (buffer.Buffer, buffer.Offset, buffer.Size, cancellationToken)) != 0)
ms.Write (buffer.Buffer, buffer.Offset, read);
new_size = (int)ms.Length;
contentLength = new_size;
readBuffer = new BufferOffsetSize (ms.GetBuffer (), 0, new_size, false);
} else {
new_size = (int)(contentLength - totalRead);
b = new byte[new_size];
int readSize = 0;
if (readBuffer != null && readBuffer.Size > 0) {
readSize = readBuffer.Size;
if (readSize > new_size)
readSize = new_size;
Buffer.BlockCopy (readBuffer.Buffer, readBuffer.Offset, b, 0, readSize);
}
int remaining = new_size - readSize;
int r = -1;
while (remaining > 0 && r != 0) {
r = await InnerReadAsync (b, readSize, remaining, cancellationToken);
remaining -= r;
readSize += r;
}
}
readBuffer = new BufferOffsetSize (b, 0, new_size, false);
totalRead = 0;
nextReadCalled = true;
completion.TrySetCompleted ();
} catch (Exception ex) {
WebConnection.Debug ($"{ME} READ ALL ASYNC EX: {ex.Message}");
completion.TrySetException (ex);
throw;
} finally {
WebConnection.Debug ($"{ME} READ ALL ASYNC #2");
pendingRead = null;
}
Operation.Finish (true);
}
public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return Task.FromException (new NotSupportedException (SR.net_readonlystream));
}
protected override void Close_internal (ref bool disposed)
{
WebConnection.Debug ($"{ME} CLOSE: {disposed} {closed} {nextReadCalled}");
if (!closed && !nextReadCalled) {
nextReadCalled = true;
if (totalRead >= contentLength) {
disposed = true;
Operation.Finish (true);
} else {
// If we have not read all the contents
closed = true;
disposed = true;
Operation.Finish (false);
}
}
}
WebException GetReadException (WebExceptionStatus status, Exception error, string where)
{
error = GetException (error);
string msg = $"Error getting response stream ({where}): {status}";
if (error == null)
return new WebException ($"Error getting response stream ({where}): {status}", status);
if (error is WebException wexc)
return wexc;
if (Operation.Aborted || error is OperationCanceledException || error is ObjectDisposedException)
return HttpWebRequest.CreateRequestAbortedException ();
return new WebException ($"Error getting response stream ({where}): {status} {error.Message}", status,
WebExceptionInternalStatus.RequestFatal, error);
}
internal async Task InitReadAsync (CancellationToken cancellationToken)
{
WebConnection.Debug ($"{ME} INIT READ ASYNC");
var buffer = new BufferOffsetSize (new byte[4096], false);
var state = ReadState.None;
int position = 0;
while (true) {
Operation.ThrowIfClosedOrDisposed (cancellationToken);
WebConnection.Debug ($"{ME} INIT READ ASYNC LOOP: {state} {position} - {buffer.Offset}/{buffer.Size}");
var nread = await InnerStream.ReadAsync (
buffer.Buffer, buffer.Offset, buffer.Size, cancellationToken).ConfigureAwait (false);
WebConnection.Debug ($"{ME} INIT READ ASYNC LOOP #1: {state} {position} - {buffer.Offset}/{buffer.Size} - {nread}");
if (nread == 0)
throw GetReadException (WebExceptionStatus.ReceiveFailure, null, "ReadDoneAsync2");
if (nread < 0)
throw GetReadException (WebExceptionStatus.ServerProtocolViolation, null, "ReadDoneAsync3");
buffer.Offset += nread;
buffer.Size -= nread;
if (state == ReadState.None) {
try {
var oldPos = position;
if (!GetResponse (buffer, ref position, ref state))
position = oldPos;
} catch (Exception e) {
WebConnection.Debug ($"{ME} INIT READ ASYNC FAILED: {e.Message}\n{e}");
throw GetReadException (WebExceptionStatus.ServerProtocolViolation, e, "ReadDoneAsync4");
}
}
if (state == ReadState.Aborted)
throw GetReadException (WebExceptionStatus.RequestCanceled, null, "ReadDoneAsync5");
if (state == ReadState.Content) {
buffer.Size = buffer.Offset - position;
buffer.Offset = position;
break;
}
int est = nread * 2;
if (est > buffer.Size) {
var newBuffer = new byte [buffer.Buffer.Length + est];
Buffer.BlockCopy (buffer.Buffer, 0, newBuffer, 0, buffer.Offset);
buffer = new BufferOffsetSize (newBuffer, buffer.Offset, newBuffer.Length - buffer.Offset, false);
}
state = ReadState.None;
position = 0;
}
WebConnection.Debug ($"{ME} INIT READ ASYNC LOOP DONE: {buffer.Offset} {buffer.Size}");
try {
Operation.ThrowIfDisposed (cancellationToken);
await Initialize (buffer, cancellationToken).ConfigureAwait (false);
} catch (Exception e) {
throw GetReadException (WebExceptionStatus.ReceiveFailure, e, "ReadDoneAsync6");
}
}
bool GetResponse (BufferOffsetSize buffer, ref int pos, ref ReadState state)
{
string line = null;
bool lineok = false;
bool isContinue = false;
bool emptyFirstLine = false;
do {
if (state == ReadState.Aborted)
throw GetReadException (WebExceptionStatus.RequestCanceled, null, "GetResponse");
if (state == ReadState.None) {
lineok = WebConnection.ReadLine (buffer.Buffer, ref pos, buffer.Offset, ref line);
if (!lineok)
return false;
if (line == null) {
emptyFirstLine = true;
continue;
}
emptyFirstLine = false;
state = ReadState.Status;
string[] parts = line.Split (' ');
if (parts.Length < 2)
throw GetReadException (WebExceptionStatus.ServerProtocolViolation, null, "GetResponse");
if (String.Compare (parts[0], "HTTP/1.1", true) == 0) {
Version = HttpVersion.Version11;
ServicePoint.SetVersion (HttpVersion.Version11);
} else {
Version = HttpVersion.Version10;
ServicePoint.SetVersion (HttpVersion.Version10);
}
StatusCode = (HttpStatusCode)UInt32.Parse (parts[1]);
if (parts.Length >= 3)
StatusDescription = String.Join (" ", parts, 2, parts.Length - 2);
else
StatusDescription = string.Empty;
if (pos >= buffer.Offset)
return true;
}
emptyFirstLine = false;
if (state == ReadState.Status) {
state = ReadState.Headers;
Headers = new WebHeaderCollection ();
var headerList = new List<string> ();
bool finished = false;
while (!finished) {
if (WebConnection.ReadLine (buffer.Buffer, ref pos, buffer.Offset, ref line) == false)
break;
if (line == null) {
// Empty line: end of headers
finished = true;
continue;
}
if (line.Length > 0 && (line[0] == ' ' || line[0] == '\t')) {
int count = headerList.Count - 1;
if (count < 0)
break;
string prev = headerList[count] + line;
headerList[count] = prev;
} else {
headerList.Add (line);
}
}
if (!finished)
return false;
// .NET uses ParseHeaders or ParseHeadersStrict which is much better
foreach (string s in headerList) {
int pos_s = s.IndexOf (':');
if (pos_s == -1)
throw new ArgumentException ("no colon found", "header");
var header = s.Substring (0, pos_s);
var value = s.Substring (pos_s + 1).Trim ();
if (WebHeaderCollection.AllowMultiValues (header)) {
Headers.AddInternal (header, value);
} else {
Headers.SetInternal (header, value);
}
}
if (StatusCode == HttpStatusCode.Continue) {
ServicePoint.SendContinue = true;
if (pos >= buffer.Offset)
return true;
if (Request.ExpectContinue) {
Request.DoContinueDelegate ((int)StatusCode, Headers);
// Prevent double calls when getting the
// headers in several packets.
Request.ExpectContinue = false;
}
state = ReadState.None;
isContinue = true;
} else {
state = ReadState.Content;
return true;
}
}
} while (emptyFirstLine || isContinue);
throw GetReadException (WebExceptionStatus.ServerProtocolViolation, null, "GetResponse");
}
}
}