Several massive changes and additions

master
Harald Wolff 2023-08-07 10:38:29 +02:00
parent e7ed29d78d
commit a0ca936554
12 changed files with 579 additions and 58 deletions

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<TargetFrameworks>net5.0;net6.0</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageVersion>0.9.0-test1</PackageVersion>
</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>

View File

@ -1,8 +1,7 @@
using System.IO;
using System.Reflection;
using System.Threading;
using ln.bootstrap;
using ln.http.router;
using ln.templates;
using ln.templates.html;
namespace ln.http.service
@ -13,19 +12,18 @@ namespace ln.http.service
{
Bootstrap
.DefaultInstance
.ServiceContainer.RegisterService<HttpServiceHelper>();
Bootstrap
.DefaultInstance
//.AddService<HttpServiceHelper>()
.Start();
}
}
/*
class HttpServiceHelper : HttpRouter
{
private TemplateDocument _templateDocument;
public HttpServiceHelper(HttpServer httpServer)
:base(httpServer)
public HttpServiceHelper(HttpRouter httpRouter)
:base()
{
using (StreamReader reader = new StreamReader(
Assembly
@ -44,7 +42,7 @@ namespace ln.http.service
{
HttpResponse httpResponse =
new HttpResponse(httpContext.HttpException?.HttpStatusCode ?? HttpStatusCode.InternalServerError);
RenderContext renderContext = new RenderContext(httpResponse.ContentWriter);
renderContext
.GetEngine()
@ -56,4 +54,5 @@ namespace ln.http.service
}
}
*/
}

View File

@ -1,31 +1,50 @@
{
"ln.http.HttpServer, ln.http": {
"services": [
],
"properties": {
"System.IO.StreamWriter, System.Runtime": {
"domain": "log.http",
"set": {
"parameters": {
"path": "http.access.log"
}
}
},
"ln.http.HttpListener, ln.http": {
"services": [
],
"properties": {
"DefaultPort": 8180
"ln.http.HttpServer": {
"resolve": {
"parameters": {
"accessLogWriter": "(log.http) System.IO.StreamWriter, System.Runtime",
"logWriter": "(log.console) ln.logging.LogWriter"
}
}
},
"ln.http.HttpsListener, ln.http": {
"services": [
],
"properties": {
"DefaultPort": 8443
"ln.http.HttpListener": {
"set": {
"properties": {
"DefaultPort": 8180
}
}
},
"ln.http.FileSystemRouter, ln.http": {
"services": [
],
"parameters": {
"path": "."
"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"
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -4,23 +4,29 @@
<OutputType>Exe</OutputType>
<LangVersion>9</LangVersion>
<TargetFramework>net6.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.2.4" />
<PackageReference Include="ln.http" Version="0.6.5" />
<PackageReference Include="ln.http.templates" Version="0.1.0" />
<PackageReference Include="ln.templates" Version="0.3.0" />
<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>

Binary file not shown.

View File

@ -11,6 +11,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.service", "ln.http.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.protocols.helper", "..\ln.protocols.helper\ln.protocols.helper.csproj", "{1C1D3A17-A615-4686-90BD-F0E221EAC89C}"
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.templates", "..\ln.templates\ln.templates\ln.templates.csproj", "{D6BA640C-352F-41BF-85EF-4B76FD928BEC}"
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
@ -69,5 +75,17 @@ Global
{1C1D3A17-A615-4686-90BD-F0E221EAC89C}.Release|x64.Build.0 = Release|Any CPU
{1C1D3A17-A615-4686-90BD-F0E221EAC89C}.Release|x86.ActiveCfg = Release|Any CPU
{1C1D3A17-A615-4686-90BD-F0E221EAC89C}.Release|x86.Build.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
{D6BA640C-352F-41BF-85EF-4B76FD928BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6BA640C-352F-41BF-85EF-4B76FD928BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6BA640C-352F-41BF-85EF-4B76FD928BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6BA640C-352F-41BF-85EF-4B76FD928BEC}.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

View File

@ -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());
}
}
}
}

View File

@ -3,44 +3,42 @@ using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using ln.http.router;
using ln.json;
using ln.type;
using NUnit.Framework;
namespace ln.http.tests
{
public class Tests
{
HttpServer server;
int testPort;
private HttpRouter TestRouter;
private int TestPort;
private Listener _httpListener;
[SetUp]
public void Setup()
{
if (server != null)
return;
TestRouter = new HttpRouter();
TestRouter.Map(new TestApiController());
FileSystemRouter fileSystemRouter = new FileSystemRouter("/static/", AppContext.BaseDirectory);
TestRouter.Map(fileSystemRouter);
server = new HttpServer();
HttpRouter testRouter = new HttpRouter(server);
testRouter.Map(HttpMethod.ANY, "/controller/*", HttpRoutePriority.NORMAL, new TestApiController().Route);
_httpListener = new Listener(TestRouter, IPAddress.Any, 0);
TestPort = _httpListener.LocalEndpoint.Port;
TestContext.Error.WriteLine("Using Port {0}", TestPort);
}
FileSystemRouter fileSystemRouter = new FileSystemRouter(AppContext.BaseDirectory);
testRouter.Map(HttpMethod.ANY, "/static/*", fileSystemRouter.Route);
HttpListener.DefaultPort = 0;
HttpListener httpListener = new HttpListener(server);
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;
HttpResponseMessage response = client.GetAsync(String.Format("http://localhost:{0}/static/test.txt", TestPort)).Result;
Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode);
@ -49,8 +47,6 @@ namespace ln.http.tests
CollectionAssert.AreEqual(fileBytes, contentBytes);
//server.Stop();
Assert.Pass();
}
@ -59,17 +55,18 @@ namespace ln.http.tests
{
JSONObject jsonPutObject = new JSONObject();
jsonPutObject["PutTest"] = JSONTrue.Instance;
StringContent jsonStringContent = new StringContent(jsonPutObject.ToString());
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;
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")]

View File

@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>9</LangVersion>
<TargetFrameworks>net5.0;net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
@ -14,11 +15,13 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<ProjectReference Include="../ln.http/ln.http.csproj" />
<PackageReference Include="ln.type" Version="0.1.7" />
<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>