Compare commits

...

5 Commits

Author SHA1 Message Date
8d55c1c533 implemented check of HttpMethod as binary flags 2025-05-22 21:42:13 +05:00
e4ee03364c updated dependencies 2025-05-22 20:43:55 +05:00
c77b3e0742 LaunchArgumentParser.UnknownArguments 2025-04-26 05:29:30 +05:00
e391f0238a DTLib v1.7.2 2025-04-26 04:20:40 +05:00
823169ca91 LaunchArgumentParser.HelpMessageHeader 2025-04-26 04:19:14 +05:00
10 changed files with 188 additions and 131 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<!--package info--> <!--package info-->
<PackageId>DTLib.Logging.Microsoft</PackageId> <PackageId>DTLib.Logging.Microsoft</PackageId>
<Version>1.1.1</Version> <Version>1.1.3</Version>
<Authors>Timerix</Authors> <Authors>Timerix</Authors>
<Description>DTLib logger wrapper with dependency injection</Description> <Description>DTLib logger wrapper with dependency injection</Description>
<RepositoryType>GIT</RepositoryType> <RepositoryType>GIT</RepositoryType>
@@ -11,7 +11,7 @@
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<!--compilation properties--> <!--compilation properties-->
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFramework>netstandard2.0</TargetFramework>
<!--language features--> <!--language features-->
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
@@ -20,7 +20,7 @@
<!--external dependencies--> <!--external dependencies-->
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
</ItemGroup> </ItemGroup>
<!--DTLib dependencies--> <!--DTLib dependencies-->
@@ -28,6 +28,6 @@
<ProjectReference Include="..\DTLib\DTLib.csproj" /> <ProjectReference Include="..\DTLib\DTLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Debug' "> <ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<PackageReference Include="DTLib" Version="1.6.*" /> <PackageReference Include="DTLib" Version="1.7.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<!--package info--> <!--package info-->
<PackageId>DTLib.Web</PackageId> <PackageId>DTLib.Web</PackageId>
<Version>1.3.0</Version> <Version>1.4.0</Version>
<Authors>Timerix</Authors> <Authors>Timerix</Authors>
<Description>HTTP Server with simple routing</Description> <Description>HTTP Server with simple routing</Description>
<RepositoryType>GIT</RepositoryType> <RepositoryType>GIT</RepositoryType>
@@ -11,7 +11,7 @@
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<!--compilation properties--> <!--compilation properties-->
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFramework>netstandard2.0</TargetFramework>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<!--language features--> <!--language features-->
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@@ -25,6 +25,6 @@
<ProjectReference Include="..\DTLib\DTLib.csproj" /> <ProjectReference Include="..\DTLib\DTLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Debug' "> <ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<PackageReference Include="DTLib" Version="1.7.1" /> <PackageReference Include="DTLib" Version="1.7.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,15 +1,18 @@
namespace DTLib.Web; namespace DTLib.Web;
/// <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods"/> /// <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods"/>
public enum HttpMethod [Flags]
public enum HttpMethod : ushort
{ {
GET, NONE = 0,
POST, GET = 1,
PUT, POST = 2,
DELETE, PUT = 4,
PATCH, DELETE = 8,
HEAD, PATCH = 16,
OPTIONS, HEAD = 32,
TRACE, OPTIONS = 64,
CONNECT TRACE = 128,
CONNECT = 256,
ANY = 65535
} }

View File

@@ -2,5 +2,5 @@ namespace DTLib.Web.Routes;
public interface IRouter public interface IRouter
{ {
Task<HttpStatusCode> Resolve(HttpListenerContext ctx, ContextLogger requestLogger); Task Resolve(HttpListenerContext ctx, ContextLogger requestLogger);
} }

View File

@@ -3,42 +3,63 @@ namespace DTLib.Web.Routes;
public class SimpleRouter : IRouter public class SimpleRouter : IRouter
{ {
/// route for any url that doesn't have its own handler /// route for any url that doesn't have its own handler
public IRouteHandler? DefaultRoute { get; set; } public record RouteWithMethod(HttpMethod method, IRouteHandler routeHandler)
{
public bool CheckMethod(HttpMethod requestMethod) => (requestMethod & method) != 0;
private readonly Dictionary<string, IRouteHandler> _routes = new(); public bool CheckMethod(string requestMethodStr)
=> Enum.TryParse<HttpMethod>(requestMethodStr, out var requestMethod)
&& CheckMethod(requestMethod);
}
private readonly Dictionary<string, RouteWithMethod> _routes = new();
private readonly ILogger _logger; private readonly ILogger _logger;
public RouteWithMethod? DefaultRoute { get; set; }
public SimpleRouter(ILogger logger) public SimpleRouter(ILogger logger)
{ {
_logger = new ContextLogger(nameof(SimpleRouter), logger); _logger = new ContextLogger(nameof(SimpleRouter), logger);
} }
public void MapRoute(string url, HttpMethod method, IRouteHandler route) => _routes.Add($"{url}:{method}", route); public void MapRoute(string url, HttpMethod method, IRouteHandler route)
=> _routes.Add(url, new RouteWithMethod(method, route));
public void MapRoute(string url, HttpMethod method, public void MapRoute(string url, HttpMethod method,
Func<HttpListenerContext, ContextLogger, Task<HttpStatusCode>> route) Func<HttpListenerContext, ContextLogger, Task<HttpStatusCode>> route)
=> MapRoute(url, method, new DelegateRouteHandler(route)); => MapRoute(url, method, new DelegateRouteHandler(route));
public async Task<HttpStatusCode> Resolve(HttpListenerContext ctx, ContextLogger requestLogger) public async Task Resolve(HttpListenerContext ctx, ContextLogger requestLogger)
{
HttpStatusCode status = HttpStatusCode.InternalServerError;
try
{ {
string? requestPath = ctx.Request.Url?.AbsolutePath; string? requestPath = ctx.Request.Url?.AbsolutePath;
if (string.IsNullOrEmpty(requestPath)) if (string.IsNullOrEmpty(requestPath))
requestPath = "/"; requestPath = "/";
if (!_routes.TryGetValue($"{requestPath}:{ctx.Request.HttpMethod}", out var route))
route = DefaultRoute;
HttpStatusCode status; if(!_routes.TryGetValue(requestPath!, out var routeWithMethod))
if (route == null) routeWithMethod = DefaultRoute;
if (routeWithMethod is null)
{ {
_logger.LogWarn(nameof(SimpleRouter), $"couldn't resolve request path {requestPath}"); _logger.LogWarn(nameof(SimpleRouter),
$"couldn't resolve request path {ctx.Request.HttpMethod} {requestPath}");
status = HttpStatusCode.NotFound; status = HttpStatusCode.NotFound;
} }
else status = await route.HandleRequest(ctx, requestLogger); else if (!routeWithMethod.CheckMethod(ctx.Request.HttpMethod))
{
_logger.LogWarn(nameof(SimpleRouter),
$"received request with invalid method {ctx.Request.HttpMethod} {requestPath}");
status = HttpStatusCode.MethodNotAllowed;
}
else status = await routeWithMethod.routeHandler.HandleRequest(ctx, requestLogger);
}
finally
{
ctx.Response.StatusCode = (int)status; ctx.Response.StatusCode = (int)status;
await ctx.Response.OutputStream.FlushAsync(); await ctx.Response.OutputStream.FlushAsync();
ctx.Response.OutputStream.Close(); ctx.Response.OutputStream.Close();
return status; }
} }
} }

View File

@@ -58,9 +58,11 @@ public class WebApp
requestLogger.LogInfo($"{ctx.Request.HttpMethod} {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint}..."); requestLogger.LogInfo($"{ctx.Request.HttpMethod} {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint}...");
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
var status = await _router.Resolve(ctx, requestLogger); await _router.Resolve(ctx, requestLogger);
stopwatch.Stop(); stopwatch.Stop();
requestLogger.LogInfo($"responded {(int)status} ({status}) in {stopwatch.ElapsedMilliseconds}ms"); requestLogger.LogInfo($"responded {ctx.Response.StatusCode}" +
$" ({(HttpStatusCode)ctx.Response.StatusCode})" +
$" in {stopwatch.ElapsedMilliseconds}ms");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -2,32 +2,135 @@ namespace DTLib.Console;
public class LaunchArgumentParser public class LaunchArgumentParser
{ {
public bool IsAllowedNoArguments; public string HelpMessageHeader = "USAGE:";
public bool AllowedNoArguments;
public bool AllowedUnknownArguments;
// ReSharper disable once CollectionNeverQueried.Global
public readonly List<string> UnknownArguments = new();
private readonly Dictionary<string, LaunchArgument> argDict = new(); private readonly Dictionary<string, LaunchArgument> argDict = new();
private readonly List<LaunchArgument> argList = new(); private readonly List<LaunchArgument> argList = new();
public class ExitAfterHelpException : Exception public LaunchArgumentParser()
{ {
internal ExitAfterHelpException() : base("your program can use this exception to exit after displaying help message") var help = new LaunchArgument(new[] { "h", "help" },
{ } "shows help message", HelpHandler);
Add(help);
var helpArg = new LaunchArgument(new[] { "ha", "helparg" },
"shows help message for specific argument",
HelpArgHandler, "argument");
Add(helpArg);
} }
public LaunchArgumentParser(ICollection<LaunchArgument> arguments) : this() => WithArgs(arguments);
public LaunchArgumentParser(params LaunchArgument[] arguments) : this() => WithArgs(arguments);
public LaunchArgumentParser WithArgs(IEnumerable<LaunchArgument> args)
{
foreach (var arg in args)
Add(arg);
return this;
}
public LaunchArgumentParser WithArgs(params LaunchArgument[] args)
{
foreach (var arg in args)
Add(arg);
return this;
}
public LaunchArgumentParser WithHelpMessageHeader(string header)
{
HelpMessageHeader = header;
return this;
}
public LaunchArgumentParser AllowNoArguments()
{
AllowedNoArguments = true;
return this;
}
public LaunchArgumentParser AllowUnknownArguments()
{
AllowedUnknownArguments = true;
return this;
}
public string CreateHelpMessage() public string CreateHelpMessage()
{ {
StringBuilder b = new(); StringBuilder b = new(HelpMessageHeader);
foreach (var arg in argList) foreach (var arg in argList)
arg.AppendHelpInfo(b).Append('\n'); {
b.Remove(b.Length-1, 1); b.Append('\n');
arg.AppendHelpInfo(b);
}
return b.ToString(); return b.ToString();
} }
public string CreateHelpArgMessage(string argAlias) public string CreateHelpArgMessage(string argAlias)
{ {
StringBuilder b = new(); StringBuilder b = new();
var arg = Parse(argAlias); if(!TryParseArg(argAlias, out var arg))
throw new Exception($"unknown argument '{argAlias}'");
arg.AppendHelpInfo(b); arg.AppendHelpInfo(b);
return b.ToString(); return b.ToString();
} }
public void Add(LaunchArgument arg)
{
argList.Add(arg);
foreach (string alias in arg.Aliases)
argDict.Add(alias, arg);
}
public bool TryParseArg(string argAlias, out LaunchArgument arg)
{
// different argument providing patterns
arg = null!;
return argAlias.StartsWith("--") && argDict.TryGetValue(argAlias.Substring(2), out arg) || // --arg
argAlias.StartsWith('-') && argDict.TryGetValue(argAlias.Substring(1), out arg) || // -arg
argAlias.StartsWith('/') && argDict.TryGetValue(argAlias.Substring(1), out arg); // /arg
}
/// <param name="args">program launch args</param>
/// <exception cref="Exception">argument {args[i]} should have a parameter after it</exception>
/// <exception cref="NullReferenceException">argument hasn't got any handlers</exception>
/// <exception cref="ExitAfterHelpException">happens after help message is displayed</exception>
public void ParseAndHandle(string[] args)
{
// show help message and throw ExitAfterHelpException
if (args.Length == 0 && !AllowedNoArguments)
HelpHandler();
List<LaunchArgument> execQueue = new();
for (int i = 0; i < args.Length; i++)
{
if (!TryParseArg(args[i], out var arg))
{
if (!AllowedUnknownArguments)
throw new Exception($"unknown argument '{args[i]}'");
UnknownArguments.Add(args[i]);
}
for (int j = 0; j < arg.Params.Length; j++)
{
if (++i >= args.Length)
throw new Exception(
$"argument '{arg.Aliases[0]}' should have parameter '{arg.Params[j]}' after it");
arg.Params[j].Value = args[i];
}
execQueue.Add(arg);
}
// ascending sort by priority
execQueue.Sort((a0, a1) => a0.Priority - a1.Priority);
// finally executing handlers
foreach (var a in execQueue)
a.Handle();
}
private void HelpHandler() private void HelpHandler()
{ {
System.Console.WriteLine(CreateHelpMessage()); System.Console.WriteLine(CreateHelpMessage());
@@ -40,83 +143,11 @@ public class LaunchArgumentParser
throw new ExitAfterHelpException(); throw new ExitAfterHelpException();
} }
public class ExitAfterHelpException : Exception
public LaunchArgumentParser() {
public ExitAfterHelpException()
: base("your program can use this exception to exit after displaying help message")
{ {
var help = new LaunchArgument(new[] { "h", "help" },
"shows help message", HelpHandler);
Add(help);
var helpArg = new LaunchArgument( new[]{ "ha", "helparg" },
"shows help message for particular argument",
HelpArgHandler, "argAlias");
Add(helpArg);
} }
public LaunchArgumentParser AllowNoArguments()
{
IsAllowedNoArguments = true;
return this;
}
public LaunchArgumentParser(ICollection<LaunchArgument> arguments) : this()
{
foreach (var arg in arguments)
Add(arg);
}
public LaunchArgumentParser(params LaunchArgument[] arguments) : this()
{
foreach (var arg in arguments)
Add(arg);
}
public void Add(LaunchArgument arg)
{
argList.Add(arg);
foreach (string alias in arg.Aliases)
argDict.Add(alias, arg);
}
public LaunchArgument Parse(string argAlias)
{
// different argument providing patterns
if (!argDict.TryGetValue(argAlias, out var arg) && // arg
!(argAlias.StartsWith("--") && argDict.TryGetValue(argAlias.Substring(2), out arg)) && // --arg
!(argAlias.StartsWith('-') && argDict.TryGetValue(argAlias.Substring(1), out arg)) && // -arg
!(argAlias.StartsWith('/') && argDict.TryGetValue(argAlias.Substring(1), out arg))) // /arg
throw new Exception($"invalid argument: {argAlias}\n{CreateHelpMessage()}");
return arg;
}
/// <param name="args">program launch args</param>
/// <exception cref="Exception">argument {args[i]} should have a parameter after it</exception>
/// <exception cref="NullReferenceException">argument hasn't got any handlers</exception>
/// <exception cref="ExitAfterHelpException">happens after help message is displayed</exception>
public void ParseAndHandle(string[] args)
{
// show help message and throw ExitAfterHelpException
if (args.Length == 0 && !IsAllowedNoArguments)
HelpHandler();
List<LaunchArgument> execQueue = new();
for (int i = 0; i < args.Length; i++)
{
LaunchArgument arg = Parse(args[i]);
for (int j = 0; j < arg.Params.Length; j++)
{
if (++i >= args.Length)
throw new Exception($"argument '{arg.Aliases[0]}' should have parameter '{arg.Params[j]}' after it");
arg.Params[j].Value = args[i];
}
execQueue.Add(arg);
}
// ascending sort by priority
execQueue.Sort((a0, a1) => a0.Priority-a1.Priority);
// finally executing handlers
foreach (var a in execQueue)
a.Handle();
} }
} }

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<!--package info--> <!--package info-->
<PackageId>DTLib</PackageId> <PackageId>DTLib</PackageId>
<Version>1.7.1</Version> <Version>1.7.4</Version>
<Authors>Timerix</Authors> <Authors>Timerix</Authors>
<Description>Library for all my C# projects</Description> <Description>Library for all my C# projects</Description>
<RepositoryType>GIT</RepositoryType> <RepositoryType>GIT</RepositoryType>
@@ -31,6 +31,6 @@
<ProjectReference Include="..\DTLib.Demystifier\DTLib.Demystifier.csproj" /> <ProjectReference Include="..\DTLib.Demystifier\DTLib.Demystifier.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Debug' "> <ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<PackageReference Include="DTLib.Demystifier" Version="1.1.0" /> <PackageReference Include="DTLib.Demystifier" Version="1.1.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>