Skip to content

Instantly share code, notes, and snippets.

@vsajip
Created August 28, 2009 11:03
Show Gist options
  • Save vsajip/2f4df2dafc3ecfb0f450 to your computer and use it in GitHub Desktop.
Save vsajip/2f4df2dafc3ecfb0f450 to your computer and use it in GitHub Desktop.
//
// Copyright 2009 Vinay Sajip
//
// This file is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
//
// This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// See http://www.gnu.org/licenses/ for more information.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
namespace HttpListenerProxy
{
class ProxyListener
{
private const int CHUNK_SIZE = 1024;
private Uri _uri;
private int _port;
private string _fileName;
private string _fileNameInUse;
private bool _downloaded;
private string _mimeType;
private HttpListener _httpListener;
private Thread _listener;
private Thread _downloader;
private FileStream _stream;
private List<byte[]> _chunks;
private AutoResetEvent _chunkAvailable;
private AutoResetEvent _downloadStarted;
private AutoResetEvent _downloadComplete;
private long _downloadSize;
private WaitHandle[] _events;
private static void Log(string message)
{
Console.WriteLine(message);
}
internal ProxyListener(Uri uri, int port, string fileName)
{
_uri = uri;
_port = port;
_fileName = fileName;
_downloaded = File.Exists(fileName);
_listener = new Thread(Listen);
if (_downloaded)
_fileNameInUse = fileName;
else
{
_fileNameInUse = String.Format("{0}.partial", fileName);
_downloader = new Thread(Download);
_chunkAvailable = new AutoResetEvent(false);
_downloadStarted = new AutoResetEvent(false);
_downloadComplete = new AutoResetEvent(false);
_events = new WaitHandle[] { _chunkAvailable, _downloadComplete };
}
}
internal void Start()
{
if (_downloader != null)
_downloader.Start();
else
_stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read);
_listener.Start();
}
internal void Cleanup()
{
_httpListener.Close();
_listener.Abort();
_listener.Join();
if ((_downloader != null) && _downloader.IsAlive)
{
_downloader.Abort();
_downloader.Join();
}
if (_chunks != null)
{
lock (_chunks)
{
_chunks.Clear();
}
}
}
void Download()
{
Log("downloader: starting");
_stream = new FileStream(_fileNameInUse, FileMode.OpenOrCreate, FileAccess.Write);
try
{
_chunks = new List<byte[]>();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_uri);
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
_mimeType = response.ContentType;
_downloadSize = response.ContentLength;
Log(String.Format("Content-type: {0}, Content-Length: {1}", _mimeType, _downloadSize));
_downloadStarted.Set();
Stream stream = response.GetResponseStream();
byte[] buffer = new byte[CHUNK_SIZE];
long bytesRead = 0;
for (; ; )
{
int count = stream.Read(buffer, 0, CHUNK_SIZE);
int chunkIndex;
if (count <= 0)
break;
bytesRead += count;
_stream.Write(buffer, 0, count);
_stream.Flush();
byte[] chunk = new byte[count];
Array.Copy(buffer, chunk, count);
lock (_chunks)
{
_chunks.Add(chunk);
chunkIndex = _chunks.Count;
_chunkAvailable.Set();
}
Log(String.Format("downloader: {0} bytes read, added at {1}", bytesRead, chunkIndex - 1));
}
_stream.Close();
File.Move(_fileNameInUse, _fileName);
_downloadComplete.Set();
_downloaded = true;
}
finally
{
Log("downloader: terminating");
}
}
void Listen()
{
_httpListener = new HttpListener();
_httpListener.Prefixes.Add(String.Format("http://*:{0}/", _port));
_httpListener.Start();
try
{
for (; ; )
{
HttpListenerContext context = _httpListener.GetContext();
HttpListenerResponse response = context.Response;
response.StatusCode = 200;
response.StatusDescription = "OK";
if (!_downloaded && (_mimeType == null))
_downloadStarted.WaitOne();
// If we already have the file, we don't know its mime type - stick in a default value for IE
response.ContentType = _mimeType ?? "application/octet-stream";
Log(String.Format("uploader: setting content type to {0}", response.ContentType));
Stream stream = response.OutputStream;
if (_downloaded)
{
response.ContentLength64 = _stream.Length;
byte[] buffer = new byte[CHUNK_SIZE];
_stream.Seek(0, SeekOrigin.Begin);
for (; ; )
{
int count = _stream.Read(buffer, 0, CHUNK_SIZE);
if (count <= 0)
break;
stream.Write(buffer, 0, count);
}
stream.Close();
}
else
{
response.ContentLength64 = _downloadSize;
long bytesRead = 0;
int chunkIndex = 0;
int numChunks;
try
{
byte[] chunk;
// write out any chunks we already have
lock (_chunks)
{
numChunks = _chunks.Count;
}
for (; chunkIndex < numChunks; chunkIndex++)
{
lock (_chunks)
{
chunk = _chunks[chunkIndex];
stream.Write(chunk, 0, chunk.Length);
bytesRead += chunk.Length;
}
Log(String.Format("uploader: {0} bytes read from {1}", bytesRead, chunkIndex));
}
// reached end of chunks. Wait for more
while (WaitHandle.WaitAny(_events) != 1)
{
lock (_chunks)
{
numChunks = _chunks.Count;
}
for (; chunkIndex < numChunks; chunkIndex++)
{
lock (_chunks)
{
chunk = _chunks[chunkIndex];
stream.Write(chunk, 0, chunk.Length);
bytesRead += chunk.Length;
}
Log(String.Format("uploader: {0} bytes read from {1}", bytesRead, chunkIndex));
}
}
stream.Close();
}
catch (HttpListenerException e)
{
Log("uploader: client closed connection: " + e.Message);
}
}
}
}
catch (HttpListenerException ex)
{
Log(String.Format("uploader: exception: {0}", ex.Message));
}
finally
{
if (_httpListener.IsListening)
_httpListener.Close();
}
}
}
class Program
{
private static Regex LINE_MATCHER = new Regex(@"^(\S+)\s+(\d+)\s+(\S+)$");
private Dictionary<string, ProxyListener> mappings = new Dictionary<string, ProxyListener>();
private void ProxyUri(Uri uri, int port, string cacheFile)
{
ProxyListener listener = new ProxyListener(uri, port, cacheFile);
string key = String.Format("{0}|{1}|{2}", uri, port, cacheFile);
if (mappings.ContainsKey(key))
{
mappings[key].Cleanup();
}
mappings[key] = listener;
listener.Start();
}
private void Cleanup()
{
foreach (ProxyListener listener in mappings.Values)
{
listener.Cleanup();
}
}
private void ProcessLine(string line)
{
Match m = LINE_MATCHER.Match(line);
bool notUnderstood = false;
if (!m.Success)
{
notUnderstood = true;
}
else
{
try
{
Uri uri = new Uri(m.Groups[1].Value);
int port = Convert.ToInt32(m.Groups[2].Value);
string fileName = m.Groups[3].Value;
//Console.WriteLine(String.Format("Understood: {0}, {1}, {2}", uri, port, fileName));
ProxyUri(uri, port, fileName);
}
catch (UriFormatException)
{
notUnderstood = true;
}
}
if (notUnderstood)
Console.WriteLine("Not understood. Enter URI, port and filename separated by spaces");
}
private void Run(string[] args)
{
List<string> prefetched = new List<string>();
// Stick in here any pre-canned lines you want to test with...
//prefetched.Add(@"http://media.railscasts.com/videos/176_searchlogic.mov 8000 c:\temp\temp.mov");
try
{
foreach (string line in prefetched)
ProcessLine(line);
Console.WriteLine("Enter URI, port and filename separated by spaces:");
for (; ; )
{
string line = Console.ReadLine();
if (line.Length == 0)
break;
ProcessLine(line);
}
Console.Write("All done. Press the Enter key to exit:");
Console.ReadKey();
}
finally
{
Cleanup();
}
}
static void Main(string[] args)
{
new Program().Run(args);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment