www.pudn.com > SharpDownload(FTP¼°WEBÏÂÔØ).zip > HTTPClient.cs
using System;
using System.Net;
using System.IO;
using System.Text;
using System.Net.Sockets;
using System.Diagnostics;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;
#region HTTP info
/*
*
* More info: http://www.w3.org/Protocols/rfc2616/rfc2616.html
STATUS CODES
"100" ; Section 10.1.1: Continue
| "101" ; Section 10.1.2: Switching Protocols
| "200" ; Section 10.2.1: OK
| "201" ; Section 10.2.2: Created
| "202" ; Section 10.2.3: Accepted
| "203" ; Section 10.2.4: Non-Authoritative Information
| "204" ; Section 10.2.5: No Content
| "205" ; Section 10.2.6: Reset Content
| "206" ; Section 10.2.7: Partial Content
| "300" ; Section 10.3.1: Multiple Choices
| "301" ; Section 10.3.2: Moved Permanently
| "302" ; Section 10.3.3: Found
| "303" ; Section 10.3.4: See Other
| "304" ; Section 10.3.5: Not Modified
| "305" ; Section 10.3.6: Use Proxy
| "307" ; Section 10.3.8: Temporary Redirect
| "400" ; Section 10.4.1: Bad Request
| "401" ; Section 10.4.2: Unauthorized
| "402" ; Section 10.4.3: Payment Required
| "403" ; Section 10.4.4: Forbidden
| "404" ; Section 10.4.5: Not Found
| "405" ; Section 10.4.6: Method Not Allowed
| "406" ; Section 10.4.7: Not Acceptable
| "407" ; Section 10.4.8: Proxy Authentication Required
| "408" ; Section 10.4.9: Request Time-out
| "409" ; Section 10.4.10: Conflict
| "410" ; Section 10.4.11: Gone
| "411" ; Section 10.4.12: Length Required
| "412" ; Section 10.4.13: Precondition Failed
| "413" ; Section 10.4.14: Request Entity Too Large
| "414" ; Section 10.4.15: Request-URI Too Large
| "415" ; Section 10.4.16: Unsupported Media Type
| "416" ; Section 10.4.17: Requested range not satisfiable
| "417" ; Section 10.4.18: Expectation Failed
| "500" ; Section 10.5.1: Internal Server Error
| "501" ; Section 10.5.2: Not Implemented
| "502" ; Section 10.5.3: Bad Gateway
| "503" ; Section 10.5.4: Service Unavailable
| "504" ; Section 10.5.5: Gateway Time-out
| "505" ; Section 10.5.6: HTTP Version not supported
*/
#endregion
namespace SharpDownload
{
///
/// HTTP-specific implementation of ProtocolClient.
/// Provides HTTP download capabilities.
///
public class HttpClient : ProtocolClient
{
///
/// HTTP Exception class
///
public class HttpException : Exception
{
public HttpException(string message) : base(message){}
public HttpException(string message, Exception innerException) : base(message,innerException){}
}
public class HttpRedirectException : ApplicationException
{
private string newURL = string.Empty;
public HttpRedirectException(string URLRedirect)
{
newURL = URLRedirect;
}
public string URL
{
get
{
return newURL;
}
}
}
private const string MODULE_NAME = "HttpClient";
private const string CRLF = "\r\n"; // 0D0A
private FileDownload.FileDownloadCommandEnum command = FileDownload.FileDownloadCommandEnum.NoCommand;
///
/// Constructor
///
public HttpClient()
{
this.Port = 80;
}
///
/// Close the Http connection.
///
public override void Close()
{
Debug.WriteLine("Closing connection to " + this.server, MODULE_NAME );
if( this.clientSocket != null )
{
this.clientSocket.Close();
this.loggedin = false;
}
this.cleanup();
}
///
/// Overridden function returns if the server supports resume
///
/// true if server supports resume
public override bool CanResume()
{
return CanResume("/");
}
///
/// Overridden function returns if the server supports resume
///
/// true if server supports resume
public override bool CanResume(string remFileName)
{
// Check if the server supports Range: command
// TODO: Need to fix this!
//return true;
if ( !this.loggedin ) this.Login();
string header;
// Create a HEAD HTTP request for the file
header = "HEAD /" + remFileName + " HTTP/1.1" + CRLF;
header += "Host: " + this.Server + CRLF;
header += "Range: bytes=0-0" + CRLF;
header += "Connection: close" + CRLF; // NOTE: HTTP/1.1 automatically keeps connection open unles 'Connection: close' specified
header += CRLF; // Header terminated with a blank line
this.sendCommand(header);
this.Close();
if ( this.resultCode == 200 | this.resultCode == 206 )
{
Debug.WriteLine("Range supported", MODULE_NAME);
return true;
}
else
{
Debug.WriteLine("Range supported", MODULE_NAME);
return false;
}
}
///
/// Return the size of a file.
///
///
///
public override long GetFileSize(string fileName)
{
if ( !this.loggedin ) this.Login();
string header;
string contentLengthHeader = "Content-Length: ";
string locationHeader = "Location: ";
// Create a HEAD HTTP request for the file
header = "HEAD /" + fileName + " HTTP/1.1" + CRLF;
header += "Host: " + this.Server + CRLF;
header += "Connection: keep-alive" + CRLF; // NOTE: HTTP/1.1 automatically keeps connection open unles 'Connection: close' specified
header += CRLF; // Header terminated with a blank line
this.sendCommand(header);
long size=0;
switch (this.resultCode)
{
case 200:
// Find content length line
foreach (string arrayVal in this.fullMessage)
{
int index = arrayVal.IndexOf(contentLengthHeader, 0);
if (index!=-1)
{
size = long.Parse(arrayVal.Substring(contentLengthHeader.Length, arrayVal.Length-contentLengthHeader.Length));
break;
}
}
break;
case 302:
// Found returned from server - get filesize from supplied URI
string newLocationURI = string.Empty;
foreach (string arrayVal in this.fullMessage)
{
int index = arrayVal.IndexOf(locationHeader, 0);
if (index!=-1)
{
newLocationURI = arrayVal.Substring(locationHeader.Length, arrayVal.Length-locationHeader.Length);
break;
}
}
if (newLocationURI==string.Empty) throw new ApplicationException("HTTPClient ERROR: 302 returned, but couldn't find new URI in header!");
Debug.WriteLine("302 Found - trying new URI (" + newLocationURI + ")", MODULE_NAME);
throw new HttpRedirectException(newLocationURI);
// Get the new URL info
// URLComponent url = new URLComponent(newLocationURI);
// this.Close();
// this.Server = url.ServerAddress;
// this.RemotePath = url.Directory + "/" + url.FileName;
// size = this.GetFileSize(newLocationURI);
default:
throw new HttpException(this.result.Substring(4));
}
this.Close();
Debug.WriteLine("File size is: " + size.ToString() + " bytes", MODULE_NAME);
return size;
}
///
/// Stops the current download
///
public override void StopDownload()
{
}
///
/// Download a remote file to the Assembly's local directory,
/// keeping the same file name.
///
///
public override DownloadResult Download(string remFileName)
{
return this.Download(remFileName,"",false);
}
///
/// Download a remote file to the Assembly's local directory,
/// keeping the same file name, and set the resume flag.
///
///
///
public override DownloadResult Download(string remFileName, Boolean resume)
{
return this.Download(remFileName, "", resume);
}
///
/// Download a remote file to a local file name which can include
/// a path. The local file name will be created or overwritten,
/// but the path must exist.
///
///
///
public override DownloadResult Download(string remFileName, string locFileName)
{
return this.Download(remFileName, locFileName, false);
}
public override DownloadResult Download(string remFileName,string locFileName, Boolean resume)
{
return this.Download(remFileName, locFileName, resume, 0);
}
public override DownloadResult Download(string remFileName,string locFileName, Boolean resume, long byteOffset)
{
return this.Download(remFileName, locFileName, resume, byteOffset, 0);
}
public override DownloadResult Download(string remFileName,string locFileName, Boolean resume, long byteOffset, long byteStopDownload)
{
return this.Download(remFileName, locFileName, resume, byteOffset, byteStopDownload, 0);
}
public override DownloadResult Download(string remFileName,string locFileName, Boolean resume, long byteOffset, long byteStopDownload, long totalFileSize)
{
return this.Download(remFileName, locFileName, resume, byteOffset, byteStopDownload, totalFileSize, false);
}
///
/// Download a remote file to a local file name which can include
/// a path, and set the resume flag. The local file name will be
/// created or overwritten, but the path must exist.
///
/// Remote file name on the server
/// Local file name to save to
/// true attempts to resume the download
/// Offset to start download
/// Point to stop download for partial
/// The total size of the file
/// Is this the last part in the list?
public override DownloadResult Download(string remFileName, string locFileName, Boolean resume, long byteOffset, long byteStopDownload, long totalFileSize, bool isLastPart)
{
bool foundStart = false;
bool stopPointReached = false;
long locFileOffset = 0;
string httpGetCommand = string.Empty;
DateTime timeout; // Timeout value for loop
// Set mode to binary
//this.BinaryMode = true;
OnDownloadStartedEvent(new DownloadStartedEventArgs());
// Get the file output stream
FileStream output = GetLocalFileStream(locFileName, remFileName);
// Check if resume requested
if ( resume )
{
// Set the byte offset for the local file to end of file
locFileOffset = output.Length;
if (locFileOffset>0)
{
// Some data already received prevously
DataReceivedEventArgs eDataReceived = new DataReceivedEventArgs(output.Length);
OnDataReceivedEvent(eDataReceived);
}
if ( locFileOffset > 0 | byteOffset > 0 | byteStopDownload > 0)
{
// Check already downloaded the part
if (locFileOffset>=totalFileSize && totalFileSize>0)
{
// This part download has already been completed
Debug.WriteLine("Part download was completed previously (" + locFileOffset + " bytes already downloaded, part size " + totalFileSize + ")", MODULE_NAME);
output.Close();
return DownloadResult.Finished;
}
// Create a HTTP GET request for the file range
httpGetCommand = "GET /" + remFileName + " HTTP/1.1" + CRLF;
httpGetCommand += "Host: " + this.Server + CRLF;
// Check if a stop download point set or if this is last part
long startByteRange = locFileOffset + byteOffset;
if (byteStopDownload==0 | isLastPart)
{
// Get all remaining bytes
httpGetCommand += "Range: bytes=" + startByteRange + "-" + CRLF;
}
else
{
// Get a range of bytes
httpGetCommand += "Range: bytes=" + startByteRange + "-" + (startByteRange + byteStopDownload) + CRLF;
}
httpGetCommand += "Connection: close" + CRLF;
httpGetCommand += CRLF; // Header terminated with a blank line
}
output.Seek( locFileOffset, SeekOrigin.Begin );
}
else
{
// Create a default GET command for the whole file
// This command is used if the server doesn't support resume
httpGetCommand = "GET /" + remFileName + " HTTP/1.1" + CRLF;
httpGetCommand += "Host: " + this.Server + CRLF;
httpGetCommand += "Connection: close" + CRLF;
httpGetCommand += CRLF; // Header terminated with a blank line
}
// Create a data socket
Socket cSocket = createDataSocket();
// Send the get command
Debug.WriteLine(httpGetCommand, MODULE_NAME);
Byte[] cmdBytes = Encoding.ASCII.GetBytes( ( httpGetCommand + "\r\n" ).ToCharArray() );
cSocket.Send(cmdBytes, cmdBytes.Length, 0);
try
{
do
{
// Reset the timeout value for the next read
// NOTE: Need this so that each time a socket receive is successful,
// we wait an additional "timeout" value for the next receive
timeout = DateTime.Now.AddSeconds(this.timeoutSeconds);
this.ResetBuffer();
this.bytes = cSocket.Receive( this.buffer, this.buffer.Length, 0 );
// Check if any data received
if ( this.bytes <= 0)
{
// No data, so the file has finished downloading
Debug.WriteLine("No data received - assumed end of stream", MODULE_NAME);
stopPointReached = true;
}
// Found the start of the file yet?
if (!foundStart)
{
this.message = ASCII.GetString( this.buffer, 0, this.bytes );
this.resultCode = int.Parse( this.message.Substring(9,3) );
// Check the HTTP result code
switch (this.resultCode)
{
case 200 | 206:
{
// Complete/partial file download
// Find blank line occurrence (/r/n/r/n)
Byte[] blankLine = ASCII.GetBytes("\r\n\r\n");
for (int i=0; i=byteStopDownload))
{
Debug.WriteLine("Stop download point reached", MODULE_NAME);
// Write the data to the output file UP TO the stop download point
Byte[] smallBuffer = CopyToSmallArray(this.buffer, (int)(byteStopDownload-output.Length+1));
output.Write(smallBuffer, 0, smallBuffer.Length);
stopPointReached = true;
}
else
{
// Write the data to the output file
output.Write(this.buffer, 0, this.bytes);
}
// Raise a data received event
try
{
DataReceivedEventArgs eDataReceived = new DataReceivedEventArgs(output.Length);
OnDataReceivedEvent(eDataReceived);
}
catch (Exception eDataReceivedError)
{
// Ignore event raise errors
// NOTE: I got errors here when the event CONSUMER raises an error - why?
Debug.WriteLine("An error occurred raising the Data Received event. [" + eDataReceivedError.Message + "]", MODULE_NAME);
}
}
// Check if the stop point was reached
if (stopPointReached)
{
Debug.WriteLine("Stop point reached, breaking...", MODULE_NAME);
break;
}
// Check if we want to stop the download
if (command!=FileDownload.FileDownloadCommandEnum.NoCommand)
{
Debug.WriteLine("Download command received, breaking...", MODULE_NAME);
break;
}
} while ( timeout > DateTime.Now);
}
catch (Exception eDownload)
{
Debug.WriteLine("An error occurred while download the file [" + eDownload.Message + "]", MODULE_NAME);
throw;
}
// Clean-up
output.Close();
if ( cSocket.Connected ) cSocket.Close();
Debug.WriteLine("Download completed, response code is: " + this.resultCode, MODULE_NAME);
// Check if result code is ok
// EXCEPT if we manually stopped at a certain point (byteStopDownload)
if( this.resultCode != 200 && this.resultCode != 206 && byteStopDownload==0)
throw new HttpException(this.result.Substring(4));
// Check if we stopped because of a command
if (this.Command!=FileDownload.FileDownloadCommandEnum.NoCommand)
{
// Command received!
Debug.WriteLine("Download stopped due to command: " + this.Command, MODULE_NAME);
return DownloadResult.Paused;
}
return DownloadResult.Finished;
}
///
/// Read the response from the server
///
private void readResponse()
{
this.message = "";
this.result = this.readLine();
if ( this.result.Length > 9 )
this.resultCode = int.Parse( this.result.Substring(9,3) );
else
this.result = null;
}
///
/// Reads lines from the buffer and appends to the return message
///
///
private string readLine()
{
while(true)
{
this.bytes = clientSocket.Receive( this.buffer, this.buffer.Length, 0 );
this.message += ASCII.GetString( this.buffer, 0, this.bytes );
if ( this.bytes < this.buffer.Length )
{
break;
}
}
// Split return string into lines
string[] msg = this.message.Split(new char[] {'\r','\n'});
// Make sure split worked
if (msg.Length==0)
{
// No return data
this.message = string.Empty;
}
else
{
// Response code should always be in first array element
this.message = msg[0];
}
// Set full message value
this.fullMessage = msg;
// TODO: Is this line needed for http?
//if ( this.message.Length > 4 && !this.message.Substring(3,1).Equals(" ") ) return this.readLine();
if ( this.verboseDebugging )
{
for(int i = 0; i < msg.Length - 1; i++)
{
Debug.Write( msg[i], "HttpClient" );
}
}
return message;
}
///
/// Sends a command to the server
///
///
private void sendCommand(String command)
{
if ( this.verboseDebugging ) Debug.WriteLine(command, MODULE_NAME);
Byte[] cmdBytes = Encoding.ASCII.GetBytes( ( command + "\r\n" ).ToCharArray() );
clientSocket.Send( cmdBytes, cmdBytes.Length, 0);
this.readResponse();
}
///
/// Overridden login method
///
public override void Login()
{
Socket socket = base.createDataSocket();
if (socket!=null)
{
this.clientSocket = socket;
this.loggedin = true;
}
}
///
/// Always release those sockets.
///
private void cleanup()
{
if ( this.clientSocket!=null )
{
this.clientSocket.Close();
this.clientSocket = null;
}
this.loggedin = false;
}
public override FileDownload.FileDownloadCommandEnum Command
{
get
{
return command;
}
set
{
command = value;
}
}
///
/// Destructor
///
~HttpClient()
{
this.cleanup();
}
}
}