#region
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using MimeTypes;
using MyServer.Properties;
#endregion
namespace MyServer
{
internal class HttpServer : IDisposable
{
private const int Kb = 1024;
private const int Mb = Kb*Kb;
private const int PacketSize = Mb*4; // Buffer size for reading from files.
private readonly string _fileDirectory;
private readonly HttpListener _httpListener;
private Thread _listenerThread;
private State _serverState;
public HttpServer(string rootFolder = @"Files\", bool relative = true)
{
rootFolder = rootFolder.EndsWith("\\") ? rootFolder : rootFolder + "\\";
_serverState = State.Stopped;
var serverDirectory = AppDomain.CurrentDomain.BaseDirectory;
_fileDirectory = relative ? Path.Combine(serverDirectory, rootFolder) : rootFolder;
_listenerThread = new Thread(ListenerThread);
Log($"Server Directory: {serverDirectory}");
Log($"File Directory: {_fileDirectory}");
_httpListener = new HttpListener();
_httpListener.Prefixes.Add(@"http://127.0.0.1:80/");
}
public void Dispose()
{
_httpListener.Close();
}
private void ListenerThread()
{
while (_httpListener.IsListening)
{
try
{
var context = _httpListener.GetContext();
ThreadPool.QueueUserWorkItem(HandleRequest, context);
}
catch (Exception e)
{
Log($"[Error] {e.Message}");
}
GC.Collect();
}
}
public void Start()
{
if (_serverState == State.Aborted) return;
if (!_httpListener.IsListening)
_httpListener.Start();
try
{
if (!_listenerThread.IsAlive)
_listenerThread = new Thread(ListenerThread);
_listenerThread.Start();
}
catch (Exception e)
{
Log(e.Message);
}
_serverState = State.Running;
Log("Starting Server.");
}
public void Stop()
{
if (_serverState == State.Aborted) return;
if (_httpListener.IsListening)
_httpListener.Stop();
_serverState = State.Stopped;
Log("Stopping Server.");
}
public void Abort()
{
if (_serverState == State.Aborted) return;
_listenerThread.Abort();
_httpListener.Abort();
_serverState = State.Aborted;
Log("Aborting Server.");
}
private static bool TryGetStream(string path, out Stream stream)
{
try
{
if (File.Exists(path))
{
stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
return true;
}
stream = Stream.Null;
return false;
}
catch (Exception e)
{
Log(e.Message);
stream = Stream.Null;
return false;
}
}
private static bool TryGetDirectory(string directory, out DirectoryInfo info)
{
try
{
info = new DirectoryInfo(directory);
return info.Exists;
}
catch (Exception e)
{
Log(e.Message);
info = new DirectoryInfo(string.Empty);
return false;
}
}
private string ToUrl(string localPath)
{
if (localPath == null) return string.Empty;
if (!localPath.StartsWith(_fileDirectory, StringComparison.CurrentCultureIgnoreCase)) return string.Empty;
if (_fileDirectory.Equals(localPath, StringComparison.CurrentCultureIgnoreCase)) return string.Empty;
if (localPath.Length < _fileDirectory.Length) return string.Empty;
return HttpUtility.HtmlEncode(localPath.Substring(_fileDirectory.Length));
}
private static void Log(object data)
{
Console.WriteLine($"{DateTime.Now:G} | {data}");
}
private enum State
{
Running,
Stopped,
Aborted
}
#region RequestHandler
private static readonly Regex RangeRegex = new Regex(@"bytes=?(?<start>\d+)?-(?<end>\d+)?",
RegexOptions.Compiled);
private void HandleRequest(object state)
{
var context = (HttpListenerContext) state;
try
{
var request = context.Request;
var response = context.Response;
var host = request.UserHostName;
var localRequest = HttpUtility.HtmlDecode(request.Url.LocalPath).Replace('/', '\\').Substring(1);
var requestedPath = Path.Combine(_fileDirectory, localRequest);
var outputStream = response.OutputStream;
Log($@"[Request] Path: \{localRequest}");
Stream stream;
DirectoryInfo directoryInfo;
var bytesToSend = new byte[0];
if (localRequest.Equals("favicon.ico"))
{
var iconBytes = Resources.favicon;
response.KeepAlive = false;
response.ContentType = "image/x-icon";
response.Headers.Add("Content-Type", "image/x-icon"); // Ask the system for the filetype.
response.Headers.Add("Content-Disposition", "inline; filename=\"favicon.ico\"");
response.Headers.Add("Date", $"{DateTime.Now:R}");
response.ContentLength64 = iconBytes.LongLength;
outputStream.Write(iconBytes, 0, iconBytes.Length);
}
else if (TryGetStream(requestedPath, out stream))
{
var fileInfo = new FileInfo(requestedPath);
var fileName = fileInfo.Name;
var fileExtension = fileInfo.Extension;
var contentType = MimeTypeMap.GetMimeType(fileExtension);
var totalLength = stream.Length;
var range = request.Headers["Range"] ?? string.Empty;
var match = RangeRegex.Match(range);
var start = match.Groups["start"].Success ? long.Parse(match.Groups["start"].Value) : 0L;
var finish = match.Groups["end"].Success ? long.Parse(match.Groups["end"].Value) : totalLength;
response.KeepAlive = false;
response.ContentType = contentType;
response.Headers.Add("Content-Type", contentType); // Ask the system for the filetype.
response.Headers.Add("Content-Disposition", $"inline; filename={fileName}");
response.Headers.Add("Date", $"{DateTime.Now:R}");
response.Headers.Add("Last-Modified", $"{fileInfo.LastWriteTime:R}");
response.Headers.Add("Accept-Ranges", "bytes");
response.Headers.Add("Content-Range", $"bytes {start}-{finish - 1}/{totalLength}");
if (start == 0 && finish == totalLength)
{
response.StatusCode = 200;
}
else if(start >= 0 && finish <= totalLength)
{
response.StatusCode = 206;
}
else
{
response.StatusCode = 416;
throw new Exception("Request range out of bounds.");
}
response.ContentLength64 = finish - start;
stream.Seek(start, SeekOrigin.Begin);
for (var i = 0; i < finish - start; i += PacketSize)
{
var buffer = new byte[PacketSize];
var bytes = stream.Read(buffer, 0, PacketSize);
var bytesToWrite = (int) Math.Min(finish - start - i, bytes);
outputStream.Write(buffer, 0, bytesToWrite);
}
stream.Close();
}
else if (TryGetDirectory(requestedPath, out directoryInfo))
{
response.KeepAlive = false;
response.ContentType = "text/html";
response.Headers.Add("Content-Type", "text/html; charset=UTF-8");
response.Headers.Add("Content-Language", "en");
response.Headers.Add("Content-Disposition", $"inline; filename={directoryInfo.Name}.html");
response.Headers.Add("Date", $"{DateTime.Now:R}");
response.Headers.Add("Last-Modified", $"{directoryInfo.LastWriteTime:R}");
response.StatusCode = 200;
var sb = new StringBuilder();
foreach (var directory in directoryInfo.EnumerateDirectories().OrderBy(s => s.Name))
{
sb.AppendLine(
$"<tr><td class=\"name\"><a href=\"//{host}/{ToUrl(directory.FullName)}/\">/{directory.Name}/</a></td><td class=\"date\">{directory.LastWriteTime:G}</td><td class=\"size\">{directory.EnumerateFiles("*", SearchOption.AllDirectories).Sum(s => s.Length)/Kb} KB</td><tr>");
}
foreach (var file in directoryInfo.EnumerateFiles().OrderBy(s => s.Name))
{
sb.AppendLine(
$"<tr><td class=\"name\"><a href=\"//{host}/{ToUrl(file.FullName)}\">/{file.Name}</a></td><td class=\"date\">{file.LastWriteTime:G}</td><td class=\"size\">{file.Length/Kb} KB</td></tr>");
}
var endString = string.Format(Resources.Folder, directoryInfo.Name, ToUrl(directoryInfo.FullName),
host, ToUrl(directoryInfo.Parent.FullName), sb, $"{DateTime.Now:R}");
bytesToSend = Encoding.UTF8.GetBytes(endString);
}
else
{
response.KeepAlive = false;
response.ContentType = "text/html";
response.Headers.Add("Content-Type", "text/html; charset=UTF-8");
response.Headers.Add("Content-Language", "en");
response.Headers.Add("Content-Disposition", $"inline; filename={directoryInfo.Name}.html");
response.Headers.Add("Date", $"{DateTime.Now:R}");
response.StatusCode = 200;
bytesToSend = Encoding.UTF8.GetBytes(string.Format(Resources.Error, localRequest));
}
if (bytesToSend.Length > 0 && outputStream.CanWrite)
{
response.ContentEncoding = Encoding.UTF8;
response.ContentLength64 = bytesToSend.LongLength;
outputStream.Write(bytesToSend, 0, bytesToSend.Length);
}
outputStream.Flush();
response.Close();
}
catch (HttpListenerException)
{
context.Response.Abort();
}
catch (Exception e)
{
context.Response.Abort();
Log($"[Error] {e.Message}");
}
}
#endregion RequestHandler
}
}
Comments
0 B
|👍
/👎