#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=?(?\d+)?-(?\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( $"/{directory.Name}/{directory.LastWriteTime:G}{directory.EnumerateFiles("*", SearchOption.AllDirectories).Sum(s => s.Length)/Kb} KB"); } foreach (var file in directoryInfo.EnumerateFiles().OrderBy(s => s.Name)) { sb.AppendLine( $"/{file.Name}{file.LastWriteTime:G}{file.Length/Kb} KB"); } 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 } }