Compare commits
37 Commits
Author | SHA1 | Date |
---|---|---|
Harald Wolff | ceaa7c1685 | |
Harald Wolff | 82499811c6 | |
Harald Wolff | 35517e431c | |
Harald Wolff | eb122b944b | |
Harald Wolff | 7511bf112b | |
Harald Wolff | 16f1b2cf06 | |
Harald Wolff | a0ca936554 | |
Harald Wolff | e7ed29d78d | |
Harald Wolff | b169066235 | |
Harald Wolff | 787e0c860c | |
Harald Wolff | 085ee59d45 | |
Harald Wolff | aebf5e1390 | |
Harald Wolff | a47ced724c | |
Harald Wolff | 48370c7564 | |
Harald Wolff | a1a55d6d10 | |
Harald Wolff | eb741c9224 | |
haraldwolff | 4f70676328 | |
Harald Wolff | 40e4eddbbd | |
Harald Wolff | 7e1291815c | |
haraldwolff | a26c4e33b5 | |
Harald Wolff | caf1ba201f | |
Harald Wolff | 744aaaa48d | |
Harald Wolff | 458954f7fb | |
Harald Wolff | fda7d695d1 | |
Harald Wolff | 8012f01dde | |
Harald Wolff | 4c0caac073 | |
Harald Wolff | 610ed7e944 | |
Harald Wolff | e2ba2b0418 | |
Harald Wolff | a2f114216c | |
Harald Wolff | a76088b628 | |
Harald Wolff | 6ac9df7c16 | |
Harald Wolff | c0ee1af74c | |
Harald Wolff | 16dbf87c66 | |
Harald Wolff | feee6636cc | |
Harald Wolff | df08265edf | |
Harald Wolff | 3e38b1381a | |
Harald Wolff | cab69d0e28 |
|
@ -0,0 +1,13 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/.idea.ln.http.iml
|
||||
/contentModel.xml
|
||||
/modules.xml
|
||||
/projectSettingsUpdater.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../ln.templates" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
40
build.ln
40
build.ln
|
@ -1,47 +1,17 @@
|
|||
{
|
||||
"templates": [
|
||||
"dotnet"
|
||||
],
|
||||
"env": {
|
||||
"NUGET_SOURCE": "https://nexus.niclas-thobaben.de/repository/l--n.de/",
|
||||
"NUGET_SOURCE": "https://nexus.l--n.de/repository/ln.net/",
|
||||
"CONFIGURATION": "Release"
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"name": "setup",
|
||||
"env": {
|
||||
"SOME_ENV_VAR": "Some text",
|
||||
},
|
||||
"commands": [
|
||||
"SH echo Setting up build environment",
|
||||
"SH set",
|
||||
"SH rm -Rf .build"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "prepare",
|
||||
"commands": [
|
||||
"SH dotnet restore",
|
||||
"SH dotnet clean"
|
||||
"dotnet prepare */*.csproj"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "build",
|
||||
"commands": [
|
||||
"SH dotnet build -c $CONFIGURATION"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pack_and_publish",
|
||||
"commands": [
|
||||
"SH dotnet pack ln.http -o .build -c $CONFIGURATION",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "push",
|
||||
"commands": [
|
||||
"SH for NUPKG in .build/ln.*.nupkg; do dotnet nuget push $NUPKG -s $NUGET_SOURCE -k $NUGET_APIKEY; done",
|
||||
],
|
||||
"secrets": {
|
||||
"NUGET_APIKEY": "https://nexus.niclas-thobaben.de"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using ln.http.route;
|
||||
using ln.json;
|
||||
using ln.json.mapping;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace ln.http.helpers
|
||||
{
|
||||
|
||||
public class HttpCaptcha : HttpRoute
|
||||
{
|
||||
private Dictionary<Guid, CaptchaInstance> _captchaInstances = new Dictionary<Guid, CaptchaInstance>();
|
||||
private CaptchaEndpoints _captchaEndpoints;
|
||||
|
||||
public HtmlColor[] CaptchaColors { get; } = new HtmlColor[]
|
||||
{
|
||||
HtmlColor.Red, HtmlColor.Green, HtmlColor.Blue, HtmlColor.Yellow, HtmlColor.Violett, HtmlColor.turquoise
|
||||
};
|
||||
|
||||
public Random _random = new Random(Environment.TickCount + Thread.CurrentThread.GetHashCode());
|
||||
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(10);
|
||||
public int SolutionLength { get; set; }
|
||||
public int GroupLength { get; set; }
|
||||
|
||||
public HttpCaptcha(HttpRouter httpRouter, string mapPath)
|
||||
:this(httpRouter, mapPath, 25, 5)
|
||||
{}
|
||||
public HttpCaptcha(HttpRouter httpRouter, string mapPath, int groupLength, int solutionLength)
|
||||
: base(HttpMethod.ANY, mapPath)
|
||||
{
|
||||
_captchaEndpoints = new CaptchaEndpoints(this);
|
||||
_routerDelegate = _captchaEndpoints.RouteRequest;
|
||||
|
||||
GroupLength = groupLength;
|
||||
SolutionLength = solutionLength;
|
||||
|
||||
httpRouter.Map(this);
|
||||
}
|
||||
|
||||
public CaptchaInstance CreateInstance() => new CaptchaInstance(this);
|
||||
|
||||
public bool Authorize(HttpRequest httpRequest)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
class CaptchaEndpoints : HttpEndpointController
|
||||
{
|
||||
private HttpCaptcha _httpCaptcha;
|
||||
|
||||
public CaptchaEndpoints(HttpCaptcha httpCaptcha)
|
||||
{
|
||||
_httpCaptcha = httpCaptcha;
|
||||
}
|
||||
|
||||
[Map(HttpMethod.GET,"")]
|
||||
public HttpResponse GetCaptcha()
|
||||
{
|
||||
return HttpResponse
|
||||
.OK()
|
||||
.Content(
|
||||
_httpCaptcha
|
||||
.CreateInstance()
|
||||
.Json()
|
||||
);
|
||||
}
|
||||
|
||||
[Map(HttpMethod.GET, "/:instance/:combination")]
|
||||
public HttpResponse DrawCombination(Guid instance, int combination)
|
||||
{
|
||||
int imageSize = 48;
|
||||
|
||||
if (!_httpCaptcha._captchaInstances.TryGetValue(instance, out CaptchaInstance captchaInstance) ||
|
||||
((combination < 0) || (combination >= captchaInstance.Combinations.Length)))
|
||||
return HttpResponse.NotFound();
|
||||
|
||||
CaptchaCombination captchaCombination = captchaInstance.Combinations[combination];
|
||||
|
||||
Image<Rgb24> combinationImage = new Image<Rgb24>(imageSize, imageSize, new Rgb24(255,255,255));
|
||||
IPath combinationPath = null;
|
||||
|
||||
switch (captchaCombination.Form)
|
||||
{
|
||||
case CaptchaForm.Stern:
|
||||
combinationPath = new Star((float)(imageSize / 2.0),
|
||||
(float)(imageSize / 2.0), captchaCombination.Corners,
|
||||
(float)(imageSize / 6.0), (float)(imageSize / 2.3));
|
||||
break;
|
||||
case CaptchaForm.Vieleck:
|
||||
combinationPath = new RegularPolygon((float)(imageSize / 2.0),
|
||||
(float)(imageSize / 2.0), captchaCombination.Corners,
|
||||
(float)(imageSize / 2.3));
|
||||
break;
|
||||
default:
|
||||
return HttpResponse.InternalServerError();
|
||||
}
|
||||
|
||||
combinationImage.Mutate(x => x.Fill(captchaCombination.Color, combinationPath.RotateDegree(_httpCaptcha._random.Next(360))));
|
||||
|
||||
StreamedContent streamedContent = new StreamedContent("image/png");
|
||||
combinationImage.SaveAsPng(streamedContent.ContentStream);
|
||||
|
||||
HttpResponse httpResponse = HttpResponse.OK().Content(streamedContent);
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
[Map(HttpMethod.POST, "/:instance")]
|
||||
public HttpResponse Solve(Guid instance, [HttpArgumentSource(HttpArgumentSource.CONTENT)] JSONValue body)
|
||||
{
|
||||
if (!_httpCaptcha._captchaInstances.TryGetValue(instance, out CaptchaInstance captchaInstance))
|
||||
return HttpResponse.NotFound();
|
||||
|
||||
if (body is JSONArray jsonSolution)
|
||||
{
|
||||
int[] solution = JSONMapper.DefaultMapper.FromJson<int[]>(jsonSolution);
|
||||
|
||||
if (captchaInstance.Solve(solution))
|
||||
{
|
||||
return HttpResponse.NoContent();
|
||||
}
|
||||
return HttpResponse.Forbidden();
|
||||
}
|
||||
else
|
||||
{
|
||||
return HttpResponse.BadRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CaptchaInstance : IDisposable
|
||||
{
|
||||
public Guid Guid { get; }
|
||||
|
||||
public CaptchaCombination[] Combinations { get; private set; }
|
||||
public int[] Solution { get; private set; }
|
||||
|
||||
|
||||
private HttpCaptcha _httpCaptcha;
|
||||
private Timer _timer;
|
||||
|
||||
public CaptchaInstance(HttpCaptcha httpCaptcha)
|
||||
{
|
||||
_httpCaptcha = httpCaptcha;
|
||||
Guid = Guid.NewGuid();
|
||||
|
||||
_httpCaptcha._captchaInstances.Add(Guid, this);
|
||||
_timer = new Timer((state) => Dispose(), null, _httpCaptcha.Timeout, TimeSpan.Zero);
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
HashSet<CaptchaCombination> hashSet = new HashSet<CaptchaCombination>();
|
||||
while (hashSet.Count < _httpCaptcha.GroupLength)
|
||||
hashSet.Add(new CaptchaCombination(this._httpCaptcha));
|
||||
|
||||
List<CaptchaCombination> combinationPool = hashSet.ToList();
|
||||
|
||||
Combinations = new CaptchaCombination[combinationPool.Count];
|
||||
for (int n = 0; n < Combinations.Length; n++)
|
||||
{
|
||||
Combinations[n] = combinationPool[_httpCaptcha._random.Next(combinationPool.Count)];
|
||||
combinationPool.Remove(Combinations[n]);
|
||||
}
|
||||
|
||||
hashSet.Clear();
|
||||
|
||||
while (hashSet.Count < _httpCaptcha.SolutionLength)
|
||||
hashSet.Add(Combinations[_httpCaptcha._random.Next(Combinations.Length)]);
|
||||
|
||||
Solution = hashSet.Select(cs => Array.IndexOf(Combinations, cs)).ToArray();
|
||||
Console.WriteLine(String.Join('-', Solution));
|
||||
Array.Sort(Solution);
|
||||
Console.WriteLine(String.Join('-', Solution));
|
||||
}
|
||||
|
||||
public bool Solve(int[] possibleSolution)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
try
|
||||
{
|
||||
possibleSolution = possibleSolution.Distinct().ToArray();
|
||||
Array.Sort(possibleSolution);
|
||||
|
||||
return Solution.SequenceEqual(possibleSolution);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public JSONObject Json()
|
||||
{
|
||||
JSONObject json = new JSONObject()
|
||||
.Add("uuid", Guid)
|
||||
.Add("questions", Solution.Select(n => Combinations[n].ToString()).ToArray())
|
||||
.Add("group", Combinations.Length);
|
||||
;
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_httpCaptcha?._captchaInstances.Remove(Guid);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => String.Join(", ", Solution.Select(n => Combinations[n].ToString()));
|
||||
}
|
||||
|
||||
public class CaptchaCombination
|
||||
{
|
||||
public HtmlColor Color { get; }
|
||||
public int Corners { get; }
|
||||
public CaptchaForm Form { get; }
|
||||
|
||||
public CaptchaCombination(HttpCaptcha httpCaptcha)
|
||||
{
|
||||
Color = httpCaptcha.CaptchaColors[httpCaptcha._random.Next(httpCaptcha.CaptchaColors.Length)];
|
||||
Corners = 4 + httpCaptcha._random.Next(4);
|
||||
|
||||
CaptchaForm[] captchaForms = Enum.GetValues<CaptchaForm>();
|
||||
Form = captchaForms[httpCaptcha._random.Next(captchaForms.Length)];
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is CaptchaCombination other && Color.Equals(other.Color) &&
|
||||
Corners.Equals(other.Corners) && Form.Equals(other.Form);
|
||||
|
||||
public override int GetHashCode() => Color.GetHashCode() ^ Corners ^ Form.GetHashCode();
|
||||
|
||||
public override string ToString() => $"ein {Form} mit {Corners} {(Form == CaptchaForm.Stern ? "Strahlen" : "Ecken")} in {Color.Name}";
|
||||
}
|
||||
|
||||
public enum CaptchaForm
|
||||
{
|
||||
Stern,
|
||||
Vieleck
|
||||
}
|
||||
|
||||
public class HtmlColor
|
||||
{
|
||||
public static readonly HtmlColor Red = new HtmlColor(255, 0, 0, "Rot");
|
||||
public static readonly HtmlColor Green = new HtmlColor(0, 255, 0, "Grün");
|
||||
public static readonly HtmlColor Blue = new HtmlColor(0, 0, 255, "Blau");
|
||||
public static readonly HtmlColor Yellow = new HtmlColor(240, 240, 0, "Gelb");
|
||||
public static readonly HtmlColor Violett = new HtmlColor(255, 0, 255, "Lila");
|
||||
public static readonly HtmlColor turquoise = new HtmlColor(0, 255, 255, "Türkis");
|
||||
|
||||
|
||||
public byte R { get; }
|
||||
public byte G { get; }
|
||||
public byte B { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public HtmlColor(byte r, byte g, byte b) : this(r, g, b, null)
|
||||
{
|
||||
}
|
||||
|
||||
public HtmlColor(byte r, byte g, byte b, string name)
|
||||
{
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public override string ToString() => $"#{Red:X2}{Green:X2}{Blue:X2}";
|
||||
|
||||
public static implicit operator Color(HtmlColor htmlColor) =>
|
||||
new Rgb24(htmlColor.R, htmlColor.G, htmlColor.B);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using ln.http.route;
|
||||
using MimeKit;
|
||||
|
||||
namespace ln.http.helpers
|
||||
{
|
||||
|
||||
public class HttpMailer : HttpRoute, IDisposable
|
||||
{
|
||||
private HttpRouter _httpRouter;
|
||||
private HttpCaptcha _httpCaptcha;
|
||||
private HttpMailerOptions _httpMailerOptions;
|
||||
private HttpMailerEndpoints _httpMailerEndpoints;
|
||||
|
||||
public HttpMailer(HttpRouter httpRouter, string mapPath, HttpMailerOptions mailerOptions)
|
||||
: this(httpRouter, mapPath, mailerOptions, null)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpMailer(HttpRouter httpRouter, string mapPath, HttpMailerOptions mailerOptions,
|
||||
HttpCaptcha httpCaptcha)
|
||||
: base(HttpMethod.ANY, mapPath)
|
||||
{
|
||||
_httpRouter = httpRouter;
|
||||
_httpCaptcha = httpCaptcha;
|
||||
_httpMailerOptions = mailerOptions;
|
||||
_httpMailerEndpoints = new HttpMailerEndpoints(this);
|
||||
_routerDelegate = _httpMailerEndpoints.RouteRequest;
|
||||
|
||||
httpRouter.Map(this);
|
||||
}
|
||||
|
||||
public class HttpMailerOptions
|
||||
{
|
||||
public string Hostname { get; set; } = "localhost";
|
||||
public int Port { get; set; } = 25;
|
||||
|
||||
public string? Username { get; set; }
|
||||
public string? Password { get; set; }
|
||||
|
||||
public SmtpEncryption Encryption { get; set; } = SmtpEncryption.None;
|
||||
}
|
||||
|
||||
class HttpMailerEndpoints : HttpEndpointController
|
||||
{
|
||||
private HttpMailer _httpMailer;
|
||||
|
||||
public HttpMailerEndpoints(HttpMailer httpMailer)
|
||||
{
|
||||
_httpMailer = httpMailer;
|
||||
}
|
||||
|
||||
|
||||
[Map(HttpMethod.POST, "/mail")]
|
||||
public HttpResponse PostMail(HttpRequest httpRequest)
|
||||
{
|
||||
using (FileStream fs = new FileStream("/tmp/post.txt", FileMode.Create, FileAccess.Write))
|
||||
httpRequest.ContentStream.CopyTo(fs);
|
||||
|
||||
return HttpResponse.NoContent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpRouter?.UnMap(this);
|
||||
_httpMailerEndpoints.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public enum SmtpEncryption
|
||||
{
|
||||
None,
|
||||
Tls,
|
||||
StartTls
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageVersion>0.9.0-test1</PackageVersion>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ln.http\ln.http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,58 @@
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
using ln.bootstrap;
|
||||
using ln.templates;
|
||||
using ln.templates.html;
|
||||
|
||||
namespace ln.http.service
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Bootstrap
|
||||
.DefaultInstance
|
||||
//.AddService<HttpServiceHelper>()
|
||||
.Start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
class HttpServiceHelper : HttpRouter
|
||||
{
|
||||
private TemplateDocument _templateDocument;
|
||||
public HttpServiceHelper(HttpRouter httpRouter)
|
||||
:base()
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(
|
||||
Assembly
|
||||
.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("ln.http.service.error.html")
|
||||
)
|
||||
)
|
||||
{
|
||||
_templateDocument = TemplateReader.ReadTemplate(reader);
|
||||
}
|
||||
|
||||
Map(HttpMethod.ANY, "/_err.html", HttpRoutePriority.LOW, this.HttpError);
|
||||
}
|
||||
|
||||
bool HttpError(HttpContext httpContext)
|
||||
{
|
||||
HttpResponse httpResponse =
|
||||
new HttpResponse(httpContext.HttpException?.HttpStatusCode ?? HttpStatusCode.InternalServerError);
|
||||
|
||||
RenderContext renderContext = new RenderContext(httpResponse.ContentWriter);
|
||||
renderContext
|
||||
.GetEngine()
|
||||
.SetValue("httpContext", httpContext);
|
||||
|
||||
_templateDocument.RenderTemplate(renderContext);
|
||||
httpContext.Response = httpResponse;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"System.IO.StreamWriter, System.Runtime": {
|
||||
"domain": "log.http",
|
||||
"set": {
|
||||
"parameters": {
|
||||
"path": "http.access.log"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.http.HttpServer": {
|
||||
"resolve": {
|
||||
"parameters": {
|
||||
"accessLogWriter": "(log.http) System.IO.StreamWriter, System.Runtime",
|
||||
"logWriter": "(log.console) ln.logging.LogWriter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.http.HttpListener": {
|
||||
"set": {
|
||||
"properties": {
|
||||
"DefaultPort": 8180
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.http.HttpsListener": {
|
||||
"set": {
|
||||
"properties": {
|
||||
"DefaultPort": 8443
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.logging.ConsoleLogSink": {
|
||||
"domain": "log.console"
|
||||
},
|
||||
"ln.logging.LogWriter": {
|
||||
"domain": "log.console",
|
||||
"resolve": {
|
||||
"parameter": {
|
||||
"logSink": "(log.console) ln.logging.ILogSink"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.logging.ConsoleWrapper": {
|
||||
"resolve": {
|
||||
"parameters": {
|
||||
"logSink": "(log.console) ln.logging.ILogSink"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Sorry, we had some trouble serving your request</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ httpContext.HttpException.HttpStatusCode }} {{ httpContext.HttpException.Message }}</h1>
|
||||
<p>The request URI that caused this was <em>{{ httpContext.SourceContext.Request.RequestUri }}</em></p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"ln.http.HttpRouter, ln.http": {
|
||||
"resolve": {
|
||||
"parameters": {
|
||||
"httpServer": "ln.http.HttpServer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.http.templates.FileSystemTemplateRouter, ln.http.templates": {
|
||||
"set": {
|
||||
"parameters": {
|
||||
"path": "/home/haraldwolff/src/www/dambacher",
|
||||
"mappingPath": "/*"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"parameters": {
|
||||
"httpRouter" : "ln.http.HttpRouter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.http.FileSystemRouter, ln.http": {
|
||||
"set": {
|
||||
"parameters": {
|
||||
"path": "/home/haraldwolff/src/www/dambacher",
|
||||
"mappingPath": "/*"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"parameters": {
|
||||
"httpRouter" : "ln.http.HttpRouter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.http.helpers.HttpCaptcha": {
|
||||
"resolve": {
|
||||
"parameters": {
|
||||
"httpRouter": "ln.http.HttpRouter"
|
||||
}
|
||||
},
|
||||
"set": {
|
||||
"parameters": {
|
||||
"mapPath": "/captcha/*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ln.http.helpers.HttpMailer": {
|
||||
"resolve": {
|
||||
"parameters": {
|
||||
"httpRouter": "ln.http.HttpRouter"
|
||||
}
|
||||
},
|
||||
"set": {
|
||||
"parameters": {
|
||||
"mapPath": "/mailer/*",
|
||||
"mailerOptions": {
|
||||
"HostName": "smtp.l--n.de",
|
||||
"Port": 465
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<LangVersion>9</LangVersion>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<PackageVersion>1.0.1</PackageVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../ln.http/ln.http.csproj" />
|
||||
<ProjectReference Include="..\..\ln.templates\ln.http.templates\ln.http.templates.csproj" />
|
||||
<ProjectReference Include="..\ln.http.helpers\ln.http.helpers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ln.bootstrap" Version="1.4.0" />
|
||||
<PackageReference Include="ln.http" Version="0.9.1-test01" />
|
||||
<!--PackageReference Include="ln.http.templates" Version="0.1.1" /-->
|
||||
<!--PackageReference Include="ln.templates" Version="0.3.0" /-->
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="bootstrap.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ln.http.HttpServer.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="error.html" />
|
||||
<EmbeddedResource Include="error.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
Binary file not shown.
31
ln.http.sln
31
ln.http.sln
|
@ -7,14 +7,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http", "ln.http\ln.http.
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.tests", "ln.http.tests\ln.http.tests.csproj", "{476CD242-9329-449C-95E4-A5317635B223}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.service", "ln.http.service\ln.http.service.csproj", "{FE139A5A-A388-4656-AD15-149012EDB9D0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.templates", "..\ln.templates\ln.http.templates\ln.http.templates.csproj", "{45709176-EA57-4BB3-815B-91D161CEB7A6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.helpers", "ln.http.helpers\ln.http.helpers.csproj", "{FAC3F9BE-6C09-4523-8584-2BD5B08BB70D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -44,5 +46,26 @@ Global
|
|||
{476CD242-9329-449C-95E4-A5317635B223}.Release|x64.Build.0 = Release|Any CPU
|
||||
{476CD242-9329-449C-95E4-A5317635B223}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{476CD242-9329-449C-95E4-A5317635B223}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FE139A5A-A388-4656-AD15-149012EDB9D0}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{45709176-EA57-4BB3-815B-91D161CEB7A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{45709176-EA57-4BB3-815B-91D161CEB7A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{45709176-EA57-4BB3-815B-91D161CEB7A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{45709176-EA57-4BB3-815B-91D161CEB7A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FAC3F9BE-6C09-4523-8584-2BD5B08BB70D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FAC3F9BE-6C09-4523-8584-2BD5B08BB70D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FAC3F9BE-6C09-4523-8584-2BD5B08BB70D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FAC3F9BE-6C09-4523-8584-2BD5B08BB70D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using ln.http.helpers;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ln.http.tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CaptchaTests
|
||||
{
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestCaptcha()
|
||||
{
|
||||
HttpCaptcha httpCaptcha = new HttpCaptcha(null, null);
|
||||
|
||||
for (int n = 0; n < 1024; n++)
|
||||
{
|
||||
HttpCaptcha.CaptchaInstance captchaInstance = httpCaptcha.CreateInstance();
|
||||
Console.WriteLine(captchaInstance.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using ln.http.client;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ln.http.tests
|
||||
{
|
||||
public class ClientTests
|
||||
{
|
||||
private HttpClient HttpClient;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
HttpClient = new HttpClient();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClient()
|
||||
{
|
||||
var o = HttpClient.Get("http://l--n.de");
|
||||
if (o && o.Value is HttpClientResponse response)
|
||||
{
|
||||
Assert.AreEqual(HttpStatusCode.Found, response.StatusCode);
|
||||
Assert.AreEqual("https://l--n.de/", response.Headers.Get("Location"));
|
||||
}
|
||||
|
||||
HttpClient.FollowRedirects = true;
|
||||
o = HttpClient.Get("http://l--n.de");
|
||||
if (o && o.Value is HttpClientResponse response2)
|
||||
{
|
||||
Assert.AreEqual(HttpStatusCode.OK, response2.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -1,18 +1,83 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using ln.json;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ln.http.tests
|
||||
{
|
||||
public class Tests
|
||||
{
|
||||
private HttpRouter TestRouter;
|
||||
private int TestPort;
|
||||
private Listener _httpListener;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
TestRouter = new HttpRouter();
|
||||
TestRouter.Map(new TestApiController());
|
||||
FileSystemRouter fileSystemRouter = new FileSystemRouter("/static/", AppContext.BaseDirectory);
|
||||
TestRouter.Map(fileSystemRouter);
|
||||
|
||||
_httpListener = new Listener(TestRouter, IPAddress.Any, 0);
|
||||
TestPort = _httpListener.LocalEndpoint.Port;
|
||||
TestContext.Error.WriteLine("Using Port {0}", TestPort);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_httpListener?.Dispose();
|
||||
TestRouter?.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
HttpClient client = new HttpClient();
|
||||
HttpResponseMessage response = client.GetAsync(String.Format("http://localhost:{0}/static/test.txt", TestPort)).Result;
|
||||
|
||||
Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
byte[] contentBytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||
byte[] fileBytes = File.ReadAllBytes("test.txt");
|
||||
|
||||
CollectionAssert.AreEqual(fileBytes, contentBytes);
|
||||
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPutJson()
|
||||
{
|
||||
JSONObject jsonPutObject = new JSONObject();
|
||||
jsonPutObject["PutTest"] = JSONTrue.Instance;
|
||||
System.Net.Http.StringContent jsonStringContent = new System.Net.Http.StringContent(jsonPutObject.ToString());
|
||||
jsonStringContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
|
||||
|
||||
HttpClient client = new HttpClient();
|
||||
HttpResponseMessage response = client.PutAsync(String.Format("http://localhost:{0}/controller/put", TestPort), jsonStringContent).Result;
|
||||
|
||||
Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Pass();
|
||||
}
|
||||
}
|
||||
|
||||
[Map(HttpMethod.ANY, "/controller")]
|
||||
class TestApiController : HttpEndpointController
|
||||
{
|
||||
[Map(HttpMethod.PUT, "/put")]
|
||||
public HttpResponse PutTest(
|
||||
[HttpArgumentSource(HttpArgumentSource.CONTENT)]
|
||||
JSONObject jObject
|
||||
)
|
||||
{
|
||||
if (jObject.ContainsKey("PutTest") && jObject["PutTest"] is JSONTrue jsonTrue && jsonTrue == JSONTrue.Instance)
|
||||
return HttpResponse.OK();
|
||||
return HttpResponse.BadRequest();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<LangVersion>9</LangVersion>
|
||||
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
|
||||
<ProjectReference Include="../ln.http/ln.http.csproj" />
|
||||
<PackageReference Include="ln.type" Version="0.1.9" />
|
||||
|
||||
<Content Include="test.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
<ProjectReference Include="..\ln.http.helpers\ln.http.helpers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
0123456789
|
||||
ÄÖÜ
|
|
@ -1,38 +0,0 @@
|
|||
// /**
|
||||
// * File: AuthenticationProvider.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace ln.http
|
||||
{
|
||||
public abstract class AuthenticationProvider
|
||||
{
|
||||
public AuthenticationProvider()
|
||||
{
|
||||
}
|
||||
|
||||
public abstract IEnumerable<HttpUser> EnumerateUsers();
|
||||
public abstract HttpUser Authenticate(HttpRequest httpRequest);
|
||||
|
||||
public virtual HttpUser GetHttpUser(String authenticationName)
|
||||
{
|
||||
foreach (HttpUser httpUser in EnumerateUsers())
|
||||
{
|
||||
if (httpUser.AuthenticationName.Equals(authenticationName))
|
||||
{
|
||||
return httpUser;
|
||||
}
|
||||
}
|
||||
throw new KeyNotFoundException();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// /**
|
||||
// * File: AuthorizationMask.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
namespace ln.http
|
||||
{
|
||||
public static class AuthorizationMask
|
||||
{
|
||||
public static readonly long A_ACCESS = (1l << 0);
|
||||
public static readonly long A_READ = (1l << 0);
|
||||
public static readonly long A_WRITE = (1l << 0);
|
||||
public static readonly long A_EXEC = (1l << 0);
|
||||
|
||||
public static readonly long A_SUPER = (-1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using ln.collections;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public class CertificateStore
|
||||
{
|
||||
private Cache<string, X509Certificate> _cache = new Cache<string, X509Certificate>();
|
||||
|
||||
public CertificateStore()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void AddCertificate(X509Certificate certificate) => _cache.Add(certificate.Subject, certificate);
|
||||
public virtual bool TryGetCertificate(string hostname, out X509Certificate certificate) =>
|
||||
_cache.TryGetValue(String.Format("CN={0}", hostname), out certificate);
|
||||
|
||||
public virtual void RemoveCertificate(string hostname) => _cache.Remove(hostname);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// /**
|
||||
// * File: FileSystemRouter.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System.IO;
|
||||
using ln.http.mime;
|
||||
using ln.http.route;
|
||||
using ln.mime;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class FileRouter : HttpRoute
|
||||
{
|
||||
public string FileName { get; }
|
||||
public string ContentType { get; }
|
||||
|
||||
public FileRouter(string filename)
|
||||
:this(null, filename)
|
||||
{
|
||||
}
|
||||
|
||||
public FileRouter(string filename, MimeTypes mimeTypes)
|
||||
:this(null, filename)
|
||||
{
|
||||
ContentType = mimeTypes.GetMimeTypeByExtension(Path.GetExtension(FileName)).ToString();
|
||||
}
|
||||
public FileRouter(string filename, string contentType)
|
||||
:base(HttpMethod.GET, null)
|
||||
{
|
||||
ContentType = contentType;
|
||||
|
||||
if (!File.Exists(filename))
|
||||
throw new FileNotFoundException();
|
||||
|
||||
FileName = filename;
|
||||
_routerDelegate = RouteToFile;
|
||||
}
|
||||
|
||||
public bool RouteToFile(HttpRequestContext requestContext, string routePath)
|
||||
{
|
||||
requestContext.Response =
|
||||
HttpResponse
|
||||
.OK()
|
||||
.Content(new FileStream(FileName, FileMode.Open, FileAccess.Read))
|
||||
.ContentType(ContentType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// /**
|
||||
// * File: FileSystemRouter.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using ln.http.route;
|
||||
using ln.mime;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class FileSystemRouter : HttpRoute
|
||||
{
|
||||
private string _rootPath;
|
||||
public String RootPath
|
||||
{
|
||||
get => _rootPath;
|
||||
private set
|
||||
{
|
||||
_rootPath = Path.GetFullPath(value);
|
||||
}
|
||||
}
|
||||
|
||||
List<string> indexNames = new List<string>();
|
||||
public String[] IndexNames => indexNames.ToArray();
|
||||
|
||||
private HttpRouter _parentRouter;
|
||||
private MimeTypes _mimeTypes;
|
||||
|
||||
public FileSystemRouter(HttpRouter parentRouter, string mapPath, MimeTypes mimeTypes, string fileSystemPath)
|
||||
:base(HttpMethod.GET, mapPath)
|
||||
{
|
||||
_parentRouter = parentRouter;
|
||||
_parentRouter?.Map(this);
|
||||
|
||||
_mimeTypes = mimeTypes;
|
||||
|
||||
if (!Directory.Exists(fileSystemPath))
|
||||
throw new FileNotFoundException();
|
||||
|
||||
RootPath = fileSystemPath;
|
||||
Console.Error.WriteLine("FileSystemRouter created ({0})", RootPath);
|
||||
|
||||
AddIndex("index.html");
|
||||
AddIndex("index.htm");
|
||||
|
||||
_routerDelegate = RouteToFileSystem;
|
||||
}
|
||||
|
||||
public FileSystemRouter(string mapPath, MimeTypes mimeTypes, string fileSystemPath)
|
||||
:this(null, mapPath, mimeTypes, fileSystemPath){}
|
||||
|
||||
public FileSystemRouter(string mapPath, string fileSystemPath)
|
||||
:this(null, mapPath, null, fileSystemPath){}
|
||||
|
||||
public FileSystemRouter(MimeTypes mimeTypes, string fileSystemPath)
|
||||
:this(null, null, mimeTypes, fileSystemPath)
|
||||
{
|
||||
}
|
||||
public FileSystemRouter(string fileSystemPath)
|
||||
:this(null, null, null, fileSystemPath)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddIndex(string indexName) => indexNames.Add(indexName);
|
||||
public void RemoveIndex(string indexName) => indexNames.Remove(indexName);
|
||||
|
||||
public bool RouteToFileSystem(HttpRequestContext requestContext, string routePath)
|
||||
{
|
||||
string filename = ChooseFile(routePath);
|
||||
|
||||
if (filename is null)
|
||||
return false;
|
||||
|
||||
requestContext.Response =
|
||||
HttpResponse
|
||||
.OK()
|
||||
.Content(new FileStream(filename, FileMode.Open, FileAccess.Read));
|
||||
|
||||
if (_mimeTypes?.TryGetMimeTypeByExtension(Path.GetExtension(filename), out MimeType mimeType) ?? false)
|
||||
requestContext.Response.ContentType(mimeType.ToString());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
string ChooseFile(string path)
|
||||
{
|
||||
string finalPath = Path.Join(_rootPath, path);
|
||||
|
||||
if (File.Exists(finalPath))
|
||||
return finalPath;
|
||||
|
||||
if (Directory.Exists(finalPath))
|
||||
{
|
||||
foreach (string indexName in indexNames)
|
||||
{
|
||||
string indexFileName = Path.Combine(finalPath, indexName);
|
||||
if (File.Exists(indexFileName))
|
||||
return indexFileName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_parentRouter?.UnMap(this);
|
||||
_parentRouter = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ln.logging;
|
||||
using ln.threading;
|
||||
using ln.application;
|
||||
using ln.http.listener;
|
||||
using ln.http.connections;
|
||||
using System.Globalization;
|
||||
using ln.http.exceptions;
|
||||
using System.Threading;
|
||||
using ln.type;
|
||||
using ln.http.router;
|
||||
using System.IO;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HTTPServer
|
||||
{
|
||||
public static int backlog = 5;
|
||||
public static int defaultPort = 8080;
|
||||
public static bool exclusivePortListener = false;
|
||||
|
||||
public IHttpRouter Router { get; set; }
|
||||
|
||||
public bool IsRunning => !shutdown && (threadPool.CurrentPoolSize > 0);
|
||||
public Logger Logger { get; set; }
|
||||
|
||||
bool shutdown = false;
|
||||
|
||||
List<Listener> listeners = new List<Listener>();
|
||||
public Listener[] Listeners => listeners.ToArray();
|
||||
|
||||
DynamicPool threadPool;
|
||||
public DynamicPool ThreadPool => threadPool;
|
||||
|
||||
HashSet<Connection> currentConnections = new HashSet<Connection>();
|
||||
public IEnumerable<Connection> CurrentConnections => currentConnections;
|
||||
|
||||
public HTTPServer()
|
||||
{
|
||||
Logger = Logger.Default;
|
||||
threadPool = new DynamicPool(1024);
|
||||
}
|
||||
public HTTPServer(IHttpRouter router)
|
||||
: this()
|
||||
{
|
||||
Router = router;
|
||||
}
|
||||
public HTTPServer(Listener listener, IHttpRouter router)
|
||||
: this(router)
|
||||
{
|
||||
AddListener(listener);
|
||||
}
|
||||
public HTTPServer(Endpoint endpoint, IHttpRouter router)
|
||||
: this(new HttpListener(endpoint), router) { }
|
||||
|
||||
public void AddListener(Listener listener)
|
||||
{
|
||||
listeners.Add(listener);
|
||||
if (IsRunning)
|
||||
StartListener(listener);
|
||||
}
|
||||
|
||||
public void StartListener(Listener listener)
|
||||
{
|
||||
listener.Open();
|
||||
|
||||
threadPool.Enqueue(
|
||||
() => listener.AcceptMany(
|
||||
(connection) => threadPool.Enqueue(
|
||||
() => this.HandleConnection(connection)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void StopListener(Listener listener)
|
||||
{
|
||||
listener.Close();
|
||||
}
|
||||
|
||||
|
||||
public void AddEndpoint(Endpoint endpoint)
|
||||
{
|
||||
AddListener(new HttpListener(endpoint));
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
threadPool.Start();
|
||||
|
||||
foreach (Listener listener in listeners)
|
||||
StartListener(listener);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
this.shutdown = true;
|
||||
}
|
||||
foreach (Listener listener in listeners)
|
||||
StopListener(listener);
|
||||
|
||||
for (int n = 0; n < 150; n++)
|
||||
{
|
||||
lock (currentConnections)
|
||||
{
|
||||
if (currentConnections.Count == 0)
|
||||
break;
|
||||
if ((n % 20) == 0)
|
||||
{
|
||||
Logging.Log(LogLevel.INFO, "HTTPServer: still waiting for {0} connections to close", currentConnections.Count);
|
||||
}
|
||||
}
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
lock (currentConnections)
|
||||
{
|
||||
foreach (Connection connection in currentConnections)
|
||||
{
|
||||
connection.Close();
|
||||
}
|
||||
}
|
||||
|
||||
threadPool.Stop(true);
|
||||
}
|
||||
|
||||
private void HandleConnection(Connection connection)
|
||||
{
|
||||
lock (this.currentConnections)
|
||||
currentConnections.Add(connection);
|
||||
|
||||
try
|
||||
{
|
||||
HttpRequest httpRequest = null;
|
||||
bool keepAlive = true;
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
httpRequest = connection.ReadRequest(this);
|
||||
|
||||
if (httpRequest == null)
|
||||
break;
|
||||
|
||||
HttpResponse response;
|
||||
try
|
||||
{
|
||||
response = Router.Route(new HttpRoutingContext(httpRequest),httpRequest);
|
||||
}
|
||||
catch (HttpException httpExc)
|
||||
{
|
||||
response = new HttpResponse((HttpStatusCode)httpExc.StatusCode).Content(httpExc.Message);
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
response = HttpResponse.NotFound().Content(String.Format("The URI {0} could not be found on this server.", httpRequest.URI));
|
||||
|
||||
keepAlive = httpRequest.GetRequestHeader("connection", "keep-alive").Equals("keep-alive") && response.GetHeader("connection", "keep-alive").Equals("keep-alive");
|
||||
response.SetHeader("connection", keepAlive ? "keep-alive" : "close");
|
||||
|
||||
SendResponse(connection.GetStream(), httpRequest, response);
|
||||
|
||||
response?.ContentStream?.Dispose();
|
||||
|
||||
} while (keepAlive);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logging.Log(e);
|
||||
|
||||
SendResponse(
|
||||
connection.GetStream(),
|
||||
httpRequest,
|
||||
HttpResponse
|
||||
.InternalServerError()
|
||||
.Content(e)
|
||||
);
|
||||
}
|
||||
|
||||
HttpRequest.ClearCurrent();
|
||||
connection.GetStream().Close();
|
||||
} finally
|
||||
{
|
||||
lock (currentConnections)
|
||||
currentConnections.Remove(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendResponse(Stream stream, HttpRequest request, HttpResponse response)
|
||||
{
|
||||
request.FinishRequest();
|
||||
response.SetHeader("Content-Length", response.ContentStream.Length.ToString());
|
||||
|
||||
StreamWriter streamWriter = new StreamWriter(stream);
|
||||
streamWriter.NewLine = "\r\n";
|
||||
|
||||
streamWriter.WriteLine("{0} {1} {2}", request.Protocol, (int)response.HttpStatusCode, response.HttpStatusCode.ToString());
|
||||
foreach (String headerName in response.GetHeaderNames())
|
||||
{
|
||||
streamWriter.WriteLine("{0}: {1}", headerName, response.GetHeader(headerName));
|
||||
}
|
||||
|
||||
foreach (HttpCookie httpCookie in response.Cookies)
|
||||
{
|
||||
streamWriter.WriteLine("Set-Cookie: {0}", httpCookie.ToString());
|
||||
}
|
||||
|
||||
streamWriter.WriteLine();
|
||||
streamWriter.Flush();
|
||||
|
||||
response.ContentStream.Position = 0;
|
||||
response.ContentStream.CopyTo(stream);
|
||||
response.ContentStream.Close();
|
||||
response.ContentStream.Dispose();
|
||||
|
||||
stream.Flush();
|
||||
}
|
||||
|
||||
public static void StartSimpleServer(string[] arguments)
|
||||
{
|
||||
ArgumentContainer argumentContainer = new ArgumentContainer(new Argument[]
|
||||
{
|
||||
new Argument('p',"port",8080),
|
||||
new Argument('l',"listen","127.0.0.1"),
|
||||
new Argument('c', "catch",null)
|
||||
});
|
||||
|
||||
argumentContainer.Parse(ref arguments);
|
||||
|
||||
SimpleRouter router = new SimpleRouter();
|
||||
router.AddSimpleRoute("/*", new RouterTarget((request) =>
|
||||
{
|
||||
HttpResponse response = new HttpResponse(request);
|
||||
response.StatusCode = 404;
|
||||
response.SetHeader("content-type", "text/plain");
|
||||
response.ContentWriter.WriteLine("404 Not Found");
|
||||
response.ContentWriter.Flush();
|
||||
return response;
|
||||
}), -100);
|
||||
|
||||
foreach (String path in arguments)
|
||||
{
|
||||
StaticRouter staticRouter = new StaticRouter(path);
|
||||
staticRouter.AddIndex("index.html");
|
||||
staticRouter.AddIndex("index.htm");
|
||||
router.AddSimpleRoute("/*", staticRouter);
|
||||
}
|
||||
|
||||
if (argumentContainer['c'].Value != null)
|
||||
router.AddSimpleRoute("/*", new RouterTarget((request) => router.Route(new HttpRoutingContext(request, argumentContainer['c'].Value), request)), 0);
|
||||
|
||||
HTTPServer server = new HTTPServer(new Endpoint(IPv6.Parse(argumentContainer['l'].Value),int.Parse(argumentContainer['p'].Value)),
|
||||
new LoggingRouter(router));
|
||||
server.Start();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
using System;
|
||||
using System.Net.Sockets;
|
||||
using ln.threading;
|
||||
using System.Net;
|
||||
using ln.type;
|
||||
using ln.logging;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
namespace ln.http
|
||||
{
|
||||
//public delegate void HTTPServerConnectionEvent(HTTPServerConnection connection);
|
||||
|
||||
//public class HTTPServerConnection : PoolJob
|
||||
//{
|
||||
// public static ThreadLocal<HTTPServerConnection> Current { get; } = new ThreadLocal<HTTPServerConnection>();
|
||||
// static HashSet<HTTPServerConnection> currentConnections = new HashSet<HTTPServerConnection>();
|
||||
// public static HTTPServerConnection[] CurrentConnections => currentConnections.ToArray();
|
||||
|
||||
// public HTTPServer HTTPServer { get; }
|
||||
// public TcpClient TcpClient { get; }
|
||||
|
||||
// public HttpRequest CurrentRequest { get; protected set; }
|
||||
|
||||
// public event HTTPServerConnectionEvent AbortRequested;
|
||||
|
||||
// public DateTime Created { get; }
|
||||
// public DateTime Interpreted { get; set; }
|
||||
// public DateTime Finished { get; set; }
|
||||
|
||||
// public HTTPServerConnection(HTTPServer httpServer,TcpClient tcpClient)
|
||||
// {
|
||||
// HTTPServer = httpServer;
|
||||
// TcpClient = tcpClient;
|
||||
// Created = DateTime.Now;
|
||||
// }
|
||||
|
||||
// public virtual HttpResponse GetResponse(HttpRequest httpRequest,HttpApplication httpApplication) => httpApplication.GetResponse(httpRequest);
|
||||
|
||||
// public virtual void Abort()
|
||||
// {
|
||||
// if (AbortRequested != null)
|
||||
// AbortRequested(this);
|
||||
// }
|
||||
|
||||
|
||||
// public override void RunJob()
|
||||
// {
|
||||
// HTTPServerConnection saveCurrent = Current.Value;
|
||||
// Current.Value = this;
|
||||
// lock (currentConnections)
|
||||
// currentConnections.Add(this);
|
||||
|
||||
// try
|
||||
// {
|
||||
// setState("reading http request");
|
||||
|
||||
// HttpReader httpReader = new HttpReader(TcpClient.GetStream());
|
||||
// httpReader.Read();
|
||||
|
||||
// if (!httpReader.Valid)
|
||||
// return;
|
||||
|
||||
// HttpResponse response = null;
|
||||
|
||||
// using (CurrentRequest = new HttpRequest(this.HTTPServer,httpReader, (IPEndPoint)TcpClient.Client.LocalEndPoint))
|
||||
// {
|
||||
// Interpreted = DateTime.Now;
|
||||
|
||||
// try
|
||||
// {
|
||||
// HttpApplication application = HTTPServer.GetHttpApplication(new URI(CurrentRequest.BaseURI.ToString()));
|
||||
|
||||
// application.Authenticate(CurrentRequest);
|
||||
// application.Authorize(CurrentRequest);
|
||||
|
||||
// setState("handling http request");
|
||||
|
||||
// response = GetResponse(CurrentRequest, application);
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
// setState("handling exception");
|
||||
|
||||
// response = new HttpResponse(CurrentRequest, "text/plain");
|
||||
// response.StatusCode = 500;
|
||||
// response.ContentWriter.WriteLine("Exception caught: {0}", e);
|
||||
// }
|
||||
|
||||
// setState("sending response");
|
||||
|
||||
// if (response == null)
|
||||
// {
|
||||
// Logging.Log(LogLevel.DEBUG, "Request {0} returned no Response", CurrentRequest);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (!response.HasCustomContentStream)
|
||||
// {
|
||||
// response.ContentWriter.Flush();
|
||||
// MemoryStream cstream = (MemoryStream)response.ContentStream;
|
||||
// cstream.Position = 0;
|
||||
|
||||
// response.SetHeader("content-length", cstream.Length.ToString());
|
||||
// }
|
||||
|
||||
// if (CurrentRequest.Session != null)
|
||||
// HTTPServer?.SessionCache?.ApplySessionID(response, CurrentRequest.Session);
|
||||
|
||||
// response.AddCookie("LN_SEEN", DateTime.Now.ToString());
|
||||
|
||||
// SendResponse(TcpClient.GetStream(), response);
|
||||
// TcpClient.Close();
|
||||
|
||||
// Finished = DateTime.Now;
|
||||
|
||||
// HTTPServer.Log(Created, (Finished - Created).TotalMilliseconds, CurrentRequest, response);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// Current.Value = saveCurrent;
|
||||
// lock (currentConnections)
|
||||
// currentConnections.Remove(this);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public static void SendResponse(Stream stream, HttpResponse response)
|
||||
// {
|
||||
// StreamWriter streamWriter = new StreamWriter(stream);
|
||||
// streamWriter.NewLine = "\r\n";
|
||||
|
||||
// streamWriter.WriteLine("{0} {1} {2}", response.HttpRequest.Protocol, response.StatusCode, response.StatusMessage);
|
||||
// foreach (String headerName in response.GetHeaderNames())
|
||||
// {
|
||||
// streamWriter.WriteLine("{0}: {1}", headerName, response.GetHeader(headerName));
|
||||
// }
|
||||
|
||||
// foreach (HttpCookie httpCookie in response.Cookies)
|
||||
// {
|
||||
// streamWriter.WriteLine("Set-Cookie: {0}", httpCookie.ToString());
|
||||
// }
|
||||
|
||||
// streamWriter.WriteLine();
|
||||
// streamWriter.Flush();
|
||||
|
||||
// response.ContentStream.CopyTo(stream);
|
||||
// response.ContentStream.Close();
|
||||
// response.ContentStream.Dispose();
|
||||
|
||||
// streamWriter.Flush();
|
||||
// }
|
||||
|
||||
//}
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HeaderContainer : IEnumerable<Header>
|
||||
{
|
||||
private Dictionary<string, Header> _headers = new Dictionary<string, Header>();
|
||||
|
||||
public HeaderContainer()
|
||||
{
|
||||
}
|
||||
public HeaderContainer(IEnumerable<Header> headers)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
Add(new Header(header));
|
||||
}
|
||||
|
||||
public HeaderContainer(Stream stream)
|
||||
{
|
||||
Read(stream);
|
||||
}
|
||||
|
||||
public string this[string headerName]
|
||||
{
|
||||
get => Get(headerName);
|
||||
set => Set(headerName, value);
|
||||
}
|
||||
|
||||
public bool TryGetHeader(string headerName, out Header header) =>
|
||||
_headers.TryGetValue(headerName.ToLower(), out header);
|
||||
|
||||
public bool TryGetValue(string headerName, out string headerValue)
|
||||
{
|
||||
if (_headers.TryGetValue(headerName.ToLower(), out Header header))
|
||||
{
|
||||
headerValue = header.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
headerValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Contains(string headerName) => _headers.ContainsKey(headerName.ToLower());
|
||||
|
||||
public void Add(string headerName, string headerValue)
|
||||
{
|
||||
string lowerHeaderName = headerName.ToLower();
|
||||
if (_headers.TryGetValue(lowerHeaderName, out Header header))
|
||||
header.Value += " " + headerValue;
|
||||
else
|
||||
_headers.Add(lowerHeaderName, new Header(headerName, headerValue));
|
||||
}
|
||||
|
||||
public void Add(Header header) => Add(header.Name, header.Value);
|
||||
|
||||
public void Remove(string headerName) => _headers.Remove(headerName.ToLower());
|
||||
public void Clear() => _headers.Clear();
|
||||
|
||||
public void Set(string headerName, String headerValue)
|
||||
{
|
||||
string lowerHeaderName = headerName.ToLower();
|
||||
if (!_headers.TryGetValue(lowerHeaderName, out Header header))
|
||||
{
|
||||
header = new Header(headerName, "");
|
||||
_headers.Add(lowerHeaderName, header);
|
||||
}
|
||||
|
||||
header.Value = headerValue;
|
||||
}
|
||||
|
||||
public string Get(string headerName) => Get(headerName, null);
|
||||
public string Get(string headerName, string defaultValue)
|
||||
{
|
||||
if (TryGetHeader(headerName, out Header header))
|
||||
return header.Value;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public int GetInteger(string headerName)
|
||||
{
|
||||
return int.Parse(Get(headerName));
|
||||
}
|
||||
|
||||
public int GetInteger(string headerName, int defaultValue)
|
||||
{
|
||||
if (TryGetValue(headerName, out string headerValue))
|
||||
return int.Parse(headerValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public bool TryGetInteger(string headerName, out int value)
|
||||
{
|
||||
value = 0;
|
||||
return TryGetValue(headerName, out string headerValue) && int.TryParse(headerValue, out value);
|
||||
}
|
||||
|
||||
public float GetFloat(string headerName)
|
||||
{
|
||||
return float.Parse(Get(headerName));
|
||||
}
|
||||
|
||||
public float GetFloat(string headerName, float defaultValue)
|
||||
{
|
||||
if (TryGetValue(headerName, out string headerValue))
|
||||
return float.Parse(headerValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public bool TryGetFloat(string headerName, out float value)
|
||||
{
|
||||
value = 0;
|
||||
return TryGetValue(headerName, out string headerValue) && float.TryParse(headerValue, out value);
|
||||
}
|
||||
|
||||
|
||||
public double GetDouble(string headerName)
|
||||
{
|
||||
return double.Parse(Get(headerName));
|
||||
}
|
||||
|
||||
public double GetDouble(string headerName, double defaultValue)
|
||||
{
|
||||
if (TryGetValue(headerName, out string headerValue))
|
||||
return double.Parse(headerValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public bool TryGetDouble(string headerName, out double value)
|
||||
{
|
||||
value = 0;
|
||||
return TryGetValue(headerName, out string headerValue) && double.TryParse(headerValue, out value);
|
||||
}
|
||||
|
||||
public IEnumerator<Header> GetEnumerator() => _headers.Values.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (var header in _headers)
|
||||
sb.AppendFormat("{0}: {1}\r\n", header.Value.Name, header.Value.Value);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
string headerName = null;
|
||||
string headerLine;
|
||||
while (!String.Empty.Equals(headerLine = HttpConnection.ReadLine(stream)))
|
||||
{
|
||||
int colon = headerLine.IndexOf(':', StringComparison.InvariantCulture);
|
||||
if (char.IsWhiteSpace(headerLine[0]))
|
||||
{
|
||||
if (headerName is string)
|
||||
Add(headerName, " " + headerLine.Substring(1).Trim());
|
||||
else
|
||||
throw new FormatException("expected header name");
|
||||
}
|
||||
else if (colon > 0)
|
||||
{
|
||||
headerName = headerLine.Substring(0, colon).Trim();
|
||||
Add(headerName, headerLine.Substring(colon + 1).Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(Stream stream)
|
||||
{
|
||||
foreach (var header in _headers.Values)
|
||||
{
|
||||
byte[] bytes = Encoding.ASCII.GetBytes($"{header.Name}: {header.Value}\r\n");
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
stream.Write(new byte[] { 0x0d, 0x0a }, 0, 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Header
|
||||
{
|
||||
private string _name;
|
||||
private Dictionary<string, string> _parameters;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
_name = value;
|
||||
UpperName = value.ToUpper();
|
||||
}
|
||||
}
|
||||
|
||||
public string UpperName { get; private set; }
|
||||
public string Value { get; set; }
|
||||
|
||||
|
||||
public Header(string headerName)
|
||||
{
|
||||
Name = headerName;
|
||||
Value = String.Empty;
|
||||
}
|
||||
|
||||
public Header(string headerName, string headerValue)
|
||||
{
|
||||
Name = headerName;
|
||||
Value = headerValue;
|
||||
}
|
||||
|
||||
public Header(Header src)
|
||||
{
|
||||
Name = src.Name;
|
||||
Value = src.Value;
|
||||
}
|
||||
|
||||
public bool TryGetParameter(string parameterName, out string parameterValue)
|
||||
{
|
||||
if (_parameters is null)
|
||||
ParseParameters();
|
||||
|
||||
return _parameters.TryGetValue(parameterName, out parameterValue);
|
||||
}
|
||||
public bool ContainsParameter(string parameterName)
|
||||
{
|
||||
if (_parameters is null)
|
||||
ParseParameters();
|
||||
|
||||
return _parameters.ContainsKey(parameterName);
|
||||
}
|
||||
|
||||
public Header ParseParameters()
|
||||
{
|
||||
_parameters = new Dictionary<string, string>();
|
||||
|
||||
int idxSemicolon = Value.IndexOf(';');
|
||||
if (idxSemicolon != -1)
|
||||
{
|
||||
string[] ptoks = Value.Split(';');
|
||||
|
||||
foreach (string ptok in ptoks)
|
||||
{
|
||||
int idxEqual = ptok.IndexOf('=');
|
||||
string pn, pv;
|
||||
if (idxEqual != -1)
|
||||
{
|
||||
pn = ptok.Substring(0, idxEqual).Trim();
|
||||
pv = ptok.Substring(idxEqual + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
pn = ptok.Trim();
|
||||
pv = "";
|
||||
}
|
||||
|
||||
_parameters.Add(pn, pv);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() => String.Format("{0}: {1}", Name, Value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using ln.http.content;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public class Http1XConnection : HttpConnection
|
||||
{
|
||||
bool _keepAlive;
|
||||
|
||||
public Http1XConnection(Listener listener, IPEndPoint remoteEndpoint, Stream connectionStream, string method, string requestUriLine, HttpVersion httpVersion)
|
||||
: base(listener, remoteEndpoint, connectionStream, method, requestUriLine, httpVersion)
|
||||
{
|
||||
if (httpVersion == HttpVersion.HTTP11)
|
||||
_keepAlive = true;
|
||||
}
|
||||
|
||||
public override void Run()
|
||||
{
|
||||
string _method = Method;
|
||||
HttpVersion _httpVersion = HttpVersion;
|
||||
HeaderContainer headerContainer = new HeaderContainer();
|
||||
string _requestUriLine = RequestUriLine;
|
||||
|
||||
while (ConnectionStream.CanRead && ConnectionStream.CanWrite)
|
||||
{
|
||||
headerContainer.Clear();
|
||||
headerContainer.Read(ConnectionStream);
|
||||
|
||||
Uri BaseURI = new Uri($"{(Listener is TlsListener ? "https" : "http")}://{headerContainer.Get("host")}");
|
||||
Uri RequestUri = new Uri(BaseURI, _requestUriLine);
|
||||
|
||||
if (
|
||||
headerContainer.TryGetHeader("connection", out Header connectionHeader)
|
||||
)
|
||||
{
|
||||
switch (connectionHeader.Value)
|
||||
{
|
||||
case "close":
|
||||
_keepAlive = false;
|
||||
break;
|
||||
case "keep-alive":
|
||||
_keepAlive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
HttpContentStream contentStream = null;
|
||||
if (headerContainer.TryGetInteger("content-length", out int contentLength))
|
||||
{
|
||||
if (headerContainer.TryGetValue("Expect", out string expectValue) && expectValue.Equals("100-continue"))
|
||||
{
|
||||
string statusLine = $"{HttpVersionSupport.ToString(_httpVersion)} 417 expectation failed\r\n";
|
||||
byte[] statusLineBytes = Encoding.ASCII.GetBytes(statusLine);
|
||||
ConnectionStream.Write(statusLineBytes);
|
||||
break;
|
||||
}
|
||||
contentStream = new HttpContentStream(ConnectionStream, contentLength);
|
||||
}
|
||||
|
||||
HttpRequestContext requestContext = new HttpRequestContext(Listener, this, ConnectionStream, new HttpRequest(_method, RequestUri, _httpVersion, false, headerContainer, contentStream));
|
||||
Listener.Dispatch(requestContext);
|
||||
|
||||
if (!ConnectionStream.CanWrite && !ConnectionStream.CanRead)
|
||||
break;
|
||||
|
||||
SendResponse(requestContext);
|
||||
ConnectionStream.Flush();
|
||||
|
||||
string responseConnectionHeader = requestContext.Response.GetHeader("connection");
|
||||
_keepAlive = responseConnectionHeader switch
|
||||
{
|
||||
"close" => false,
|
||||
"keep-alive" => true,
|
||||
_ => _keepAlive
|
||||
};
|
||||
|
||||
requestContext.Dispose();
|
||||
|
||||
if (!_keepAlive)
|
||||
break;
|
||||
|
||||
if (!HttpConnection.ReadRequestLine(ConnectionStream, out _method, out _requestUriLine, out _httpVersion))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SendResponse(HttpRequestContext requestContext)
|
||||
{
|
||||
foreach (var httpCookie in requestContext.Response.Cookies)
|
||||
requestContext.Response.Headers.Add("Set-Cookie", httpCookie.ToString());
|
||||
|
||||
string statusLine = $"{HttpVersionSupport.ToString(requestContext.Request.HttpVersion)} {(int)requestContext.Response.HttpStatusCode} {requestContext.Response.HttpStatusCode.ToString()}\r\n";
|
||||
byte[] statusLineBytes = Encoding.ASCII.GetBytes(statusLine);
|
||||
requestContext.ConnectionStream.Write(statusLineBytes, 0, statusLineBytes.Length);
|
||||
if (!requestContext.Response.Headers.Contains("content-type"))
|
||||
requestContext.Response.Headers.Set("content-type", requestContext.Response.HttpContent?.ContentType);
|
||||
requestContext.Response.Headers.Set("content-length", requestContext.Response.HttpContent?.Length.ToString() ?? "0");
|
||||
requestContext.Response.Headers.CopyTo(requestContext.ConnectionStream);
|
||||
requestContext.Response.HttpContent?.CopyTo(requestContext.ConnectionStream);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public class Http2Connection : HttpConnection
|
||||
{
|
||||
public Http2Connection(Listener listener, IPEndPoint remoteEndpoint, Stream connectionStream, string method, string requestUriLine, HttpVersion httpVersion)
|
||||
: base(listener, remoteEndpoint, connectionStream, method, requestUriLine, httpVersion)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Run()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void SendResponse(HttpRequestContext requestContext)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
|
||||
using System;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
[Flags]
|
||||
public enum HttpArgumentSource : int{
|
||||
AUTO = -1,
|
||||
CONTENT = (1<<0),
|
||||
PARAMETER = (1<<1),
|
||||
HEADER = (1<<2),
|
||||
QUERY = (1<<3)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class HttpArgumentSourceAttribute : Attribute
|
||||
{
|
||||
public HttpArgumentSource ArgumentSource { get; set; }
|
||||
public string ArgumentName { get; set; }
|
||||
|
||||
public HttpArgumentSourceAttribute(){}
|
||||
|
||||
public HttpArgumentSourceAttribute(HttpArgumentSource argumentSource)
|
||||
{
|
||||
ArgumentSource = argumentSource;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public abstract class HttpConnection
|
||||
{
|
||||
public Listener Listener { get; }
|
||||
public IPEndPoint LocalEndpoint => Listener.LocalEndpoint;
|
||||
public IPEndPoint RemoteEndpoint { get; }
|
||||
public Stream ConnectionStream { get; }
|
||||
public string Method { get; }
|
||||
//public Uri RequestUri { get; }
|
||||
public HttpVersion HttpVersion { get; }
|
||||
|
||||
public string RequestUriLine { get; set; }
|
||||
|
||||
public HttpConnection(Listener listener, IPEndPoint remoteEndpoint, Stream connectionStream, string method,
|
||||
String requestUriLine, HttpVersion httpVersion)
|
||||
{
|
||||
Listener = listener;
|
||||
RemoteEndpoint = remoteEndpoint;
|
||||
ConnectionStream = connectionStream;
|
||||
|
||||
Method = method;
|
||||
RequestUriLine = requestUriLine;
|
||||
HttpVersion = httpVersion;
|
||||
}
|
||||
|
||||
public abstract void Run();
|
||||
|
||||
public static string ReadLine(Stream stream) => ReadLine(stream, Encoding.ASCII);
|
||||
|
||||
public static string ReadLine(Stream stream, Encoding encoding)
|
||||
{
|
||||
if (!stream.CanRead)
|
||||
return null;
|
||||
|
||||
byte[] line = new byte[1024];
|
||||
int n = 0;
|
||||
int ch;
|
||||
while ((ch = stream.ReadByte()) != -1)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case '\r':
|
||||
break;
|
||||
case '\n':
|
||||
return encoding.GetString(line, 0, n);
|
||||
default:
|
||||
line[n++] = (byte)ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (n > 0)
|
||||
return encoding.GetString(line, 0, n);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool ReadRequestLine(Stream stream, out string method, out string requestUri,
|
||||
out HttpVersion httpVersion)
|
||||
{
|
||||
string requestLine = ReadLine(stream);
|
||||
if (requestLine is null)
|
||||
{
|
||||
method = null;
|
||||
requestUri = null;
|
||||
httpVersion = HttpVersion.None;
|
||||
return false;
|
||||
}
|
||||
|
||||
int idxSpace1 = requestLine.IndexOf(' ');
|
||||
int idxSpace2 = requestLine.IndexOf(' ', idxSpace1 + 1);
|
||||
|
||||
if ((idxSpace1 > 0) && (idxSpace2 > 0))
|
||||
{
|
||||
method = requestLine.Substring(0, idxSpace1);
|
||||
requestUri = requestLine.Substring(idxSpace1 + 1, (idxSpace2 - idxSpace1 - 1));
|
||||
string protocol = requestLine.Substring(idxSpace2 + 1);
|
||||
httpVersion = HttpVersion.None;
|
||||
|
||||
if ("PRI".Equals(method) && Http2PrefaceUri.Equals(requestUri) && "HTTP/2.0".Equals(protocol))
|
||||
httpVersion = HttpVersion.HTTP2;
|
||||
else if ("HTTP/1.0".Equals(protocol))
|
||||
httpVersion = HttpVersion.HTTP10;
|
||||
else if ("HTTP/1.1".Equals(protocol))
|
||||
httpVersion = HttpVersion.HTTP11;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
method = null;
|
||||
requestUri = null;
|
||||
httpVersion = HttpVersion.None;
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void SendResponse(HttpRequestContext requestContext);
|
||||
|
||||
private static string Http2PrefaceUri = "*";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
[Flags]
|
||||
public enum HttpConnectionFlags
|
||||
{
|
||||
TLS,
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using ln.json;
|
||||
using ln.mime;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public abstract class HttpContent : IDisposable
|
||||
{
|
||||
public HttpContent(string contentType)
|
||||
{
|
||||
ContentType = contentType;
|
||||
}
|
||||
|
||||
public string ContentType { get; }
|
||||
public abstract long Length { get; }
|
||||
public abstract void CopyTo(Stream targetStream);
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class StringContent : HttpContent
|
||||
{
|
||||
public string Text { get; set; } = String.Empty;
|
||||
public Encoding Encoding { get; set; }
|
||||
|
||||
public StringContent(string text, string contentType)
|
||||
:base(contentType)
|
||||
{
|
||||
Encoding = Encoding.UTF8;
|
||||
Text = text;
|
||||
}
|
||||
public StringContent(string text)
|
||||
:this(text, "text/plain; charset=utf-8")
|
||||
{
|
||||
}
|
||||
|
||||
public override long Length => Encoding.GetBytes(Text ).Length;
|
||||
public override void CopyTo(Stream targetStream)
|
||||
{
|
||||
byte[] bytes = Encoding.GetBytes(Text);
|
||||
targetStream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public class FileContent : HttpContent
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
|
||||
public FileContent(string filename, string contentType)
|
||||
: base(contentType)
|
||||
{
|
||||
FileName = filename;
|
||||
}
|
||||
public FileContent(string filename)
|
||||
:this(filename, new MimeTypes().GetMimeTypeByExtension(Path.GetExtension(filename)).ToString())
|
||||
{
|
||||
}
|
||||
|
||||
public override long Length => new FileInfo(FileName).Length;
|
||||
public override void CopyTo(Stream targetStream)
|
||||
{
|
||||
using FileStream fs = new FileStream(FileName, FileMode.Open, FileAccess.Read);
|
||||
fs.CopyTo(targetStream);
|
||||
}
|
||||
}
|
||||
|
||||
public class JsonContent : HttpContent
|
||||
{
|
||||
public JSONValue Value { get; set; }
|
||||
|
||||
private byte[] _bytes;
|
||||
|
||||
public JsonContent(JSONValue json)
|
||||
:base("application/json")
|
||||
{
|
||||
Value = json;
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_bytes is null)
|
||||
Serialize();
|
||||
|
||||
return _bytes.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CopyTo(Stream targetStream)
|
||||
{
|
||||
if (_bytes is null)
|
||||
Serialize();
|
||||
|
||||
targetStream.Write(_bytes, 0, _bytes.Length);
|
||||
}
|
||||
|
||||
public void Serialize()
|
||||
{
|
||||
string jsonText = Value.ToString();
|
||||
_bytes = Encoding.UTF8.GetBytes(jsonText);
|
||||
}
|
||||
}
|
||||
|
||||
public class StreamContent : HttpContent
|
||||
{
|
||||
public Stream Stream { get; protected set; }
|
||||
public bool DisposeStream { get; }
|
||||
|
||||
public StreamContent(Stream stream)
|
||||
: this(stream, true, "application/octet-stream")
|
||||
{
|
||||
}
|
||||
|
||||
public StreamContent(Stream stream, bool disposeStream)
|
||||
:this(stream, disposeStream, "application/octet-stream")
|
||||
{}
|
||||
|
||||
public StreamContent(Stream stream, string contentType)
|
||||
:this(stream, true, contentType)
|
||||
{
|
||||
}
|
||||
public StreamContent(Stream stream, bool disposeStream, string contentType)
|
||||
:base(contentType)
|
||||
{
|
||||
Stream = stream;
|
||||
DisposeStream = disposeStream;
|
||||
}
|
||||
|
||||
public override long Length => Stream.CanSeek ? Stream.Length - Stream.Position : Stream.Length;
|
||||
public override void CopyTo(Stream targetStream) => Stream.CopyTo(targetStream);
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (DisposeStream)
|
||||
Stream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class StreamedContent : HttpContent
|
||||
{
|
||||
public MemoryStream ContentStream { get; }
|
||||
|
||||
public StreamedContent(string contentType)
|
||||
:base(contentType)
|
||||
{
|
||||
ContentStream = new MemoryStream();
|
||||
}
|
||||
|
||||
public override long Length => ContentStream.Length;
|
||||
|
||||
public override void CopyTo(Stream targetStream)
|
||||
{
|
||||
ContentStream.Position = 0;
|
||||
ContentStream.CopyTo(targetStream);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
ContentStream?.Dispose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using ln.json;
|
||||
using ln.json.mapping;
|
||||
using ln.type;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public abstract class HttpEndpointController : HttpRouter, IDisposable
|
||||
{
|
||||
private HttpRouter _httpRouter;
|
||||
|
||||
public HttpEndpointController():this(null){}
|
||||
public HttpEndpointController(HttpRouter httpRouter)
|
||||
{
|
||||
_httpRouter = httpRouter;
|
||||
MapAttribute mapAttribute = GetType().GetCustomAttribute<MapAttribute>();
|
||||
if (mapAttribute is not null)
|
||||
{
|
||||
HttpMethod = mapAttribute.Method != HttpMethod.NONE ? mapAttribute.Method : HttpMethod.ANY;
|
||||
Route = mapAttribute.Path is not null ? new Regex(mapAttribute.Path) : null;
|
||||
}
|
||||
|
||||
Initialize();
|
||||
|
||||
_httpRouter?.Map(this);
|
||||
}
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
foreach (MethodInfo methodInfo in GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
foreach (MapAttribute mapAttribute in methodInfo.GetCustomAttributes<MapAttribute>())
|
||||
{
|
||||
MappedEndpoint mappedEndpoint = CreateMapping(mapAttribute, methodInfo);
|
||||
Map(mappedEndpoint.HttpMethod, mappedEndpoint.Path, mappedEndpoint.Route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MappedEndpoint CreateMapping(MapAttribute mapAttribute, MethodInfo methodInfo)
|
||||
{
|
||||
if (methodInfo.ReturnType == typeof(void))
|
||||
return new MappedEndpoint.VoidEndpoint(this, mapAttribute, methodInfo);
|
||||
return new MappedEndpoint.NonVoidEndpoint(this, mapAttribute, methodInfo);
|
||||
}
|
||||
|
||||
public abstract class MappedEndpoint
|
||||
{
|
||||
public HttpMethod HttpMethod { get; }
|
||||
public string Path { get; }
|
||||
private HttpEndpointController EndpointController;
|
||||
|
||||
public MethodInfo MethodInfo { get; }
|
||||
|
||||
private ParameterInfo[] _parameterInfos;
|
||||
private HttpArgumentSourceAttribute[] _argumentSourceAttributes;
|
||||
private Type _returnType;
|
||||
|
||||
private RequiresAttribute[] _requiresAttributes;
|
||||
|
||||
public MappedEndpoint(HttpEndpointController endpointController, MapAttribute mapAttribute, MethodInfo methodInfo)
|
||||
{
|
||||
EndpointController = endpointController;
|
||||
HttpMethod = mapAttribute.Method;
|
||||
Path = mapAttribute.Path;
|
||||
MethodInfo = methodInfo;
|
||||
_parameterInfos = MethodInfo.GetParameters();
|
||||
_returnType = MethodInfo.ReturnType;
|
||||
_argumentSourceAttributes = _parameterInfos
|
||||
.Select((pi) => pi.GetCustomAttribute<HttpArgumentSourceAttribute>()).ToArray();
|
||||
_requiresAttributes = methodInfo.GetCustomAttributes<RequiresAttribute>().ToArray();
|
||||
}
|
||||
|
||||
public bool Route(HttpRequestContext httpContext, string routePath)
|
||||
{
|
||||
if (_requiresAttributes.Length > 0){
|
||||
if (httpContext.AuthenticatedPrincipal is HttpPrincipal)
|
||||
{
|
||||
foreach (var requiresAttribute in _requiresAttributes)
|
||||
{
|
||||
if (requiresAttribute.Roles.All((role) =>
|
||||
httpContext.AuthenticatedPrincipal.Roles.Contains(role)))
|
||||
{
|
||||
return Route2(httpContext, routePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
httpContext.Response = HttpResponse.Unauthorized();
|
||||
return true;
|
||||
}
|
||||
return Route2(httpContext, routePath);
|
||||
}
|
||||
|
||||
public abstract bool Route2(HttpRequestContext httpContext, string routePath);
|
||||
|
||||
bool TryFindParameterValue(HttpRequestContext context, ParameterInfo parameterInfo, out object value)
|
||||
{
|
||||
if (parameterInfo.GetCustomAttribute<JsonBodyAttribute>() is JsonBodyAttribute jsonBodyAttribute)
|
||||
{
|
||||
if (jsonBodyAttribute.Property is null)
|
||||
{
|
||||
value = context.Request.Json(parameterInfo.ParameterType);
|
||||
return true;
|
||||
}
|
||||
else if (context.Request.Json().TryGetValue(jsonBodyAttribute.Property, out JToken property))
|
||||
{
|
||||
value = property.ToObject(parameterInfo.ParameterType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((parameterInfo.GetCustomAttribute<RouteAttribute>() is RouteAttribute routeAttribute) &&
|
||||
context.TryGetParameter(routeAttribute.Name ?? parameterInfo.Name, out string pvalue))
|
||||
{
|
||||
value = pvalue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((parameterInfo.GetCustomAttribute<QueryAttribute>() is QueryAttribute queryAttribute) &&
|
||||
context.Request.Query.TryGetValue(queryAttribute.Name ?? parameterInfo.Name, out string svalue))
|
||||
{
|
||||
value = svalue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parameterInfo.ParameterType.Equals(typeof(Stream)))
|
||||
{
|
||||
value = context.Request.ContentStream;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parameterInfo.ParameterType.Equals(typeof(HttpRequestContext)))
|
||||
{
|
||||
value = context;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parameterInfo.ParameterType.Equals(typeof(HttpRequest)))
|
||||
{
|
||||
value = context.Request;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parameterInfo.ParameterType.Equals(typeof(HttpPrincipal)))
|
||||
{
|
||||
value = context.AuthenticatedPrincipal;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryApplyParameters(HttpRequestContext requestContext, out object[] parameters)
|
||||
{
|
||||
parameters = new object[_parameterInfos.Length];
|
||||
|
||||
for (int n = 0; n < _parameterInfos.Length; n++)
|
||||
{
|
||||
ParameterInfo parameterInfo = _parameterInfos[n];
|
||||
|
||||
if (!TryFindParameterValue(requestContext, parameterInfo, out parameters[n]))
|
||||
parameters[n] = parameterInfo.DefaultValue;
|
||||
|
||||
if ((parameters[n] != null) &&
|
||||
(!parameterInfo.ParameterType.IsAssignableFrom(parameters[n].GetType())))
|
||||
{
|
||||
if (!Cast.To(parameters[n], _parameterInfos[n].ParameterType, out parameters[n]))
|
||||
{
|
||||
parameters[n] = TypeDescriptor
|
||||
.GetConverter(parameterInfo.ParameterType)
|
||||
.ConvertFrom(parameters[n]);
|
||||
}
|
||||
}
|
||||
|
||||
if ((parameters[n] == null) && (parameterInfo.ParameterType.IsValueType))
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private object InvokeMethod(HttpRequestContext httpContext)
|
||||
{
|
||||
object[] parameters = null;
|
||||
try
|
||||
{
|
||||
if (!TryApplyParameters(httpContext, out parameters))
|
||||
return HttpResponse.InternalServerError().Content("could not apply parameters");
|
||||
else
|
||||
{
|
||||
return MethodInfo.Invoke(EndpointController, parameters);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeParameters(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
public class NonVoidEndpoint : MappedEndpoint
|
||||
{
|
||||
public NonVoidEndpoint(HttpEndpointController endpointController, MapAttribute mapAttribute, MethodInfo methodInfo)
|
||||
: base(endpointController, mapAttribute, methodInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Route2(HttpRequestContext httpContext, string routePath)
|
||||
{
|
||||
object returnedValue = InvokeMethod(httpContext);
|
||||
|
||||
if (returnedValue is HttpResponse httpResponse)
|
||||
httpContext.Response = httpResponse;
|
||||
else if ((returnedValue is JSONValue jsonResult) || (JSONMapper.DefaultMapper.Serialize(returnedValue, out jsonResult)))
|
||||
httpContext.Response = HttpResponse.Default(httpContext.Request.Method)
|
||||
.Content(jsonResult);
|
||||
else
|
||||
httpContext.Response = HttpResponse.InternalServerError().Content("Method result could not be serialized");
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class VoidEndpoint : MappedEndpoint
|
||||
{
|
||||
public VoidEndpoint(HttpEndpointController endpointController, MapAttribute mapAttribute, MethodInfo methodInfo)
|
||||
: base(endpointController, mapAttribute, methodInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool Route2(HttpRequestContext httpContext, string routePath)
|
||||
{
|
||||
object returnedValue = InvokeMethod(httpContext);
|
||||
if (returnedValue is HttpResponse httpResponse)
|
||||
httpContext.Response = httpResponse;
|
||||
else
|
||||
httpContext.Response ??= HttpResponse.Default(httpContext.Request.Method);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeParameters(object[] parameters)
|
||||
{
|
||||
if (parameters is null)
|
||||
return;
|
||||
|
||||
foreach (var o in parameters)
|
||||
{
|
||||
if (o is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpRouter?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using System;
|
||||
namespace ln.http
|
||||
{
|
||||
public class HttpHeader
|
||||
{
|
||||
public String Name { get; }
|
||||
public String Value { get; }
|
||||
|
||||
public HttpHeader(String rawHeader)
|
||||
{
|
||||
int colon = rawHeader.IndexOf(':');
|
||||
if (colon < 0)
|
||||
throw new FormatException("rawHeader must contain at least one colon");
|
||||
|
||||
Name = rawHeader.Substring(0, colon).Trim().ToUpper();
|
||||
Value = rawHeader.Substring(colon + 1).Trim();
|
||||
}
|
||||
|
||||
public HttpHeader(String headerName,String headerValue)
|
||||
{
|
||||
if (String.Empty.Equals(headerName))
|
||||
throw new ArgumentException("headerName needs to contain at least one character", nameof(headerName));
|
||||
|
||||
Name = headerName.ToUpper();
|
||||
Value = headerValue.ToUpper();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
using System;
|
||||
using ln.collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HttpHeaders : IEnumerable<HttpHeader>
|
||||
{
|
||||
MappingBTree<string, HttpHeader> headers = new MappingBTree<string, HttpHeader>((value) => value.Name);
|
||||
|
||||
public HttpHeaders()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpHeader this[string headerName] => headers[headerName.ToUpper()];
|
||||
public bool Contains(string headerName) => headers.ContainsKey(headerName.ToUpper());
|
||||
|
||||
public void Add(String headerName, String headerValue) => Add(new HttpHeader(headerName, headerValue));
|
||||
public void Add(HttpHeader httpHeader) => headers.Add(httpHeader);
|
||||
|
||||
public void Remove(HttpHeader httpHeader) => headers.Remove(httpHeader);
|
||||
public void Remove(string headerName) => headers.RemoveKey(headerName.ToUpper());
|
||||
|
||||
public void Set(String headerName, String headerValue)
|
||||
{
|
||||
if (headers.ContainsKey(headerName))
|
||||
Remove(headerName);
|
||||
|
||||
Add(new HttpHeader(headerName, headerValue));
|
||||
}
|
||||
|
||||
public IEnumerator<HttpHeader> GetEnumerator() => headers.Values.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => headers.Values.GetEnumerator();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
|
||||
using System;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
[Flags]
|
||||
public enum HttpMethod {
|
||||
NONE = 0,
|
||||
HEAD, GET, POST, PUT, PATCH, DELETE, CONNECT, OPTIONS, TRACE, PROPFIND, MKCOL, LOCK, UNLOCK,
|
||||
ANY = -1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HttpPrincipal
|
||||
{
|
||||
public string UniqueId { get; set; }
|
||||
public string Username { get; set; }
|
||||
|
||||
/**
|
||||
* If this principal is a delegated one, authenticated by another principal
|
||||
*/
|
||||
public HttpPrincipal AuthenticatedPrincipal { get; }
|
||||
|
||||
private HashSet<string> _roles = new HashSet<string>();
|
||||
public IReadOnlySet<string> Roles => _roles;
|
||||
|
||||
public bool HasRole(string role) => _roles.Contains(role);
|
||||
|
||||
public HttpPrincipal(string uniquedId, string username, string[] roles)
|
||||
{
|
||||
UniqueId = uniquedId;
|
||||
Username = username;
|
||||
foreach (var role in roles)
|
||||
_roles.Add(role);
|
||||
}
|
||||
|
||||
public HttpPrincipal(string uniquedId, string username, string[] roles, HttpPrincipal authenticatedPrincipal) :
|
||||
this(uniquedId, username, roles)
|
||||
{
|
||||
AuthenticatedPrincipal = authenticatedPrincipal;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (AuthenticatedPrincipal is null)
|
||||
return string.Format("{0}[{1}]", Username, UniqueId);
|
||||
else
|
||||
return string.Format("{2}=>{0}[{1}]", Username, UniqueId, AuthenticatedPrincipal.ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,320 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using ln.type;
|
||||
using ln.http.message;
|
||||
using ln.http.io;
|
||||
using ln.http.message.parser;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
//public class HttpReader
|
||||
//{
|
||||
// delegate bool ReadCondition(int b);
|
||||
|
||||
// public Stream Stream { get; }
|
||||
|
||||
// private byte[] buffer = new byte[8192];
|
||||
// private int hlen;
|
||||
// private int blen;
|
||||
// private int bptr;
|
||||
|
||||
// public Endpoint RemoteEndpoint { get; private set; }
|
||||
|
||||
// public HeaderContainer Headers { get; private set; }
|
||||
|
||||
// public String Method { get; private set; }
|
||||
// public String URL { get; private set; }
|
||||
// public String Protocol { get; private set; }
|
||||
|
||||
// //public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>();
|
||||
|
||||
// public bool Valid { get; private set; } = false;
|
||||
|
||||
// public HttpReader(Stream stream)
|
||||
// {
|
||||
// Stream = stream;
|
||||
// }
|
||||
// public HttpReader(Stream stream,Endpoint remoteEndpoint)
|
||||
// {
|
||||
// Stream = stream;
|
||||
// RemoteEndpoint = remoteEndpoint;
|
||||
// }
|
||||
|
||||
// public void Read()
|
||||
// {
|
||||
// UnbufferedStreamReader reader = new UnbufferedStreamReader(Stream);
|
||||
// string requestLine = reader.ReadLine();
|
||||
// string[] requestTokens = requestLine.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// if (requestTokens.Length != 3)
|
||||
// throw new FormatException("request line malformed");
|
||||
|
||||
// Method = requestTokens[0];
|
||||
// URL = requestTokens[1];
|
||||
// Protocol = requestTokens[2];
|
||||
|
||||
// Headers = HTTP.ReadHeader(reader);
|
||||
|
||||
// Valid = true;
|
||||
// }
|
||||
|
||||
// public int ReadByte()
|
||||
// {
|
||||
// if (bptr >= blen)
|
||||
// return -1;
|
||||
// return buffer[bptr++];
|
||||
// }
|
||||
|
||||
// public int Current
|
||||
// {
|
||||
// get
|
||||
// {
|
||||
// if (bptr >= blen)
|
||||
// return -1;
|
||||
// return buffer[bptr];
|
||||
// }
|
||||
// }
|
||||
|
||||
// public void Reverse()
|
||||
// {
|
||||
// if ((bptr > 0) && (bptr < blen))
|
||||
// bptr--;
|
||||
// }
|
||||
|
||||
// public void ReadRequestHead()
|
||||
// {
|
||||
// bptr = 0;
|
||||
// do
|
||||
// {
|
||||
// int rlen = Stream.Read(buffer, blen, buffer.Length - blen);
|
||||
// if (rlen == 0)
|
||||
// throw new IOException();
|
||||
|
||||
// blen += rlen;
|
||||
// while (bptr <= (blen - 4))
|
||||
// {
|
||||
// if (
|
||||
// (buffer[bptr + 0] == '\r') &&
|
||||
// (buffer[bptr + 1] == '\n') &&
|
||||
// (buffer[bptr + 2] == '\r') &&
|
||||
// (buffer[bptr + 3] == '\n')
|
||||
// )
|
||||
// {
|
||||
// hlen = bptr;
|
||||
// bptr = 0;
|
||||
// return;
|
||||
// }
|
||||
// bptr++;
|
||||
// }
|
||||
|
||||
// byte[] nbuffer = new byte[buffer.Length << 1];
|
||||
// Array.Copy(buffer, nbuffer, buffer.Length);
|
||||
// buffer = nbuffer;
|
||||
// } while (blen >= buffer.Length);
|
||||
// bptr = 0;
|
||||
// }
|
||||
|
||||
// private String ReadConditional(ReadCondition readCondition,bool reverse = true)
|
||||
// {
|
||||
// StringBuilder stringBuilder = new StringBuilder();
|
||||
// int b = ReadByte();
|
||||
// while ((b != -1) && (readCondition(b)) )
|
||||
// {
|
||||
// stringBuilder.Append((char)b);
|
||||
// b = ReadByte();
|
||||
// }
|
||||
// if (reverse)
|
||||
// Reverse();
|
||||
|
||||
// return stringBuilder.ToString();
|
||||
// }
|
||||
|
||||
// public void SkipWhiteSpace()
|
||||
// {
|
||||
// while (Char.IsWhiteSpace((char)ReadByte())) { };
|
||||
// Reverse();
|
||||
// }
|
||||
|
||||
// public String ReadToken()
|
||||
// {
|
||||
// return ReadConditional((b) => !Char.IsWhiteSpace((char)b));
|
||||
// }
|
||||
|
||||
// public String ReadLine()
|
||||
// {
|
||||
// int p = bptr;
|
||||
// while (p < blen - 1)
|
||||
// {
|
||||
// if ((buffer[p] == '\r') && (buffer[p + 1] == '\n'))
|
||||
// {
|
||||
// break;
|
||||
// }
|
||||
// p++;
|
||||
// }
|
||||
|
||||
// string result = Encoding.ASCII.GetString(buffer, bptr, p - bptr);
|
||||
// bptr = p + 2;
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// public String ReadHeaderName()
|
||||
// {
|
||||
// return ReadConditional((b) => b != ':', false).ToUpper();
|
||||
// }
|
||||
// public String ReadHeaderValue()
|
||||
// {
|
||||
// String value = ReadLine();
|
||||
// while (Char.IsWhiteSpace((char)Current))
|
||||
// {
|
||||
// value = value + ReadLine();
|
||||
// }
|
||||
// return value.Trim();
|
||||
// }
|
||||
|
||||
// //public void ReadHeaders()
|
||||
// //{
|
||||
// // while (bptr < hlen)
|
||||
// // {
|
||||
// // String name = ReadHeaderName();
|
||||
// // String value = ReadHeaderValue();
|
||||
|
||||
// // Headers.Add(name, value);
|
||||
// // }
|
||||
// //}
|
||||
|
||||
// public int ReadRequestBody(byte[] dst,int offset,int length)
|
||||
// {
|
||||
// //int nRead = 0;
|
||||
// //if (bptr < blen)
|
||||
// //{
|
||||
// // int len = Math.Min(length, blen - bptr);
|
||||
// // Array.Copy(buffer, bptr, dst, offset, len);
|
||||
// // bptr += len;
|
||||
// // length -= len;
|
||||
// // offset += len;
|
||||
// // nRead += len;
|
||||
// //}
|
||||
// int nRead = 0;
|
||||
// while (length > 0)
|
||||
// {
|
||||
// int nr = Stream.Read(dst, offset, length);
|
||||
// if (nr > 0)
|
||||
// {
|
||||
// nRead += nr;
|
||||
// length -= nr;
|
||||
// offset += nr;
|
||||
// }
|
||||
// }
|
||||
// return nRead;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// /*
|
||||
// byte[] buffer;
|
||||
// int blen = 0;
|
||||
// int bptr = 0;
|
||||
|
||||
// public HttpReader(Stream stream)
|
||||
// {
|
||||
// Stream = stream;
|
||||
// buffer = new byte[1024];
|
||||
// }
|
||||
// public HttpReader(Stream stream, int buffersize)
|
||||
// {
|
||||
// Stream = stream;
|
||||
// buffer = new byte[buffersize];
|
||||
// }
|
||||
|
||||
// private int read()
|
||||
// {
|
||||
// if (bptr >= blen)
|
||||
// {
|
||||
// bptr = 0;
|
||||
// blen = Stream.Read(buffer, 0, buffer.Length);
|
||||
// if (blen <= 0)
|
||||
// throw new EndOfStreamException();
|
||||
// }
|
||||
// return buffer[bptr++];
|
||||
// }
|
||||
|
||||
// private int ReadTo(byte[] b,int offset,byte[] mark)
|
||||
// {
|
||||
// int pm = 0;
|
||||
// int p = offset;
|
||||
// while (p < b.Length)
|
||||
// {
|
||||
// b[p] = (byte)read();
|
||||
// if (b[p] == mark[pm])
|
||||
// {
|
||||
// pm++;
|
||||
// if (pm >= mark.Length)
|
||||
// {
|
||||
// p++;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// pm = 0;
|
||||
// }
|
||||
// p++;
|
||||
// }
|
||||
// return p;
|
||||
// }
|
||||
|
||||
|
||||
// public String ReadRequestLine(int maxSize = 1024)
|
||||
// {
|
||||
// byte[] b = new byte[maxSize];
|
||||
// int l = ReadTo(b, 0, new byte[] { 0x0d, 0x0a });
|
||||
// return Encoding.ASCII.GetString(b, 0, l);
|
||||
// }
|
||||
|
||||
// public Dictionary<string,string> ReadHTTPHeaders()
|
||||
// {
|
||||
// byte[] b = new byte[8192];
|
||||
// int hlen = ReadTo(b, 0, new byte[] { 0x0d, 0x0a, 0x0d, 0x0a });
|
||||
|
||||
// Dictionary<string, string> headers = new Dictionary<string, string>();
|
||||
|
||||
// string rawHeaders = Encoding.ASCII.GetString(b, 0, hlen);
|
||||
// String[] rawLines = rawHeaders.Split(new String[] { "\r\n" }, StringSplitOptions.None);
|
||||
|
||||
// for (int n = rawLines.Length-1; n >= 0 ; n--)
|
||||
// {
|
||||
// if ((rawLines[n].Length > 0) && (Char.IsWhiteSpace(rawLines[n][0])))
|
||||
// {
|
||||
// rawLines[n - 1] = rawLines[n - 1] + rawLines[n];
|
||||
// rawLines[n] = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// foreach (String rawLine in rawLines)
|
||||
// {
|
||||
// if (rawLine != null)
|
||||
// {
|
||||
// int colon = rawLine.IndexOf(':');
|
||||
// if (colon > 0)
|
||||
// {
|
||||
// String name = rawLine.Substring(0, colon).Trim().ToUpper();
|
||||
// String value = rawLine.Substring(colon + 1).Trim();
|
||||
// headers.Add(name, value);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return headers;
|
||||
// }
|
||||
|
||||
// public byte[] ReadRequestBody(byte[] buffer)
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
// */
|
||||
//}
|
||||
|
||||
}
|
|
@ -2,193 +2,99 @@
|
|||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ln.http.content;
|
||||
using ln.json;
|
||||
using ln.type;
|
||||
using ln.http.exceptions;
|
||||
using System.Threading;
|
||||
using ln.http.session;
|
||||
using ln.http.message;
|
||||
using ln.http.io;
|
||||
using ln.http.message.parser;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HttpRequest : IDisposable
|
||||
{
|
||||
static ThreadLocal<HttpRequest> current = new ThreadLocal<HttpRequest>();
|
||||
static public HttpRequest Current => current.Value;
|
||||
|
||||
//Dictionary<String, String> requestHeaders;
|
||||
HeaderContainer requestHeaders;
|
||||
Dictionary<String, String> requestCookies;
|
||||
Dictionary<string, String> requestParameters;
|
||||
|
||||
public HTTPServer HTTPServer { get; }
|
||||
|
||||
public Endpoint RemoteEndpoint { get; private set; }
|
||||
public Endpoint LocalEndpoint { get; private set; }
|
||||
|
||||
public Uri BaseURI { get; set; }
|
||||
public Uri URI { get; private set; }
|
||||
|
||||
public String Method { get; private set; }
|
||||
public String RequestURL { get; private set; }
|
||||
public String Protocol { get; private set; }
|
||||
|
||||
public String Hostname { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
|
||||
public class HttpRequest : IDisposable
|
||||
{
|
||||
public string RequestMethodName { get; }
|
||||
public Uri RequestUri { get; private set; }
|
||||
public HttpVersion HttpVersion { get; }
|
||||
public HeaderContainer Headers { get; }
|
||||
|
||||
public HttpMethod Method { get; private set; }
|
||||
public String Host { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
public bool ViaTLS { get; private set; }
|
||||
|
||||
public QueryStringParameters Query { get; private set; }
|
||||
|
||||
public Session Session { get; set; }
|
||||
public HttpUser CurrentUser => Session.CurrentUser;
|
||||
|
||||
public HeaderContainer RequestHeaders => requestHeaders;
|
||||
|
||||
MemoryStream contentStream;
|
||||
public MemoryStream ContentStream
|
||||
public Uri BaseUri { get; private set; }
|
||||
|
||||
|
||||
Dictionary<String, String> requestCookies;
|
||||
public HttpContentStream ContentStream { get; }
|
||||
|
||||
public HttpRequest(string requestMethodName, Uri requestUri, HttpVersion httpVersion, bool viaTLS, HeaderContainer headerContainer, HttpContentStream contentStream)
|
||||
{
|
||||
get
|
||||
{
|
||||
if (contentStream == null)
|
||||
ReadRequestBody();
|
||||
return contentStream;
|
||||
}
|
||||
}
|
||||
public TextReader ContentReader
|
||||
{
|
||||
get
|
||||
{
|
||||
if (contentReader == null)
|
||||
contentReader = new StreamReader(ContentStream);
|
||||
return contentReader;
|
||||
}
|
||||
RequestMethodName = requestMethodName;
|
||||
RequestUri = requestUri;
|
||||
HttpVersion = httpVersion;
|
||||
Headers = headerContainer;
|
||||
ViaTLS = viaTLS;
|
||||
ContentStream = contentStream;
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
int requestBodyLength;
|
||||
byte[] requestBody;
|
||||
StreamReader contentReader;
|
||||
//private static Regex reHostPort = new Regex("(?<host>(\\w[^:]*|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\\[\\]]+))(:(?<port>\\d+))?");
|
||||
|
||||
Stream connectionStream;
|
||||
UnbufferedStreamReader connectionReader;
|
||||
|
||||
public Stream GetConnectionStream() => connectionStream;
|
||||
|
||||
public HttpRequest(HTTPServer httpServer, Stream clientStream, Endpoint localEndpoint, Endpoint remoteEndpoint)
|
||||
void Initialize()
|
||||
{
|
||||
HTTPServer = httpServer;
|
||||
connectionStream = clientStream;
|
||||
connectionReader = new UnbufferedStreamReader(connectionStream);
|
||||
if (Enum.TryParse(RequestMethodName, out HttpMethod httpMethod))
|
||||
Method = httpMethod;
|
||||
else
|
||||
Method = HttpMethod.NONE;
|
||||
|
||||
LocalEndpoint = localEndpoint;
|
||||
RemoteEndpoint = remoteEndpoint;
|
||||
if (Headers.TryGetValue("Host", out string host))
|
||||
{
|
||||
int colon = host.LastIndexOf(':');
|
||||
if (colon != -1)
|
||||
{
|
||||
Host = host.Substring(0, colon).Trim();
|
||||
Port = int.Parse(host.Substring(colon + 1).Trim());
|
||||
}
|
||||
else
|
||||
{
|
||||
Host = host.Trim();
|
||||
}
|
||||
} else
|
||||
{
|
||||
Host = RequestUri.Host;
|
||||
Port = RequestUri.Port;
|
||||
}
|
||||
|
||||
ReadRequestLine();
|
||||
/*
|
||||
if (Headers.TryGetValue("X-Forwarded-Host", out string forwardedHost))
|
||||
host = forwardedHost;
|
||||
if (Headers.TryGetValue("X-Forwarded-For", out string forwardedFor))
|
||||
ClientAddress = IPAddress.Parse(forwardedFor.ReadToken(','));
|
||||
if (Headers.TryGetValue("X-Forwarded-Proto", out string forwardedProto))
|
||||
TLS = forwardedProto.Equals("https");
|
||||
|
||||
requestHeaders = HTTP.ReadHeader(connectionReader);
|
||||
if (Headers.TryGetValue("Forwarded", out string forwarded))
|
||||
{
|
||||
// ToDo: Implement parser
|
||||
}
|
||||
*/
|
||||
BaseUri = new UriBuilder(ViaTLS ? "https:" : "http:", Host, Port).Uri;
|
||||
RequestUri = new Uri(BaseUri, RequestUri);
|
||||
Query = new QueryStringParameters(RequestUri.Query);
|
||||
|
||||
requestCookies = new Dictionary<string, string>();
|
||||
requestParameters = new Dictionary<string, string>();
|
||||
|
||||
Setup();
|
||||
|
||||
requestBodyLength = int.Parse(GetRequestHeader("content-length", "0"));
|
||||
|
||||
if (Headers.TryGetValue("Cookie", out string cookies))
|
||||
SetupCookies(cookies);
|
||||
}
|
||||
|
||||
void ReadRequestLine()
|
||||
|
||||
private void SetupCookies(string cookies)
|
||||
{
|
||||
string requestLine = connectionReader.ReadLine();
|
||||
|
||||
if (requestLine == null)
|
||||
throw new ConnectionClosedException("UnbufferedStreamReader.ReadLine() returned null");
|
||||
|
||||
string[] requestTokens = requestLine.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (requestTokens.Length != 3)
|
||||
throw new BadRequestException();
|
||||
|
||||
Method = requestTokens[0];
|
||||
RequestURL = requestTokens[1];
|
||||
Protocol = requestTokens[2];
|
||||
}
|
||||
|
||||
public void ReadRequestBody()
|
||||
{
|
||||
requestBody = new byte[requestBodyLength];
|
||||
|
||||
if (requestBodyLength > 0)
|
||||
{
|
||||
int nRead = 0;
|
||||
int length = requestBodyLength;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
int nr = connectionStream.Read(requestBody, nRead, length);
|
||||
if (nr > 0)
|
||||
{
|
||||
nRead += nr;
|
||||
length -= nr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentStream = new MemoryStream(requestBody);
|
||||
}
|
||||
|
||||
public void FinishRequest()
|
||||
{
|
||||
if ((requestBodyLength > 0) && (requestBody == null))
|
||||
{
|
||||
int nRead = 0;
|
||||
int length = requestBodyLength;
|
||||
byte[] discard = new byte[8192];
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
int nr = connectionStream.Read(discard,0,length > discard.Length ? discard.Length : length);
|
||||
if (nr > 0)
|
||||
{
|
||||
nRead += nr;
|
||||
length -= nr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void MakeCurrent() => current.Value = this;
|
||||
public static void ClearCurrent() => current.Value = null;
|
||||
|
||||
private void Setup()
|
||||
{
|
||||
SetupResourceURI();
|
||||
SetupCookies();
|
||||
}
|
||||
|
||||
/*
|
||||
* SetupResourceURI()
|
||||
*
|
||||
* Setup the following fields:
|
||||
*
|
||||
* - Hostname
|
||||
* - Port
|
||||
* - BaseURI
|
||||
* - URI
|
||||
* - Query
|
||||
*
|
||||
*/
|
||||
private void SetupResourceURI()
|
||||
{
|
||||
String host = GetRequestHeader("HOST");
|
||||
String[] hostTokens = host.Split(':');
|
||||
|
||||
Hostname = hostTokens[0];
|
||||
Port = (hostTokens.Length > 1) ? int.Parse(hostTokens[1]) : LocalEndpoint.Port;
|
||||
BaseURI = new UriBuilder("http", Hostname, Port).Uri;
|
||||
URI = new Uri(BaseURI, RequestURL);
|
||||
Query = new QueryStringParameters(URI.Query);
|
||||
}
|
||||
|
||||
private void SetupCookies()
|
||||
{
|
||||
string cookies = GetRequestHeader("COOKIE");
|
||||
foreach (String cookie in cookies.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
string[] c = cookie.Split(new char[] { '=' }, 2);
|
||||
|
@ -204,73 +110,77 @@ namespace ln.http
|
|||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
//return string.Format("[HttpRequest: RemoteEndpoint={0}, Hostname={1} Port={2} URI={4}, Method={4}, RequestURL={5}, Protocol={6} Query={7}]", RemoteEndpoint, URI, Method, RequestURL, Protocol, Hostname, Port,Query);
|
||||
return base.ToString();
|
||||
}
|
||||
public override string ToString() =>
|
||||
$"[HttpRequest: Host={Host} Port={Port} HttpVersion={HttpVersion} Method={Method} URI={RequestUri} Query={Query}]";
|
||||
|
||||
public String GetRequestHeader(String name)
|
||||
{
|
||||
return GetRequestHeader(name, "");
|
||||
}
|
||||
|
||||
public String GetRequestHeader(String name, String def)
|
||||
{
|
||||
name = name.ToUpper();
|
||||
|
||||
if (requestHeaders.ContainsKey(name))
|
||||
return requestHeaders[name].Value;
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
public String[] RequestHeaderNames => requestHeaders.Keys.ToArray();
|
||||
{
|
||||
if (Headers.TryGetValue(name, out string value))
|
||||
return value;
|
||||
return def;
|
||||
}
|
||||
|
||||
public String[] CookieNames => requestCookies.Keys.ToArray();
|
||||
public bool ContainsCookie(String name)
|
||||
public bool ContainsCookie(String name) => this.requestCookies.ContainsKey(name);
|
||||
public String GetCookie(String name) => requestCookies[name];
|
||||
|
||||
public bool TryGetCookie(string cookieName, out string cookieValue) =>
|
||||
requestCookies.TryGetValue(cookieName, out cookieValue);
|
||||
|
||||
public HttpResponse Redirect(string location, params object[] p) => Redirect(HttpStatusCode.SeeOther, location, p);
|
||||
|
||||
public HttpResponse Redirect(HttpStatusCode statusCode, string location, params object[] p)
|
||||
{
|
||||
return this.requestCookies.ContainsKey(name);
|
||||
}
|
||||
public String GetCookie(String name)
|
||||
{
|
||||
return requestCookies[name];
|
||||
}
|
||||
if (p?.Length > 0)
|
||||
location = string.Format(location, p);
|
||||
|
||||
public bool ContainsParameter(string parameterName) => requestParameters.ContainsKey(parameterName);
|
||||
public String GetParameter(String parameterName) => GetParameter(parameterName, null);
|
||||
public String GetParameter(String parameterName,String defaultValue)
|
||||
{
|
||||
if (!requestParameters.TryGetValue(parameterName, out string value))
|
||||
value = defaultValue;
|
||||
return value;
|
||||
}
|
||||
public void SetParameter(String parameterName,String parameterValue) => requestParameters[parameterName] = parameterValue;
|
||||
public IEnumerable<string> ParameterNames => requestParameters.Keys;
|
||||
|
||||
|
||||
public string self()
|
||||
{
|
||||
return BaseURI.ToString();
|
||||
}
|
||||
|
||||
|
||||
public HttpResponse Redirect(string location, params object[] p) => Redirect(303, location, p);
|
||||
public HttpResponse Redirect(int status, string location, params object[] p)
|
||||
{
|
||||
location = string.Format(location, p);
|
||||
|
||||
HttpResponse response = new HttpResponse(this);
|
||||
response.StatusCode = status;
|
||||
response.SetHeader("location", location);
|
||||
HttpResponse response = new HttpResponse(statusCode);
|
||||
response.SetHeader("Location", location);
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public JSONValue ToJsonValue()
|
||||
{
|
||||
contentReader?.Dispose();
|
||||
ContentStream?.Dispose();
|
||||
long p = ContentStream.Position;
|
||||
try
|
||||
{
|
||||
using (TextReader reader = new StreamReader(ContentStream))
|
||||
return JSONParser.Parse(reader);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentStream.Position = p;
|
||||
}
|
||||
}
|
||||
|
||||
private string? _bodyText;
|
||||
public string Text
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_bodyText is null)
|
||||
_bodyText = Encoding.UTF8.GetString(ContentStream.ReadToEnd());
|
||||
return _bodyText;
|
||||
}
|
||||
}
|
||||
|
||||
private JObject? _jobject;
|
||||
public JObject Json()
|
||||
{
|
||||
if (_jobject is null)
|
||||
_jobject = JObject.Load(new JsonTextReader(new StringReader(Text)));
|
||||
return _jobject;
|
||||
}
|
||||
|
||||
public T Json<T>() => JsonConvert.DeserializeObject<T>(Text);
|
||||
public object Json(Type nativeType) => JsonConvert.DeserializeObject(Text, nativeType);
|
||||
|
||||
|
||||
public void Dispose() => ContentStream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using ln.collections;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public sealed class HttpRequestContext : IDisposable
|
||||
{
|
||||
public Listener Listener { get; }
|
||||
public IPEndPoint LocalEndpoint => Listener.LocalEndpoint;
|
||||
public HttpConnection HttpConnection { get; }
|
||||
public Stream ConnectionStream { get; }
|
||||
public HttpRequest Request { get; protected set; }
|
||||
public HttpResponse Response { get; set; }
|
||||
public HttpRequestContext ParentContext { get; }
|
||||
public HttpPrincipal AuthenticatedPrincipal { get; set; }
|
||||
|
||||
|
||||
Dictionary<string, String> _parameters = new Dictionary<string, string>();
|
||||
|
||||
public HttpRequestContext(HttpRequestContext parentContext, HttpRequest request)
|
||||
: this(parentContext.Listener, parentContext.HttpConnection, parentContext.ConnectionStream, request)
|
||||
{
|
||||
ParentContext = parentContext;
|
||||
}
|
||||
|
||||
public HttpRequestContext(Listener listener, HttpConnection httpConnection, Stream connectionStream, HttpRequest request)
|
||||
{
|
||||
Listener = listener;
|
||||
Request = request;
|
||||
HttpConnection = httpConnection;
|
||||
ConnectionStream = connectionStream;
|
||||
}
|
||||
|
||||
protected HttpRequestContext(Listener listener, HttpConnection httpConnection, Stream connectionStream)
|
||||
{
|
||||
Listener = listener;
|
||||
HttpConnection = httpConnection;
|
||||
ConnectionStream = connectionStream;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Request?.Dispose();
|
||||
Response?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public bool ContainsParameter(string parameterName) => _parameters.ContainsKey(parameterName);
|
||||
public String GetParameter(String parameterName) => GetParameter(parameterName, null);
|
||||
|
||||
public String GetParameter(String parameterName, String defaultValue)
|
||||
{
|
||||
if (!_parameters.TryGetValue(parameterName, out string value))
|
||||
value = defaultValue;
|
||||
return value;
|
||||
}
|
||||
|
||||
public bool TryGetParameter(String parameterName, out string parameterValue) =>
|
||||
_parameters.TryGetValue(parameterName, out parameterValue);
|
||||
public void SetParameter(String parameterName, String parameterValue) =>
|
||||
_parameters[parameterName] = parameterValue;
|
||||
|
||||
public IEnumerable<string> ParameterNames => _parameters.Keys;
|
||||
}
|
|
@ -1,121 +1,63 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ln.json;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HttpResponse
|
||||
public class HttpResponse : IDisposable
|
||||
{
|
||||
//public HttpRequest HttpRequest { get; }
|
||||
List<HttpCookie> _cookies = new List<HttpCookie>();
|
||||
|
||||
public Stream ContentStream { get; private set; }
|
||||
public TextWriter ContentWriter { get; private set; }
|
||||
public bool HasCustomContentStream { get; private set; }
|
||||
|
||||
Dictionary<string, List<String>> headers = new Dictionary<string, List<string>>();
|
||||
List<HttpCookie> cookies = new List<HttpCookie>();
|
||||
|
||||
|
||||
public HttpResponse() : this(HttpStatusCode.OK)
|
||||
{
|
||||
}
|
||||
public HttpResponse(HttpStatusCode statusCode)
|
||||
{
|
||||
HttpStatusCode = statusCode;
|
||||
ContentStream = new MemoryStream();
|
||||
ContentWriter = new StreamWriter(ContentStream);
|
||||
|
||||
SetHeader("content-type", "text/html");
|
||||
Headers = new HeaderContainer();
|
||||
}
|
||||
public HttpResponse() : this(HttpStatusCode.OK)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public HttpResponse(HttpRequest httpRequest) : this() { }
|
||||
[Obsolete]
|
||||
public HttpResponse(HttpRequest httpRequest,string contentType) : this(contentType) { }
|
||||
public HttpResponse(string contentType)
|
||||
:this()
|
||||
{
|
||||
SetHeader("content-type", contentType);
|
||||
}
|
||||
|
||||
public HeaderContainer Headers { get; }
|
||||
public HttpContent HttpContent { get; set; }
|
||||
|
||||
[Obsolete]
|
||||
public HttpResponse(HttpRequest httpRequest, Stream contentStream) : this(contentStream) { }
|
||||
public HttpResponse(Stream contentStream)
|
||||
{
|
||||
ContentStream = contentStream;
|
||||
ContentWriter = null;
|
||||
HasCustomContentStream = true;
|
||||
public String GetHeader(string name) => Headers.Get(name);
|
||||
public String GetHeader(string name, string defValue) => Headers.Get(name, defValue);
|
||||
|
||||
HttpStatusCode = HttpStatusCode.OK;
|
||||
SetHeader("content-type", "text/html");
|
||||
}
|
||||
public void SetHeader(String name, String value) => Headers.Set(name, value);
|
||||
|
||||
[Obsolete]
|
||||
public HttpResponse(HttpRequest httpRequest, Stream contentStream,string contentType) : this(contentStream, contentType){ }
|
||||
public HttpResponse(Stream contentStream,string contentType)
|
||||
:this(contentStream)
|
||||
{
|
||||
SetHeader("content-type", contentType);
|
||||
}
|
||||
|
||||
public String GetHeader(string name) => GetHeader(name, null);
|
||||
public String GetHeader(string name, string defValue)
|
||||
{
|
||||
return headers.ContainsKey(name) ? String.Join(",", headers[name.ToUpper()]) : defValue;
|
||||
}
|
||||
|
||||
public String[] GetHeaderValues(string name)
|
||||
{
|
||||
return headers[name.ToUpper()].ToArray();
|
||||
}
|
||||
public String[] GetHeaderNames()
|
||||
{
|
||||
return headers.Keys.ToArray();
|
||||
}
|
||||
|
||||
public void SetHeader(String name, String value)
|
||||
{
|
||||
name = name.ToUpper();
|
||||
|
||||
headers[name] = new List<string>();
|
||||
headers[name].Add(value);
|
||||
}
|
||||
public void AddHeader(String name, String value)
|
||||
{
|
||||
name = name.ToUpper();
|
||||
|
||||
if (!headers.ContainsKey(name))
|
||||
headers[name] = new List<string>();
|
||||
|
||||
headers[name].Add(value);
|
||||
}
|
||||
public void RemoveHeader(String name) => headers.Remove(name.ToUpper());
|
||||
public bool ContainsHeader(string headerName) => headers.ContainsKey(headerName.ToUpper());
|
||||
public void AddHeader(String name, String value) => Headers.Add(name, value);
|
||||
public void RemoveHeader(String name) => Headers.Remove(name);
|
||||
public bool ContainsHeader(string headerName) => Headers.Contains(headerName);
|
||||
|
||||
public void AddCookie(string name,string value)
|
||||
{
|
||||
AddCookie(new HttpCookie(name, value));
|
||||
}
|
||||
public void AddCookie(HttpCookie httpCookie) => cookies.Add(httpCookie);
|
||||
public void RemoveCookie(HttpCookie httpCookie) => cookies.Remove(httpCookie);
|
||||
public HttpCookie[] Cookies => cookies.ToArray();
|
||||
public void AddCookie(HttpCookie httpCookie) => _cookies.Add(httpCookie);
|
||||
public void RemoveCookie(HttpCookie httpCookie) => _cookies.Remove(httpCookie);
|
||||
public HttpCookie[] Cookies => _cookies.ToArray();
|
||||
|
||||
public HttpStatusCode HttpStatusCode { get; set; }
|
||||
|
||||
[Obsolete]
|
||||
public int StatusCode
|
||||
{
|
||||
get => (int)HttpStatusCode;
|
||||
set => HttpStatusCode = (HttpStatusCode)value;
|
||||
}
|
||||
[Obsolete]
|
||||
public String StatusMessage
|
||||
{
|
||||
get => HttpStatusCode.ToString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public HttpResponse Header(string name,string value)
|
||||
{
|
||||
SetHeader(name, value);
|
||||
return this;
|
||||
}
|
||||
public HttpResponse ContentType(string contentType)
|
||||
{
|
||||
SetHeader("content-type", contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static HttpResponse OK() => new HttpResponse(HttpStatusCode.OK);
|
||||
|
@ -123,7 +65,12 @@ namespace ln.http
|
|||
public static HttpResponse Accepted() => new HttpResponse(HttpStatusCode.Accepted);
|
||||
public static HttpResponse NoContent() => new HttpResponse(HttpStatusCode.NoContent);
|
||||
|
||||
public static HttpResponse MultiStatus() =>
|
||||
new HttpResponse(HttpStatusCode.MultiStatus).Header("content-type", "application/xml");
|
||||
|
||||
public static HttpResponse MovedPermanently() => new HttpResponse(HttpStatusCode.MovedPermanently);
|
||||
public static HttpResponse SeeOther() => new HttpResponse(HttpStatusCode.SeeOther);
|
||||
public static HttpResponse SeeOther(string uri) => new HttpResponse(HttpStatusCode.SeeOther).Header("Location", uri);
|
||||
public static HttpResponse TemporaryRedirect() => new HttpResponse(HttpStatusCode.TemporaryRedirect);
|
||||
public static HttpResponse PermanentRedirect() => new HttpResponse(HttpStatusCode.PermanentRedirect);
|
||||
|
||||
|
@ -133,6 +80,8 @@ namespace ln.http
|
|||
public static HttpResponse Forbidden() => new HttpResponse(HttpStatusCode.Forbidden);
|
||||
public static HttpResponse MethodNotAllowed() => new HttpResponse(HttpStatusCode.MethodNotAllowed);
|
||||
public static HttpResponse RequestTimeout() => new HttpResponse(HttpStatusCode.RequestTimeout);
|
||||
public static HttpResponse Conflict() => new HttpResponse(HttpStatusCode.Conflict);
|
||||
public static HttpResponse UnsupportedMediaType() => new HttpResponse(HttpStatusCode.UnsupportedMediaType);
|
||||
public static HttpResponse ImATeapot() => new HttpResponse(HttpStatusCode.ImATeapot);
|
||||
public static HttpResponse UpgradeRequired() => new HttpResponse(HttpStatusCode.UpgradeRequired);
|
||||
|
||||
|
@ -142,28 +91,58 @@ namespace ln.http
|
|||
public static HttpResponse ServiceUnavailable() => new HttpResponse(HttpStatusCode.ServiceUnavailable);
|
||||
public static HttpResponse GatewayTimeout() => new HttpResponse(HttpStatusCode.GatewayTimeout);
|
||||
|
||||
public static HttpResponse Default(HttpMethod httpMethod)
|
||||
{
|
||||
switch (httpMethod)
|
||||
{
|
||||
case HttpMethod.DELETE:
|
||||
return NoContent();
|
||||
case HttpMethod.POST:
|
||||
return Created();
|
||||
default:
|
||||
return OK();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public HttpResponse Content(Exception exception)
|
||||
{
|
||||
SetHeader("content-type", "text/plain");
|
||||
ContentWriter.WriteLine("{0}", exception);
|
||||
ContentWriter.Flush();
|
||||
|
||||
HttpContent = new StringContent(exception.ToString());
|
||||
return this;
|
||||
}
|
||||
public HttpResponse Content(string text)
|
||||
{
|
||||
ContentWriter.Write(text);
|
||||
ContentWriter.Flush();
|
||||
|
||||
HttpContent = new StringContent(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse Content(JSONValue json)
|
||||
{
|
||||
HttpContent = new JsonContent(json);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse Content(Stream contentStream)
|
||||
{
|
||||
HttpContent = new StreamContent(contentStream);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpResponse Content(HttpContent httpContent)
|
||||
{
|
||||
HttpContent = httpContent;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public HttpResponse Json<T>(T content)
|
||||
{
|
||||
HttpContent = new StringContent(JsonConvert.SerializeObject(content), "application/json");
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
HttpContent?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
using ln.http.router;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HttpRolePermission
|
||||
{
|
||||
public string RoleName { get; set; }
|
||||
public HttpAccessRights AccessRights { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,36 +1,94 @@
|
|||
using System;
|
||||
using ln.logging;
|
||||
using ln.http.router;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ln.http.route;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
//public abstract class HttpRouter : IHttpRouter
|
||||
//{
|
||||
// public HttpRouter()
|
||||
// {
|
||||
// }
|
||||
public delegate bool HttpRouterDelegate(HttpRequestContext context, string routedPath);
|
||||
public delegate bool HttpAuthorizationDelegate(HttpRequestContext httpRequestContext);
|
||||
public delegate void HttpFilterDelegate(HttpRequestContext httpRequestContext);
|
||||
|
||||
// public abstract IHTTPResource FindResource(HttpRequest httpRequest);
|
||||
|
||||
// public virtual HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// IHTTPResource resource = FindResource(httpRequest);
|
||||
// return resource.GetResponse(httpRequest);
|
||||
// } catch (Exception e)
|
||||
// {
|
||||
// Logging.Log(e);
|
||||
// if (httpRequest != null)
|
||||
// {
|
||||
// HttpResponse httpResponse = new HttpResponse(httpRequest);
|
||||
// httpResponse.StatusCode = 500;
|
||||
// httpResponse.ContentWriter.WriteLine("500 Internal Server Error");
|
||||
public enum HttpRoutePriority : int { HIGHEST = 0, HIGH = 1, NORMAL = 2, LOW = 3, LOWEST = 4 }
|
||||
|
||||
// return httpResponse;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
public class HttpRouter : HttpRoute, IDisposable
|
||||
{
|
||||
public event HttpFilterDelegate HttpFilters;
|
||||
public event HttpFilterDelegate HttpFixups;
|
||||
|
||||
private HttpRouter _parentRouter;
|
||||
private List<HttpRoute> _routes = new List<HttpRoute>();
|
||||
|
||||
public HttpRouter()
|
||||
:this(HttpMethod.ANY, null)
|
||||
{
|
||||
_routerDelegate = RouteRequest;
|
||||
}
|
||||
|
||||
//}
|
||||
public HttpRouter(HttpMethod httpMethod, string mapPath)
|
||||
: base(httpMethod, mapPath)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpRouter(HttpRouter parentRouter, string mapPath)
|
||||
:base(HttpMethod.ANY, mapPath)
|
||||
{
|
||||
_parentRouter = parentRouter;
|
||||
_parentRouter.Map(this);
|
||||
}
|
||||
|
||||
public void Map(HttpRoute httpRoute) => _routes.Add(httpRoute);
|
||||
public void UnMap(HttpRoute httpRoute) => _routes.Remove(httpRoute);
|
||||
|
||||
public void Map(HttpMethod httpMethod, string route, HttpRouterDelegate routerDelegate)
|
||||
=> Map(new HttpRoute(httpMethod, route, routerDelegate));
|
||||
|
||||
|
||||
public virtual bool RouteRequest(HttpRequestContext httpRequestContext, string routePath)
|
||||
{
|
||||
HttpFilters?.Invoke(httpRequestContext);
|
||||
|
||||
foreach (var httpRoute in _routes)
|
||||
{
|
||||
if (httpRoute.TryRoute(httpRequestContext, routePath))
|
||||
{
|
||||
HttpFixups?.Invoke(httpRequestContext);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_parentRouter?.Dispose();
|
||||
}
|
||||
|
||||
public override string ToString() => $"[HttpRouter]";
|
||||
|
||||
/*private string RegexFromRoute(string route)
|
||||
{
|
||||
string[] parts = route.Split(new char[] { '/' });
|
||||
string[] reparts = parts.Select((part) =>
|
||||
{
|
||||
if (part.StartsWith(":", StringComparison.InvariantCulture))
|
||||
if (part.EndsWith("*", StringComparison.InvariantCulture))
|
||||
return string.Format("(?<{0}>[^/]+)(?<_>/.*)?", part.Substring(1, part.Length - 2));
|
||||
else
|
||||
return string.Format("(?<{0}>[^/]+)", part.Substring(1));
|
||||
else if (part.Equals("*"))
|
||||
return string.Format("(?<_>.*)");
|
||||
else if (part.EndsWith("*", StringComparison.InvariantCulture))
|
||||
return string.Format("{0}[^/]+(?<_>/.*)?", part.Substring(0, part.Length-1));
|
||||
else
|
||||
return string.Format("{0}", part);
|
||||
}).ToArray();
|
||||
|
||||
return string.Format("^{0}/?$", string.Join("/", reparts));
|
||||
}*/
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ln.logging;
|
||||
using ln.http.exceptions;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
|
||||
/*
|
||||
public class HttpServer
|
||||
{
|
||||
public static bool DefaultRouteAuthentication { get; set; } = false;
|
||||
|
||||
private HashSet<HttpRouterDelegate> _routerDelegates = new HashSet<HttpRouterDelegate>();
|
||||
public IEnumerable<HttpRouterDelegate> Routers => _routerDelegates;
|
||||
|
||||
public TextWriter AccessLogWriter { get; set; }
|
||||
private ILogWriter _logWriter;
|
||||
|
||||
|
||||
private HashSet<IHttpAuthenticationSource> _authenticationSources = new HashSet<IHttpAuthenticationSource>();
|
||||
public IEnumerable<IHttpAuthenticationSource> AuthenticationSources => _authenticationSources;
|
||||
|
||||
|
||||
public HttpServer() : this(Console.Out, new LogWriter(new ConsoleLogSink()))
|
||||
{
|
||||
}
|
||||
|
||||
public HttpServer(TextWriter accessLogWriter, ILogWriter logWriter)
|
||||
{
|
||||
AccessLogWriter = accessLogWriter;
|
||||
_logWriter = logWriter;
|
||||
}
|
||||
public HttpServer(HttpRouterDelegate router)
|
||||
: this()
|
||||
{
|
||||
AddRouter(router);
|
||||
}
|
||||
|
||||
public void RegisterAuthenticationSource(IHttpAuthenticationSource authenticationSource) =>
|
||||
_authenticationSources.Add(authenticationSource);
|
||||
public void UnregisterAuthenticationSource(IHttpAuthenticationSource authenticationSource) =>
|
||||
_authenticationSources.Remove(authenticationSource);
|
||||
|
||||
public void AddRouter(HttpRouter httpRouter) => AddRouter(httpRouter.RouteRequest);
|
||||
public void AddRouter(HttpRouterDelegate routerDelegate) => _routerDelegates.Add(routerDelegate);
|
||||
|
||||
public void RemoveRouter(HttpRouter httpRouter) => RemoveRouter(httpRouter.RouteRequest);
|
||||
public void RemoveRouter(HttpRouterDelegate routerDelegate) => _routerDelegates.Remove(routerDelegate);
|
||||
|
||||
public void Connection(HttpConnection httpConnection) =>
|
||||
ThreadPool.QueueUserWorkItem((state => ConnectionWorker(httpConnection)));
|
||||
|
||||
public void ConnectionWorker(HttpConnection httpConnection)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool keepalive = false;
|
||||
do
|
||||
{
|
||||
DateTime start = DateTime.Now;
|
||||
|
||||
using (HttpRequest httpRequest = ReadRequest(httpConnection))
|
||||
{
|
||||
if (httpRequest == null)
|
||||
break;
|
||||
|
||||
HttpContext httpContext = new HttpContext(this, httpRequest);
|
||||
|
||||
try
|
||||
{
|
||||
if (!RouteRequest(httpContext))
|
||||
throw new HttpException(HttpStatusCode.NotFound);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logWriter?.Log(exception);
|
||||
|
||||
if (exception is HttpException httpException)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpContext errorContext = new HttpContext(httpContext, httpException);
|
||||
if (!RouteRequest(errorContext))
|
||||
{
|
||||
errorContext.ResetRoutingStack(String.Format("/_err.html",
|
||||
(int)httpException.HttpStatusCode));
|
||||
RouteRequest(errorContext);
|
||||
}
|
||||
httpContext.Response = errorContext.Response;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (httpContext.Response is null)
|
||||
httpContext.Response = HttpResponse
|
||||
.InternalServerError()
|
||||
.Content(
|
||||
String.Format("An internal error occured ({0})", exception.ToString()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Response = HttpResponse.InternalServerError()
|
||||
.Content(String.Format("An internal error occured ({0})", exception.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
httpContext.Response.WriteTo(httpConnection.ClientStream);
|
||||
httpContext.Response?.ContentStream?.Dispose();
|
||||
}
|
||||
catch (IOException ioexception)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
DateTime end = DateTime.Now;
|
||||
TimeSpan duration = end - start;
|
||||
|
||||
AccessLogWriter?.WriteLine("{0} {1} {2} {3} {4} {5} {6} {7}",
|
||||
start,
|
||||
end,
|
||||
duration,
|
||||
httpConnection.RemoteEndPoint?.ToString(),
|
||||
httpContext.Response?.StatusCode.ToString() ?? "-",
|
||||
httpContext.AuthenticatedPrincipal?.ToString() ?? "-",
|
||||
httpContext.Request.Method,
|
||||
httpContext.Request.RequestUri
|
||||
);
|
||||
AccessLogWriter?.Flush();
|
||||
|
||||
keepalive = httpContext.Response.GetHeader("connection", "keep-alive").Equals("keep-alive") &&
|
||||
httpRequest
|
||||
.GetRequestHeader("connection",
|
||||
httpRequest.Protocol.Equals("HTTP/1.1") ? "keep-alive" : "close").Contains(
|
||||
"keep-alive",
|
||||
StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
} while (keepalive);
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpConnection.ClientStream.Close();
|
||||
httpConnection.ClientStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool RouteRequest(HttpContext httpContext)
|
||||
{
|
||||
foreach (var routerDelegate in _routerDelegates)
|
||||
{
|
||||
if (routerDelegate(httpContext))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private HttpRequest ReadRequest(HttpConnection httpConnection)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (HttpLikeProtocolReader.ReadRequest(httpConnection.ClientStream, Encoding.UTF8, out Request request))
|
||||
return new HttpRequest(this, request);
|
||||
return null;
|
||||
} catch (IOException)
|
||||
{
|
||||
return null;
|
||||
} catch (ConnectionClosedException)
|
||||
{
|
||||
return null;
|
||||
} catch (Exception e)
|
||||
{
|
||||
_logWriter?.Log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -19,6 +19,7 @@ namespace ln.http
|
|||
{ 206, "Partial Content"},
|
||||
{ 207, "Multi Status"},
|
||||
{ 208, "Already Reported"},
|
||||
{ 400, "Bad Request"},
|
||||
{ 403, "Access denied" },
|
||||
{ 404, "Not Found" },
|
||||
{ 500, "Internal Error" }
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
// /**
|
||||
// * File: HttpUser.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public class HttpUser
|
||||
{
|
||||
public String AuthenticationName { get; private set; }
|
||||
public virtual String DisplayName { get; private set; }
|
||||
|
||||
public long AccessRightsMask { get; private set; }
|
||||
|
||||
public HttpUser()
|
||||
{
|
||||
AuthenticationName = "";
|
||||
DisplayName = "Anonymous";
|
||||
AccessRightsMask = 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
[Flags]
|
||||
public enum HttpVersion
|
||||
{
|
||||
None = 0,
|
||||
HTTP10 = (1<<0), // HTTP/1.0
|
||||
HTTP11 = (1<<1), // HTTP/1.1
|
||||
HTTP2 = (1<<2), // HTTP/2.0
|
||||
|
||||
ALL = HTTP10 | HTTP11 | HTTP2
|
||||
}
|
||||
|
||||
public static class HttpVersionSupport
|
||||
{
|
||||
|
||||
public static string ToString(HttpVersion httpVersion)
|
||||
{
|
||||
switch (httpVersion)
|
||||
{
|
||||
case HttpVersion.None:
|
||||
return "None";
|
||||
case HttpVersion.ALL:
|
||||
return "ALL";
|
||||
case HttpVersion.HTTP2:
|
||||
return "HTTP/2";
|
||||
case HttpVersion.HTTP10:
|
||||
return "HTTP/1.0";
|
||||
case HttpVersion.HTTP11:
|
||||
return "HTTP/1.1";
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpVersion Parse(string httpVersion)
|
||||
{
|
||||
switch (httpVersion)
|
||||
{
|
||||
case "HTTP/1.0":
|
||||
return HttpVersion.HTTP10;
|
||||
case "HTTP/1.1":
|
||||
return HttpVersion.HTTP10;
|
||||
default:
|
||||
return HttpVersion.None;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
using System;
|
||||
namespace ln.http
|
||||
{
|
||||
public interface IHTTPResource
|
||||
{
|
||||
HttpResponse GetResponse(HttpRequest httpRequest);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace ln.http
|
||||
{
|
||||
public interface IHttpAuthenticationSource
|
||||
{
|
||||
bool AuthenticatePrincipal(HttpRequestContext requestContext, out HttpPrincipal httpPrincipal);
|
||||
}
|
||||
}
|
|
@ -2,8 +2,5 @@
|
|||
using ln.http.router;
|
||||
namespace ln.http
|
||||
{
|
||||
public interface IHttpRouter
|
||||
{
|
||||
HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class JsonBodyAttribute : Attribute
|
||||
{
|
||||
public string Property { get; set; }
|
||||
public JsonBodyAttribute(){}
|
||||
public JsonBodyAttribute(string property)
|
||||
{
|
||||
Property = property;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using ln.http.exceptions;
|
||||
using ln.threading;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public class Listener : IDisposable
|
||||
{
|
||||
static string _http2_preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
|
||||
static string _http2_preface1 = "PRI * HTTP/2.0";
|
||||
|
||||
protected Socket Socket;
|
||||
|
||||
public IPEndPoint LocalEndpoint => (IPEndPoint)Socket.LocalEndPoint;
|
||||
public HttpVersion AcceptedHttpVersion { get; set; } = HttpVersion.ALL;
|
||||
|
||||
public HttpRouter HttpRouter { get; set; }
|
||||
|
||||
public Listener(HttpRouter httpRouter)
|
||||
: this(httpRouter, new IPEndPoint(IPAddress.IPv6Any, 0))
|
||||
{
|
||||
}
|
||||
public Listener(HttpRouter httpRouter, int port)
|
||||
: this(httpRouter, new IPEndPoint(IPAddress.IPv6Any, port))
|
||||
{
|
||||
}
|
||||
|
||||
public Listener(HttpRouter httpRouter, IPAddress bind, int port)
|
||||
: this(httpRouter, new IPEndPoint(bind, port))
|
||||
{
|
||||
}
|
||||
|
||||
public Listener(HttpRouter httpRouter, IPEndPoint bind)
|
||||
{
|
||||
HttpRouter = httpRouter;
|
||||
|
||||
Socket = new Socket(bind?.AddressFamily ?? AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
|
||||
Socket.ExclusiveAddressUse = false;
|
||||
|
||||
if (bind is null)
|
||||
bind = new IPEndPoint(IPAddress.IPv6Any, 0);
|
||||
|
||||
Socket.Bind(bind);
|
||||
Socket.Listen();
|
||||
Socket.ReceiveTimeout = 10000;
|
||||
|
||||
DynamicThreadPool.DefaultPool.Enqueue(ListenerLoop);
|
||||
}
|
||||
|
||||
private void ListenerLoop()
|
||||
{
|
||||
while (Socket.IsBound)
|
||||
{
|
||||
Socket clientSocket = Socket.Accept();
|
||||
ThreadPool.QueueUserWorkItem((state => ConnectionHandler(clientSocket)));
|
||||
// DynamicThreadPool.DefaultPool.Enqueue(()=>ConnectionHandler(clientSocket));
|
||||
}
|
||||
}
|
||||
|
||||
private void ConnectionHandler(Socket clientSocket)
|
||||
{
|
||||
// clientSocket.ReceiveTimeout = 10000;
|
||||
|
||||
try
|
||||
{
|
||||
using (NetworkStream networkStream = new NetworkStream(clientSocket))
|
||||
Accepted(clientSocket, networkStream);
|
||||
}
|
||||
catch (SocketException se)
|
||||
{
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
clientSocket.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Accepted(Socket connectedSocket, Stream connectionStream)
|
||||
{
|
||||
if (HttpConnection.ReadRequestLine(connectionStream, out string method, out string requestUri, out HttpVersion httpVersion))
|
||||
{
|
||||
if ((AcceptedHttpVersion & httpVersion) == HttpVersion.None)
|
||||
return;
|
||||
|
||||
HttpConnection httpConnection;
|
||||
|
||||
switch (httpVersion)
|
||||
{
|
||||
case HttpVersion.HTTP2:
|
||||
httpConnection = new Http2Connection(this, (IPEndPoint) connectedSocket.RemoteEndPoint, connectionStream, method, requestUri, httpVersion);
|
||||
break;
|
||||
case HttpVersion.HTTP10:
|
||||
case HttpVersion.HTTP11:
|
||||
default:
|
||||
httpConnection = new Http1XConnection(this, (IPEndPoint) connectedSocket.RemoteEndPoint, connectionStream, method, requestUri, httpVersion);
|
||||
break;
|
||||
}
|
||||
|
||||
Accepted(httpConnection);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected virtual void Accepted(HttpConnection httpConnection)
|
||||
{
|
||||
httpConnection.Run();
|
||||
}
|
||||
|
||||
public void Dispatch(HttpRequestContext requestContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!HttpRouter.RouteRequest(requestContext, requestContext.Request.RequestUri.AbsolutePath))
|
||||
throw new NotFoundException();
|
||||
}
|
||||
catch (HttpException httpException)
|
||||
{
|
||||
requestContext.Response = new HttpResponse(httpException.HttpStatusCode);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
requestContext.Response = HttpResponse.InternalServerError().Content(e);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Socket?.Dispose();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
|
||||
public class MapAttribute : Attribute
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public HttpMethod Method { get; set; }
|
||||
|
||||
public MapAttribute(){}
|
||||
public MapAttribute(HttpMethod method, string path)
|
||||
{
|
||||
Method = method;
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class QueryAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public QueryAttribute(){}
|
||||
}
|
|
@ -29,7 +29,8 @@ namespace ln.http
|
|||
|
||||
public string this[string key] {
|
||||
get => parameters[key];
|
||||
set => throw new NotImplementedException(); }
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public ICollection<string> Keys => parameters.Keys;
|
||||
public ICollection<string> Values => parameters.Values;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public class RequiresAttribute : Attribute
|
||||
{
|
||||
public string[] Roles { get; set; }
|
||||
|
||||
public RequiresAttribute(params string[] roles)
|
||||
{
|
||||
Roles = roles;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using System.Reflection;
|
||||
using ln.http.router;
|
||||
|
||||
namespace ln.http
|
||||
{
|
||||
public static class RoleAuthorization
|
||||
{
|
||||
public static HttpAuthorizationDelegate Require(string roleName)
|
||||
{
|
||||
return context => context.AuthenticatedPrincipal?.HasRole(roleName) ?? false;
|
||||
}
|
||||
|
||||
public static HttpAuthorizationDelegate RequireAll(params HttpAuthorizationDelegate[] authorizationDelegates)
|
||||
{
|
||||
return context =>
|
||||
{
|
||||
foreach (HttpAuthorizationDelegate authorizationDelegate in authorizationDelegates)
|
||||
{
|
||||
if (!authorizationDelegate(context))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public static HttpAuthorizationDelegate RequireOneOf(params HttpAuthorizationDelegate[] authorizationDelegates)
|
||||
{
|
||||
return context =>
|
||||
{
|
||||
foreach (HttpAuthorizationDelegate authorizationDelegate in authorizationDelegates)
|
||||
{
|
||||
if (authorizationDelegate(context))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class RouteAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public RouteAttribute(){}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace ln.http;
|
||||
|
||||
public class TlsListener : Listener
|
||||
{
|
||||
private CertificateStore _certificateStore;
|
||||
public X509Certificate DefaultCertificate { get; set; }
|
||||
|
||||
public TlsListener(HttpRouter httpRouter, CertificateStore certificateStore)
|
||||
:this(httpRouter, null, certificateStore){}
|
||||
public TlsListener(HttpRouter httpRouter, IPAddress bind, int port, CertificateStore certificateStore)
|
||||
:this(httpRouter, new IPEndPoint(bind, port), certificateStore){}
|
||||
|
||||
public TlsListener(HttpRouter httpRouter, IPEndPoint bind, CertificateStore certificateStore)
|
||||
: this(httpRouter, bind, certificateStore, null)
|
||||
{
|
||||
}
|
||||
public TlsListener(HttpRouter httpRouter, IPEndPoint bind, CertificateStore certificateStore, X509Certificate defaultCertificate) :base(httpRouter, bind)
|
||||
{
|
||||
_certificateStore = certificateStore;
|
||||
DefaultCertificate = defaultCertificate;
|
||||
|
||||
if (DefaultCertificate is null)
|
||||
{
|
||||
if (_certificateStore.TryGetCertificate("localhost", out defaultCertificate))
|
||||
DefaultCertificate = defaultCertificate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void Accepted(Socket connectedSocket, Stream connectionStream)
|
||||
{
|
||||
SslStream sslStream = new SslStream(connectionStream, false, null, CertificateSelectionCallback);
|
||||
try
|
||||
{
|
||||
sslStream.AuthenticateAsServer(DefaultCertificate);
|
||||
|
||||
// ToDo: Check for correct ALPN protocol identifier
|
||||
|
||||
base.Accepted(connectedSocket, sslStream);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
sslStream?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private X509Certificate CertificateSelectionCallback(object sender, string targethost,
|
||||
X509CertificateCollection localcertificates, X509Certificate? remotecertificate, string[] acceptableissuers)
|
||||
{
|
||||
if (!_certificateStore.TryGetCertificate(targethost, out X509Certificate certificate))
|
||||
return null;
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
private X509Certificate2 buildSelfSignedServerCertificate()
|
||||
{
|
||||
SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddIpAddress(LocalEndpoint.Address);
|
||||
sanBuilder.AddDnsName(Environment.MachineName);
|
||||
|
||||
X500DistinguishedName distinguishedName = new X500DistinguishedName($"CN={LocalEndpoint.Address.ToString()}");
|
||||
|
||||
using (RSA rsa = RSA.Create(4096))
|
||||
{
|
||||
var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);
|
||||
request.CertificateExtensions.Add(
|
||||
new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature , false));
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
|
||||
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||
|
||||
var certificate= request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650)));
|
||||
return certificate;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
using ln.http.exceptions;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace ln.http.router
|
||||
{
|
||||
public class VirtualHostRouter : HttpRouter
|
||||
{
|
||||
public HttpRouter DefaultRoute { get; set; }
|
||||
|
||||
Dictionary<string, HttpRouter> virtualHosts = new Dictionary<string, HttpRouter>();
|
||||
|
||||
public VirtualHostRouter()
|
||||
{
|
||||
}
|
||||
public VirtualHostRouter(HttpRouter defaultRoute)
|
||||
{
|
||||
DefaultRoute = defaultRoute;
|
||||
}
|
||||
public VirtualHostRouter(IEnumerable<KeyValuePair<string, HttpRouter>> routes)
|
||||
{
|
||||
foreach (KeyValuePair<string, HttpRouter> route in routes)
|
||||
virtualHosts.Add(route.Key, route.Value);
|
||||
}
|
||||
public VirtualHostRouter(HttpRouter defaultRoute,IEnumerable<KeyValuePair<string, HttpRouter>> routes)
|
||||
:this(routes)
|
||||
{
|
||||
DefaultRoute = defaultRoute;
|
||||
}
|
||||
public VirtualHostRouter(VirtualHostRouter source)
|
||||
: this(source.virtualHosts) { }
|
||||
|
||||
public void Add(string hostname,HttpRouter router) => virtualHosts.Add(hostname, router);
|
||||
public void Remove(string hostname) => virtualHosts.Remove(hostname);
|
||||
public bool Contains(string hostname) => virtualHosts.ContainsKey(hostname);
|
||||
|
||||
public bool TryGetValue(string hostname, out HttpRouter router) => virtualHosts.TryGetValue(hostname, out router);
|
||||
|
||||
public bool Route(HttpRequestContext requestContext, string routePath)
|
||||
{
|
||||
if (virtualHosts.TryGetValue(requestContext.Request.Host, out HttpRouter virtualHost))
|
||||
return virtualHost.RouteRequest(requestContext, routePath);
|
||||
if (DefaultRoute != null)
|
||||
return DefaultRoute.RouteRequest(requestContext, routePath);
|
||||
|
||||
throw new HttpException(HttpStatusCode.Gone, string.Format("Gone. Hostname {0} not found on this server.", requestContext.Request.Host));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,243 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using ln.http.content;
|
||||
using ln.logging;
|
||||
using ln.patterns;
|
||||
using ln.type;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ln.http.client
|
||||
{
|
||||
public class HttpClient
|
||||
{
|
||||
CookieContainer Cookies { get; set; } = new CookieContainer();
|
||||
private CookieContainer _cookies = new CookieContainer();
|
||||
private HeaderContainer _defaultHeaders = new HeaderContainer();
|
||||
private ILogWriter _logWriter;
|
||||
|
||||
public HeaderContainer DefaultHeaders => _defaultHeaders;
|
||||
public CookieContainer Cookies => _cookies;
|
||||
|
||||
public HttpClient() :this(null) { }
|
||||
|
||||
public bool FollowRedirects { get; set; }
|
||||
|
||||
public HttpClient()
|
||||
public HttpClient(ILogWriter logWriter)
|
||||
{
|
||||
_logWriter = logWriter ?? new LogWriter(new ConsoleLogSink(true));
|
||||
_defaultHeaders.Add("User-Agent", "Mozilla/5.0 (ln.http.client.HttpClient)");
|
||||
}
|
||||
|
||||
public HttpClientRequest CreateRequest(URI uri)
|
||||
private delegate bool TryConnectDelegate(Uri uri, IPAddress ip, int port, out IDisposable disposable,
|
||||
out Stream connectionStream);
|
||||
public Optional<HttpClientResponse> Request(HttpMethod method, Uri uri, IEnumerable<Header> headers,
|
||||
Stream body)
|
||||
{
|
||||
return new HttpClientRequest(this, uri);
|
||||
TryConnectDelegate tryConnectDelegate = null;
|
||||
|
||||
switch (uri.Scheme)
|
||||
{
|
||||
case "http":
|
||||
tryConnectDelegate = this.TryConnect;
|
||||
break;
|
||||
case "https":
|
||||
tryConnectDelegate = this.TryConnectTLS;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"URI scheme { uri.Scheme } not supported by HttpClient");
|
||||
}
|
||||
|
||||
foreach (var address in GetIPAddresses(uri))
|
||||
{
|
||||
if (tryConnectDelegate(uri, address, uri.Port, out IDisposable disposable, out Stream connectionStream))
|
||||
{
|
||||
try
|
||||
{
|
||||
return _Request(connectionStream, method, uri, headers, body);
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectionStream?.Dispose();
|
||||
disposable?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SocketException();
|
||||
}
|
||||
|
||||
private IPAddress[] GetIPAddresses(Uri uri)
|
||||
{
|
||||
switch (uri.HostNameType)
|
||||
{
|
||||
case UriHostNameType.Dns:
|
||||
return Dns.GetHostAddresses(uri.Host);
|
||||
case UriHostNameType.IPv4:
|
||||
case UriHostNameType.IPv6:
|
||||
return new[] { IPAddress.Parse(uri.Host), };
|
||||
default:
|
||||
return new IPAddress[0];
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryConnect(Uri uri, IPAddress ip, int port, out IDisposable disposable, out Stream connectionStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
TcpClient tcpClient = new TcpClient();
|
||||
tcpClient.Connect(new IPEndPoint(ip, port));
|
||||
disposable = tcpClient;
|
||||
connectionStream = tcpClient.GetStream();
|
||||
return true;
|
||||
}
|
||||
catch (SocketException se)
|
||||
{
|
||||
}
|
||||
|
||||
disposable = null;
|
||||
connectionStream = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryConnectTLS(Uri uri, IPAddress ip, int port, out IDisposable disposable, out Stream connectionStream)
|
||||
{
|
||||
if (TryConnect(uri, ip, port, out IDisposable rawDisposable, out Stream rawConnectionStream))
|
||||
{
|
||||
SslStream sslStream = new SslStream(rawConnectionStream, false);
|
||||
sslStream.AuthenticateAsClient(uri.Host);
|
||||
|
||||
disposable = rawDisposable;
|
||||
connectionStream = sslStream;
|
||||
return true;
|
||||
}
|
||||
|
||||
disposable = null;
|
||||
connectionStream = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<HttpClientResponse> _Request(Stream connectionStream, HttpMethod method, Uri uri,
|
||||
IEnumerable<Header> headers, Stream body)
|
||||
{
|
||||
HeaderContainer requestHeaders = new HeaderContainer(_defaultHeaders);
|
||||
requestHeaders.Set("Host", uri.Host);
|
||||
if (body is Stream)
|
||||
requestHeaders.Set("Content-Length", body.Length.ToString());
|
||||
|
||||
connectionStream.WriteBytes(Encoding.ASCII.GetBytes(String.Format("{0} {1} HTTP/1.1\r\n",
|
||||
method.ToString(),
|
||||
uri.PathAndQuery
|
||||
)));
|
||||
requestHeaders.CopyTo(connectionStream);
|
||||
|
||||
if ((method != HttpMethod.HEAD) && (body is Stream))
|
||||
body.CopyTo(connectionStream);
|
||||
|
||||
connectionStream.Flush();
|
||||
|
||||
if (!ReadResponseLine(connectionStream, out HttpVersion httpVersion, out HttpStatusCode statusCode,
|
||||
out string reason))
|
||||
return new ProtocolViolationException();
|
||||
|
||||
HeaderContainer responseHeaders = new HeaderContainer(connectionStream);
|
||||
Stream contentStream = null;
|
||||
|
||||
if (method != HttpMethod.HEAD)
|
||||
{
|
||||
if (responseHeaders.TryGetValue("Transfer-Encoding", out string transferEncoding) &&
|
||||
(transferEncoding.Equals("chunked")))
|
||||
contentStream = new HttpContentStreamChunked(connectionStream);
|
||||
else if (responseHeaders.TryGetInteger("Content-Length", out int contentLength))
|
||||
contentStream = new HttpContentStream(connectionStream, contentLength);
|
||||
}
|
||||
|
||||
if (FollowRedirects)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case HttpStatusCode.Found:
|
||||
case HttpStatusCode.MovedPermanently:
|
||||
case HttpStatusCode.TemporaryRedirect:
|
||||
case HttpStatusCode.PermanentRedirect:
|
||||
if (body != null)
|
||||
body.Position = 0;
|
||||
return Request(method, new Uri(responseHeaders.Get("Location")), headers, body);
|
||||
case HttpStatusCode.SeeOther:
|
||||
return Get(responseHeaders.Get("Location"), headers);
|
||||
}
|
||||
}
|
||||
return new HttpClientResponse(method, uri, statusCode, reason, responseHeaders, contentStream);
|
||||
}
|
||||
|
||||
public Optional<HttpClientResponse> Get(Uri uri) => Request(HttpMethod.GET, uri, null, null);
|
||||
public Optional<HttpClientResponse> Get(Uri uri, IEnumerable<Header> headers) => Request(HttpMethod.GET, uri, headers, null);
|
||||
public Optional<HttpClientResponse> Post(Uri uri, Stream body) => Request(HttpMethod.POST, uri, null, body);
|
||||
public Optional<HttpClientResponse> Post(Uri uri, IEnumerable<Header> headers, Stream body) => Request(HttpMethod.POST, uri, headers, body);
|
||||
public Optional<HttpClientResponse> Get(string uri) => Request(HttpMethod.GET, new Uri(uri), null, null);
|
||||
public Optional<HttpClientResponse> Get(string uri, IEnumerable<Header> headers) => Request(HttpMethod.GET, new Uri(uri), headers, null);
|
||||
public Optional<HttpClientResponse> Post(string uri, Stream body) => Request(HttpMethod.POST, new Uri(uri), null, body);
|
||||
public Optional<HttpClientResponse> Post(string uri, IEnumerable<Header> headers, Stream body) => Request(HttpMethod.POST, new Uri(uri), headers, body);
|
||||
|
||||
|
||||
public Optional<HttpClientResponse> Post(string uri, Newtonsoft.Json.JsonToken json) => Post(uri, null, json);
|
||||
public Optional<HttpClientResponse> Post(string uri, IEnumerable<Header> headers,
|
||||
Newtonsoft.Json.JsonToken json)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json.ToString())))
|
||||
return Request(HttpMethod.POST, new Uri(uri), headers, stream);
|
||||
}
|
||||
|
||||
public Optional<HttpClientResponse> Post(string uri, IEnumerable<Header> headers, string body)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(body)))
|
||||
return Request(HttpMethod.POST, new Uri(uri), headers, stream);
|
||||
}
|
||||
|
||||
|
||||
public Optional<HttpClientResponse> Post<T>(string uri, IEnumerable<Header> headers, T body) =>
|
||||
Post(new Uri(uri), headers, body);
|
||||
public Optional<HttpClientResponse> Post<T>(Uri uri, IEnumerable<Header> headers, T body)
|
||||
{
|
||||
HeaderContainer headerContainer = new HeaderContainer(headers);
|
||||
if (!headerContainer.Contains("Content-Type"))
|
||||
headerContainer.Add("Content-Type", "application/json");
|
||||
|
||||
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(body))))
|
||||
return Request(HttpMethod.POST, uri, headerContainer, stream);
|
||||
}
|
||||
|
||||
public static bool ReadResponseLine(Stream stream, out HttpVersion httpVersion, out HttpStatusCode statusCode,
|
||||
out string reason)
|
||||
{
|
||||
string requestLine = HttpConnection.ReadLine(stream);
|
||||
if (requestLine is null)
|
||||
{
|
||||
httpVersion = HttpVersion.None;
|
||||
statusCode = 0;
|
||||
reason = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
int idxSpace1 = requestLine.IndexOf(' ');
|
||||
int idxSpace2 = requestLine.IndexOf(' ', idxSpace1 + 1);
|
||||
|
||||
if ((idxSpace1 > 0) && (idxSpace2 > 0))
|
||||
{
|
||||
httpVersion = HttpVersionSupport.Parse(requestLine.Substring(0, idxSpace1));
|
||||
statusCode = (HttpStatusCode)int.Parse(requestLine.Substring(idxSpace1 + 1, (idxSpace2 - idxSpace1 - 1)));
|
||||
reason = requestLine.Substring(idxSpace2 + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
httpVersion = HttpVersion.None;
|
||||
statusCode = 0;
|
||||
reason = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
using System;
|
||||
using ln.type;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.Security;
|
||||
namespace ln.http.client
|
||||
{
|
||||
public class HttpClientRequest
|
||||
{
|
||||
public HttpClient HttpClient { get; }
|
||||
public URI URI
|
||||
{
|
||||
get => uri;
|
||||
set
|
||||
{
|
||||
if (!value.Scheme.Equals("http") && !value.Scheme.Equals("https"))
|
||||
throw new ArgumentOutOfRangeException(nameof(value), String.Format("unsupported url scheme: {0}", value.Scheme));
|
||||
uri = value;
|
||||
}
|
||||
}
|
||||
|
||||
public HttpHeaders Headers { get; } = new HttpHeaders();
|
||||
public String Method { get; set; }
|
||||
|
||||
URI uri;
|
||||
MemoryStream contentStream;
|
||||
HttpClientResponse clientResponse;
|
||||
|
||||
public HttpClientRequest(HttpClient httpClient, URI uri)
|
||||
{
|
||||
HttpClient = httpClient;
|
||||
URI = uri;
|
||||
|
||||
Headers.Add("user-agent", "ln.http.client");
|
||||
|
||||
}
|
||||
|
||||
public Stream GetContentStream()
|
||||
{
|
||||
if (clientResponse != null)
|
||||
throw new NotSupportedException("Request has already been executed, content stream has been disposed");
|
||||
|
||||
if (contentStream == null)
|
||||
contentStream = new MemoryStream();
|
||||
|
||||
return contentStream;
|
||||
}
|
||||
|
||||
public HttpClientResponse GetResponse()
|
||||
{
|
||||
if (clientResponse == null)
|
||||
{
|
||||
executeRequest();
|
||||
}
|
||||
return clientResponse;
|
||||
}
|
||||
|
||||
private Stream OpenConnection()
|
||||
{
|
||||
TcpClient tcpClient = null;
|
||||
SslStream sslStream = null;
|
||||
|
||||
try
|
||||
{
|
||||
tcpClient = new TcpClient();
|
||||
tcpClient.ExclusiveAddressUse = false;
|
||||
|
||||
tcpClient.Connect(URI.Host, int.Parse(URI.Port));
|
||||
if (!tcpClient.Connected)
|
||||
throw new IOException(String.Format("could not connect to host {0}",uri.Host));
|
||||
|
||||
if (uri.Scheme.Equals("https"))
|
||||
{
|
||||
sslStream = new SslStream(tcpClient.GetStream());
|
||||
return sslStream;
|
||||
}
|
||||
return tcpClient.GetStream();
|
||||
} catch (Exception)
|
||||
{
|
||||
if (sslStream != null)
|
||||
sslStream.Dispose();
|
||||
if (tcpClient != null)
|
||||
tcpClient.Dispose();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void executeRequest()
|
||||
{
|
||||
if (contentStream.Length > 0)
|
||||
{
|
||||
byte[] requestContent = contentStream.ToArray();
|
||||
Headers.Add("content-length", requestContent.Length.ToString());
|
||||
}
|
||||
|
||||
Stream connectionStream = OpenConnection();
|
||||
|
||||
|
||||
clientResponse = new HttpClientResponse(this);
|
||||
|
||||
|
||||
contentStream.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,13 +1,52 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using ln.http.content;
|
||||
using ln.type;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ln.http.client
|
||||
{
|
||||
public class HttpClientResponse
|
||||
{
|
||||
public HttpClientRequest ClientRequest { get; }
|
||||
public HttpMethod Method { get; }
|
||||
public Uri Uri { get; }
|
||||
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
public string Reason { get; }
|
||||
|
||||
public HttpClientResponse(HttpClientRequest clientRequest)
|
||||
private HeaderContainer _responseHeaders;
|
||||
private Stream _contentStream;
|
||||
|
||||
public HeaderContainer Headers => _responseHeaders;
|
||||
public Stream ContentStream => _contentStream;
|
||||
|
||||
public HttpClientResponse(HttpMethod method, Uri uri, HttpStatusCode statusCode, string reason, HeaderContainer responseHeaders, Stream contentStream)
|
||||
{
|
||||
ClientRequest = clientRequest;
|
||||
Method = method;
|
||||
Uri = uri;
|
||||
|
||||
StatusCode = statusCode;
|
||||
Reason = reason;
|
||||
|
||||
_responseHeaders = responseHeaders;
|
||||
_contentStream = contentStream;
|
||||
}
|
||||
|
||||
private string? _bodyText;
|
||||
public string Text
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_bodyText is null)
|
||||
_bodyText = Encoding.UTF8.GetString(ContentStream.ReadToEnd());
|
||||
return _bodyText;
|
||||
}
|
||||
}
|
||||
|
||||
public T Json<T>() => JsonConvert.DeserializeObject<T>(Text);
|
||||
|
||||
|
||||
public override string ToString() => String.Format("{0} {1}", (int)StatusCode, StatusCode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
// /**
|
||||
// * File: Connection.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using ln.type;
|
||||
using System.IO;
|
||||
using ln.logging;
|
||||
using ln.http.listener;
|
||||
using ln.http.exceptions;
|
||||
|
||||
namespace ln.http.connections
|
||||
{
|
||||
public abstract class Connection : IDisposable
|
||||
{
|
||||
public Listener Listener { get; private set; }
|
||||
|
||||
public abstract IPv6 RemoteHost { get; }
|
||||
public abstract int RemotePort { get; }
|
||||
|
||||
public abstract Stream GetStream();
|
||||
|
||||
public Connection(Listener listener)
|
||||
{
|
||||
Listener = listener;
|
||||
}
|
||||
|
||||
public virtual HttpRequest ReadRequest(HTTPServer httpServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new HttpRequest(httpServer, GetStream(), Listener.LocalEndpoint, new Endpoint(RemoteHost, RemotePort));
|
||||
} catch (IOException)
|
||||
{
|
||||
return null;
|
||||
} catch (ConnectionClosedException)
|
||||
{
|
||||
return null;
|
||||
} catch (Exception e)
|
||||
{
|
||||
Logging.Log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void Close();
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Close();
|
||||
Listener = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
// /**
|
||||
// * File: HttpConnection.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.IO;
|
||||
using ln.type;
|
||||
using System.Net.Sockets;
|
||||
using ln.http.listener;
|
||||
|
||||
namespace ln.http.connections
|
||||
{
|
||||
public class HttpConnection : Connection
|
||||
{
|
||||
public TcpClient TcpClient { get; }
|
||||
public Endpoint RemoteEndpoint { get; }
|
||||
|
||||
public HttpConnection(Listener listener, TcpClient tcpClient)
|
||||
:base(listener)
|
||||
{
|
||||
TcpClient = tcpClient;
|
||||
RemoteEndpoint = new Endpoint(TcpClient.Client.RemoteEndPoint);
|
||||
}
|
||||
|
||||
public override IPv6 RemoteHost => RemoteEndpoint.Address;
|
||||
public override int RemotePort => RemoteEndpoint.Port;
|
||||
|
||||
public override Stream GetStream() => TcpClient.GetStream();
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
TcpClient.Close();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// /**
|
||||
// * File: HttpsConnection.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.AccessControl;
|
||||
using ln.type;
|
||||
using System.Net.Security;
|
||||
using ln.http.listener;
|
||||
|
||||
namespace ln.http.connections
|
||||
{
|
||||
public class HttpsConnection : Connection
|
||||
{
|
||||
Connection Connection { get; }
|
||||
SslStream sslStream { get; }
|
||||
|
||||
public override IPv6 RemoteHost => Connection.RemoteHost;
|
||||
public override int RemotePort => Connection.RemotePort;
|
||||
|
||||
public HttpsConnection(Listener listener,Connection connection,LocalCertificateSelectionCallback localCertificateSelectionCallback)
|
||||
:base(listener)
|
||||
{
|
||||
Connection = connection;
|
||||
sslStream = new SslStream(connection.GetStream(),false, null, localCertificateSelectionCallback);
|
||||
}
|
||||
|
||||
public override HttpRequest ReadRequest(HTTPServer server)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Stream GetStream() => sslStream;
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
sslStream.Close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace ln.http.content;
|
||||
|
||||
public class HttpContentStream : Stream
|
||||
{
|
||||
public int MemoryLimit { get; set; } = 1024 * 1024 * 10;
|
||||
|
||||
private Stream _stream;
|
||||
private string _tempFileName;
|
||||
|
||||
public HttpContentStream(Stream connectionStream, int length)
|
||||
{
|
||||
byte[] transferBuffer;
|
||||
// if (length <= MemoryLimit)
|
||||
// {
|
||||
// transferBuffer = new byte[length];
|
||||
// if (connectionStream.ReadExactly(transferBuffer, 0, transferBuffer.Length) != length)
|
||||
// throw new IOException();
|
||||
//
|
||||
// _stream = new MemoryStream(transferBuffer);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
_tempFileName = Path.GetTempFileName();
|
||||
_stream = new FileStream(_tempFileName, FileMode.Open, FileAccess.ReadWrite);
|
||||
|
||||
transferBuffer = new byte[MemoryLimit];
|
||||
while (length > 0)
|
||||
{
|
||||
int size = length > transferBuffer.Length ? transferBuffer.Length : length;
|
||||
int nread = connectionStream.Read(transferBuffer, 0, size);
|
||||
_stream.Write(transferBuffer, 0, nread);
|
||||
length -= nread;
|
||||
}
|
||||
|
||||
_stream.Position = 0;
|
||||
// }
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count);
|
||||
public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
|
||||
public override void SetLength(long value) => throw new System.NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new System.NotImplementedException();
|
||||
public override void Flush() => throw new System.NotImplementedException();
|
||||
|
||||
public override bool CanRead { get; } = true;
|
||||
public override bool CanSeek { get; } = true;
|
||||
public override bool CanWrite { get; } = false;
|
||||
public override long Length => _stream.Length;
|
||||
public override long Position { get => _stream.Position; set => _stream.Position = value; }
|
||||
|
||||
public override int Read(Span<byte> buffer) => _stream.Read(buffer);
|
||||
public override void Close() => _stream.Close();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_stream.Dispose();
|
||||
if (_tempFileName is not null)
|
||||
File.Delete(_tempFileName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace ln.http.content;
|
||||
|
||||
public class HttpContentStreamChunked : Stream
|
||||
{
|
||||
private Stream _stream;
|
||||
private string _tempFileName;
|
||||
|
||||
public HttpContentStreamChunked(Stream connectionStream)
|
||||
{
|
||||
byte[] transferBuffer;
|
||||
_tempFileName = Path.GetTempFileName();
|
||||
_stream = new FileStream(_tempFileName, FileMode.Open, FileAccess.ReadWrite);
|
||||
|
||||
transferBuffer = new byte[1024 * 1024];
|
||||
|
||||
int nChunkSize;
|
||||
do
|
||||
{
|
||||
string chunkSize = HttpConnection.ReadLine(connectionStream);
|
||||
nChunkSize = int.Parse(chunkSize, System.Globalization.NumberStyles.HexNumber);
|
||||
int length = nChunkSize;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
int size = length > transferBuffer.Length ? transferBuffer.Length : length;
|
||||
int nread = connectionStream.Read(transferBuffer, 0, size);
|
||||
_stream.Write(transferBuffer, 0, nread);
|
||||
length -= nread;
|
||||
}
|
||||
|
||||
HttpConnection.ReadLine(connectionStream);
|
||||
} while (nChunkSize != 0);
|
||||
|
||||
_stream.Position = 0;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count);
|
||||
public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
|
||||
public override void SetLength(long value) => throw new System.NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new System.NotImplementedException();
|
||||
public override void Flush() => throw new System.NotImplementedException();
|
||||
|
||||
public override bool CanRead { get; } = true;
|
||||
public override bool CanSeek { get; } = true;
|
||||
public override bool CanWrite { get; } = false;
|
||||
public override long Length => _stream.Length;
|
||||
public override long Position { get => _stream.Position; set => _stream.Position = value; }
|
||||
|
||||
public override int Read(Span<byte> buffer) => _stream.Read(buffer);
|
||||
public override void Close() => _stream.Close();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_stream.Dispose();
|
||||
if (_tempFileName is not null)
|
||||
File.Delete(_tempFileName);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ namespace ln.http.exceptions
|
|||
public class BadRequestException: HttpException
|
||||
{
|
||||
public BadRequestException()
|
||||
:base(400,"Bad Request")
|
||||
:base(HttpStatusCode.BadRequest)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,25 +4,27 @@ namespace ln.http.exceptions
|
|||
{
|
||||
public class HttpException : Exception
|
||||
{
|
||||
public int StatusCode { get; } = 500;
|
||||
public HttpStatusCode HttpStatusCode { get; }
|
||||
|
||||
public HttpException(String message)
|
||||
public HttpException(HttpStatusCode httpStatusCode)
|
||||
: base(httpStatusCode.ToString())
|
||||
{
|
||||
HttpStatusCode = httpStatusCode;
|
||||
}
|
||||
public HttpException(HttpStatusCode httpStatusCode, Exception innerException)
|
||||
:base(httpStatusCode.ToString(), innerException)
|
||||
{
|
||||
HttpStatusCode = httpStatusCode;
|
||||
}
|
||||
public HttpException(HttpStatusCode httpStatusCode, String message)
|
||||
: base(message)
|
||||
{
|
||||
HttpStatusCode = httpStatusCode;
|
||||
}
|
||||
public HttpException(String message, Exception innerException)
|
||||
public HttpException(HttpStatusCode httpStatusCode,String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
public HttpException(int statusCode,String message)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
public HttpException(int statusCode,String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
HttpStatusCode = httpStatusCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace ln.http.exceptions
|
|||
public class MethodNotAllowedException : HttpException
|
||||
{
|
||||
public MethodNotAllowedException()
|
||||
:base(405,"Method not allowed")
|
||||
:base(HttpStatusCode.MethodNotAllowed)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
namespace ln.http.exceptions
|
||||
{
|
||||
public class NotFoundException : HttpException
|
||||
{
|
||||
public NotFoundException()
|
||||
: base(HttpStatusCode.NotFound)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
namespace ln.http.exceptions
|
||||
{
|
||||
public class PayloadTooLargeException: HttpException
|
||||
{
|
||||
public PayloadTooLargeException()
|
||||
:base(HttpStatusCode.PayloadTooLarge)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
using System;
|
||||
namespace ln.http.exceptions
|
||||
{
|
||||
public class ResourceNotFoundException : HttpException
|
||||
{
|
||||
public ResourceNotFoundException(String resourcePath, String nextResource)
|
||||
: base(404, String.Format("Could not find resource \"{0}\" within \"{1}\"", nextResource, resourcePath))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ namespace ln.http.exceptions
|
|||
public class UnsupportedMediaTypeException : HttpException
|
||||
{
|
||||
public UnsupportedMediaTypeException()
|
||||
: base(415, "Unsupported Media Type")
|
||||
: base(HttpStatusCode.UnsupportedMediaType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
// /**
|
||||
// * File: UnbufferedStreamreader.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
namespace ln.http.io
|
||||
{
|
||||
public class UnbufferedStreamReader : TextReader
|
||||
{
|
||||
public Stream Stream { get; }
|
||||
|
||||
public UnbufferedStreamReader(Stream stream)
|
||||
{
|
||||
Stream = stream;
|
||||
}
|
||||
|
||||
public override int Read() => Stream.ReadByte();
|
||||
|
||||
public override string ReadLine()
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
char ch;
|
||||
int _ch = 0;
|
||||
|
||||
while ((_ch = Stream.ReadByte()) != -1)
|
||||
{
|
||||
ch = (char)_ch;
|
||||
if (ch == '\r')
|
||||
{
|
||||
ch = (char)Stream.ReadByte();
|
||||
if (ch == '\n')
|
||||
return stringBuilder.ToString();
|
||||
|
||||
stringBuilder.Append('\r');
|
||||
}
|
||||
stringBuilder.Append(ch);
|
||||
}
|
||||
|
||||
if ((_ch == -1) && (stringBuilder.Length == 0))
|
||||
return null;
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public string ReadToken()
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
char ch = (char)Stream.ReadByte();
|
||||
|
||||
while (char.IsWhiteSpace(ch))
|
||||
ch = (char)Stream.ReadByte();
|
||||
|
||||
while (!char.IsWhiteSpace(ch))
|
||||
{
|
||||
stringBuilder.Append(ch);
|
||||
ch = (char)Stream.ReadByte();
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// /**
|
||||
// * File: HttpListener.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System.Net.Sockets;
|
||||
using ln.type;
|
||||
using ln.http.connections;
|
||||
|
||||
namespace ln.http.listener
|
||||
{
|
||||
public class HttpListener : Listener
|
||||
{
|
||||
|
||||
protected TcpListener tcpListener;
|
||||
|
||||
public HttpListener(int port) :this(IPv6.ANY,port){}
|
||||
public HttpListener(Endpoint endpoint) : this(endpoint.Address, endpoint.Port) {}
|
||||
public HttpListener(IPv6 listen, int port)
|
||||
: base(listen, port)
|
||||
{
|
||||
}
|
||||
|
||||
public override Connection Accept() => new HttpConnection(this,tcpListener.AcceptTcpClient());
|
||||
|
||||
public override bool IsOpen => (tcpListener != null);
|
||||
|
||||
public override void Open()
|
||||
{
|
||||
tcpListener = new TcpListener(Listen, Port);
|
||||
tcpListener.Start();
|
||||
}
|
||||
public override void Close()
|
||||
{
|
||||
if (tcpListener != null)
|
||||
{
|
||||
tcpListener.Stop();
|
||||
tcpListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (IsOpen)
|
||||
Close();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
// /**
|
||||
// * File: HttpsListener.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using ln.type;
|
||||
using ln.http.connections;
|
||||
using ln.http.cert;
|
||||
namespace ln.http.listener
|
||||
{
|
||||
public class HttpsListener : HttpListener
|
||||
{
|
||||
public HttpsListener(int port) : this(IPv6.ANY, port) { }
|
||||
public HttpsListener(IPv6 listen, int port) : base(listen, port) { }
|
||||
|
||||
public CertContainer CertContainer { get; set; } = new CertContainer();
|
||||
|
||||
public override Connection Accept()
|
||||
{
|
||||
return new HttpsConnection(this, base.Accept(),CertContainer.SelectCertificate);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// /**
|
||||
// * File: Listener.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using ln.http.connections;
|
||||
using ln.type;
|
||||
namespace ln.http.listener
|
||||
{
|
||||
public abstract class Listener : IDisposable
|
||||
{
|
||||
public IPv6 Listen { get; }
|
||||
public int Port { get; }
|
||||
|
||||
public Endpoint LocalEndpoint => new Endpoint(Listen, Port);
|
||||
|
||||
public abstract bool IsOpen { get; }
|
||||
|
||||
protected Listener(IPv6 listen, int port)
|
||||
{
|
||||
Listen = listen;
|
||||
Port = port;
|
||||
}
|
||||
|
||||
public virtual void AcceptMany(Action<Connection> handler)
|
||||
{
|
||||
while (IsOpen)
|
||||
handler(Accept());
|
||||
}
|
||||
|
||||
public abstract void Open();
|
||||
public abstract void Close();
|
||||
|
||||
public abstract Connection Accept();
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
}
|
|
@ -1,22 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Version>0.2.2</Version>
|
||||
<Version>0.4.3-ci</Version>
|
||||
<Authors>Harald Wolff-Thobaben</Authors>
|
||||
<Company>l--n.de</Company>
|
||||
<Description />
|
||||
<Copyright>(c) 2020 Harald Wolff-Thobaben</Copyright>
|
||||
<PackageTags>http server</PackageTags>
|
||||
<LangVersion>default</LangVersion>
|
||||
<PackageVersion>0.9.10-preview0</PackageVersion>
|
||||
<AssemblyVersion>0.6.2.0</AssemblyVersion>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ln.application" Version="0.1.1" />
|
||||
<PackageReference Include="ln.collections" Version="0.1.2" />
|
||||
<PackageReference Include="ln.logging" Version="1.0.0" />
|
||||
<PackageReference Include="ln.threading" Version="0.1.0" />
|
||||
<PackageReference Include="ln.type" Version="0.1.1" />
|
||||
<PackageReference Include="ln.collections" Version="0.2.2" />
|
||||
<PackageReference Include="ln.json" Version="1.3.0" />
|
||||
<PackageReference Include="ln.mime" Version="1.1.1" />
|
||||
<PackageReference Include="ln.patterns" Version="0.1.0-preview7" />
|
||||
<PackageReference Include="ln.threading" Version="0.2.2" />
|
||||
<PackageReference Include="ln.type" Version="0.1.10-preview0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Pandoc" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
// /**
|
||||
// * File: Header.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
namespace ln.http.message
|
||||
{
|
||||
public class Header
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
string rawvalue;
|
||||
public string RawValue {
|
||||
get => rawvalue;
|
||||
set => SetValue(value);
|
||||
}
|
||||
|
||||
string comments;
|
||||
public string Comments => comments;
|
||||
|
||||
string value;
|
||||
public string Value
|
||||
{
|
||||
get => value;
|
||||
set => SetValue(value);
|
||||
}
|
||||
|
||||
Dictionary<string, string> parameters;
|
||||
|
||||
public Header(string headerLine)
|
||||
{
|
||||
int colon = headerLine.IndexOf(':');
|
||||
if (colon == -1)
|
||||
throw new FormatException("expected to find :");
|
||||
|
||||
Name = headerLine.Substring(0, colon).ToUpper();
|
||||
SetValue(headerLine.Substring(colon + 1));
|
||||
}
|
||||
public Header(string name, string value)
|
||||
{
|
||||
Name = name.ToUpper();
|
||||
SetValue(value);
|
||||
}
|
||||
|
||||
public void SetValue(string newValue)
|
||||
{
|
||||
rawvalue = newValue;
|
||||
value = ParseValue(new StringReader(newValue.Trim()),out comments);
|
||||
|
||||
// at least MIME Content-* header follow the parameter syntax...
|
||||
if (Name.StartsWith("CONTENT-", StringComparison.InvariantCulture))
|
||||
{
|
||||
ParseParameters();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsParameter(string parameterName) => parameters.ContainsKey(parameterName.ToUpper());
|
||||
public string GetParameter(string parameterName) => parameters[parameterName.ToUpper()];
|
||||
public string GetParameter(string parameterName,string defaultValue) => parameters[parameterName.ToUpper()];
|
||||
|
||||
string ParseComment(TextReader reader)
|
||||
{
|
||||
StringBuilder commentBuilder = new StringBuilder();
|
||||
ParseComment(reader, commentBuilder);
|
||||
return commentBuilder.ToString();
|
||||
}
|
||||
void ParseComment(TextReader reader,StringBuilder commentBuilder)
|
||||
{
|
||||
int ch;
|
||||
while (((ch = reader.Read()) != -1) && (ch != ')'))
|
||||
commentBuilder.Append((char)ch);
|
||||
}
|
||||
public virtual string ParseValue(TextReader reader,out string parsedComments)
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
StringBuilder commentBuilder = new StringBuilder();
|
||||
|
||||
int ch;
|
||||
while (((ch = reader.Read())!=-1))
|
||||
{
|
||||
if (ch == '(')
|
||||
{
|
||||
commentBuilder.Append(ParseComment(reader));
|
||||
} else
|
||||
{
|
||||
stringBuilder.Append((char)ch);
|
||||
}
|
||||
}
|
||||
parsedComments = commentBuilder.ToString().Trim();
|
||||
return stringBuilder.ToString().Trim();
|
||||
}
|
||||
|
||||
public void ParseParameters()
|
||||
{
|
||||
if (parameters != null)
|
||||
return;
|
||||
|
||||
parameters = new Dictionary<string, string>();
|
||||
|
||||
int semicolon = value.IndexOf(';');
|
||||
if (semicolon > 0)
|
||||
{
|
||||
TokenReader tokenReader = new TokenReader(new StringReader(value.Substring(semicolon)));
|
||||
while (tokenReader.Peek() != -1)
|
||||
{
|
||||
if (tokenReader.Read() != ';')
|
||||
throw new FormatException();
|
||||
|
||||
string pName = tokenReader.ReadToken().ToUpper();
|
||||
if (tokenReader.Read() != '=')
|
||||
throw new FormatException("expected =");
|
||||
|
||||
string pValue = (tokenReader.Peek() == '"') ? tokenReader.ReadQuotedString() : tokenReader.ReadToken();
|
||||
parameters.Add(pName, pValue);
|
||||
}
|
||||
|
||||
value = value.Substring(0, semicolon).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
//void parseValue(string v)
|
||||
//{
|
||||
// rawValue = v;
|
||||
// TokenReader tokenReader = new TokenReader(parseComments(new StringReader(v)));
|
||||
// StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
// int ch;
|
||||
// while (((ch = tokenReader.Read()) != -1) && (ch != ';'))
|
||||
// stringBuilder.Append((char)ch);
|
||||
|
||||
// Value = stringBuilder.ToString();
|
||||
|
||||
// while (tokenReader.Peek() != -1)
|
||||
// {
|
||||
// string pName = tokenReader.ReadToken();
|
||||
// if (tokenReader.Read() != '=')
|
||||
// throw new FormatException("expected =");
|
||||
|
||||
// string pValue = (tokenReader.Peek() == '"') ? tokenReader.ReadQuotedString() : tokenReader.ReadToken();
|
||||
// parameters.Add(pName, pValue);
|
||||
// }
|
||||
|
||||
|
||||
//}
|
||||
|
||||
public override int GetHashCode() => Name.GetHashCode();
|
||||
public override bool Equals(object obj) => (obj is Header you) && Name.Equals(you.Name);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
// /**
|
||||
// * File: HeaderContainer.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using ln.http.io;
|
||||
namespace ln.http.message
|
||||
{
|
||||
public class HeaderContainer
|
||||
{
|
||||
Dictionary<string, Header> headers = new Dictionary<string, Header>();
|
||||
|
||||
public HeaderContainer()
|
||||
{
|
||||
}
|
||||
public HeaderContainer(Stream stream):this(new UnbufferedStreamReader(stream))
|
||||
{
|
||||
}
|
||||
public HeaderContainer(TextReader reader)
|
||||
{
|
||||
List<String> headerLines = new List<string>();
|
||||
string currentline = reader.ReadLine();
|
||||
while (!currentline.Equals(string.Empty))
|
||||
{
|
||||
if (char.IsWhiteSpace(currentline[0]))
|
||||
{
|
||||
headerLines[headerLines.Count - 1] = headerLines[headerLines.Count - 1] + currentline;
|
||||
}
|
||||
else
|
||||
{
|
||||
headerLines.Add(currentline);
|
||||
}
|
||||
currentline = reader.ReadLine();
|
||||
}
|
||||
|
||||
foreach (string headerLine in headerLines)
|
||||
{
|
||||
Header header = new Header(headerLine);
|
||||
headers.Add(header.Name, header);
|
||||
}
|
||||
}
|
||||
|
||||
public Header this[string name]
|
||||
{
|
||||
get => headers[name.ToUpper()];
|
||||
}
|
||||
|
||||
public void Add(Header header)=> headers.Add(header.Name, header);
|
||||
|
||||
public bool ContainsKey(string name) => headers.ContainsKey(name.ToUpper());
|
||||
public bool Contains(string name) => headers.ContainsKey(name.ToUpper());
|
||||
public string Get(string name) => this[name].Value;
|
||||
public void Set(string name,string value)
|
||||
{
|
||||
name = name.ToUpper();
|
||||
if (!headers.TryGetValue(name,out Header header))
|
||||
{
|
||||
header = new Header(name);
|
||||
headers.Add(name, header);
|
||||
}
|
||||
header.Value = value;
|
||||
}
|
||||
public void Remove(string name) => headers.Remove(name.ToUpper());
|
||||
|
||||
public IEnumerable<string> Keys => headers.Keys;
|
||||
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
// /**
|
||||
// * File: Message.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using ln.http.io;
|
||||
using ln.type;
|
||||
using ln.http.message.parser;
|
||||
namespace ln.http.message
|
||||
{
|
||||
public class Message
|
||||
{
|
||||
public HeaderContainer Headers { get; private set; }
|
||||
|
||||
byte[] bodyData;
|
||||
int bodyOffset;
|
||||
int bodyLength;
|
||||
|
||||
List<Message> parts;
|
||||
|
||||
bool isMultipart;
|
||||
public bool IsMultipart => isMultipart;
|
||||
|
||||
public Message()
|
||||
{
|
||||
Setup(new HeaderContainer(), new byte[0], 0, 0);
|
||||
}
|
||||
|
||||
public Message(HeaderContainer headers, byte[] body)
|
||||
: this(headers, body, 0, body.Length) { }
|
||||
|
||||
public Message(HeaderContainer headers, byte[] body, int offset, int length)
|
||||
{
|
||||
Setup(headers, body, offset, length);
|
||||
}
|
||||
|
||||
public Message(byte[] body, int offset, int length)
|
||||
{
|
||||
MemoryStream memoryStream = new MemoryStream(body, offset, length);
|
||||
HeaderContainer headers = MIME.ReadHeader(new UnbufferedStreamReader(memoryStream));
|
||||
|
||||
if (memoryStream.Position >= length)
|
||||
throw new FormatException("MIME header section too long");
|
||||
|
||||
Setup(headers, body, offset + (int)memoryStream.Position, length - (int)memoryStream.Position);
|
||||
}
|
||||
|
||||
public Message(Stream stream)
|
||||
{
|
||||
HeaderContainer headers = MIME.ReadHeader(new UnbufferedStreamReader(stream));
|
||||
|
||||
byte[] data = stream.ReadToEnd();
|
||||
|
||||
Setup(headers, data, 0, data.Length);
|
||||
}
|
||||
|
||||
private void Setup(HeaderContainer headers, byte[] body, int offset, int length)
|
||||
{
|
||||
Headers = headers;
|
||||
|
||||
bodyData = body;
|
||||
bodyOffset = offset;
|
||||
bodyLength = length;
|
||||
|
||||
string ct = Headers["Content-Type"].Value;
|
||||
isMultipart = ct.StartsWith("multipart/", StringComparison.InvariantCulture) || ct.StartsWith("message/", StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
public void ReadParts()
|
||||
{
|
||||
parts = new List<Message>();
|
||||
|
||||
if (isMultipart)
|
||||
{
|
||||
string boundary = Headers["Content-Type"].GetParameter("boundary");
|
||||
string delimiter = "--" + boundary;
|
||||
int[] indeces = FindIndeces(bodyData, bodyOffset, bodyLength, Encoding.ASCII.GetBytes(delimiter));
|
||||
|
||||
for (int n = 1; n < indeces.Length; n++)
|
||||
{
|
||||
Message part = new Message(bodyData, indeces[n - 1], indeces[n] - indeces[n - 1]);
|
||||
parts.Add(part);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
int[] FindIndeces(byte[] data,int offset,int length,byte[] pattern)
|
||||
{
|
||||
List<int> offsets = new List<int>();
|
||||
List<int> validated = new List<int>();
|
||||
|
||||
for (int n = offset; n < (length - pattern.Length); n++)
|
||||
{
|
||||
int p = 0;
|
||||
while ((p < pattern.Length) && (data[n + p] == pattern[p]))
|
||||
p++;
|
||||
|
||||
if (p == pattern.Length)
|
||||
{
|
||||
if ((n == offset) || ((n >= (offset + 2)) && (data[offset + n - 2] == '\r') && (data[offset + n - 1] == '\n')))
|
||||
{
|
||||
n += pattern.Length;
|
||||
|
||||
while ((n < (offset + length)) && (data[n - 2] != '\r') && (data[n - 1] != '\n'))
|
||||
n++;
|
||||
|
||||
validated.Add(n);
|
||||
|
||||
|
||||
if (((offset + length) > (n + 1)) && (data[n] == '-') && (data[n + 1] == '-'))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validated.ToArray();
|
||||
}
|
||||
|
||||
|
||||
public Stream OpenBodyStream() => new MemoryStream(bodyData, bodyOffset, bodyLength);
|
||||
|
||||
public IEnumerable<Message> Parts
|
||||
{
|
||||
get
|
||||
{
|
||||
if (parts == null)
|
||||
ReadParts();
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasHeader(string name) => Headers.Contains(name);
|
||||
public Header GetHeader(string name) => Headers[name];
|
||||
public void SetHeader(string name, string value) => Headers.Set(name, value);
|
||||
public void RemoveHeader(String name) => Headers.Remove(name);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,767 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ln.http.message
|
||||
{
|
||||
public static class MimeTypeMap
|
||||
{
|
||||
private static readonly Lazy<IDictionary<string, string>> _mappings = new Lazy<IDictionary<string, string>>(BuildMappings);
|
||||
|
||||
private static IDictionary<string, string> BuildMappings()
|
||||
{
|
||||
var mappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {
|
||||
|
||||
#region Big freaking list of mime types
|
||||
|
||||
// maps both ways,
|
||||
// extension -> mime type
|
||||
// and
|
||||
// mime type -> extension
|
||||
//
|
||||
// any mime types on left side not pre-loaded on right side, are added automatically
|
||||
// some mime types can map to multiple extensions, so to get a deterministic mapping,
|
||||
// add those to the dictionary specifcially
|
||||
//
|
||||
// combination of values from Windows 7 Registry and
|
||||
// from C:\Windows\System32\inetsrv\config\applicationHost.config
|
||||
// some added, including .7z and .dat
|
||||
//
|
||||
// Some added based on http://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
// which lists mime types, but not extensions
|
||||
//
|
||||
{".323", "text/h323"},
|
||||
{".3g2", "video/3gpp2"},
|
||||
{".3gp", "video/3gpp"},
|
||||
{".3gp2", "video/3gpp2"},
|
||||
{".3gpp", "video/3gpp"},
|
||||
{".7z", "application/x-7z-compressed"},
|
||||
{".aa", "audio/audible"},
|
||||
{".AAC", "audio/aac"},
|
||||
{".aaf", "application/octet-stream"},
|
||||
{".aax", "audio/vnd.audible.aax"},
|
||||
{".ac3", "audio/ac3"},
|
||||
{".aca", "application/octet-stream"},
|
||||
{".accda", "application/msaccess.addin"},
|
||||
{".accdb", "application/msaccess"},
|
||||
{".accdc", "application/msaccess.cab"},
|
||||
{".accde", "application/msaccess"},
|
||||
{".accdr", "application/msaccess.runtime"},
|
||||
{".accdt", "application/msaccess"},
|
||||
{".accdw", "application/msaccess.webapplication"},
|
||||
{".accft", "application/msaccess.ftemplate"},
|
||||
{".acx", "application/internet-property-stream"},
|
||||
{".AddIn", "text/xml"},
|
||||
{".ade", "application/msaccess"},
|
||||
{".adobebridge", "application/x-bridge-url"},
|
||||
{".adp", "application/msaccess"},
|
||||
{".ADT", "audio/vnd.dlna.adts"},
|
||||
{".ADTS", "audio/aac"},
|
||||
{".afm", "application/octet-stream"},
|
||||
{".ai", "application/postscript"},
|
||||
{".aif", "audio/aiff"},
|
||||
{".aifc", "audio/aiff"},
|
||||
{".aiff", "audio/aiff"},
|
||||
{".air", "application/vnd.adobe.air-application-installer-package+zip"},
|
||||
{".amc", "application/mpeg"},
|
||||
{".anx", "application/annodex"},
|
||||
{".apk", "application/vnd.android.package-archive" },
|
||||
{".application", "application/x-ms-application"},
|
||||
{".art", "image/x-jg"},
|
||||
{".asa", "application/xml"},
|
||||
{".asax", "application/xml"},
|
||||
{".ascx", "application/xml"},
|
||||
{".asd", "application/octet-stream"},
|
||||
{".asf", "video/x-ms-asf"},
|
||||
{".ashx", "application/xml"},
|
||||
{".asi", "application/octet-stream"},
|
||||
{".asm", "text/plain"},
|
||||
{".asmx", "application/xml"},
|
||||
{".aspx", "application/xml"},
|
||||
{".asr", "video/x-ms-asf"},
|
||||
{".asx", "video/x-ms-asf"},
|
||||
{".atom", "application/atom+xml"},
|
||||
{".au", "audio/basic"},
|
||||
{".avi", "video/x-msvideo"},
|
||||
{".axa", "audio/annodex"},
|
||||
{".axs", "application/olescript"},
|
||||
{".axv", "video/annodex"},
|
||||
{".bas", "text/plain"},
|
||||
{".bcpio", "application/x-bcpio"},
|
||||
{".bin", "application/octet-stream"},
|
||||
{".bmp", "image/bmp"},
|
||||
{".c", "text/plain"},
|
||||
{".cab", "application/octet-stream"},
|
||||
{".caf", "audio/x-caf"},
|
||||
{".calx", "application/vnd.ms-office.calx"},
|
||||
{".cat", "application/vnd.ms-pki.seccat"},
|
||||
{".cc", "text/plain"},
|
||||
{".cd", "text/plain"},
|
||||
{".cdda", "audio/aiff"},
|
||||
{".cdf", "application/x-cdf"},
|
||||
{".cer", "application/x-x509-ca-cert"},
|
||||
{".cfg", "text/plain"},
|
||||
{".chm", "application/octet-stream"},
|
||||
{".class", "application/x-java-applet"},
|
||||
{".clp", "application/x-msclip"},
|
||||
{".cmd", "text/plain"},
|
||||
{".cmx", "image/x-cmx"},
|
||||
{".cnf", "text/plain"},
|
||||
{".cod", "image/cis-cod"},
|
||||
{".config", "application/xml"},
|
||||
{".contact", "text/x-ms-contact"},
|
||||
{".coverage", "application/xml"},
|
||||
{".cpio", "application/x-cpio"},
|
||||
{".cpp", "text/plain"},
|
||||
{".crd", "application/x-mscardfile"},
|
||||
{".crl", "application/pkix-crl"},
|
||||
{".crt", "application/x-x509-ca-cert"},
|
||||
{".cs", "text/plain"},
|
||||
{".csdproj", "text/plain"},
|
||||
{".csh", "application/x-csh"},
|
||||
{".csproj", "text/plain"},
|
||||
{".css", "text/css"},
|
||||
{".csv", "text/csv"},
|
||||
{".cur", "application/octet-stream"},
|
||||
{".cxx", "text/plain"},
|
||||
{".dat", "application/octet-stream"},
|
||||
{".datasource", "application/xml"},
|
||||
{".dbproj", "text/plain"},
|
||||
{".dcr", "application/x-director"},
|
||||
{".def", "text/plain"},
|
||||
{".deploy", "application/octet-stream"},
|
||||
{".der", "application/x-x509-ca-cert"},
|
||||
{".dgml", "application/xml"},
|
||||
{".dib", "image/bmp"},
|
||||
{".dif", "video/x-dv"},
|
||||
{".dir", "application/x-director"},
|
||||
{".disco", "text/xml"},
|
||||
{".divx", "video/divx"},
|
||||
{".dll", "application/x-msdownload"},
|
||||
{".dll.config", "text/xml"},
|
||||
{".dlm", "text/dlm"},
|
||||
{".doc", "application/msword"},
|
||||
{".docm", "application/vnd.ms-word.document.macroEnabled.12"},
|
||||
{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
|
||||
{".dot", "application/msword"},
|
||||
{".dotm", "application/vnd.ms-word.template.macroEnabled.12"},
|
||||
{".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"},
|
||||
{".dsp", "application/octet-stream"},
|
||||
{".dsw", "text/plain"},
|
||||
{".dtd", "text/xml"},
|
||||
{".dtsConfig", "text/xml"},
|
||||
{".dv", "video/x-dv"},
|
||||
{".dvi", "application/x-dvi"},
|
||||
{".dwf", "drawing/x-dwf"},
|
||||
{".dwg", "application/acad"},
|
||||
{".dwp", "application/octet-stream"},
|
||||
{".dxf", "application/x-dxf" },
|
||||
{".dxr", "application/x-director"},
|
||||
{".eml", "message/rfc822"},
|
||||
{".emz", "application/octet-stream"},
|
||||
{".eot", "application/vnd.ms-fontobject"},
|
||||
{".eps", "application/postscript"},
|
||||
{".es", "application/ecmascript"},
|
||||
{".etl", "application/etl"},
|
||||
{".etx", "text/x-setext"},
|
||||
{".evy", "application/envoy"},
|
||||
{".exe", "application/vnd.microsoft.portable-executable"},
|
||||
{".exe.config", "text/xml"},
|
||||
{".f4v", "video/mp4"},
|
||||
{".fdf", "application/vnd.fdf"},
|
||||
{".fif", "application/fractals"},
|
||||
{".filters", "application/xml"},
|
||||
{".fla", "application/octet-stream"},
|
||||
{".flac", "audio/flac"},
|
||||
{".flr", "x-world/x-vrml"},
|
||||
{".flv", "video/x-flv"},
|
||||
{".fsscript", "application/fsharp-script"},
|
||||
{".fsx", "application/fsharp-script"},
|
||||
{".generictest", "application/xml"},
|
||||
{".gif", "image/gif"},
|
||||
{".gpx", "application/gpx+xml"},
|
||||
{".group", "text/x-ms-group"},
|
||||
{".gsm", "audio/x-gsm"},
|
||||
{".gtar", "application/x-gtar"},
|
||||
{".gz", "application/x-gzip"},
|
||||
{".h", "text/plain"},
|
||||
{".hdf", "application/x-hdf"},
|
||||
{".hdml", "text/x-hdml"},
|
||||
{".hhc", "application/x-oleobject"},
|
||||
{".hhk", "application/octet-stream"},
|
||||
{".hhp", "application/octet-stream"},
|
||||
{".hlp", "application/winhlp"},
|
||||
{".hpp", "text/plain"},
|
||||
{".hqx", "application/mac-binhex40"},
|
||||
{".hta", "application/hta"},
|
||||
{".htc", "text/x-component"},
|
||||
{".htm", "text/html"},
|
||||
{".html", "text/html"},
|
||||
{".htt", "text/webviewhtml"},
|
||||
{".hxa", "application/xml"},
|
||||
{".hxc", "application/xml"},
|
||||
{".hxd", "application/octet-stream"},
|
||||
{".hxe", "application/xml"},
|
||||
{".hxf", "application/xml"},
|
||||
{".hxh", "application/octet-stream"},
|
||||
{".hxi", "application/octet-stream"},
|
||||
{".hxk", "application/xml"},
|
||||
{".hxq", "application/octet-stream"},
|
||||
{".hxr", "application/octet-stream"},
|
||||
{".hxs", "application/octet-stream"},
|
||||
{".hxt", "text/html"},
|
||||
{".hxv", "application/xml"},
|
||||
{".hxw", "application/octet-stream"},
|
||||
{".hxx", "text/plain"},
|
||||
{".i", "text/plain"},
|
||||
{".ico", "image/x-icon"},
|
||||
{".ics", "application/octet-stream"},
|
||||
{".idl", "text/plain"},
|
||||
{".ief", "image/ief"},
|
||||
{".iii", "application/x-iphone"},
|
||||
{".inc", "text/plain"},
|
||||
{".inf", "application/octet-stream"},
|
||||
{".ini", "text/plain"},
|
||||
{".inl", "text/plain"},
|
||||
{".ins", "application/x-internet-signup"},
|
||||
{".ipa", "application/x-itunes-ipa"},
|
||||
{".ipg", "application/x-itunes-ipg"},
|
||||
{".ipproj", "text/plain"},
|
||||
{".ipsw", "application/x-itunes-ipsw"},
|
||||
{".iqy", "text/x-ms-iqy"},
|
||||
{".isp", "application/x-internet-signup"},
|
||||
{".isma", "application/octet-stream"},
|
||||
{".ismv", "application/octet-stream"},
|
||||
{".ite", "application/x-itunes-ite"},
|
||||
{".itlp", "application/x-itunes-itlp"},
|
||||
{".itms", "application/x-itunes-itms"},
|
||||
{".itpc", "application/x-itunes-itpc"},
|
||||
{".IVF", "video/x-ivf"},
|
||||
{".jar", "application/java-archive"},
|
||||
{".java", "application/octet-stream"},
|
||||
{".jck", "application/liquidmotion"},
|
||||
{".jcz", "application/liquidmotion"},
|
||||
{".jfif", "image/pjpeg"},
|
||||
{".jnlp", "application/x-java-jnlp-file"},
|
||||
{".jpb", "application/octet-stream"},
|
||||
{".jpe", "image/jpeg"},
|
||||
{".jpeg", "image/jpeg"},
|
||||
{".jpg", "image/jpeg"},
|
||||
{".js", "application/javascript"},
|
||||
{".json", "application/json"},
|
||||
{".jsx", "text/jscript"},
|
||||
{".jsxbin", "text/plain"},
|
||||
{".latex", "application/x-latex"},
|
||||
{".library-ms", "application/windows-library+xml"},
|
||||
{".lit", "application/x-ms-reader"},
|
||||
{".loadtest", "application/xml"},
|
||||
{".lpk", "application/octet-stream"},
|
||||
{".lsf", "video/x-la-asf"},
|
||||
{".lst", "text/plain"},
|
||||
{".lsx", "video/x-la-asf"},
|
||||
{".lzh", "application/octet-stream"},
|
||||
{".m13", "application/x-msmediaview"},
|
||||
{".m14", "application/x-msmediaview"},
|
||||
{".m1v", "video/mpeg"},
|
||||
{".m2t", "video/vnd.dlna.mpeg-tts"},
|
||||
{".m2ts", "video/vnd.dlna.mpeg-tts"},
|
||||
{".m2v", "video/mpeg"},
|
||||
{".m3u", "audio/x-mpegurl"},
|
||||
{".m3u8", "audio/x-mpegurl"},
|
||||
{".m4a", "audio/m4a"},
|
||||
{".m4b", "audio/m4b"},
|
||||
{".m4p", "audio/m4p"},
|
||||
{".m4r", "audio/x-m4r"},
|
||||
{".m4v", "video/x-m4v"},
|
||||
{".mac", "image/x-macpaint"},
|
||||
{".mak", "text/plain"},
|
||||
{".man", "application/x-troff-man"},
|
||||
{".manifest", "application/x-ms-manifest"},
|
||||
{".map", "text/plain"},
|
||||
{".master", "application/xml"},
|
||||
{".mbox", "application/mbox"},
|
||||
{".mda", "application/msaccess"},
|
||||
{".mdb", "application/x-msaccess"},
|
||||
{".mde", "application/msaccess"},
|
||||
{".mdp", "application/octet-stream"},
|
||||
{".me", "application/x-troff-me"},
|
||||
{".mfp", "application/x-shockwave-flash"},
|
||||
{".mht", "message/rfc822"},
|
||||
{".mhtml", "message/rfc822"},
|
||||
{".mid", "audio/mid"},
|
||||
{".midi", "audio/mid"},
|
||||
{".mix", "application/octet-stream"},
|
||||
{".mk", "text/plain"},
|
||||
{".mk3d", "video/x-matroska-3d"},
|
||||
{".mka", "audio/x-matroska"},
|
||||
{".mkv", "video/x-matroska"},
|
||||
{".mmf", "application/x-smaf"},
|
||||
{".mno", "text/xml"},
|
||||
{".mny", "application/x-msmoney"},
|
||||
{".mod", "video/mpeg"},
|
||||
{".mov", "video/quicktime"},
|
||||
{".movie", "video/x-sgi-movie"},
|
||||
{".mp2", "video/mpeg"},
|
||||
{".mp2v", "video/mpeg"},
|
||||
{".mp3", "audio/mpeg"},
|
||||
{".mp4", "video/mp4"},
|
||||
{".mp4v", "video/mp4"},
|
||||
{".mpa", "video/mpeg"},
|
||||
{".mpe", "video/mpeg"},
|
||||
{".mpeg", "video/mpeg"},
|
||||
{".mpf", "application/vnd.ms-mediapackage"},
|
||||
{".mpg", "video/mpeg"},
|
||||
{".mpp", "application/vnd.ms-project"},
|
||||
{".mpv2", "video/mpeg"},
|
||||
{".mqv", "video/quicktime"},
|
||||
{".ms", "application/x-troff-ms"},
|
||||
{".msg", "application/vnd.ms-outlook"},
|
||||
{".msi", "application/octet-stream"},
|
||||
{".mso", "application/octet-stream"},
|
||||
{".mts", "video/vnd.dlna.mpeg-tts"},
|
||||
{".mtx", "application/xml"},
|
||||
{".mvb", "application/x-msmediaview"},
|
||||
{".mvc", "application/x-miva-compiled"},
|
||||
{".mxf", "application/mxf"},
|
||||
{".mxp", "application/x-mmxp"},
|
||||
{".nc", "application/x-netcdf"},
|
||||
{".nsc", "video/x-ms-asf"},
|
||||
{".nws", "message/rfc822"},
|
||||
{".ocx", "application/octet-stream"},
|
||||
{".oda", "application/oda"},
|
||||
{".odb", "application/vnd.oasis.opendocument.database"},
|
||||
{".odc", "application/vnd.oasis.opendocument.chart"},
|
||||
{".odf", "application/vnd.oasis.opendocument.formula"},
|
||||
{".odg", "application/vnd.oasis.opendocument.graphics"},
|
||||
{".odh", "text/plain"},
|
||||
{".odi", "application/vnd.oasis.opendocument.image"},
|
||||
{".odl", "text/plain"},
|
||||
{".odm", "application/vnd.oasis.opendocument.text-master"},
|
||||
{".odp", "application/vnd.oasis.opendocument.presentation"},
|
||||
{".ods", "application/vnd.oasis.opendocument.spreadsheet"},
|
||||
{".odt", "application/vnd.oasis.opendocument.text"},
|
||||
{".oga", "audio/ogg"},
|
||||
{".ogg", "audio/ogg"},
|
||||
{".ogv", "video/ogg"},
|
||||
{".ogx", "application/ogg"},
|
||||
{".one", "application/onenote"},
|
||||
{".onea", "application/onenote"},
|
||||
{".onepkg", "application/onenote"},
|
||||
{".onetmp", "application/onenote"},
|
||||
{".onetoc", "application/onenote"},
|
||||
{".onetoc2", "application/onenote"},
|
||||
{".opus", "audio/ogg"},
|
||||
{".orderedtest", "application/xml"},
|
||||
{".osdx", "application/opensearchdescription+xml"},
|
||||
{".otf", "application/font-sfnt"},
|
||||
{".otg", "application/vnd.oasis.opendocument.graphics-template"},
|
||||
{".oth", "application/vnd.oasis.opendocument.text-web"},
|
||||
{".otp", "application/vnd.oasis.opendocument.presentation-template"},
|
||||
{".ots", "application/vnd.oasis.opendocument.spreadsheet-template"},
|
||||
{".ott", "application/vnd.oasis.opendocument.text-template"},
|
||||
{".oxt", "application/vnd.openofficeorg.extension"},
|
||||
{".p10", "application/pkcs10"},
|
||||
{".p12", "application/x-pkcs12"},
|
||||
{".p7b", "application/x-pkcs7-certificates"},
|
||||
{".p7c", "application/pkcs7-mime"},
|
||||
{".p7m", "application/pkcs7-mime"},
|
||||
{".p7r", "application/x-pkcs7-certreqresp"},
|
||||
{".p7s", "application/pkcs7-signature"},
|
||||
{".pbm", "image/x-portable-bitmap"},
|
||||
{".pcast", "application/x-podcast"},
|
||||
{".pct", "image/pict"},
|
||||
{".pcx", "application/octet-stream"},
|
||||
{".pcz", "application/octet-stream"},
|
||||
{".pdf", "application/pdf"},
|
||||
{".pfb", "application/octet-stream"},
|
||||
{".pfm", "application/octet-stream"},
|
||||
{".pfx", "application/x-pkcs12"},
|
||||
{".pgm", "image/x-portable-graymap"},
|
||||
{".pic", "image/pict"},
|
||||
{".pict", "image/pict"},
|
||||
{".pkgdef", "text/plain"},
|
||||
{".pkgundef", "text/plain"},
|
||||
{".pko", "application/vnd.ms-pki.pko"},
|
||||
{".pls", "audio/scpls"},
|
||||
{".pma", "application/x-perfmon"},
|
||||
{".pmc", "application/x-perfmon"},
|
||||
{".pml", "application/x-perfmon"},
|
||||
{".pmr", "application/x-perfmon"},
|
||||
{".pmw", "application/x-perfmon"},
|
||||
{".png", "image/png"},
|
||||
{".pnm", "image/x-portable-anymap"},
|
||||
{".pnt", "image/x-macpaint"},
|
||||
{".pntg", "image/x-macpaint"},
|
||||
{".pnz", "image/png"},
|
||||
{".pot", "application/vnd.ms-powerpoint"},
|
||||
{".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"},
|
||||
{".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"},
|
||||
{".ppa", "application/vnd.ms-powerpoint"},
|
||||
{".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"},
|
||||
{".ppm", "image/x-portable-pixmap"},
|
||||
{".pps", "application/vnd.ms-powerpoint"},
|
||||
{".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"},
|
||||
{".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"},
|
||||
{".ppt", "application/vnd.ms-powerpoint"},
|
||||
{".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"},
|
||||
{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
|
||||
{".prf", "application/pics-rules"},
|
||||
{".prm", "application/octet-stream"},
|
||||
{".prx", "application/octet-stream"},
|
||||
{".ps", "application/postscript"},
|
||||
{".psc1", "application/PowerShell"},
|
||||
{".psd", "application/octet-stream"},
|
||||
{".psess", "application/xml"},
|
||||
{".psm", "application/octet-stream"},
|
||||
{".psp", "application/octet-stream"},
|
||||
{".pst", "application/vnd.ms-outlook"},
|
||||
{".pub", "application/x-mspublisher"},
|
||||
{".pwz", "application/vnd.ms-powerpoint"},
|
||||
{".qht", "text/x-html-insertion"},
|
||||
{".qhtm", "text/x-html-insertion"},
|
||||
{".qt", "video/quicktime"},
|
||||
{".qti", "image/x-quicktime"},
|
||||
{".qtif", "image/x-quicktime"},
|
||||
{".qtl", "application/x-quicktimeplayer"},
|
||||
{".qxd", "application/octet-stream"},
|
||||
{".ra", "audio/x-pn-realaudio"},
|
||||
{".ram", "audio/x-pn-realaudio"},
|
||||
{".rar", "application/x-rar-compressed"},
|
||||
{".ras", "image/x-cmu-raster"},
|
||||
{".rat", "application/rat-file"},
|
||||
{".rc", "text/plain"},
|
||||
{".rc2", "text/plain"},
|
||||
{".rct", "text/plain"},
|
||||
{".rdlc", "application/xml"},
|
||||
{".reg", "text/plain"},
|
||||
{".resx", "application/xml"},
|
||||
{".rf", "image/vnd.rn-realflash"},
|
||||
{".rgb", "image/x-rgb"},
|
||||
{".rgs", "text/plain"},
|
||||
{".rm", "application/vnd.rn-realmedia"},
|
||||
{".rmi", "audio/mid"},
|
||||
{".rmp", "application/vnd.rn-rn_music_package"},
|
||||
{".roff", "application/x-troff"},
|
||||
{".rpm", "audio/x-pn-realaudio-plugin"},
|
||||
{".rqy", "text/x-ms-rqy"},
|
||||
{".rtf", "application/rtf"},
|
||||
{".rtx", "text/richtext"},
|
||||
{".rvt", "application/octet-stream" },
|
||||
{".ruleset", "application/xml"},
|
||||
{".s", "text/plain"},
|
||||
{".safariextz", "application/x-safari-safariextz"},
|
||||
{".scd", "application/x-msschedule"},
|
||||
{".scr", "text/plain"},
|
||||
{".sct", "text/scriptlet"},
|
||||
{".sd2", "audio/x-sd2"},
|
||||
{".sdp", "application/sdp"},
|
||||
{".sea", "application/octet-stream"},
|
||||
{".searchConnector-ms", "application/windows-search-connector+xml"},
|
||||
{".setpay", "application/set-payment-initiation"},
|
||||
{".setreg", "application/set-registration-initiation"},
|
||||
{".settings", "application/xml"},
|
||||
{".sgimb", "application/x-sgimb"},
|
||||
{".sgml", "text/sgml"},
|
||||
{".sh", "application/x-sh"},
|
||||
{".shar", "application/x-shar"},
|
||||
{".shtml", "text/html"},
|
||||
{".sit", "application/x-stuffit"},
|
||||
{".sitemap", "application/xml"},
|
||||
{".skin", "application/xml"},
|
||||
{".skp", "application/x-koan" },
|
||||
{".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"},
|
||||
{".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"},
|
||||
{".slk", "application/vnd.ms-excel"},
|
||||
{".sln", "text/plain"},
|
||||
{".slupkg-ms", "application/x-ms-license"},
|
||||
{".smd", "audio/x-smd"},
|
||||
{".smi", "application/octet-stream"},
|
||||
{".smx", "audio/x-smd"},
|
||||
{".smz", "audio/x-smd"},
|
||||
{".snd", "audio/basic"},
|
||||
{".snippet", "application/xml"},
|
||||
{".snp", "application/octet-stream"},
|
||||
{".sol", "text/plain"},
|
||||
{".sor", "text/plain"},
|
||||
{".spc", "application/x-pkcs7-certificates"},
|
||||
{".spl", "application/futuresplash"},
|
||||
{".spx", "audio/ogg"},
|
||||
{".src", "application/x-wais-source"},
|
||||
{".srf", "text/plain"},
|
||||
{".SSISDeploymentManifest", "text/xml"},
|
||||
{".ssm", "application/streamingmedia"},
|
||||
{".sst", "application/vnd.ms-pki.certstore"},
|
||||
{".stl", "application/vnd.ms-pki.stl"},
|
||||
{".sv4cpio", "application/x-sv4cpio"},
|
||||
{".sv4crc", "application/x-sv4crc"},
|
||||
{".svc", "application/xml"},
|
||||
{".svg", "image/svg+xml"},
|
||||
{".swf", "application/x-shockwave-flash"},
|
||||
{".step", "application/step"},
|
||||
{".stp", "application/step"},
|
||||
{".t", "application/x-troff"},
|
||||
{".tar", "application/x-tar"},
|
||||
{".tcl", "application/x-tcl"},
|
||||
{".testrunconfig", "application/xml"},
|
||||
{".testsettings", "application/xml"},
|
||||
{".tex", "application/x-tex"},
|
||||
{".texi", "application/x-texinfo"},
|
||||
{".texinfo", "application/x-texinfo"},
|
||||
{".tgz", "application/x-compressed"},
|
||||
{".thmx", "application/vnd.ms-officetheme"},
|
||||
{".thn", "application/octet-stream"},
|
||||
{".tif", "image/tiff"},
|
||||
{".tiff", "image/tiff"},
|
||||
{".tlh", "text/plain"},
|
||||
{".tli", "text/plain"},
|
||||
{".toc", "application/octet-stream"},
|
||||
{".tr", "application/x-troff"},
|
||||
{".trm", "application/x-msterminal"},
|
||||
{".trx", "application/xml"},
|
||||
{".ts", "video/vnd.dlna.mpeg-tts"},
|
||||
{".tsv", "text/tab-separated-values"},
|
||||
{".ttf", "application/font-sfnt"},
|
||||
{".tts", "video/vnd.dlna.mpeg-tts"},
|
||||
{".txt", "text/plain"},
|
||||
{".u32", "application/octet-stream"},
|
||||
{".uls", "text/iuls"},
|
||||
{".user", "text/plain"},
|
||||
{".ustar", "application/x-ustar"},
|
||||
{".vb", "text/plain"},
|
||||
{".vbdproj", "text/plain"},
|
||||
{".vbk", "video/mpeg"},
|
||||
{".vbproj", "text/plain"},
|
||||
{".vbs", "text/vbscript"},
|
||||
{".vcf", "text/x-vcard"},
|
||||
{".vcproj", "application/xml"},
|
||||
{".vcs", "text/plain"},
|
||||
{".vcxproj", "application/xml"},
|
||||
{".vddproj", "text/plain"},
|
||||
{".vdp", "text/plain"},
|
||||
{".vdproj", "text/plain"},
|
||||
{".vdx", "application/vnd.ms-visio.viewer"},
|
||||
{".vml", "text/xml"},
|
||||
{".vscontent", "application/xml"},
|
||||
{".vsct", "text/xml"},
|
||||
{".vsd", "application/vnd.visio"},
|
||||
{".vsi", "application/ms-vsi"},
|
||||
{".vsix", "application/vsix"},
|
||||
{".vsixlangpack", "text/xml"},
|
||||
{".vsixmanifest", "text/xml"},
|
||||
{".vsmdi", "application/xml"},
|
||||
{".vspscc", "text/plain"},
|
||||
{".vss", "application/vnd.visio"},
|
||||
{".vsscc", "text/plain"},
|
||||
{".vssettings", "text/xml"},
|
||||
{".vssscc", "text/plain"},
|
||||
{".vst", "application/vnd.visio"},
|
||||
{".vstemplate", "text/xml"},
|
||||
{".vsto", "application/x-ms-vsto"},
|
||||
{".vsw", "application/vnd.visio"},
|
||||
{".vsx", "application/vnd.visio"},
|
||||
{".vtt", "text/vtt"},
|
||||
{".vtx", "application/vnd.visio"},
|
||||
{".wasm", "application/wasm"},
|
||||
{".wav", "audio/wav"},
|
||||
{".wave", "audio/wav"},
|
||||
{".wax", "audio/x-ms-wax"},
|
||||
{".wbk", "application/msword"},
|
||||
{".wbmp", "image/vnd.wap.wbmp"},
|
||||
{".wcm", "application/vnd.ms-works"},
|
||||
{".wdb", "application/vnd.ms-works"},
|
||||
{".wdp", "image/vnd.ms-photo"},
|
||||
{".webarchive", "application/x-safari-webarchive"},
|
||||
{".webm", "video/webm"},
|
||||
{".webp", "image/webp"}, /* https://en.wikipedia.org/wiki/WebP */
|
||||
{".webtest", "application/xml"},
|
||||
{".wiq", "application/xml"},
|
||||
{".wiz", "application/msword"},
|
||||
{".wks", "application/vnd.ms-works"},
|
||||
{".WLMP", "application/wlmoviemaker"},
|
||||
{".wlpginstall", "application/x-wlpg-detect"},
|
||||
{".wlpginstall3", "application/x-wlpg3-detect"},
|
||||
{".wm", "video/x-ms-wm"},
|
||||
{".wma", "audio/x-ms-wma"},
|
||||
{".wmd", "application/x-ms-wmd"},
|
||||
{".wmf", "application/x-msmetafile"},
|
||||
{".wml", "text/vnd.wap.wml"},
|
||||
{".wmlc", "application/vnd.wap.wmlc"},
|
||||
{".wmls", "text/vnd.wap.wmlscript"},
|
||||
{".wmlsc", "application/vnd.wap.wmlscriptc"},
|
||||
{".wmp", "video/x-ms-wmp"},
|
||||
{".wmv", "video/x-ms-wmv"},
|
||||
{".wmx", "video/x-ms-wmx"},
|
||||
{".wmz", "application/x-ms-wmz"},
|
||||
{".woff", "application/font-woff"},
|
||||
{".woff2", "application/font-woff2"},
|
||||
{".wpl", "application/vnd.ms-wpl"},
|
||||
{".wps", "application/vnd.ms-works"},
|
||||
{".wri", "application/x-mswrite"},
|
||||
{".wrl", "x-world/x-vrml"},
|
||||
{".wrz", "x-world/x-vrml"},
|
||||
{".wsc", "text/scriptlet"},
|
||||
{".wsdl", "text/xml"},
|
||||
{".wvx", "video/x-ms-wvx"},
|
||||
{".x", "application/directx"},
|
||||
{".xaf", "x-world/x-vrml"},
|
||||
{".xaml", "application/xaml+xml"},
|
||||
{".xap", "application/x-silverlight-app"},
|
||||
{".xbap", "application/x-ms-xbap"},
|
||||
{".xbm", "image/x-xbitmap"},
|
||||
{".xdr", "text/plain"},
|
||||
{".xht", "application/xhtml+xml"},
|
||||
{".xhtml", "application/xhtml+xml"},
|
||||
{".xla", "application/vnd.ms-excel"},
|
||||
{".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"},
|
||||
{".xlc", "application/vnd.ms-excel"},
|
||||
{".xld", "application/vnd.ms-excel"},
|
||||
{".xlk", "application/vnd.ms-excel"},
|
||||
{".xll", "application/vnd.ms-excel"},
|
||||
{".xlm", "application/vnd.ms-excel"},
|
||||
{".xls", "application/vnd.ms-excel"},
|
||||
{".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"},
|
||||
{".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"},
|
||||
{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
|
||||
{".xlt", "application/vnd.ms-excel"},
|
||||
{".xltm", "application/vnd.ms-excel.template.macroEnabled.12"},
|
||||
{".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"},
|
||||
{".xlw", "application/vnd.ms-excel"},
|
||||
{".xml", "text/xml"},
|
||||
{".xmp", "application/octet-stream" },
|
||||
{".xmta", "application/xml"},
|
||||
{".xof", "x-world/x-vrml"},
|
||||
{".XOML", "text/plain"},
|
||||
{".xpm", "image/x-xpixmap"},
|
||||
{".xps", "application/vnd.ms-xpsdocument"},
|
||||
{".xrm-ms", "text/xml"},
|
||||
{".xsc", "application/xml"},
|
||||
{".xsd", "text/xml"},
|
||||
{".xsf", "text/xml"},
|
||||
{".xsl", "text/xml"},
|
||||
{".xslt", "text/xml"},
|
||||
{".xsn", "application/octet-stream"},
|
||||
{".xss", "application/xml"},
|
||||
{".xspf", "application/xspf+xml"},
|
||||
{".xtp", "application/octet-stream"},
|
||||
{".xwd", "image/x-xwindowdump"},
|
||||
{".z", "application/x-compress"},
|
||||
{".zip", "application/zip"},
|
||||
|
||||
{"application/fsharp-script", ".fsx"},
|
||||
{"application/msaccess", ".adp"},
|
||||
{"application/msword", ".doc"},
|
||||
{"application/octet-stream", ".bin"},
|
||||
{"application/onenote", ".one"},
|
||||
{"application/postscript", ".eps"},
|
||||
{"application/step", ".step"},
|
||||
{"application/vnd.ms-excel", ".xls"},
|
||||
{"application/vnd.ms-powerpoint", ".ppt"},
|
||||
{"application/vnd.ms-works", ".wks"},
|
||||
{"application/vnd.visio", ".vsd"},
|
||||
{"application/x-director", ".dir"},
|
||||
{"application/x-shockwave-flash", ".swf"},
|
||||
{"application/x-x509-ca-cert", ".cer"},
|
||||
{"application/x-zip-compressed", ".zip"},
|
||||
{"application/xhtml+xml", ".xhtml"},
|
||||
{"application/xml", ".xml"}, // anomoly, .xml -> text/xml, but application/xml -> many thingss, but all are xml, so safest is .xml
|
||||
{"audio/aac", ".AAC"},
|
||||
{"audio/aiff", ".aiff"},
|
||||
{"audio/basic", ".snd"},
|
||||
{"audio/mid", ".midi"},
|
||||
{"audio/wav", ".wav"},
|
||||
{"audio/x-m4a", ".m4a"},
|
||||
{"audio/x-mpegurl", ".m3u"},
|
||||
{"audio/x-pn-realaudio", ".ra"},
|
||||
{"audio/x-smd", ".smd"},
|
||||
{"image/bmp", ".bmp"},
|
||||
{"image/jpeg", ".jpg"},
|
||||
{"image/pict", ".pic"},
|
||||
{"image/png", ".png"}, //Defined in [RFC-2045], [RFC-2048]
|
||||
{"image/x-png", ".png"}, //See https://www.w3.org/TR/PNG/#A-Media-type :"It is recommended that implementations also recognize the media type "image/x-png"."
|
||||
{"image/tiff", ".tiff"},
|
||||
{"image/x-macpaint", ".mac"},
|
||||
{"image/x-quicktime", ".qti"},
|
||||
{"message/rfc822", ".eml"},
|
||||
{"text/html", ".html"},
|
||||
{"text/plain", ".txt"},
|
||||
{"text/scriptlet", ".wsc"},
|
||||
{"text/xml", ".xml"},
|
||||
{"video/3gpp", ".3gp"},
|
||||
{"video/3gpp2", ".3gp2"},
|
||||
{"video/mp4", ".mp4"},
|
||||
{"video/mpeg", ".mpg"},
|
||||
{"video/quicktime", ".mov"},
|
||||
{"video/vnd.dlna.mpeg-tts", ".m2t"},
|
||||
{"video/x-dv", ".dv"},
|
||||
{"video/x-la-asf", ".lsf"},
|
||||
{"video/x-ms-asf", ".asf"},
|
||||
{"x-world/x-vrml", ".xof"},
|
||||
|
||||
#endregion
|
||||
|
||||
};
|
||||
|
||||
var cache = mappings.ToList(); // need ToList() to avoid modifying while still enumerating
|
||||
|
||||
foreach (var mapping in cache)
|
||||
{
|
||||
if (!mappings.ContainsKey(mapping.Value))
|
||||
{
|
||||
mappings.Add(mapping.Value, mapping.Key);
|
||||
}
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
public static string GetMimeType(string extension)
|
||||
{
|
||||
if (extension == null)
|
||||
{
|
||||
throw new ArgumentNullException("extension");
|
||||
}
|
||||
|
||||
if (!extension.StartsWith("."))
|
||||
{
|
||||
extension = "." + extension;
|
||||
}
|
||||
|
||||
string mime;
|
||||
|
||||
return _mappings.Value.TryGetValue(extension, out mime) ? mime : "application/octet-stream";
|
||||
}
|
||||
|
||||
public static string GetExtension(string mimeType)
|
||||
{
|
||||
return GetExtension(mimeType, true);
|
||||
}
|
||||
|
||||
public static string GetExtension(string mimeType, bool throwErrorIfNotFound)
|
||||
{
|
||||
if (mimeType == null)
|
||||
{
|
||||
throw new ArgumentNullException("mimeType");
|
||||
}
|
||||
|
||||
if (mimeType.StartsWith("."))
|
||||
{
|
||||
throw new ArgumentException("Requested mime type is not valid: " + mimeType);
|
||||
}
|
||||
|
||||
string extension;
|
||||
|
||||
if (_mappings.Value.TryGetValue(mimeType, out extension))
|
||||
{
|
||||
return extension;
|
||||
}
|
||||
if (throwErrorIfNotFound)
|
||||
{
|
||||
throw new ArgumentException("Requested mime type is not registered: " + mimeType);
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
// /**
|
||||
// * File: TokenReader.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
namespace ln.http.message
|
||||
{
|
||||
public class TokenReader : TextReader
|
||||
{
|
||||
public static char[] specialChars = new char[] { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=' };
|
||||
|
||||
public TextReader BaseReader { get; }
|
||||
|
||||
public TokenReader(String text)
|
||||
:this(new StringReader(text))
|
||||
{}
|
||||
public TokenReader(TextReader baseReader)
|
||||
{
|
||||
BaseReader = baseReader;
|
||||
}
|
||||
|
||||
public override int Read() => BaseReader.Read();
|
||||
public override int Peek() => BaseReader.Peek();
|
||||
|
||||
public string ReadToken()
|
||||
{
|
||||
while ((BaseReader.Peek() != -1) && char.IsWhiteSpace((char)BaseReader.Peek()))
|
||||
BaseReader.Read();
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
int ch;
|
||||
do
|
||||
{
|
||||
ch = BaseReader.Peek();
|
||||
|
||||
if ((ch <= ' ') || specialChars.Contains((char)ch))
|
||||
return stringBuilder.ToString();
|
||||
|
||||
stringBuilder.Append((char)BaseReader.Read());
|
||||
} while (ch != -1);
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public string ReadQuotedString()
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
if (BaseReader.Read() != '"')
|
||||
throw new FormatException("quoted string must start with \"");
|
||||
|
||||
int ch;
|
||||
while (((ch = BaseReader.Read()) != -1) && (ch != '"'))
|
||||
stringBuilder.Append((char)ch);
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
// /**
|
||||
// * File: HTTP.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using ln.http.exceptions;
|
||||
namespace ln.http.message.parser
|
||||
{
|
||||
public static class HTTP
|
||||
{
|
||||
public static HeaderContainer ReadHeader(TextReader reader)
|
||||
{
|
||||
List<String> headerLines = new List<string>();
|
||||
string currentline = reader.ReadLine();
|
||||
while (!currentline.Equals(string.Empty))
|
||||
{
|
||||
if (char.IsWhiteSpace(currentline[0]))
|
||||
throw new BadRequestException();
|
||||
|
||||
headerLines.Add(currentline.Trim());
|
||||
|
||||
currentline = reader.ReadLine();
|
||||
}
|
||||
|
||||
HeaderContainer headerContainer = new HeaderContainer();
|
||||
|
||||
foreach (string headerLine in headerLines)
|
||||
headerContainer.Add(new Header(headerLine));
|
||||
|
||||
return headerContainer;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// /**
|
||||
// * File: MIME.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
namespace ln.http.message.parser
|
||||
{
|
||||
public static class MIME
|
||||
{
|
||||
public static HeaderContainer ReadHeader(TextReader reader)
|
||||
{
|
||||
List<String> headerLines = new List<string>();
|
||||
string currentline = reader.ReadLine();
|
||||
while (!currentline.Equals(string.Empty))
|
||||
{
|
||||
if (char.IsWhiteSpace(currentline[0]))
|
||||
{
|
||||
headerLines[headerLines.Count - 1] = headerLines[headerLines.Count - 1] + currentline;
|
||||
}
|
||||
else
|
||||
{
|
||||
headerLines.Add(currentline);
|
||||
}
|
||||
currentline = reader.ReadLine();
|
||||
}
|
||||
|
||||
HeaderContainer headerContainer = new HeaderContainer();
|
||||
|
||||
foreach (string headerLine in headerLines)
|
||||
headerContainer.Add(new Header(headerLine));
|
||||
|
||||
return headerContainer;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,71 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ln.http.rest
|
||||
{
|
||||
|
||||
public abstract partial class CRUDObjectContainer
|
||||
{
|
||||
class RegisteredType
|
||||
{
|
||||
public CRUDObjectContainer ObjectContainer { get; }
|
||||
public Type ObjectType { get; }
|
||||
|
||||
Func<object,object> _idGetter;
|
||||
Dictionary<object, object> objectCache = new Dictionary<object, object>();
|
||||
|
||||
public IEnumerable<FieldDescriptor> FieldDescriptors => fieldDescriptors;
|
||||
List<FieldDescriptor> fieldDescriptors = new List<FieldDescriptor>();
|
||||
|
||||
public RegisteredType(CRUDObjectContainer objectContainer,Type objectType,Func<object,object> idGetter)
|
||||
{
|
||||
ObjectContainer = objectContainer;
|
||||
ObjectType = objectType;
|
||||
_idGetter = idGetter;
|
||||
}
|
||||
|
||||
public bool TryGetObjectID(object objectInstance,out object objectIdentifier)
|
||||
{
|
||||
try
|
||||
{
|
||||
objectIdentifier = _idGetter(objectInstance);
|
||||
} catch (Exception e)
|
||||
{
|
||||
objectIdentifier = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddCachedObject(object objectInstance)
|
||||
{
|
||||
Type objectType = objectInstance.GetType();
|
||||
if (!ObjectType.Equals(objectType))
|
||||
throw new ArgumentException();
|
||||
|
||||
if (!TryGetObjectID(objectInstance, out object objectIdentifier))
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
objectCache.Add(objectIdentifier, objectInstance);
|
||||
}
|
||||
|
||||
public void RemoveCachedObject(object objectInstance)
|
||||
{
|
||||
Type objectType = objectInstance.GetType();
|
||||
if (!ObjectType.Equals(objectType))
|
||||
throw new ArgumentException();
|
||||
|
||||
if (!TryGetObjectID(objectInstance, out object objectIdentifier))
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
objectCache.Remove(objectIdentifier);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ln.http.rest
|
||||
{
|
||||
public delegate void CRUDEvent(CRUDObjectContainer sender, CRUDEventArgs args);
|
||||
public enum CRUDEventType { CREATE, READ, UPDATE, DELETE }
|
||||
|
||||
public class CRUDEventArgs : EventArgs
|
||||
{
|
||||
public CRUDEventType EventType { get; }
|
||||
public Type ObjectType { get; }
|
||||
public object ObjectIdentity { get; }
|
||||
public object ObjectInstance { get; }
|
||||
public FieldValueList FieldValues { get; }
|
||||
|
||||
public CRUDEventArgs(CRUDEventType eventType, Type objectType, object objectIdentity, object objectInstance, FieldValueList fieldValues)
|
||||
{
|
||||
EventType = eventType;
|
||||
ObjectType = objectType;
|
||||
ObjectIdentity = objectIdentity;
|
||||
ObjectInstance = objectInstance;
|
||||
FieldValues = fieldValues;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract partial class CRUDObjectContainer
|
||||
{
|
||||
public event CRUDEvent OnCRUDEvent;
|
||||
|
||||
|
||||
Dictionary<Type, RegisteredType> registeredTypes = new Dictionary<Type, RegisteredType>();
|
||||
|
||||
public CRUDObjectContainer()
|
||||
{ }
|
||||
|
||||
public abstract bool TryGetObjectIdentifier(object objectInstance, out object objectIdentifier);
|
||||
|
||||
public abstract bool TryCreateObject(Type objectType, FieldValueList fieldValues, out object objectInstance);
|
||||
public abstract bool TryLookupObject(Type objectType, object objectIdentifier, out object objectInstance);
|
||||
public abstract bool TryUpdateObject(Type objectType, object objectIdentifier, FieldValueList fieldValues);
|
||||
public abstract bool TryDeleteObject(Type objectType, object objectIdentifier);
|
||||
|
||||
public virtual bool TryUpdateObject(object objectInstance, FieldValueList fieldValues)
|
||||
{
|
||||
if (TryGetObjectIdentifier(objectInstance, out object objectIdentifier))
|
||||
return TryUpdateObject(objectInstance.GetType(), objectIdentifier, fieldValues);
|
||||
return false;
|
||||
}
|
||||
public virtual bool TryDeleteObject(object objectInstance)
|
||||
{
|
||||
if (TryGetObjectIdentifier(objectInstance, out object objectIdentifier))
|
||||
return TryDeleteObject(objectInstance.GetType(), objectIdentifier);
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void FireCRUDEvent(CRUDEventArgs args) => OnCRUDEvent?.Invoke(this, args);
|
||||
protected void FireCRUDEVent(CRUDEventType EventType, Type ObjectType, object ObjectIdentity, object ObjectInstance, FieldValueList FieldValues)
|
||||
=> FireCRUDEvent(new CRUDEventArgs(EventType, ObjectType, ObjectIdentity, ObjectInstance, FieldValues));
|
||||
|
||||
public virtual IEnumerable<Type> RegisteredTypes => registeredTypes.Keys;
|
||||
public virtual bool ContainsRegisteredType(Type objectType) => registeredTypes.ContainsKey(objectType);
|
||||
|
||||
public virtual bool TryRegisterType(Type objectType) => TryRegisterType(objectType, null);
|
||||
public virtual bool TryRegisterType(Type objectType, Func<object,object> idGetter)
|
||||
{
|
||||
if (ContainsRegisteredType(objectType))
|
||||
throw new ArgumentException("objectType already registered", nameof(objectType));
|
||||
|
||||
RegisteredType registeredType = new RegisteredType(this, objectType, idGetter);
|
||||
|
||||
registeredTypes.Add(objectType, registeredType);
|
||||
|
||||
return true;
|
||||
}
|
||||
public virtual bool TryGetFieldDescriptors(Type objectType, out FieldDescriptor[] fieldDescriptors)
|
||||
{
|
||||
if (registeredTypes.TryGetValue(objectType, out RegisteredType registeredType))
|
||||
{
|
||||
fieldDescriptors = registeredType.FieldDescriptors.ToArray();
|
||||
return true;
|
||||
}
|
||||
fieldDescriptors = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual IEnumerable<object> EnumerateObjectInstances(Type objectType) => EnumerateObjectInstances(objectType, null);
|
||||
public abstract IEnumerable<object> EnumerateObjectInstances(Type objectType, FieldValueList conditionalFieldValues);
|
||||
|
||||
public abstract bool AddForeignObjectInstance(object objectInstance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace ln.http.rest
|
||||
{
|
||||
public class FieldDescriptor
|
||||
{
|
||||
Action<object, object> setter;
|
||||
Func<object, object> getter;
|
||||
|
||||
public Type OwningType { get; }
|
||||
|
||||
public string FieldName { get; }
|
||||
public Type FieldType { get; }
|
||||
|
||||
public bool CanWrite { get; }
|
||||
|
||||
public FieldDescriptor(FieldInfo fieldInfo)
|
||||
{
|
||||
OwningType = fieldInfo.DeclaringType;
|
||||
FieldName = fieldInfo.Name;
|
||||
FieldType = fieldInfo.FieldType;
|
||||
|
||||
CanWrite = !fieldInfo.IsInitOnly;
|
||||
|
||||
getter = fieldInfo.GetValue;
|
||||
setter = fieldInfo.SetValue;
|
||||
}
|
||||
|
||||
public FieldDescriptor(PropertyInfo propertyInfo)
|
||||
{
|
||||
OwningType = propertyInfo.DeclaringType;
|
||||
FieldName = propertyInfo.Name;
|
||||
FieldType = propertyInfo.PropertyType;
|
||||
|
||||
CanWrite = propertyInfo.CanWrite;
|
||||
|
||||
getter = propertyInfo.GetValue;
|
||||
setter = propertyInfo.SetValue;
|
||||
}
|
||||
|
||||
public void SetFieldValue(object objectInstance, object fieldValue) => setter(objectInstance, fieldValue);
|
||||
public object GetFieldValue(object objectInstance) => getter(objectInstance);
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ln.http.rest
|
||||
{
|
||||
public class FieldValueList
|
||||
{
|
||||
public Type ObjectType { get; }
|
||||
|
||||
Dictionary<string, object> fieldValues = new Dictionary<string, object>();
|
||||
|
||||
public FieldValueList(Type objectType)
|
||||
{
|
||||
ObjectType = objectType;
|
||||
}
|
||||
|
||||
public IEnumerable<string> FieldNames => fieldValues.Keys;
|
||||
public object GetFieldValue(string fieldName) => fieldValues[fieldName];
|
||||
public void SetFieldValue(string fieldName, object fieldValue) => fieldValues[fieldName] = fieldValue;
|
||||
public void RemoveFieldValue(string fieldName) => fieldValues.Remove(fieldName);
|
||||
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
using ln.http.router;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace ln.http.rest
|
||||
{
|
||||
public class RestAPIAdapter : IHttpRouter
|
||||
{
|
||||
public CRUDObjectContainer ObjectContainer { get; }
|
||||
|
||||
public RestAPIAdapter(CRUDObjectContainer objectContainer)
|
||||
{
|
||||
ObjectContainer = objectContainer;
|
||||
}
|
||||
|
||||
public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ln.http.route;
|
||||
|
||||
public class HttpRoute
|
||||
{
|
||||
protected HttpRouterDelegate _routerDelegate;
|
||||
|
||||
public HttpMethod HttpMethod { get; protected set; }
|
||||
public Regex Route { get; protected set; }
|
||||
|
||||
public HttpRoute(HttpMethod httpMethod, string route, HttpRouterDelegate routerDelegate)
|
||||
:this(httpMethod, route)
|
||||
{
|
||||
_routerDelegate = routerDelegate;
|
||||
}
|
||||
|
||||
protected HttpRoute(HttpMethod httpMethod, string route)
|
||||
{
|
||||
HttpMethod = httpMethod;
|
||||
Route = route is string ? new Regex(route) : null;
|
||||
}
|
||||
|
||||
public bool TryRoute(HttpRequestContext httpRequestContext, string routePath)
|
||||
{
|
||||
bool matched = false;
|
||||
|
||||
if (
|
||||
(HttpMethod == HttpMethod.ANY) ||
|
||||
(HttpMethod == httpRequestContext.Request.Method)
|
||||
)
|
||||
{
|
||||
if (Route is null)
|
||||
{
|
||||
matched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Match match = Route.Match(routePath);
|
||||
if (match.Success)
|
||||
{
|
||||
foreach (Group group in match.Groups.Values)
|
||||
if (group.Success)
|
||||
httpRequestContext.SetParameter(group.Name, group.Value);
|
||||
|
||||
routePath = routePath.Substring(match.Length);
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matched)
|
||||
return _routerDelegate(httpRequestContext, routePath) && (httpRequestContext.Response is not null);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[HttpRoute HttpMethod={HttpMethod} Route={Route}]";
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
// /**
|
||||
// * File: FileSystemRouter.cs
|
||||
// * Author: haraldwolff
|
||||
// *
|
||||
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||
// *
|
||||
// *
|
||||
// **/
|
||||
using System.IO;
|
||||
using ln.http.message;
|
||||
namespace ln.http.router
|
||||
{
|
||||
public class FileRouter : IHttpRouter
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
|
||||
public FileRouter(string filename)
|
||||
{
|
||||
if (!File.Exists(filename))
|
||||
throw new FileNotFoundException();
|
||||
|
||||
FileName = filename;
|
||||
}
|
||||
|
||||
public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest)
|
||||
{
|
||||
HttpResponse httpResponse = new HttpResponse(httpRequest, new FileStream(FileName, FileMode.Open));
|
||||
httpResponse.SetHeader("content-type", MimeTypeMap.GetMimeType(Path.GetExtension(FileName)));
|
||||
|
||||
return httpResponse;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
|
||||
namespace ln.http.router
|
||||
{
|
||||
[Flags]
|
||||
public enum HttpAccessRights
|
||||
{
|
||||
READ = (1<<0),
|
||||
WRITE = (1<<1),
|
||||
EXECUTE = (1<<2),
|
||||
SPECIAL = (1<<3)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ namespace ln.http.router
|
|||
public string Path { get; set; }
|
||||
public string RoutedPath { get; set; }
|
||||
|
||||
public HttpRoutingContext(HttpRequest httpRequest) : this(httpRequest, httpRequest.URI.AbsolutePath) { }
|
||||
public HttpRoutingContext(HttpRequest httpRequest) : this(httpRequest, httpRequest?.RequestUri?.AbsolutePath ?? "") { }
|
||||
public HttpRoutingContext(HttpRequest httpRequest, string path)
|
||||
{
|
||||
HttpRequest = httpRequest;
|
||||
|
@ -39,7 +39,7 @@ namespace ln.http.router
|
|||
|
||||
int indSlash = next.IndexOf('/',1);
|
||||
if (indSlash > 0)
|
||||
next = next.Substring(0, indSlash-1);
|
||||
next = next.Substring(0, indSlash);
|
||||
|
||||
nextRoutingContext = Routed(Path.Substring(next.Length));
|
||||
|
||||
|
@ -48,6 +48,5 @@ namespace ln.http.router
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue