698 lines
24 KiB
C#
698 lines
24 KiB
C#
// ----------------------------------------------------------------------------
|
|
//
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
//
|
|
// ----------------------------------------------------------------------------
|
|
|
|
//
|
|
// This helper class assists in forming HTTP requests and retrieving the
|
|
// resulting data.
|
|
//
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
|
|
public class HttpRequest
|
|
{
|
|
private string fRequestUri, fHost, fResource, fMethod, fContentType;
|
|
private int fHostPort;
|
|
private byte[] fRequestData;
|
|
private Hashtable fRequestHeaders;
|
|
private long fTimeout; // Zero for infinite
|
|
|
|
// CONSTANTS
|
|
private static string proxyHost = null;
|
|
private static int proxyPort = 0;
|
|
const int readIncrementSize = 1024 * 2; // 2Kbytes
|
|
const int bodySizeGuess = 5 * 1024; // 5KBytes
|
|
|
|
public static void ConfigureProxy(string host, int port)
|
|
{
|
|
proxyHost = host;
|
|
proxyPort = port;
|
|
}
|
|
|
|
public HttpRequest(string! uri)
|
|
{
|
|
fRequestUri = uri;
|
|
|
|
// Parse the URI into a host-part and a path-part.
|
|
const string httpPrefix = "http://";
|
|
if (!uri.StartsWith(httpPrefix)) {
|
|
throw new ArgumentException("URI must begin with 'http://'");
|
|
}
|
|
|
|
int startIndex = httpPrefix.Length;
|
|
int firstSlash = uri.IndexOf('/',startIndex);
|
|
|
|
string fHost;
|
|
if (firstSlash != -1) {
|
|
fHost = uri.Substring(startIndex, firstSlash - startIndex);
|
|
fResource = uri.Substring(firstSlash);
|
|
}
|
|
else {
|
|
// Host is the entire Uri
|
|
fHost = uri.Substring(startIndex);
|
|
fResource = String.Empty;
|
|
}
|
|
|
|
int firstColon = fHost.IndexOf(':');
|
|
|
|
if (firstColon != -1) {
|
|
fHostPort = Int32.Parse(fHost.Substring(firstColon + 1));
|
|
}
|
|
else {
|
|
fHostPort = 80; // default
|
|
}
|
|
|
|
this.fRequestHeaders = new Hashtable();
|
|
this.fMethod = "GET";
|
|
this.fHost = fHost;
|
|
}
|
|
|
|
public void AddHeader(string! headerName, string headerValue)
|
|
{
|
|
fRequestHeaders[headerName] = headerValue;
|
|
}
|
|
|
|
public string Method
|
|
{
|
|
get { return fMethod; }
|
|
set { fMethod = value; }
|
|
}
|
|
|
|
public string ContentType
|
|
{
|
|
get { return fContentType; }
|
|
set { fContentType = value; }
|
|
}
|
|
|
|
public byte[] RequestData
|
|
{
|
|
get { return fRequestData; }
|
|
set { fRequestData = value; }
|
|
}
|
|
|
|
public long Timeout
|
|
{
|
|
get { return fTimeout; }
|
|
set { fTimeout = value; }
|
|
}
|
|
|
|
public string Resource
|
|
{
|
|
get { return fResource; }
|
|
}
|
|
|
|
// Perform the HTTP hit and return the result!
|
|
public HttpResponse GetResponse()
|
|
{
|
|
//
|
|
// Step 1: Connect to the remote server
|
|
//
|
|
Socket httpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
|
|
ProtocolType.Tcp);
|
|
|
|
string serverName, requestUri;
|
|
int serverPort;
|
|
|
|
if (proxyPort > 0) {
|
|
serverName = proxyHost;
|
|
serverPort = proxyPort;
|
|
requestUri = fRequestUri; // Use the full URI
|
|
}
|
|
else {
|
|
serverName = fHost;
|
|
serverPort = fHostPort;
|
|
requestUri = fResource; // Use just the resource
|
|
}
|
|
|
|
IPAddress serverAddress;
|
|
|
|
try {
|
|
serverAddress = IPAddress.Parse(serverName);
|
|
}
|
|
catch (Exception) {
|
|
// Hostname didn't parse as an IP address; try to resolve it
|
|
IPHostEntry! entry = (!)Dns.Resolve(serverName);
|
|
IPAddress[]! addresses = (!)entry.AddressList;
|
|
|
|
if (addresses.Length == 0) {
|
|
throw new Exception("Couldn't resolve host name");
|
|
}
|
|
|
|
serverAddress = addresses[0];
|
|
}
|
|
|
|
IPEndPoint serverEndpoint = new IPEndPoint(serverAddress, serverPort);
|
|
|
|
// Connect to the remote server
|
|
httpSocket.Connect(serverEndpoint);
|
|
|
|
//
|
|
// Step 2: Formulate and transmit the request line and any supporting data
|
|
//
|
|
// Send the request line
|
|
string requestLine = fMethod + " " + requestUri + " HTTP/1.1\r\nHost: " +
|
|
fHost + "\r\nConnection: Close\r\n";
|
|
|
|
// Send an indication of the request data content type and size, if appropriate
|
|
if (fRequestData != null) {
|
|
requestLine += "Content-Length: " + fRequestData.Length + "\r\n" +
|
|
"Content-Type: " + fContentType + "\r\n";
|
|
}
|
|
|
|
// Add any user-specified headers
|
|
foreach (string! headerName in fRequestHeaders.Keys) {
|
|
requestLine += headerName + ": " + fRequestHeaders[headerName] + "\r\n";
|
|
}
|
|
|
|
requestLine += "\r\n";
|
|
byte[] requestBytes = Encoding.ASCII.GetBytes(requestLine);
|
|
httpSocket.Send(requestBytes);
|
|
|
|
// Send any request data
|
|
if (fRequestData != null) {
|
|
httpSocket.Send(fRequestData);
|
|
}
|
|
|
|
// Signal we're done sending
|
|
httpSocket.Shutdown(SocketShutdown.Send);
|
|
|
|
//
|
|
// Step 3: Parse the response line and headers
|
|
//
|
|
|
|
// Pump the response into an HttpHeadersParser
|
|
ByteBuffer scratchBuffer = new ByteBuffer();
|
|
HttpHeadersParser headerParser = new HttpHeadersParser(scratchBuffer);
|
|
bool doneWithout100Continue = false, doneWithHeaders = false;
|
|
int nextWritePos = 0;
|
|
HttpResponse response = null;
|
|
|
|
do
|
|
{
|
|
while (!doneWithHeaders) {
|
|
// Make 2K available for each read
|
|
scratchBuffer.EnsureSize(nextWritePos + readIncrementSize);
|
|
int numReadBytes = httpSocket.Receive(scratchBuffer.UnderlyingBuffer, nextWritePos,
|
|
scratchBuffer.Size - nextWritePos, SocketFlags.None);
|
|
|
|
if (numReadBytes == 0) {
|
|
// We ran out of data before we finished parsing headers.
|
|
throw new Exception("Unexpected end of data");
|
|
}
|
|
|
|
nextWritePos += numReadBytes;
|
|
|
|
if (headerParser.Pump(nextWritePos)) {
|
|
// Finished parsing headers!
|
|
response = headerParser.GetResponse();
|
|
doneWithHeaders = true;
|
|
}
|
|
}
|
|
|
|
if (response == null) {
|
|
throw new Exception("HTTP response data unexpectedly null");
|
|
}
|
|
|
|
// Special case: if we see a 100-continue, we want to carefully start
|
|
// over at the next chunk of data!
|
|
if (response.StatusCode == 100) {
|
|
// Switch to a new scratch buffer that will contain the data
|
|
// following the 100-continue.
|
|
int remainderBeginning = headerParser.BodyDataOffset;
|
|
int remainderLength = nextWritePos - remainderBeginning;
|
|
ByteBuffer newScratch = new ByteBuffer();
|
|
ByteBuffer oldScratch = scratchBuffer;
|
|
|
|
// Reset parsing by creating a new parser
|
|
headerParser = new HttpHeadersParser(newScratch);
|
|
|
|
// Switch over!
|
|
scratchBuffer = newScratch;
|
|
nextWritePos = remainderLength;
|
|
|
|
if (remainderLength > 0) {
|
|
// Copy the beginning of the next data chunk into newScratch
|
|
newScratch.EnsureSize(remainderLength);
|
|
Buffer.BlockCopy(oldScratch.UnderlyingBuffer, remainderBeginning,
|
|
newScratch.UnderlyingBuffer, 0, remainderLength);
|
|
|
|
// Make sure the fragment we were already holding gets pumped in
|
|
if (headerParser.Pump(nextWritePos)) {
|
|
// Interestingly, we're already done
|
|
response = headerParser.GetResponse();
|
|
if (response == null) {
|
|
throw new Exception("HTTP response data unexpectedly null");
|
|
}
|
|
Debug.Assert(response.StatusCode != 100);
|
|
doneWithout100Continue = true;
|
|
}
|
|
else {
|
|
// Go back and carry on
|
|
doneWithHeaders = false;
|
|
}
|
|
}
|
|
else {
|
|
// The chunk we were chewing on exactly contained the 100-continue,
|
|
// so we definitely need to go back and read some more...
|
|
doneWithHeaders = false;
|
|
}
|
|
}
|
|
else {
|
|
// The response was not 100-continue, so we're all done
|
|
doneWithout100Continue = true;
|
|
}
|
|
}
|
|
while (!doneWithout100Continue);
|
|
|
|
//
|
|
// Step 4: Process the body data
|
|
//
|
|
// Now figure out how we're going to deal with the actual body
|
|
// of the response. We almost certainly already have an initial
|
|
// chunk of the body data in the scratchBuffer, since we read the
|
|
// headers in 2K chunks
|
|
//
|
|
int bodyScratchBeginning = headerParser.BodyDataOffset;
|
|
int scratchBodyLength = nextWritePos - bodyScratchBeginning;
|
|
|
|
string transferEncoding = response.GetHeader("Transfer-Encoding");
|
|
if (transferEncoding != null &&
|
|
transferEncoding.ToLower().Equals("chunked"))
|
|
{
|
|
// Chunked encoding is special: the beginning of the body data
|
|
// already specifies some chunk information. Run this through our
|
|
// chunk decoder to straighten it out.
|
|
byte[] initialBodyChunk = scratchBuffer.TrimAndCopy(bodyScratchBeginning, scratchBodyLength);
|
|
ChunkedEncodingParser chunkParser = new ChunkedEncodingParser(initialBodyChunk, httpSocket);
|
|
ByteBuffer bodyData = chunkParser.Run();
|
|
response.BodyData = bodyData.TrimAndCopy(bodyData.Size);
|
|
}
|
|
else {
|
|
ByteBuffer bodyBuffer;
|
|
|
|
// Non-chunked encoding. First, move the beginning of the body
|
|
// data to a new ByteBuffer for sanity.
|
|
// Separate out any initial body data into a new ByteBuffer, for sanity
|
|
int bodySize = (response.ContentLength > 0) ? response.ContentLength : bodySizeGuess;
|
|
|
|
// Hard to imagine how this would happen...
|
|
if (bodySize < scratchBodyLength) {
|
|
bodySize = scratchBodyLength;
|
|
}
|
|
|
|
bodyBuffer = new ByteBuffer(bodySize);
|
|
Buffer.BlockCopy(scratchBuffer.UnderlyingBuffer, bodyScratchBeginning,
|
|
bodyBuffer.UnderlyingBuffer, 0, scratchBodyLength);
|
|
int nextBodyPos = scratchBodyLength;
|
|
|
|
if (response.ContentLength > 0) {
|
|
// Read until we've gotten exactly the expected number of bytes
|
|
int bodyDataLeft = response.ContentLength - scratchBodyLength;
|
|
|
|
while (bodyDataLeft > 0) {
|
|
int numReadBytes = httpSocket.Receive(bodyBuffer.UnderlyingBuffer, nextBodyPos,
|
|
bodyDataLeft, SocketFlags.None);
|
|
|
|
if (numReadBytes == 0) {
|
|
throw new Exception("Connection closed unexpectedly");
|
|
}
|
|
|
|
nextBodyPos += numReadBytes;
|
|
bodyDataLeft -= numReadBytes;
|
|
}
|
|
}
|
|
else {
|
|
// No indicated ContentLength; Just read until the connection gets closed!
|
|
int numReadBytes = 0;
|
|
|
|
// Read until the remote side closes
|
|
do
|
|
{
|
|
bodyBuffer.EnsureSize(bodyBuffer.Size + readIncrementSize);
|
|
numReadBytes = httpSocket.Receive(bodyBuffer.UnderlyingBuffer, nextBodyPos,
|
|
bodyBuffer.Size - nextBodyPos, SocketFlags.None);
|
|
nextBodyPos += numReadBytes;
|
|
}
|
|
while (numReadBytes > 0);
|
|
}
|
|
|
|
response.BodyData = bodyBuffer.TrimAndCopy(nextBodyPos);
|
|
}
|
|
|
|
// All done!
|
|
httpSocket.Close();
|
|
|
|
return response;
|
|
}
|
|
|
|
private class HttpHeadersParser
|
|
{
|
|
// The buffer we are chewing on
|
|
ByteBuffer fBuffer;
|
|
|
|
// The response we are forming
|
|
private HttpResponse fResponse;
|
|
private int fContentLength;
|
|
|
|
// Working state
|
|
private State fState;
|
|
private int fPosition;
|
|
private int fBufferLimit; // One more than the last usable index in the buffer
|
|
|
|
private enum State
|
|
{
|
|
BeforeStatusLine,
|
|
ParsingHeaders,
|
|
Complete
|
|
}
|
|
|
|
public HttpHeadersParser(ByteBuffer buffer)
|
|
{
|
|
fBuffer = buffer;
|
|
fResponse = new HttpResponse();
|
|
}
|
|
|
|
public bool Pump(int newBufferLimit)
|
|
{
|
|
bool allDone = false, outOfRoom = false;
|
|
|
|
while ((!allDone) && (!outOfRoom)) {
|
|
PumpInternal(newBufferLimit, out allDone, out outOfRoom);
|
|
}
|
|
|
|
return allDone;
|
|
}
|
|
|
|
private void PumpInternal(int newBufferLimit, out bool allDone, out bool outOfRoom)
|
|
{
|
|
fBufferLimit = newBufferLimit;
|
|
|
|
if (fState == State.Complete) {
|
|
// We've already completed
|
|
allDone = true;
|
|
outOfRoom = false;
|
|
return;
|
|
}
|
|
|
|
int CRLFOffset = FindNextCRLF(fPosition, fBufferLimit);
|
|
|
|
if (fState == State.BeforeStatusLine) {
|
|
if (CRLFOffset != -1) {
|
|
HandleStatusLine(fPosition, CRLFOffset - fPosition);
|
|
fState = State.ParsingHeaders;
|
|
fPosition = CRLFOffset + 2;
|
|
allDone = false;
|
|
outOfRoom = false;
|
|
return;
|
|
}
|
|
else {
|
|
// We couldn't see another CRLF before the end of the
|
|
// usable part of the buffer, so we're out of room.
|
|
allDone = false;
|
|
outOfRoom = true;
|
|
return;
|
|
}
|
|
}
|
|
else if (fState == State.ParsingHeaders) {
|
|
// Special case: if the unprocessed part starts with CRLF it
|
|
// means we are looking at the end of the headers, since we consume
|
|
// the trailing CRLF on a single header line when parsing.
|
|
if (CRLFOffset == fPosition) {
|
|
fState = State.Complete;
|
|
fPosition += 2;
|
|
allDone = true;
|
|
outOfRoom = false;
|
|
return;
|
|
}
|
|
else if (CRLFOffset != -1) {
|
|
HandleHeaderLine(fPosition, CRLFOffset - fPosition);
|
|
fPosition = CRLFOffset + 2;
|
|
allDone = false;
|
|
outOfRoom = false;
|
|
return;
|
|
}
|
|
else {
|
|
// We couldn't see another CRLF before the end of the
|
|
// usable part of the buffer, so we're out of room.
|
|
allDone = false;
|
|
outOfRoom = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Someone must have added a state or mucked up the code
|
|
allDone = false;
|
|
outOfRoom = false;
|
|
Debug.Assert(false);
|
|
}
|
|
|
|
public HttpResponse GetResponse()
|
|
{
|
|
if (fState == State.Complete) {
|
|
return fResponse;
|
|
}
|
|
else {
|
|
// Not allowed to have half-baked results!
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public int BodyDataOffset
|
|
{
|
|
get
|
|
{
|
|
if (fState == State.Complete) {
|
|
return fPosition;
|
|
}
|
|
else {
|
|
// No answer if we're not done processing
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
public int ContentLength
|
|
{
|
|
get
|
|
{
|
|
if (fState == State.Complete) {
|
|
return fContentLength;
|
|
}
|
|
else {
|
|
// Not available if we're not done processing
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleStatusLine(int startOffset, int length)
|
|
{
|
|
string status = Encoding.ASCII.GetString(fBuffer.UnderlyingBuffer, startOffset, length);
|
|
const string HTTP11 = "HTTP/1.1 ";
|
|
|
|
if (!status.StartsWith(HTTP11)) {
|
|
throw new Exception("Response was not HTTP/1.1");
|
|
}
|
|
|
|
int secondSpace = status.IndexOf(' ', HTTP11.Length);
|
|
string statusString;
|
|
|
|
if (secondSpace == -1) {
|
|
// Assume this means there is a status code with no explanation string
|
|
statusString = status.Substring(HTTP11.Length);
|
|
}
|
|
else {
|
|
// Grab just the status code
|
|
statusString = status.Substring(HTTP11.Length, secondSpace - HTTP11.Length);
|
|
}
|
|
|
|
int statusCode = Int32.Parse(statusString);
|
|
fResponse.StatusCode = statusCode;
|
|
}
|
|
|
|
private void HandleHeaderLine(int startOffset, int length)
|
|
{
|
|
string header = Encoding.ASCII.GetString(fBuffer.UnderlyingBuffer, startOffset, length);
|
|
int firstColon = header.IndexOf(':');
|
|
|
|
if (firstColon == -1) {
|
|
throw new Exception("Malformed header string");
|
|
}
|
|
|
|
string headerName = header.Substring(0, firstColon);
|
|
string headerValue = header.Substring(firstColon + 1).Trim();
|
|
|
|
if (headerName.ToLower().Equals("content-length")) {
|
|
fContentLength = Int32.Parse(headerValue);
|
|
}
|
|
else {
|
|
fResponse.AddHeader(headerName, headerValue);
|
|
}
|
|
}
|
|
|
|
private int FindNextCRLF(int startOffset, int limitOffset)
|
|
{
|
|
int offset = startOffset;
|
|
|
|
while (offset < limitOffset) {
|
|
if (fBuffer[offset] == (byte)'\r') {
|
|
if ((offset + 1 < limitOffset) && (fBuffer[offset + 1] == (byte)'\n')) {
|
|
return offset;
|
|
}
|
|
}
|
|
|
|
offset++;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
private class ChunkedEncodingParser
|
|
{
|
|
// This buffer is what we start with; it has an initial
|
|
// fragment of the body data
|
|
private byte[] fInitialFragment;
|
|
private int fFragmentOffset; // How far into fInitialFragment we are
|
|
private Socket fSocket; // Socket to read additional data from
|
|
private byte[] fSingleByte;
|
|
|
|
public ChunkedEncodingParser(byte[] initialFragment, Socket readSocket)
|
|
{
|
|
fInitialFragment = initialFragment;
|
|
fSocket = readSocket;
|
|
fSingleByte = new byte[1];
|
|
}
|
|
|
|
public ByteBuffer! Run()
|
|
{
|
|
ByteBuffer bodyData = new ByteBuffer();
|
|
int chunkSize = 0;
|
|
|
|
while (true) {
|
|
ByteBuffer! chunkLine = ReadToCRLF();
|
|
string! chunkString = (!)Encoding.ASCII.GetString(chunkLine.UnderlyingBuffer, 0, chunkLine.Size);
|
|
chunkString = (!)chunkString.Trim();
|
|
int firstSemi = chunkString.IndexOf(';');
|
|
|
|
if (firstSemi != -1) {
|
|
// Use only the portion before the semicolon
|
|
chunkSize = Int32.Parse(chunkString.Substring(0, firstSemi),
|
|
NumberStyles.AllowHexSpecifier);
|
|
}
|
|
else {
|
|
// No semicolon; use the entire line
|
|
chunkSize = Int32.Parse(chunkString, NumberStyles.AllowHexSpecifier);
|
|
}
|
|
|
|
if (chunkSize == 0) {
|
|
// We're done! Don't bother reading the trailer
|
|
return bodyData;
|
|
}
|
|
|
|
// Read in the amount indicated by the chunk header. It may take multiple passes
|
|
// to do this.
|
|
int writeOffset = bodyData.Size;
|
|
bodyData.EnsureSize(bodyData.Size + chunkSize);
|
|
|
|
while (chunkSize > 0) {
|
|
int numBytesRead = MassRead(bodyData.UnderlyingBuffer, writeOffset, chunkSize);
|
|
chunkSize -= numBytesRead;
|
|
writeOffset += numBytesRead;
|
|
}
|
|
|
|
// Peel off the CRLF that trails a chunk
|
|
ByteBuffer emptyLine = ReadToCRLF();
|
|
if (emptyLine.Size != 0) {
|
|
throw new Exception("Malformed HTTP/1.1 chunk -- no trailing lone CRLF");
|
|
}
|
|
}
|
|
}
|
|
|
|
private ByteBuffer! ReadToCRLF()
|
|
{
|
|
ByteBuffer retval = new ByteBuffer();
|
|
InnerReadToCRLF(retval);
|
|
return retval;
|
|
}
|
|
|
|
private void InnerReadToCRLF(ByteBuffer buffer) // buffer can be null
|
|
{
|
|
// Read characters into buffer until we see a CRLF (don't include)
|
|
byte nextByte = GetNextByte();
|
|
|
|
while (true) {
|
|
if (nextByte == (byte)'\r') {
|
|
nextByte = GetNextByte();
|
|
|
|
if (nextByte == (byte)'\n') {
|
|
// Done
|
|
return;
|
|
}
|
|
else {
|
|
if (buffer != null) {
|
|
buffer.Add((byte)'\r');
|
|
}
|
|
|
|
// Don't read again; nextByte might
|
|
// be the first byte of CRLF.
|
|
}
|
|
}
|
|
else {
|
|
if (buffer != null) {
|
|
buffer.Add(nextByte);
|
|
}
|
|
nextByte = GetNextByte();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DiscardToCRLF()
|
|
{
|
|
InnerReadToCRLF(null);
|
|
}
|
|
|
|
private byte GetNextByte()
|
|
{
|
|
if (fFragmentOffset < fInitialFragment.Length) {
|
|
return fInitialFragment[fFragmentOffset++];
|
|
}
|
|
else {
|
|
int received = fSocket.Receive(fSingleByte, 0, 1, SocketFlags.None);
|
|
if (received == 1) {
|
|
return fSingleByte[0];
|
|
}
|
|
else {
|
|
throw new Exception("Connection closed unexpectedly");
|
|
}
|
|
}
|
|
}
|
|
|
|
private int MassRead(byte[] buffer, int offset, int maxLength)
|
|
{
|
|
if (fFragmentOffset < fInitialFragment.Length) {
|
|
// Still data left in the original fragment; return as much
|
|
// of that as possible.
|
|
int initialFragmentLeft = fInitialFragment.Length - fFragmentOffset;
|
|
int amountToUse = maxLength <= initialFragmentLeft ? maxLength : initialFragmentLeft;
|
|
Buffer.BlockCopy(fInitialFragment, fFragmentOffset, buffer, offset, amountToUse);
|
|
fFragmentOffset += amountToUse;
|
|
return amountToUse;
|
|
}
|
|
else {
|
|
// No data left in the initial fragment; read from the network.
|
|
return fSocket.Receive(buffer, offset, maxLength, SocketFlags.None);
|
|
}
|
|
}
|
|
}
|
|
}
|