commit c144565f703a46a509a712c9c99b5f2bcf11573b Author: Michael Nolan Date: Sat Apr 2 13:59:12 2022 -0500 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c7f0e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.svclog +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml +*.azurePubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +packages/ +## TODO: If the tool you use requires repositories.config, also uncomment the next line +!packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +![Ss]tyle[Cc]op.targets +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml + +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +# ========================= +# Windows detritus +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac desktop service store files +.DS_Store + +_NCrunch* diff --git a/.vs/Tesses.WebServer/xs/UserPrefs.xml b/.vs/Tesses.WebServer/xs/UserPrefs.xml new file mode 100644 index 0000000..b722524 --- /dev/null +++ b/.vs/Tesses.WebServer/xs/UserPrefs.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tesses.WebServer.Console/Program.cs b/Tesses.WebServer.Console/Program.cs new file mode 100644 index 0000000..c347068 --- /dev/null +++ b/Tesses.WebServer.Console/Program.cs @@ -0,0 +1,14 @@ +namespace Tesses.WebServer.ConsoleApp +{ + + class MainClass + { + public static void Main(string[] args) + { + var ip=System.Net.IPAddress.Any; + StaticServer server = new StaticServer("/home/ddlovato/Videos/"); + HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),server); + s.ListenAsync(System.Threading.CancellationToken.None).Wait(); + } + } +} diff --git a/Tesses.WebServer.Console/Properties/AssemblyInfo.cs b/Tesses.WebServer.Console/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..81ee792 --- /dev/null +++ b/Tesses.WebServer.Console/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Tesses.WebServer.Console")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj new file mode 100644 index 0000000..95c4aeb --- /dev/null +++ b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj @@ -0,0 +1,54 @@ + + + + Debug + x86 + {3E464D71-CC54-4E71-9C8F-60B0ADF11EC1} + Exe + Tesses.WebServer.Console + Tesses.WebServer.Console + v4.7 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + true + x86 + + + true + bin\Release + prompt + 4 + true + x86 + + + + + ..\packages\MimeTypesMap.1.0.8\lib\net452\MimeTypesMap.dll + + + ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + + + + + + + + {24949E8A-6661-4853-B2A2-FC957C1B58C3} + Tesses.WebServer + + + + + + + \ No newline at end of file diff --git a/Tesses.WebServer.Console/packages.config b/Tesses.WebServer.Console/packages.config new file mode 100644 index 0000000..5feed8f --- /dev/null +++ b/Tesses.WebServer.Console/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Tesses.WebServer.sln b/Tesses.WebServer.sln new file mode 100644 index 0000000..f467bda --- /dev/null +++ b/Tesses.WebServer.sln @@ -0,0 +1,23 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesses.WebServer.Console", "Tesses.WebServer.Console\Tesses.WebServer.Console.csproj", "{3E464D71-CC54-4E71-9C8F-60B0ADF11EC1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesses.WebServer", "Tesses.WebServer\Tesses.WebServer.csproj", "{24949E8A-6661-4853-B2A2-FC957C1B58C3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3E464D71-CC54-4E71-9C8F-60B0ADF11EC1}.Debug|x86.ActiveCfg = Debug|x86 + {3E464D71-CC54-4E71-9C8F-60B0ADF11EC1}.Debug|x86.Build.0 = Debug|x86 + {3E464D71-CC54-4E71-9C8F-60B0ADF11EC1}.Release|x86.ActiveCfg = Release|x86 + {3E464D71-CC54-4E71-9C8F-60B0ADF11EC1}.Release|x86.Build.0 = Release|x86 + {24949E8A-6661-4853-B2A2-FC957C1B58C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {24949E8A-6661-4853-B2A2-FC957C1B58C3}.Debug|x86.Build.0 = Debug|Any CPU + {24949E8A-6661-4853-B2A2-FC957C1B58C3}.Release|x86.ActiveCfg = Release|Any CPU + {24949E8A-6661-4853-B2A2-FC957C1B58C3}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tesses.WebServer/MyClass.cs b/Tesses.WebServer/MyClass.cs new file mode 100644 index 0000000..f6efc12 --- /dev/null +++ b/Tesses.WebServer/MyClass.cs @@ -0,0 +1,357 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.IO; +using System.Threading; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using HeyRed.Mime; +using Newtonsoft.Json; + +namespace Tesses.WebServer +{ +public static class Extensions + { + const string BYTES_RANGE_HEADER = "Range"; + + private static async Task WriteHeadersAsync(this ServerContext ctx) + { + string status_line = $"HTTP/1.1 {ctx.StatusCode} {StatusCodeMap.GetStatusString(ctx.StatusCode)}\r\n"; + StringBuilder b = new StringBuilder(status_line); + foreach (var hdr in ctx.ResponseHeaders) + { + foreach (var v in hdr.Value) + { + b.Append($"{hdr.Key}: {v}\r\n"); + } + } + b.Append("\r\n"); + var data = Encoding.UTF8.GetBytes(b.ToString()); + await ctx.NetworkStream.WriteAsync(data, 0, data.Length); + } + public static async Task SendFileAsync(this ServerContext ctx, string file) + { + using (var strm = File.OpenRead(file)) + { + await ctx.SendStreamAsync( strm, MimeTypesMap.GetMimeType(file)); + } + } + public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex) + { + string name = ex.GetType().FullName; + string j = $"{WebUtility.HtmlEncode(name)} thrown

{WebUtility.HtmlEncode(name)} thrown

Description: {WebUtility.HtmlEncode(ex.Message)}

"; + ctx.StatusCode = 500; + await ctx.SendTextAsync(j); + } + public static async Task SendJsonAsync(this ServerContext ctx,object value) + { + await ctx.SendTextAsync(JsonConvert.SerializeObject(value), "application/json"); + } + public static async Task SendTextAsync(this ServerContext ctx, string data, string content_type = "text/html") + { + await ctx.SendBytesAsync(Encoding.UTF8.GetBytes(data), content_type); + } + public static async Task SendBytesAsync(this ServerContext ctx, byte[] array, string content_type = "application/octet-stream") + { + using (var ms = new MemoryStream(array)) + { + await ctx.SendStreamAsync( ms, content_type); + } + } + public static async Task SendStreamAsync(this ServerContext ctx, Stream strm, string content_type = "application/octet-stream") + { + //ctx.StatusCode = 200; + int start = 0, end = (int)strm.Length - 1; + if (ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER)) + { + if (ctx.RequestHeaders[BYTES_RANGE_HEADER].Count > 1) + { + throw new NotSupportedException("Multiple 'Range' headers are not supported."); + } + var range = ctx.RequestHeaders[BYTES_RANGE_HEADER][0].Replace("bytes=", String.Empty) + .Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => Int32.Parse(x)) + .ToArray(); + + start = (range.Length > 0) ? range[0] : 0; + end = (range.Length > 1) ? range[1] : (int)(strm.Length - 1); + + + var hdrs = ctx.ResponseHeaders; + hdrs.Add("Accept-Ranges", "bytes"); + hdrs.Add("Content-Range", "bytes " + start + "-" + end + "/" + strm.Length); + ctx.StatusCode = 206; + + + } + ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString()); + ctx.ResponseHeaders.Add("Content-Type", content_type); + + await WriteHeadersAsync(ctx); + if (!ctx.Method.Equals("HEAD",StringComparison.Ordinal)) + { + try + { + strm.Position = start; + strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); + } + finally + { + strm.Close(); + ctx.NetworkStream.Close(); + } + } + } + + public static T2 GetFirst(this Dictionary> args,T1 key) + { + return args[key][0]; + } + public static void Add(this Dictionary> list,T1 key,T2 item) + { + if (list.ContainsKey(key)) + { + list[key].Add(item); + } + else + { + List items = new List(); + items.Add(item); + list.Add(key, items); + } + } + + public static void AddRange(this Dictionary> list,T1 key,IEnumerable items) + { + if (list.ContainsKey(key)) + { + list[key].AddRange(items); + } + else + { + List items2 = new List(); + items2.AddRange(items); + list.Add(key, items2); + + } + } + + public static bool EndsWith(this StringBuilder sb, string test, + StringComparison comparison) + { + if (sb.Length < test.Length) + return false; + + string end = sb.ToString(sb.Length - test.Length, test.Length); + return end.Equals(test, comparison); + } + } + public class NotFoundServer : Server + { + public NotFoundServer(string html) + { + _html = html; + } + public NotFoundServer() + { + _html = "File {url} not found

404 Not Found

{url}

"; + } + string _html; + public override async Task GetAsync(ServerContext ctx) + { + ctx.StatusCode = 404; + await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.UrlPath))); + } + } + + public class StaticServer : Server + { + string _path; + IServer _server; + public StaticServer(string path) + { + _path = path; + _server = new NotFoundServer(); + _defaultFileNames = new string[] {"index.html","index.htm","default.html","default.htm" }; + } + string[] _defaultFileNames; + public StaticServer(string path,string[] defaultFileNames,IServer notfoundserver) + { + _path = path; + _server = notfoundserver; + _defaultFileNames = defaultFileNames; + } + public bool DefaultFileExists(string path,out string name) + { + foreach(var def in _defaultFileNames) + { + name = Path.Combine(path, def); + if(File.Exists(name)) + { + return true; + } + } + name = ""; + return false; + } + + + public override async Task GetAsync(ServerContext ctx) + { + string someUrl = Path.Combine(_path,WebUtility.UrlDecode(ctx.UrlPath.Substring(1)).Replace('/', Path.DirectorySeparatorChar)); + Console.WriteLine(someUrl); + if (Directory.Exists(someUrl)) + { + string name; + if(DefaultFileExists(someUrl,out name)) + { + await ctx.SendFileAsync(name); + } + } + else if (File.Exists(someUrl)) + { + await ctx.SendFileAsync(someUrl); + } + else + { + await _server.GetAsync(ctx); + } + } + + } + public abstract class Server : IServer + { + public abstract Task GetAsync(ServerContext ctx); + public virtual async Task PostAsync(ServerContext ctx) + { + ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + await ctx.SendTextAsync("Method Not Supported"); + } + } + + public interface IServer + { + Task GetAsync(ServerContext ctx); + Task PostAsync(ServerContext ctx); + } + + public sealed class HttpServerListener + { + IServer _server; + TcpListener _listener; + public HttpServerListener(IPEndPoint endPoint,IServer server) + { + _listener = new TcpListener(endPoint); + _server = server; + } + + public async Task ListenAsync(CancellationToken token) + { + _listener.Start(); + using (var r = token.Register(() => _listener.Stop())) { + while (!token.IsCancellationRequested) + { + var socket=await _listener.AcceptTcpClientAsync(); + await CommunicateHostAsync(socket); + } + } + } + + private string ReadHeaders(NetworkStream strm) + { + StringBuilder s = new StringBuilder(); + + var decoder = Encoding.UTF8.GetDecoder(); + var nextChar = new char[1]; + while (!s.EndsWith("\r\n\r\n",StringComparison.Ordinal)) + { + int data = strm.ReadByte(); + if(data == -1) + { + break; + } + int charCount=decoder.GetChars(new byte[] { (byte)data }, 0, 1, nextChar, 0); + if (charCount == 0) continue; + s.Append(nextChar); + } + + return s.ToString(); + } + + + private Dictionary> Headers(string s,out string req_line) + { + + Dictionary> items = new Dictionary>(); + string[] lines = s.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); + req_line = lines[0]; + for(int i=1;i HTTP/1.1\r\n + //HEADER1\r\n + //HEADER2\r\n + //...... + //HEADERN\r\n + //\r\n + //OPTIONAL REQUEST BODY + + //RESPONSE + + using (NetworkStream strm = clt.GetStream()) + { + + string request_line = ""; + string res=ReadHeaders(strm); + var headers=Headers(res,out request_line); + + // {Method} {Path} HTTP/1.1 + ServerContext ctx=null; + string[] request=request_line.Split(new char[] { ' ' }, 3); + string method = request[0]; + try + { + switch (method) + { + case "HEAD": + case "GET": + string path = request[1]; + string ver = request[2]; + ctx = new ServerContext(method,strm, path, headers); + await _server.GetAsync(ctx); + break; + case "POST": + break; + } + }catch(Exception ex) + { + try + { + await ctx.SendExceptionAsync(ex); + }catch(Exception ex2) + { + _ = ex2; + } + } + + } + + + } + + //protected abstract Task Get(string url,Dictionary headers); + + //protected abstract Task GetAsync(ServerContext ctx); + } +} diff --git a/Tesses.WebServer/Properties/AssemblyInfo.cs b/Tesses.WebServer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d0fa19d --- /dev/null +++ b/Tesses.WebServer/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Tesses.WebServer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/Tesses.WebServer/ServerContext.cs b/Tesses.WebServer/ServerContext.cs new file mode 100644 index 0000000..3847802 --- /dev/null +++ b/Tesses.WebServer/ServerContext.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.IO; +using System; +using System.Net; + +namespace Tesses.WebServer +{ + public class ServerContext + { + public string Method { get; set; } + public ServerContext(string method,Stream strm,string path,Dictionary> headers) + { + Method = method; + NetworkStream = strm; + RequestHeaders = headers; + ResponseHeaders = new Dictionary>(); + var qp = new Dictionary>(); + QueryParams = qp; + StatusCode = 200; + + // /joel/path/luigi?local=jim&john_surname=connor&demi_surname=lovato&local=tim + + string[] splitUrl = path.Split(new char[] { '?' }, 2); + + if (splitUrl.Length > 0) + { + UrlPath = splitUrl[0]; + if (splitUrl.Length == 2) + { + //local=jim&john_surname=connor&demi_surname=lovato&local=tim + //we want to split on & + foreach(var item in splitUrl[1].Split(new char[] { '&'},2)) + { + var itemSplit = item.Split(new char[] { '=' }, 2); + if(itemSplit.Length > 0) + { + string key = itemSplit[0]; + string value = ""; + if(itemSplit.Length ==2) + { + value = itemSplit[1]; + } + qp.Add(key, value); //hince qp is reference to QueryParams + } + } + } + } + + } + private string get_host() + { + if(RequestHeaders.ContainsKey("Host")) + { + return RequestHeaders.GetFirst("Host"); + } + return ""; + } + public string Host { get { return get_host(); } } + public string UrlPath { get; set; } + public Dictionary> QueryParams { get; set; } + public Dictionary> RequestHeaders { get; set; } + public Dictionary> ResponseHeaders { get; set; } + public Stream NetworkStream { get; set; } + public int StatusCode { get; internal set; } + } +} \ No newline at end of file diff --git a/Tesses.WebServer/StatusCodeMap.cs b/Tesses.WebServer/StatusCodeMap.cs new file mode 100644 index 0000000..50b4b63 --- /dev/null +++ b/Tesses.WebServer/StatusCodeMap.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Tesses.WebServer +{ + public class StatusCodeMap + { + public StatusCodeMap() + { + + } + public static string GetStatusString(int status_code) + { + string res; + if(_statusCodeMap.Value.TryGetValue(status_code,out res)) + { + return res; + } + return ""; + } + public static int GetStatusCode(string status) + { + return _statusCodeMap.Value.FirstOrDefault(e=> e.Value== status).Key; + } + + private static Lazy> _statusCodeMap = new Lazy>(() => new Dictionary() + { + [202] = "Accepted", + [502] = "Bad Gateway", + [400] = "Bad Request", + [409] = "Conflict", + [100] = "Continue", + [201] = "Created", + [417] = "Expectation Failed", + [403] = "Forbidden", + [302] = "Found", + [504] = "Gateway Timeout", + [410] = "Gone", + [505] = "HTTP Version Not Supported", + [500] = "Internal Server Error", + [411] = "Length Required", + [405] = "Method Not Allowed", + [301] = "Moved Permanently", + [300] = "Multiple Choices", + [204] = "No Content", + [203] = "Non Authoritative Information", + [406] = "Not Acceptable", + [404] = "Not Found", + [501] = "Not Implemented", + [304] = "Not Modified", + [200] = "OK", + [206] = "Partial Content", + [402] = "Payment Required", + [412] = "Precondition Failed", + [407] = "Proxy Authentication Required", + [302] = "Redirect", + [307] = "Redirect Keep Verb", + [303] = "Redirect Method", + [416] = "Requested Range Not Satisfiable", + [413] = "Request Entry Too Large", + [408] = "Request Timeout", + [414] = "Request Uri Too Long", + [205] = "Reset Content", + [503] = "Service Unavailable", + [101] = "Switching Protocols", + [307] = "Temporary Redirect", + [401] = "Unauthorized", + [415] = "Unsupported Media Type", + [306] = "Unused", + [426] = "Upgrade Required", + [305] = "Use Proxy", + [429] = "Too Many Requests" + + }); + } +} diff --git a/Tesses.WebServer/Tesses.WebServer.csproj b/Tesses.WebServer/Tesses.WebServer.csproj new file mode 100644 index 0000000..f1b9374 --- /dev/null +++ b/Tesses.WebServer/Tesses.WebServer.csproj @@ -0,0 +1,48 @@ + + + + Debug + AnyCPU + {24949E8A-6661-4853-B2A2-FC957C1B58C3} + Library + Tesses.WebServer + Tesses.WebServer + v4.7 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + false + + + true + bin\Release + prompt + 4 + false + + + + + ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\packages\MimeTypesMap.1.0.8\lib\net452\MimeTypesMap.dll + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tesses.WebServer/packages.config b/Tesses.WebServer/packages.config new file mode 100644 index 0000000..5feed8f --- /dev/null +++ b/Tesses.WebServer/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file