feat: initial development
This commit is contained in:
29
NaiveHttpServer/Context.cs
Normal file
29
NaiveHttpServer/Context.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Net;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public delegate bool ParameterProvider(string key, out string value);
|
||||
|
||||
public class Context
|
||||
{
|
||||
public HttpListenerRequest Request { get; }
|
||||
|
||||
public HttpListenerResponse Response { get; }
|
||||
|
||||
public ILogger Logger { get; }
|
||||
|
||||
public ParameterProvider TryGetParameter { get; set; }
|
||||
|
||||
public Context(HttpListenerRequest request, HttpListenerResponse response, ILogger logger)
|
||||
{
|
||||
Request = request;
|
||||
Response = response;
|
||||
Logger = logger;
|
||||
TryGetParameter = (string _, out string value) =>
|
||||
{
|
||||
value = null!;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
38
NaiveHttpServer/DefaultLogger.cs
Normal file
38
NaiveHttpServer/DefaultLogger.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
internal class DefaultLogger : ILogger
|
||||
{
|
||||
public void Error(string message, Exception? exception = null)
|
||||
{
|
||||
Write(nameof(Error), message, exception);
|
||||
}
|
||||
|
||||
public void Warning(string message, Exception? exception = null)
|
||||
{
|
||||
Write(nameof(Warning), message, exception);
|
||||
}
|
||||
|
||||
public void Info(string message, Exception? exception = null)
|
||||
{
|
||||
Write(nameof(Info), message, exception);
|
||||
}
|
||||
|
||||
public void Debug(string message, Exception? exception = null)
|
||||
{
|
||||
Write(nameof(Debug), message, exception);
|
||||
}
|
||||
|
||||
private static void Write(string level, string message, Exception? exception)
|
||||
{
|
||||
Thread thread = Thread.CurrentThread;
|
||||
string threadName = string.IsNullOrEmpty(thread.Name) ? thread.ManagedThreadId.ToString() : thread.Name!;
|
||||
|
||||
string exceptionString = exception is null ? string.Empty : $"{Environment.NewLine}{exception.Message}{Environment.NewLine}{exception.StackTrace}{Environment.NewLine}";
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {level.ToUpperInvariant()} [{threadName}] {message}{exceptionString}");
|
||||
}
|
||||
}
|
||||
}
|
||||
9
NaiveHttpServer/ErrorCodes.cs
Normal file
9
NaiveHttpServer/ErrorCodes.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public static class ErrorCodes
|
||||
{
|
||||
public const string Unknown = "UNKNOWN";
|
||||
public const string NotFoundApi = "NOT_FOUND_API";
|
||||
public const string NotFoundFile = "NOT_FOUND_FILE";
|
||||
}
|
||||
}
|
||||
83
NaiveHttpServer/Extensions.cs
Normal file
83
NaiveHttpServer/Extensions.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static readonly JsonSerializerSettings JsonSettings = new()
|
||||
{
|
||||
MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
};
|
||||
|
||||
public static async Task Json(this HttpListenerResponse response, object value)
|
||||
{
|
||||
string jsonText = value.ToJson();
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(jsonText);
|
||||
|
||||
response.ContentEncoding = Encoding.UTF8;
|
||||
response.ContentType = "application/json";
|
||||
response.ContentLength64 = bytes.Length;
|
||||
await response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public static async Task File(this HttpListenerResponse response, string filePath)
|
||||
{
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await FileHelper.ReadAsync(filePath, async stream =>
|
||||
{
|
||||
response.ContentType = MimeTypes.GetMimeType(filePath);
|
||||
response.ContentLength64 = stream.Length;
|
||||
await stream.CopyToAsync(response.OutputStream);
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task Html(this HttpListenerResponse response, string template)
|
||||
{
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(template);
|
||||
response.ContentType = System.Net.Mime.MediaTypeNames.Text.Html;
|
||||
response.ContentLength64 = buffer.Length;
|
||||
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public static async Task Error(this HttpListenerResponse response, string errorCode, string message, int statusCode = 500)
|
||||
{
|
||||
response.StatusCode = statusCode;
|
||||
await response.Json(new
|
||||
{
|
||||
errorCode,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<T?> JsonFromBody<T>(this HttpListenerRequest request)
|
||||
{
|
||||
string jsonText = await request.TextFromBody();
|
||||
return jsonText.ToObject<T>();
|
||||
}
|
||||
|
||||
public static async Task<string> TextFromBody(this HttpListenerRequest request)
|
||||
{
|
||||
using StreamReader reader = new(request.InputStream);
|
||||
return await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
public static string ToJson(this object value, Formatting formatting = Formatting.None)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value, formatting, JsonSettings);
|
||||
}
|
||||
|
||||
public static T? ToObject<T>(this string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json, JsonSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
89
NaiveHttpServer/FileHelper.cs
Normal file
89
NaiveHttpServer/FileHelper.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public static class FileHelper
|
||||
{
|
||||
public static ILogger? Logger { get; set; }
|
||||
|
||||
public static async Task WriteAsync(string path, Func<Stream, Task> writer, bool isBackup = true)
|
||||
{
|
||||
string tempFilePath = $"{path}.writing";
|
||||
using (var stream = new FileStream(
|
||||
tempFilePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.None,
|
||||
0x10000,
|
||||
FileOptions.SequentialScan)
|
||||
)
|
||||
{
|
||||
await writer(stream);
|
||||
}
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
await SpinRetry(() => File.Replace(tempFilePath, path, isBackup ? BackupPath(path) : null, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
await SpinRetry(() => File.Move(tempFilePath, path));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ReadAsync(string path, Func<Stream, Task> reader)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CriticalReadAsync(path, reader);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string backupPath = BackupPath(path);
|
||||
if (!File.Exists(backupPath))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
Logger?.Warning($"Can not read {path}, turn back to backup.", e);
|
||||
await CriticalReadAsync(backupPath, reader);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CriticalReadAsync(string path, Func<Stream, Task> reader)
|
||||
{
|
||||
using FileStream stream = new(
|
||||
path,
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
0x10000,
|
||||
FileOptions.SequentialScan);
|
||||
await reader(stream);
|
||||
}
|
||||
|
||||
private static string BackupPath(string path) => $"{path}.backup";
|
||||
|
||||
private static async Task SpinRetry(Action action, int retryCount = 10)
|
||||
{
|
||||
for (int i = 0; i < retryCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
await Task.Delay(100);
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (i == retryCount - 1)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
NaiveHttpServer/HttpMethods.cs
Normal file
10
NaiveHttpServer/HttpMethods.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public static class HttpMethods
|
||||
{
|
||||
public const string Get = "GET";
|
||||
public const string Post = "POST";
|
||||
public const string Put = "PUT";
|
||||
public const string Delete = "DELETE";
|
||||
}
|
||||
}
|
||||
15
NaiveHttpServer/ILogger.cs
Normal file
15
NaiveHttpServer/ILogger.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Error(string message, Exception? exception = null);
|
||||
|
||||
void Warning(string message, Exception? exception = null);
|
||||
|
||||
void Info(string message, Exception? exception = null);
|
||||
|
||||
void Debug(string message, Exception? exception = null);
|
||||
}
|
||||
}
|
||||
18
NaiveHttpServer/IRouterBuilder.cs
Normal file
18
NaiveHttpServer/IRouterBuilder.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public interface IRouterBuilder
|
||||
{
|
||||
IRouterBuilder Get(string url, Func<Context, Task> handler);
|
||||
|
||||
IRouterBuilder Post(string url, Func<Context, Task> handler);
|
||||
|
||||
IRouterBuilder Delete(string url, Func<Context, Task> handler);
|
||||
|
||||
IRouterBuilder Put(string url, Func<Context, Task> handler);
|
||||
|
||||
Middleware<Context> Build();
|
||||
}
|
||||
}
|
||||
134
NaiveHttpServer/Middlewares.cs
Normal file
134
NaiveHttpServer/Middlewares.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public delegate Task Middleware<in T>(T ctx, Func<Task> next);
|
||||
|
||||
public static class Middlewares
|
||||
{
|
||||
public static Middleware<T> Empty<T>() => (_, next) => next();
|
||||
|
||||
public static async Task Log(Context ctx, Func<Task> next)
|
||||
{
|
||||
ILogger logger = ctx.Logger;
|
||||
HttpListenerRequest request = ctx.Request;
|
||||
HttpListenerResponse response = ctx.Response;
|
||||
|
||||
logger.Debug($"[In] {request.HttpMethod} {request.RawUrl}");
|
||||
await next();
|
||||
logger.Debug($"[Out] {request.HttpMethod} [{response.StatusCode}] {request.RawUrl}");
|
||||
}
|
||||
|
||||
public static async Task ExceptionHandling(Context ctx, Func<Task> next)
|
||||
{
|
||||
using HttpListenerResponse response = ctx.Response;
|
||||
try
|
||||
{
|
||||
await next();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ctx.Logger.Warning("Unexpected exception occurred.", e);
|
||||
await response.Error(ErrorCodes.Unknown, e.Message);
|
||||
response.StatusCode = e switch
|
||||
{
|
||||
FileNotFoundException => 404,
|
||||
DirectoryNotFoundException => 404,
|
||||
UnauthorizedAccessException => 403,
|
||||
_ => 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static Middleware<Context> NotFound(string documentUrl)
|
||||
{
|
||||
return async (ctx, _) =>
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.Error(
|
||||
ErrorCodes.NotFoundApi,
|
||||
$"Not found this api: '{ctx.Request.RawUrl}', and please read the API document: {documentUrl}.");
|
||||
};
|
||||
}
|
||||
|
||||
public static Middleware<Context> StaticFile(string route, string rootDir)
|
||||
{
|
||||
return async (ctx, next) =>
|
||||
{
|
||||
// Don't use Request.RawUrl, because it contains url parameters. (e.g. '?a=1&b=2')
|
||||
string relativePath = ctx.Request.Url.AbsolutePath.TrimStart('/');
|
||||
bool handled = relativePath.StartsWith(route);
|
||||
if (!handled)
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
string requestPath = HttpUtility.UrlDecode(relativePath)
|
||||
.Substring(route.Length)
|
||||
.ToLowerInvariant()
|
||||
.TrimStart('/', '\\');
|
||||
string filePath = Path.Combine(rootDir, requestPath);
|
||||
|
||||
switch (ctx.Request.HttpMethod)
|
||||
{
|
||||
case HttpMethods.Get:
|
||||
await ReadLocalFile(filePath, ctx.Response, ctx.Logger);
|
||||
break;
|
||||
case HttpMethods.Put:
|
||||
await WriteLocalFile(filePath, ctx.Request);
|
||||
ctx.Response.StatusCode = 204;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task ReadLocalFile(string filePath, HttpListenerResponse response, ILogger logger)
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
await response.File(filePath);
|
||||
}
|
||||
else if (Directory.Exists(filePath))
|
||||
{
|
||||
string[] filePaths = Directory
|
||||
.GetFileSystemEntries(filePath)
|
||||
.Select(Path.GetFileName)
|
||||
.ToArray();
|
||||
await response.Json(filePaths);
|
||||
}
|
||||
else
|
||||
{
|
||||
string message = $"Not found the file: '{filePath}'.";
|
||||
logger.Warning(message);
|
||||
await response.Error(ErrorCodes.NotFoundFile, message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task WriteLocalFile(string filePath, HttpListenerRequest request)
|
||||
{
|
||||
return FileHelper.WriteAsync(filePath, stream => request.InputStream.CopyToAsync(stream));
|
||||
}
|
||||
|
||||
public static Middleware<T> Then<T>(this Middleware<T> middleware, Middleware<T> nextMiddleware)
|
||||
{
|
||||
return (ctx, next) => middleware(ctx, () => nextMiddleware(ctx, next));
|
||||
}
|
||||
|
||||
public static Task Run<T>(this Middleware<T> middleware, T ctx)
|
||||
{
|
||||
return middleware(ctx, () =>
|
||||
#if NET45
|
||||
Task.FromResult(0)
|
||||
#else
|
||||
Task.CompletedTask
|
||||
#endif
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
1032
NaiveHttpServer/MimeTypes.cs
Normal file
1032
NaiveHttpServer/MimeTypes.cs
Normal file
File diff suppressed because it is too large
Load Diff
95
NaiveHttpServer/MimeTypes.tt
Normal file
95
NaiveHttpServer/MimeTypes.tt
Normal file
@@ -0,0 +1,95 @@
|
||||
<#@ template debug="false" hostspecific="false" language="C#" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="System.Linq" #>
|
||||
<#@ import namespace="System.Text" #>
|
||||
<#@ import namespace="System.Net" #>
|
||||
<#@ import namespace="System.Collections.Generic" #>
|
||||
<#@ output extension=".cs" #>
|
||||
<# var mediaTypes = GetMediaTypeList(); #>
|
||||
// <auto-generated />
|
||||
|
||||
namespace <#=System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint").ToString()#>
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utilities for mapping file names and extensions to MIME-types.
|
||||
/// </summary>
|
||||
[CompilerGenerated]
|
||||
[DebuggerNonUserCode]
|
||||
public static class MimeTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// The fallback MIME-type. Defaults to <c>application/octet-stream</c>.
|
||||
/// </summary>
|
||||
public static string FallbackMimeType { get; set; }
|
||||
|
||||
private static readonly Dictionary<string, string> TypeMap;
|
||||
|
||||
static MimeTypes()
|
||||
{
|
||||
FallbackMimeType = "application/octet-stream";
|
||||
|
||||
TypeMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
<# foreach (var mediaType in mediaTypes) { #>
|
||||
{ "<#= mediaType.Item1 #>", "<#= mediaType.Item2 #>" },
|
||||
<# } #>
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MIME-type for the given file name,
|
||||
/// or <see cref="FallbackMimeType"/> if a mapping doesn't exist.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file.</param>
|
||||
/// <returns>The MIME-type for the given file name.</returns>
|
||||
public static string GetMimeType(string fileName)
|
||||
{
|
||||
var dotIndex = fileName.LastIndexOf('.');
|
||||
|
||||
return dotIndex != -1 &&
|
||||
fileName.Length > dotIndex + 1 &&
|
||||
TypeMap.TryGetValue(fileName.Substring(dotIndex + 1), out var result)
|
||||
? result
|
||||
: FallbackMimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
<#+
|
||||
private static IList<Tuple<string, string>> GetMediaTypeList()
|
||||
{
|
||||
using (var client = new WebClient())
|
||||
{
|
||||
var list = client.DownloadString(new Uri("http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types"));
|
||||
|
||||
var lines = SplitString(list, '\r', '\n');
|
||||
|
||||
return GetMediaTypes(lines).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Tuple<string, string>> GetMediaTypes(IEnumerable<string> lines)
|
||||
{
|
||||
return lines.Where(x => !x.StartsWith("#"))
|
||||
.Select(line => SplitString(line, '\t', ' '))
|
||||
.SelectMany(CreateMediaTypes)
|
||||
.GroupBy(x => x.Item1)
|
||||
.Where(x => x.Count() == 1)
|
||||
.Select(x => x.Single())
|
||||
.OrderBy(x => x.Item1);
|
||||
}
|
||||
|
||||
private static string[] SplitString(string line, params char[] separator)
|
||||
{
|
||||
return line.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
private static IEnumerable<Tuple<string, string>> CreateMediaTypes(string[] parts)
|
||||
{
|
||||
return parts.Skip(1).Select(extension => Tuple.Create(extension, parts.First()));
|
||||
}
|
||||
#>
|
||||
50
NaiveHttpServer/NaiveHttpServer.csproj
Normal file
50
NaiveHttpServer/NaiveHttpServer.csproj
Normal file
@@ -0,0 +1,50 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462</TargetFrameworks>
|
||||
<Version>1.0.0</Version>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<GeneratePackageOnBuild Condition="'$(Configuration)'=='Release'">true</GeneratePackageOnBuild>
|
||||
<Authors>Dingping Zhang</Authors>
|
||||
<Copyright>Copyright (c) 2020-2021 Dingping Zhang</Copyright>
|
||||
<PackageProjectUrl>https://github.com/DingpingZhang/NaiveHttpServer</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/DingpingZhang/NaiveHttpServer</RepositoryUrl>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>A simple C# http server based on the HttpListener.</Description>
|
||||
<PackageTags>http;http-server;file-server;simple-http-server</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="[13.0.1,)" />
|
||||
<PackageReference Condition="'$(TargetFramework)' == 'net462'" Include="System.ValueTuple" Version="[4.5.0,)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Condition="'$(TargetFramework)' == 'net462'" Include="System.Web" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="MimeTypes.tt">
|
||||
<Generator>TextTemplatingFileGenerator</Generator>
|
||||
<LastGenOutput>MimeTypes.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="MimeTypes.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>MimeTypes.tt</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
50
NaiveHttpServer/NetAclChecker.cs
Normal file
50
NaiveHttpServer/NetAclChecker.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public static class NetAclChecker
|
||||
{
|
||||
public static ILogger? Logger { get; set; }
|
||||
|
||||
public static void AddAddress(string address)
|
||||
{
|
||||
AddAddress(address, Environment.UserDomainName, Environment.UserName);
|
||||
}
|
||||
|
||||
public static void AddAddress(string address, string domain, string user)
|
||||
{
|
||||
string args = $@"http add urlacl url={address}, user={domain}\{user}";
|
||||
|
||||
try
|
||||
{
|
||||
ProcessStartInfo processStartInfo = new("netsh", args)
|
||||
{
|
||||
Verb = "runas",
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = true,
|
||||
};
|
||||
|
||||
var process = Process.Start(processStartInfo);
|
||||
process?.WaitForExit();
|
||||
}
|
||||
catch (Win32Exception e)
|
||||
{
|
||||
if (e.NativeErrorCode == 1223)
|
||||
{
|
||||
Logger?.Info("User canceled the operation by rejected the UAC.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger?.Warning($"Failed to 'netsh http add urlacl {address}' with an {nameof(Win32Exception)}.", e);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger?.Warning($"Failed to 'netsh http add urlacl {address}'.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
NaiveHttpServer/RouterBuilder.cs
Normal file
135
NaiveHttpServer/RouterBuilder.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public class RouterBuilder : IRouterBuilder
|
||||
{
|
||||
private static readonly Regex PathParameterRegex = new("(?<=/):(.+?)(?:(?=/)|$)", RegexOptions.Compiled);
|
||||
private static readonly char[] Separator = { '/' };
|
||||
|
||||
private readonly List<(string url, Func<Context, Task> handler)> _getRoutes = new();
|
||||
private readonly List<(string url, Func<Context, Task> handler)> _postRoutes = new();
|
||||
private readonly List<(string url, Func<Context, Task> handler)> _deleteRoutes = new();
|
||||
private readonly List<(string url, Func<Context, Task> handler)> _putRoutes = new();
|
||||
|
||||
public IRouterBuilder Get(string url, Func<Context, Task> handler)
|
||||
{
|
||||
_getRoutes.Add((url, handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRouterBuilder Post(string url, Func<Context, Task> handler)
|
||||
{
|
||||
_postRoutes.Add((url, handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRouterBuilder Delete(string url, Func<Context, Task> handler)
|
||||
{
|
||||
_deleteRoutes.Add((url, handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRouterBuilder Put(string url, Func<Context, Task> handler)
|
||||
{
|
||||
_putRoutes.Add((url, handler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Middleware<Context> Build()
|
||||
{
|
||||
var getRoutes = GenerateRegexRoutes(_getRoutes);
|
||||
var postRoutes = GenerateRegexRoutes(_postRoutes);
|
||||
var deleteRoutes = GenerateRegexRoutes(_deleteRoutes);
|
||||
var putRoutes = GenerateRegexRoutes(_putRoutes);
|
||||
|
||||
return async (ctx, next) =>
|
||||
{
|
||||
bool handled = ctx.Request.HttpMethod.ToUpperInvariant() switch
|
||||
{
|
||||
HttpMethods.Get => await TryMatch(getRoutes, ctx),
|
||||
HttpMethods.Post => await TryMatch(postRoutes, ctx),
|
||||
HttpMethods.Delete => await TryMatch(deleteRoutes, ctx),
|
||||
HttpMethods.Put => await TryMatch(putRoutes, ctx),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (!handled)
|
||||
{
|
||||
await next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<(Regex regex, Func<Context, Task> handler)> GenerateRegexRoutes(IEnumerable<(string url, Func<Context, Task> handler)> routes)
|
||||
{
|
||||
var toSortRoutes = routes
|
||||
.Select(item => (
|
||||
fragmentCount: item.url.Split(Separator, StringSplitOptions.RemoveEmptyEntries).Length,
|
||||
item.url,
|
||||
item.handler))
|
||||
.ToList();
|
||||
toSortRoutes.Sort((x, y) => y.fragmentCount - x.fragmentCount);
|
||||
|
||||
return toSortRoutes
|
||||
.Select(item => (regex: GetPathRegex(item.url), item.handler))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static Regex GetPathRegex(string url)
|
||||
{
|
||||
HashSet<string> parameterNames = new();
|
||||
string urlRegex = PathParameterRegex
|
||||
.Replace(url.Trim('/'), match =>
|
||||
{
|
||||
if (!parameterNames.Add(match.Value))
|
||||
{
|
||||
throw new ArgumentException($"Cannot contains duplicate variable name: '{match.Value}'.", nameof(url));
|
||||
}
|
||||
|
||||
return $"(?<{match.Groups[1]}>.+?)";
|
||||
});
|
||||
|
||||
return new Regex($"{urlRegex}$", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
private static async Task<bool> TryMatch(IEnumerable<(Regex regex, Func<Context, Task> handler)> routes, Context ctx)
|
||||
{
|
||||
string requestPath = ctx.Request.Url.LocalPath.ToLowerInvariant();
|
||||
|
||||
foreach ((Regex regex, Func<Context, Task> handler) in routes)
|
||||
{
|
||||
Match match = regex.Match(requestPath);
|
||||
if (match.Success)
|
||||
{
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(ctx.Request.Url.Query, Encoding.UTF8);
|
||||
|
||||
ctx.TryGetParameter = (string key, out string value) =>
|
||||
{
|
||||
Group group = match.Groups[key];
|
||||
value = HttpUtility.UrlDecode(group.Value);
|
||||
|
||||
if (!group.Success)
|
||||
{
|
||||
value = query.Get(key);
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(value);
|
||||
};
|
||||
|
||||
await handler(ctx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
NaiveHttpServer/Server.cs
Normal file
114
NaiveHttpServer/Server.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NaiveHttpServer
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
private ILogger _logger = new DefaultLogger();
|
||||
private HttpListener? _listener;
|
||||
private Middleware<Context> _middleware = Middlewares.Empty<Context>();
|
||||
|
||||
public bool IsRunning => _listener is { IsListening: true };
|
||||
|
||||
public string HostUrl { get; }
|
||||
|
||||
public Server(string host, int port)
|
||||
{
|
||||
HostUrl = $"http://{host}:{port}/";
|
||||
}
|
||||
|
||||
public Server Use(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Server Use(Middleware<Context> middleware)
|
||||
{
|
||||
_middleware = _middleware.Then(middleware);
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
StartHttpListener();
|
||||
return true;
|
||||
}
|
||||
catch (HttpListenerException e)
|
||||
{
|
||||
_logger.Warning("Failed to start HttpListener.", e);
|
||||
if (e.ErrorCode == 5)
|
||||
{
|
||||
NetAclChecker.AddAddress(HostUrl);
|
||||
StartHttpListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_listener is { IsListening: true })
|
||||
{
|
||||
_listener.Stop();
|
||||
_logger.Info("Http server has been stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartHttpListener()
|
||||
{
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add(HostUrl);
|
||||
_listener.Start();
|
||||
|
||||
AsyncProcessRequest();
|
||||
|
||||
_logger.Info($"Http server has started listening: {HostUrl}...");
|
||||
}
|
||||
|
||||
private void AsyncProcessRequest()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (_listener!.IsListening)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpListenerContext context = await _listener.GetContextAsync();
|
||||
Context ctx = new(context.Request, context.Response, _logger);
|
||||
|
||||
#pragma warning disable 4014
|
||||
_middleware.Run(ctx);
|
||||
#pragma warning restore 4014
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.Warning(nameof(IOException), e);
|
||||
}
|
||||
catch (HttpListenerException e)
|
||||
{
|
||||
const int errorOperationAborted = 995;
|
||||
if (e.ErrorCode == errorOperationAborted)
|
||||
{
|
||||
// The IO operation has been aborted because of either a thread exit or an application request.
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.Warning(nameof(HttpListenerException), e);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
_logger.Warning(nameof(InvalidOperationException), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user