Remove unused Http server
This commit is contained in:
Родитель
b341070e1f
Коммит
75013dbb32
|
@ -1,185 +0,0 @@
|
|||
//
|
||||
// Copyright (C) 2018 Fluendo S.A.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XAMLator.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Http host that handles HTTP request and dispatch them to a <see cref="IRequestProcessor"/>.
|
||||
/// </summary>
|
||||
public class HttpHost
|
||||
{
|
||||
readonly IRequestProcessor requestProcessor;
|
||||
readonly string baseUri;
|
||||
HttpListener listener;
|
||||
|
||||
public event EventHandler ClientConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:VAS.Services.HttpServer.HttpHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="baseUri">Base URI.</param>
|
||||
/// <param name="requestProcessor">Request processor.</param>
|
||||
public HttpHost(string baseUri, IRequestProcessor requestProcessor)
|
||||
{
|
||||
this.baseUri = baseUri;
|
||||
this.requestProcessor = requestProcessor;
|
||||
}
|
||||
|
||||
public static async Task<HttpHost> StartServer(IRequestProcessor processor, int port, int portsRange)
|
||||
{
|
||||
HttpHost host = null;
|
||||
|
||||
for (int i = 0; i < portsRange; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
host = new HttpHost($"http://+:{port}/", processor);
|
||||
await host.StartListening();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is SocketException || ex is HttpListenerException)
|
||||
{
|
||||
host = null;
|
||||
port++;
|
||||
Log.Exception(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the host.
|
||||
/// </summary>
|
||||
public Task<bool> StartListening()
|
||||
{
|
||||
var taskCompletion = new TaskCompletionSource<bool>();
|
||||
Task.Factory.StartNew(() => Run(taskCompletion), TaskCreationOptions.LongRunning);
|
||||
return taskCompletion.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop the host.
|
||||
/// </summary>
|
||||
public void StopListening()
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
|
||||
async Task Run(TaskCompletionSource<bool> tcs)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener = new HttpListener();
|
||||
listener.Prefixes.Add(baseUri);
|
||||
listener.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
Log.Information($"Http server listening at {baseUri}");
|
||||
tcs.SetResult(true);
|
||||
|
||||
// Loop
|
||||
for (; ; )
|
||||
{
|
||||
var c = await listener.GetContextAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
ClientConnected?.Invoke(this, null);
|
||||
await ProcessRequest(c).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task ProcessRequest(HttpListenerContext context)
|
||||
{
|
||||
HttpResponse response;
|
||||
try
|
||||
{
|
||||
Log.Information($"Handling incomming request {context.Request.Url}");
|
||||
var request = await GetRequest(context.Request);
|
||||
response = await requestProcessor.HandleRequest(request);
|
||||
Log.Information($"Request handled with response {response.StatusCode}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Internal server error");
|
||||
Log.Exception(ex);
|
||||
response = new JsonHttpResponse { StatusCode = HttpStatusCode.InternalServerError };
|
||||
}
|
||||
try
|
||||
{
|
||||
await WriteResponse(response, context.Response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
async Task WriteResponse(HttpResponse response, HttpListenerResponse httpListenerResponse)
|
||||
{
|
||||
httpListenerResponse.StatusCode = (int)response.StatusCode;
|
||||
httpListenerResponse.ContentType = response.ContentType;
|
||||
|
||||
foreach (var header in response.Headers)
|
||||
{
|
||||
httpListenerResponse.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
using (var output = httpListenerResponse.OutputStream)
|
||||
{
|
||||
await response.Stream(output);
|
||||
}
|
||||
}
|
||||
|
||||
async Task<HttpRequest> GetRequest(HttpListenerRequest request)
|
||||
{
|
||||
var headers = request.Headers.AllKeys.ToDictionary<string, string, IEnumerable<string>>(
|
||||
key => key, request.Headers.GetValues);
|
||||
if (request.ContentType == "application/json")
|
||||
{
|
||||
StreamReader sr = new StreamReader(request.InputStream, Encoding.UTF8);
|
||||
string json = await sr.ReadToEndAsync();
|
||||
return new JsonHttpRequest(request.HttpMethod, request.Url, request.InputStream, headers, json);
|
||||
}
|
||||
|
||||
return new HttpRequest(request.HttpMethod, request.Url, request.InputStream, headers);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
//
|
||||
// Copyright (C) 2018 Fluendo S.A.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace XAMLator.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Http request.
|
||||
/// </summary>
|
||||
public class HttpRequest
|
||||
{
|
||||
public HttpRequest(string httpMethod, Uri url, Stream stream, Dictionary<string, IEnumerable<string>> headers)
|
||||
{
|
||||
HttpMethod = httpMethod;
|
||||
Url = url;
|
||||
Body = stream;
|
||||
Parameters = new ExpandoObject();
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Http method.
|
||||
/// </summary>
|
||||
/// <value>The Http method.</value>
|
||||
public string HttpMethod { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL.
|
||||
/// </summary>
|
||||
/// <value>The URL.</value>
|
||||
public Uri Url { get; private set; }
|
||||
|
||||
public Stream Body { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the query or url parameters of the request.
|
||||
/// </summary>
|
||||
/// <value>The parameters.</value>
|
||||
public dynamic Parameters { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the headers of the request.
|
||||
/// </summary>
|
||||
/// <value>The headers.</value>
|
||||
public Dictionary<string, IEnumerable<string>> Headers { get; private set; }
|
||||
}
|
||||
|
||||
public class JsonHttpRequest : HttpRequest
|
||||
{
|
||||
public JsonHttpRequest(string httpMethod, Uri url, Stream stream, Dictionary<string,
|
||||
IEnumerable<string>> headers, string json) :
|
||||
base(httpMethod, url, stream, headers)
|
||||
{
|
||||
object data = Serializer.DeserializeJson(json);
|
||||
if (data is JObject jData)
|
||||
{
|
||||
Data = jData.ToObject<Dictionary<string, string>>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data of the request, deseralized from the request body
|
||||
/// </summary>
|
||||
/// <value>The data.</value>
|
||||
public object Data { get; private set; }
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
//
|
||||
// Copyright (C) 2018 Fluendo S.A.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XAMLator.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Http response.
|
||||
/// </summary>
|
||||
public abstract class HttpResponse
|
||||
{
|
||||
public HttpResponse()
|
||||
{
|
||||
Stream = WriteResponse;
|
||||
Headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response data.
|
||||
/// </summary>
|
||||
/// <value>The response data.</value>
|
||||
public Func<Stream, Task> Stream { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content-type of the response
|
||||
/// </summary>
|
||||
/// <value>The content-type.</value>
|
||||
public string ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status code of the response.
|
||||
/// </summary>
|
||||
/// <value>The status code.</value>
|
||||
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the headers of the request.
|
||||
/// </summary>
|
||||
/// <value>The headers.</value>
|
||||
public Dictionary<string, string> Headers { get; private set; }
|
||||
|
||||
protected abstract Task WriteResponse(Stream stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON Http response.
|
||||
/// </summary>
|
||||
public class JsonHttpResponse : HttpResponse
|
||||
{
|
||||
public JsonHttpResponse()
|
||||
{
|
||||
ContentType = "application/json";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response data.
|
||||
/// </summary>
|
||||
/// <value>The response data.</value>
|
||||
public object Data { get; set; }
|
||||
|
||||
protected override async Task WriteResponse(Stream stream)
|
||||
{
|
||||
if (Data != null)
|
||||
{
|
||||
var json = Serializer.SerializeJson(Data);
|
||||
using (var sw = new StreamWriter(stream, new UTF8Encoding(false)))
|
||||
{
|
||||
await sw.WriteAsync(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
//
|
||||
// Copyright (C) 2018 Fluendo S.A.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XAMLator.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Process HTTP request from the <see cref="HttpHost"/>.
|
||||
/// </summary>
|
||||
public interface IRequestProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the Http request.
|
||||
/// </summary>
|
||||
/// <returns>The response.</returns>
|
||||
/// <param name="request">The request.</param>
|
||||
Task<HttpResponse> HandleRequest(HttpRequest request);
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
//
|
||||
// Copyright (C) 2018 Fluendo S.A.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XAMLator.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Process request based on their URL and the http method.
|
||||
/// </summary>
|
||||
public abstract class RequestProcessor : IRequestProcessor
|
||||
{
|
||||
const string GET = "GET";
|
||||
const string POST = "POST";
|
||||
const string PUT = "PUT";
|
||||
|
||||
readonly List<Route> routes = new List<Route>();
|
||||
|
||||
public async Task<HttpResponse> HandleRequest(HttpRequest request)
|
||||
{
|
||||
var route = routes.FirstOrDefault(r => r.IsMatch(request));
|
||||
if (route == null)
|
||||
{
|
||||
return new JsonHttpResponse { StatusCode = HttpStatusCode.NotFound };
|
||||
}
|
||||
return await route.Invoke(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callbacks router for Get requests
|
||||
/// </summary>
|
||||
protected RouteBuilder Get { get { return new RouteBuilder(GET, this); } }
|
||||
|
||||
/// <summary>
|
||||
/// Callbacks router for Post requests
|
||||
/// </summary>
|
||||
protected RouteBuilder Post { get { return new RouteBuilder(POST, this); } }
|
||||
|
||||
/// <summary>
|
||||
/// Callbacks router for Put requests
|
||||
/// </summary>
|
||||
protected RouteBuilder Put { get { return new RouteBuilder(PUT, this); } }
|
||||
|
||||
protected class RouteBuilder
|
||||
{
|
||||
readonly string method;
|
||||
readonly RequestProcessor requestProcessor;
|
||||
|
||||
public RouteBuilder(string method, RequestProcessor requestProcessor)
|
||||
{
|
||||
this.method = method;
|
||||
this.requestProcessor = requestProcessor;
|
||||
}
|
||||
|
||||
public Func<HttpRequest, Task<HttpResponse>> this[string path]
|
||||
{
|
||||
set
|
||||
{
|
||||
AddRoute(path, value);
|
||||
}
|
||||
}
|
||||
|
||||
void AddRoute(string path, Func<HttpRequest, Task<HttpResponse>> value)
|
||||
{
|
||||
requestProcessor.routes.Add(new Route(method, path, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
//
|
||||
// Copyright (C) 2018 Fluendo S.A.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XAMLator.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Route that binds a handler for a resource based in the Http method and the path of the request.
|
||||
/// </summary>
|
||||
public class Route
|
||||
{
|
||||
static readonly Regex PathRegex = new Regex(@"\{(?<param>\S+)\}", RegexOptions.Compiled);
|
||||
|
||||
readonly string[] paramNames;
|
||||
|
||||
public Route(string method, string path, Func<HttpRequest, Task<HttpResponse>> action)
|
||||
{
|
||||
Method = method;
|
||||
Path = path;
|
||||
Action = action;
|
||||
|
||||
var paramNames = new List<string>();
|
||||
Regex = new Regex("^" + PathRegex.Replace(path, m =>
|
||||
{
|
||||
var p = m.Groups["param"].Value; paramNames.Add(p);
|
||||
return String.Format(@"(?<{0}>\S+)", p);
|
||||
}) + "$");
|
||||
this.paramNames = paramNames.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Http method.
|
||||
/// </summary>
|
||||
/// <value>The Http method.</value>
|
||||
string Method { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resource path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action that will handle the request.
|
||||
/// </summary>
|
||||
/// <value>The action.</value>
|
||||
Func<HttpRequest, Task<HttpResponse>> Action { get; set; }
|
||||
|
||||
Regex Regex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a request should be handled by this match.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c>, if there is match, <c>false</c> otherwise.</returns>
|
||||
/// <param name="request">Request.</param>
|
||||
public bool IsMatch(HttpRequest request)
|
||||
{
|
||||
return request.HttpMethod == Method && Regex.IsMatch(request.Url.AbsolutePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the resource handler registered for this route.
|
||||
/// </summary>
|
||||
/// <returns>The response.</returns>
|
||||
/// <param name="request">The request.</param>
|
||||
public Task<HttpResponse> Invoke(HttpRequest request)
|
||||
{
|
||||
var match = Regex.Match(request.Url.AbsolutePath);
|
||||
var urlParameters = paramNames.ToDictionary(k => k, k => match.Groups[k].Value);
|
||||
//NameValueCollection queryParameters = HttpUtility.ParseQueryString(request.Url.Query);
|
||||
NameValueCollection queryParameters = new NameValueCollection();
|
||||
|
||||
var parameters = request.Parameters as IDictionary<string, Object>;
|
||||
foreach (var kv in urlParameters)
|
||||
{
|
||||
parameters.Add(kv.Key, kv.Value);
|
||||
}
|
||||
foreach (String key in queryParameters.AllKeys)
|
||||
{
|
||||
parameters.Add(key, queryParameters[key]);
|
||||
}
|
||||
return Action.Invoke(request);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,16 +15,7 @@
|
|||
<Compile Include="$(MSBuildThisFileDirectory)Constants.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)EvalRequest.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ExtensionMethods.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HttpServer\HttpHost.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HttpServer\HttpRequest.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HttpServer\HttpResponse.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HttpServer\IRequestProcessor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HttpServer\RequestProcessor.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)HttpServer\Route.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)NetworkUtils.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)TcpCommunicator.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="$(MSBuildThisFileDirectory)HttpServer\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -6,7 +6,6 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using XAMLator.HttpServer;
|
||||
|
||||
namespace XAMLator.Server
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче