|
// ==++==
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// ==--==
//============================================================
//
// File: HttpStreams.cs
//
// Summary: Defines streams used by HTTP channels
//
//============================================================
using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Remoting.Channels;
using System.Text;
using System.Threading;
using System.Globalization;
namespace System.Runtime.Remoting.Channels.Http
{
internal abstract class HttpServerResponseStream : Stream
{
public override bool CanRead { get { return false; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
} // HttpServerResponseStream
internal sealed class HttpFixedLengthResponseStream : HttpServerResponseStream
{
private Stream _outputStream = null; // funnel http data into here
private static int _length;
internal HttpFixedLengthResponseStream(Stream outputStream, int length)
{
_outputStream = outputStream;
_length = length;
} // HttpFixedLengthResponseStream
protected override void Dispose(bool disposing)
{
try {
if (disposing)
_outputStream.Flush();
}
finally {
base.Dispose(disposing);
}
} // Close
public override void Flush()
{
_outputStream.Flush();
} // Flush
public override void Write(byte[] buffer, int offset, int count)
{
_outputStream.Write(buffer, offset, count);
} // Write
public override void WriteByte(byte value)
{
_outputStream.WriteByte(value);
} // WriteByte
} // class HttpFixedLengthResponseStream
internal sealed class HttpChunkedResponseStream : HttpServerResponseStream
{
private static byte[] _trailer = Encoding.ASCII.GetBytes("0\r\n\r\n"); // 0-length, no trailer, end chunked
private static byte[] _endChunk = Encoding.ASCII.GetBytes("\r\n");
private Stream _outputStream = null; // funnel chunked http data into here
private byte[] _chunk; // chunk of data to write
private int _chunkSize; // size of chunk
private int _chunkOffset; // next byte to write in to chunk
private byte[] _byteBuffer = new byte[1]; // buffer for writing bytes
internal HttpChunkedResponseStream(Stream outputStream)
{
_outputStream = outputStream;
_chunk = CoreChannel.BufferPool.GetBuffer();
_chunkSize = _chunk.Length - 2; // reserve space for _endChunk directly in buffer
_chunkOffset = 0;
// write end chunk bytes at end of buffer (avoids extra socket write)
_chunk[_chunkSize - 2] = (byte)'\r';
_chunk[_chunkSize - 1] = (byte)'\n';
} // HttpChunkedResponseStream
protected override void Dispose(bool disposing)
{
try {
if (disposing) {
if (_chunkOffset > 0)
FlushChunk();
_outputStream.Write(_trailer, 0, _trailer.Length);
_outputStream.Flush();
}
CoreChannel.BufferPool.ReturnBuffer(_chunk);
_chunk = null;
}
finally {
base.Dispose(disposing);
}
} // Close
public override void Flush()
{
if (_chunkOffset > 0)
FlushChunk();
_outputStream.Flush();
} // Flush
public override void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
{
if ((_chunkOffset == 0) && (count >= _chunkSize))
{
// just write the rest as a chunk directly to the wire
WriteChunk(buffer, offset, count);
break;
}
else
{
// write bytes to current chunk buffer
int writeCount = Math.Min(_chunkSize - _chunkOffset, count);
Array.Copy(buffer, offset, _chunk, _chunkOffset, writeCount);
_chunkOffset += writeCount;
count -= writeCount;
offset += writeCount;
// see if we need to terminate the chunk
if (_chunkOffset == _chunkSize)
FlushChunk();
}
}
} // Write
public override void WriteByte(byte value)
{
_byteBuffer[0] = value;
Write(_byteBuffer, 0, 1);
} // WriteByte
private void FlushChunk()
{
WriteChunk(_chunk, 0, _chunkOffset);
_chunkOffset = 0;
}
private void WriteChunk(byte[] buffer, int offset, int count)
{
byte[] size = IntToHexChars(count);
_outputStream.Write(size, 0, size.Length);
if (buffer == _chunk)
{
// _chunk already has end chunk encoding at end
_outputStream.Write(_chunk, offset, count + 2);
}
else
{
_outputStream.Write(buffer, offset, count);
_outputStream.Write(_endChunk, 0, _endChunk.Length);
}
} // WriteChunk
private byte[] IntToHexChars(int i)
{
String str = "";
while (i > 0)
{
int val = i % 16;
switch (val)
{
case 15: str = 'F' + str; break;
case 14: str = 'E' + str; break;
case 13: str = 'D' + str; break;
case 12: str = 'C' + str; break;
case 11: str = 'B' + str; break;
case 10: str = 'A' + str; break;
default: str = (char)(val + (int)'0') + str; break;
}
i = i / 16;
}
str += "\r\n";
return Encoding.ASCII.GetBytes(str);
} // IntToHexChars
} // HttpChunkedResponseStream
internal abstract class HttpReadingStream : Stream
{
public virtual bool ReadToEnd()
{
// This will never be called at a point where it is valid
// for someone to use the remaining data, so we don't
// need to buffer it.
byte[] buffer = new byte[16];
int bytesRead = 0;
do
{
bytesRead = Read(buffer, 0, 16);
} while (bytesRead > 0);
return bytesRead == 0;
}
public virtual bool FoundEnd { get { return false; } }
public override bool CanRead { get { return true; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return false; } }
public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get{ throw new NotSupportedException(); }
set{ throw new NotSupportedException(); }
}
public override void Flush() { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
} // HttpReadingStream
internal sealed class HttpFixedLengthReadingStream : HttpReadingStream
{
private HttpSocketHandler _inputStream = null; // read content data from here
private int _bytesLeft; // bytes left in current chunk
internal HttpFixedLengthReadingStream(HttpSocketHandler inputStream, int contentLength)
{
_inputStream = inputStream;
_bytesLeft = contentLength;
} // HttpFixedLengthReadingStream
public override bool FoundEnd { get { return _bytesLeft == 0; } }
protected override void Dispose(bool disposing)
{
try {
//
}
finally {
base.Dispose(disposing);
}
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_bytesLeft == 0)
return 0;
int readCount = _inputStream.Read(buffer, offset, Math.Min(_bytesLeft, count));
if (readCount > 0)
_bytesLeft -= readCount;
return readCount;
} // Read
public override int ReadByte()
{
if (_bytesLeft == 0)
return -1;
_bytesLeft -= 1;
return _inputStream.ReadByte();
} // ReadByte
} // HttpFixedLengthReadingStream
// Stream class to read chunked data for HTTP
// (assumes that provided outputStream will be positioned for
// reading the body)
internal sealed class HttpChunkedReadingStream : HttpReadingStream
{
private static byte[] _trailer = Encoding.ASCII.GetBytes("0\r\n\r\n\r\n"); // 0-length, null trailer, end chunked
private static byte[] _endChunk = Encoding.ASCII.GetBytes("\r\n");
private HttpSocketHandler _inputStream = null; // read chunked http data from here
private int _bytesLeft; // bytes left in current chunk
private bool _bFoundEnd = false; // has end of stream been reached?
private byte[] _byteBuffer = new byte[1]; // buffer for reading bytes
internal HttpChunkedReadingStream(HttpSocketHandler inputStream)
{
_inputStream = inputStream;
_bytesLeft = 0;
} // HttpChunkedReadingStream
public override bool FoundEnd { get { return _bFoundEnd; } }
protected override void Dispose(bool disposing)
{
try {
//
}
finally {
base.Dispose(disposing);
}
} // Close
public override int Read(byte[] buffer, int offset, int count)
{
int bytesRead = 0;
while (!_bFoundEnd && (count > 0))
{
// see if we need to start reading a new chunk
if (_bytesLeft == 0)
{
// this loop stops when the end of line is found
for (;;)
{
byte b = (byte)_inputStream.ReadByte();
// see if this is the end of the length
if (b == '\r')
{
// This had better be '\n'
if ((char)_inputStream.ReadByte() != '\n')
{
throw new RemotingException(
CoreChannel.GetResourceString(
"Remoting_Http_ChunkedEncodingError"));
}
else
break; // we've finished reading the length
}
else
{
int value = HttpChannelHelper.CharacterHexDigitToDecimal(b);
// make sure value is a hex-digit
if ((value < 0) || (value > 15))
{
throw new RemotingException(
CoreChannel.GetResourceString(
"Remoting_Http_ChunkedEncodingError"));
}
// update _bytesLeft value to account for new digit on the right
_bytesLeft = (_bytesLeft * 16) + value;
}
}
if (_bytesLeft == 0)
{
// read off trailing headers and end-line
String trailerHeader;
do
{
trailerHeader = _inputStream.ReadToEndOfLine();
} while (!(trailerHeader.Length == 0));
_bFoundEnd = true;
}
}
if (!_bFoundEnd)
{
int readCount = Math.Min(_bytesLeft, count);
int bytesReadThisTime = _inputStream.Read(buffer, offset, readCount);
if (bytesReadThisTime <= 0)
{
throw new RemotingException(
CoreChannel.GetResourceString(
"Remoting_Http_ChunkedEncodingError"));
}
_bytesLeft -= bytesReadThisTime;
count -= bytesReadThisTime;
offset += bytesReadThisTime;
bytesRead += bytesReadThisTime;
// see if the end of the chunk was found
if (_bytesLeft == 0)
{
// read off "\r\n"
char ch = (char)_inputStream.ReadByte();
if (ch != '\r')
{
throw new RemotingException(
CoreChannel.GetResourceString(
"Remoting_Http_ChunkedEncodingError"));
}
ch = (char)_inputStream.ReadByte();
if (ch != '\n')
{
throw new RemotingException(
CoreChannel.GetResourceString(
"Remoting_Http_ChunkedEncodingError"));
}
}
}
} // while (count > 0)
return bytesRead;
} // Read
public override int ReadByte()
{
int readCount = Read(_byteBuffer, 0, 1);
if (readCount == 0)
return -1;
return _byteBuffer[0];
} // ReadByte
} // class HttpChunkedReadingStream
[Serializable]
internal enum HttpVersion
{
V1_0,
V1_1
} // HttpVersion
// Maintains control of a socket connection.
internal sealed class HttpServerSocketHandler : HttpSocketHandler
{
// Used to make sure verb characters are valid
private static ValidateByteDelegate s_validateVerbDelegate =
new ValidateByteDelegate(HttpServerSocketHandler.ValidateVerbCharacter);
// Used to keep track of socket connections
private static Int64 _connectionIdCounter = 0;
// primed buffer data
private static byte[] _bufferhttpContinue = Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n");
// stream manager data
private HttpReadingStream _requestStream = null; // request stream we handed out.
private HttpServerResponseStream _responseStream = null; // response stream we handed out.
private Int64 _connectionId; // id for this connection
// request state flags
private HttpVersion _version; // http version used by client
private int _contentLength = 0; // Content-Length value if found
private bool _chunkedEncoding = false; // does request stream use chunked encoding?
private bool _keepAlive = false; // does the client want to keep the connection alive?
internal HttpServerSocketHandler(Socket socket, RequestQueue requestQueue, Stream stream) : base(socket, requestQueue, stream)
{
_connectionId = Interlocked.Increment(ref _connectionIdCounter);
} // HttpServerSocketHandler
//
public bool AllowChunkedResponse { get { return false; } }
// Determine if it's possible to service another request
public bool CanServiceAnotherRequest()
{
if (_keepAlive && (_requestStream != null))
{
if (_requestStream.FoundEnd || _requestStream.ReadToEnd())
return true;
}
return false;
} // CanServiceAnotherRequest
// Prepare for reading a new request off of the same socket
protected override void PrepareForNewMessage()
{
_requestStream = null;
_responseStream = null;
_contentLength = 0;
_chunkedEncoding = false;
_keepAlive = false;
} // PrepareForNewRequest
string GenerateFaultString(Exception e) {
//If the user has specified it's a development server (versus a production server) in remoting config,
//then we should just return e.ToString instead of extracting the list of messages.
if (!CustomErrorsEnabled())
return e.ToString();
else {
return CoreChannel.GetResourceString("Remoting_InternalError");
}
}
protected override void SendErrorMessageIfPossible(Exception e)
{
// If we haven't started sending a response back, we can do the following.
if ((_responseStream == null) && !(e is SocketException))
{
Stream outputStream = new MemoryStream();
StreamWriter writer = new StreamWriter(outputStream, new UTF8Encoding(false));
writer.WriteLine(GenerateFaultString(e));
writer.Flush();
SendResponse(outputStream, "500", CoreChannel.GetResourceString("Remoting_InternalError"), null);
}
} // SendErrorMessageIfPossible
private static bool ValidateVerbCharacter(byte b)
{
if (Char.IsLetter((char)b) ||
(b == '-'))
{
return true;
}
return false;
} // ValidateVerbCharacter
// read headers
public BaseTransportHeaders ReadHeaders()
{
bool bSendContinue = false;
BaseTransportHeaders headers = new BaseTransportHeaders();
// read first line
String verb, requestURI, version;
ReadFirstLine(out verb, out requestURI, out version);
if ((verb == null) || (requestURI == null) || (version == null))
{
throw new RemotingException(
CoreChannel.GetResourceString(
"Remoting_Http_UnableToReadFirstLine"));
}
if (version.Equals("HTTP/1.1")) // most common case
_version = HttpVersion.V1_1;
else
if (version.Equals("HTTP/1.0"))
_version = HttpVersion.V1_0;
else
_version = HttpVersion.V1_1; // (assume it will understand 1.1)
if (_version == HttpVersion.V1_1)
{
_keepAlive = true;
}
else // it's a 1.0 client
{
_keepAlive = false;
}
// update request uri to be sure that it has no channel data
String channelURI;
String objectURI;
channelURI = HttpChannelHelper.ParseURL(requestURI, out objectURI);
if (channelURI == null)
{
objectURI = requestURI;
}
headers["__RequestVerb"] = verb;
headers.RequestUri = objectURI;
headers["__HttpVersion"] = version;
// check to see if we must send continue
if ((_version == HttpVersion.V1_1) &&
(verb.Equals("POST") || verb.Equals("PUT")))
{
bSendContinue = true;
}
ReadToEndOfHeaders(headers, out _chunkedEncoding, out _contentLength,
ref _keepAlive, ref bSendContinue);
if (bSendContinue && (_version != HttpVersion.V1_0))
SendContinue();
// add IP address and Connection Id to headers
headers[CommonTransportKeys.IPAddress] = ((IPEndPoint)NetSocket.RemoteEndPoint).Address;
headers[CommonTransportKeys.ConnectionId] = _connectionId;
return headers;
} // ReadHeaders
public Stream GetRequestStream()
{
if (_chunkedEncoding)
_requestStream = new HttpChunkedReadingStream(this);
else
_requestStream = new HttpFixedLengthReadingStream(this, _contentLength);
return _requestStream;
} // GetRequestStream
public Stream GetResponseStream(String statusCode, String reasonPhrase,
ITransportHeaders headers)
{
bool contentLengthPresent = false;
bool useChunkedEncoding = false;
int contentLength = 0;
// check for custom user status code and reason phrase
Object userStatusCode = headers["__HttpStatusCode"]; // someone might have stored an int
String userReasonPhrase = headers["__HttpReasonPhrase"] as String;
if (userStatusCode != null)
statusCode = userStatusCode.ToString();
if (userReasonPhrase != null)
reasonPhrase = userReasonPhrase;
// see if we can handle any more requests on this socket
if (!CanServiceAnotherRequest())
{
headers["Connection"] = "Close";
}
// check for content length
Object contentLengthEntry = headers["Content-Length"];
if (contentLengthEntry != null)
{
contentLengthPresent = true;
if (contentLengthEntry is int)
contentLength = (int)contentLengthEntry;
else
contentLength = Convert.ToInt32(contentLengthEntry, CultureInfo.InvariantCulture);
}
// see if we are going to use chunked-encoding
useChunkedEncoding = AllowChunkedResponse && !contentLengthPresent;
if (useChunkedEncoding)
headers["Transfer-Encoding"] = "chunked";
// write headers to stream
ChunkedMemoryStream headerStream = new ChunkedMemoryStream(CoreChannel.BufferPool);
WriteResponseFirstLine(statusCode, reasonPhrase, headerStream);
WriteHeaders(headers, headerStream);
headerStream.WriteTo(NetStream);
headerStream.Close();
// return stream ready for content
if (useChunkedEncoding)
_responseStream = new HttpChunkedResponseStream(NetStream);
else
_responseStream = new HttpFixedLengthResponseStream(NetStream, contentLength);
return _responseStream;
} // GetResponseStream
private bool ReadFirstLine(out String verb, out String requestURI, out String version)
{
verb = null;
requestURI = null;
version = null;
verb = ReadToChar(' ', s_validateVerbDelegate);
byte[] requestUriBytes = ReadToByte((byte)' ');
int decodedUriLength;
HttpChannelHelper.DecodeUriInPlace(requestUriBytes, out decodedUriLength);
requestURI = Encoding.UTF8.GetString(requestUriBytes, 0, decodedUriLength);
version = ReadToEndOfLine();
return true;
} // ReadFirstLine
private void SendContinue()
{
// Output:
// HTTP/1.1 100 Continue
// Send the continue response back to the client
NetStream.Write(_bufferhttpContinue, 0, _bufferhttpContinue.Length);
} // SendContinue
public void SendResponse(Stream httpContentStream,
String statusCode, String reasonPhrase,
ITransportHeaders headers)
{
if (_responseStream != null)
{
_responseStream.Close();
if (_responseStream != httpContentStream)
{
throw new RemotingException(
CoreChannel.GetResourceString("Remoting_Http_WrongResponseStream"));
}
// we are done with the response stream
_responseStream = null;
}
else
{
if (headers == null)
headers = new TransportHeaders();
String serverHeader = (String)headers["Server"];
if (serverHeader != null)
serverHeader = HttpServerTransportSink.ServerHeader + ", " + serverHeader;
else
serverHeader = HttpServerTransportSink.ServerHeader;
headers["Server"] = serverHeader;
// Add length to response headers if necessary
if (!AllowChunkedResponse && (httpContentStream != null))
headers["Content-Length"] = httpContentStream.Length.ToString(CultureInfo.InvariantCulture);
else
if (httpContentStream == null)
headers["Content-Length"] = "0";
GetResponseStream(statusCode, reasonPhrase, headers);
// write HTTP content
if(httpContentStream != null)
{
StreamHelper.CopyStream(httpContentStream, _responseStream);
_responseStream.Close();
httpContentStream.Close();
}
// we are done with the response stream
_responseStream = null;
}
} // SendResponse
} // class HttpServerSocketHandler
} // namespace System.Runtime.Remoting.Channels.Http
|