mirror of
https://github.com/FastTunnel/FastTunnel.git
synced 2025-02-08 10:59:31 +08:00
replace log4net by serilog
use kestrelMiddleware
This commit is contained in:
parent
f352be9004
commit
d22084f6ce
|
@ -7,6 +7,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0-preview.3.22175.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="6.1.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -20,9 +21,6 @@
|
|||
<None Update="install.bat">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="log4net.config">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="uninstall.bat">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
|
|
@ -10,40 +10,51 @@ using System;
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using FastTunnel.Core;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
|
||||
namespace FastTunnel.Client;
|
||||
|
||||
class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// The initial "bootstrap" logger is able to log errors during start-up. It's completely replaced by the
|
||||
// logger configured in `UseSerilog()` below, once configuration and dependency-injection have both been
|
||||
// set up successfully.
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console()
|
||||
.CreateBootstrapLogger();
|
||||
|
||||
Log.Information("Starting up!");
|
||||
|
||||
try
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
|
||||
Log.Information("Stopped cleanly");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
Log.Fatal(ex, "An unhandled exception occured during bootstrapping");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseWindowsService()
|
||||
.UseSerilog((context, services, configuration) => configuration
|
||||
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
|
||||
.WriteTo.Console())
|
||||
.ConfigureServices((hostContext, services) =>
|
||||
{
|
||||
// -------------------FastTunnel START------------------
|
||||
services.AddFastTunnelClient(hostContext.Configuration.GetSection("ClientSettings"));
|
||||
// -------------------FastTunnel EDN--------------------
|
||||
})
|
||||
.ConfigureLogging((HostBuilderContext context, ILoggingBuilder logging) =>
|
||||
{
|
||||
var enableFileLog = (bool)(context.Configuration.GetSection("EnableFileLog")?.Get(typeof(bool)) ?? false);
|
||||
if (enableFileLog)
|
||||
{
|
||||
logging.ClearProviders();
|
||||
logging.SetMinimumLevel(LogLevel.Trace);
|
||||
logging.AddLog4Net();
|
||||
}
|
||||
// -------------------FastTunnel START------------------
|
||||
services.AddFastTunnelClient(hostContext.Configuration.GetSection("ClientSettings"));
|
||||
// -------------------FastTunnel EDN--------------------
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<log4net>
|
||||
<!-- If you are looking here and want more output, first thing to do is change root/priority/@value to "INFO" or "ALL". -->
|
||||
<root>
|
||||
Value of priority may be ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF.
|
||||
<priority value="ALL" />
|
||||
<appender-ref ref="error-file" />
|
||||
<appender-ref ref="debug-file" />
|
||||
<appender-ref ref="info-console" />
|
||||
</root>
|
||||
|
||||
<!-- Example of turning on the output from a component or namespace. -->
|
||||
<logger name="Common">
|
||||
<appender-ref ref="debugger"/>
|
||||
<priority value="DEBUG" />
|
||||
</logger>
|
||||
|
||||
<appender name="debugger" type="log4net.Appender.DebugAppender">
|
||||
<!-- Sends log messages to Visual Studio if attached. -->
|
||||
<immediateFlush value="true" />
|
||||
<layout type="log4net.Layout.SimpleLayout" />
|
||||
</appender>
|
||||
|
||||
<appender name="info-console" type="log4net.Appender.ConsoleAppender">
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date |%level%| %message%newline" />
|
||||
</layout>
|
||||
|
||||
<filter type="log4net.Filter.LevelRangeFilter">
|
||||
<param name="LevelMin" value="Info"/>
|
||||
<param name="LevelMax" value="Fatal"/>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<appender name="debug-file" type="log4net.Appender.RollingFileAppender">
|
||||
<param name="Encoding" value="utf-8" />
|
||||
<file value="Logs/debug" />
|
||||
<appendToFile value="true" />
|
||||
<!-- Immediate flush on error log, to avoid data loss with sudden termination. -->
|
||||
<immediateFlush value="true" />
|
||||
<staticLogFileName value="false" />
|
||||
<rollingStyle value="Date" />
|
||||
<datepattern value="-yyyy.MM.dd'.log'" />
|
||||
<!-- Prevents Orchard.exe from displaying locking debug messages. -->
|
||||
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date %level% [%thread] %logger - %P{Tenant} - %message%newline" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<appender name="error-file" type="log4net.Appender.RollingFileAppender">
|
||||
<param name="Encoding" value="utf-8" />
|
||||
<file value="Logs/error" />
|
||||
<appendToFile value="true" />
|
||||
<!-- Immediate flush on error log, to avoid data loss with sudden termination. -->
|
||||
<immediateFlush value="true" />
|
||||
<staticLogFileName value="false" />
|
||||
<rollingStyle value="Date" />
|
||||
<datepattern value="-yyyy.MM.dd'.log'" />
|
||||
<!-- Prevents Orchard.exe from displaying locking debug messages. -->
|
||||
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
|
||||
<filter type="log4net.Filter.LevelRangeFilter">
|
||||
<!-- Only ERROR and FATAL log messages end up in this target, even if child loggers accept lower priority. -->
|
||||
<param name="LevelMin" value="Error"/>
|
||||
<param name="LevelMax" value="Fatal"/>
|
||||
</filter>
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date [%thread] %logger - %P{Tenant} - %message [%P{Url}]%newline" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
</log4net>
|
31
FastTunnel.Core/Extensions/ListenOptionsSwapExtensions.cs
Normal file
31
FastTunnel.Core/Extensions/ListenOptionsSwapExtensions.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License").
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://github.com/FastTunnel/FastTunnel/edit/v2/LICENSE
|
||||
// Copyright (c) 2019 Gui.H
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FastTunnel.Core.Client;
|
||||
using FastTunnel.Core.Forwarder.MiddleWare;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FastTunnel.Core.Extensions;
|
||||
|
||||
public static class ListenOptionsSwapExtensions
|
||||
{
|
||||
public static ListenOptions UseFastTunnelSwap(this ListenOptions listenOptions)
|
||||
{
|
||||
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger<SwapConnectionMiddleware>();
|
||||
var fastTunnelServer = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<FastTunnelServer>();
|
||||
|
||||
listenOptions.Use(next => new SwapConnectionMiddleware(next, logger, fastTunnelServer).OnConnectionAsync);
|
||||
return listenOptions;
|
||||
}
|
||||
}
|
51
FastTunnel.Core/Extensions/ValueTaskExtensions.cs
Normal file
51
FastTunnel.Core/Extensions/ValueTaskExtensions.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License").
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://github.com/FastTunnel/FastTunnel/edit/v2/LICENSE
|
||||
// Copyright (c) 2019 Gui.H
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastTunnel.Core.Extensions
|
||||
{
|
||||
internal static class ValueTaskExtensions
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Task GetAsTask(this in ValueTask<FlushResult> valueTask)
|
||||
{
|
||||
// Try to avoid the allocation from AsTask
|
||||
if (valueTask.IsCompletedSuccessfully)
|
||||
{
|
||||
// Signal consumption to the IValueTaskSource
|
||||
valueTask.GetAwaiter().GetResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
return valueTask.AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ValueTask GetAsValueTask(this in ValueTask<FlushResult> valueTask)
|
||||
{
|
||||
// Try to avoid the allocation from AsTask
|
||||
if (valueTask.IsCompletedSuccessfully)
|
||||
{
|
||||
// Signal consumption to the IValueTaskSource
|
||||
valueTask.GetAwaiter().GetResult();
|
||||
return default;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValueTask(valueTask.AsTask());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
107
FastTunnel.Core/Forwarder/ForwarderClientFactory.cs
Normal file
107
FastTunnel.Core/Forwarder/ForwarderClientFactory.cs
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) 2019-2022 Gui.H. https://github.com/FastTunnel/FastTunnel
|
||||
// The FastTunnel licenses this file to you under the Apache License Version 2.0.
|
||||
// For more details,You may obtain License file at: https://github.com/FastTunnel/FastTunnel/blob/v2/LICENSE
|
||||
|
||||
using FastTunnel.Core.Client;
|
||||
using FastTunnel.Core.Extensions;
|
||||
using FastTunnel.Core.Models;
|
||||
using FastTunnel.Core.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Yarp.ReverseProxy.Forwarder;
|
||||
|
||||
namespace FastTunnel.Core.Forwarder
|
||||
{
|
||||
public class ForwarderClientFactory : ForwarderHttpClientFactory
|
||||
{
|
||||
readonly ILogger<ForwarderClientFactory> logger;
|
||||
readonly FastTunnelServer fastTunnelServer;
|
||||
|
||||
public ForwarderClientFactory(ILogger<ForwarderClientFactory> logger, FastTunnelServer fastTunnelServer)
|
||||
{
|
||||
this.fastTunnelServer = fastTunnelServer;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
protected override void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler)
|
||||
{
|
||||
base.ConfigureHandler(context, handler);
|
||||
handler.ConnectCallback = ConnectCallback;
|
||||
}
|
||||
|
||||
private async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var host = context.InitialRequestMessage.RequestUri.Host;
|
||||
|
||||
try
|
||||
{
|
||||
var res = await proxyAsync(host, context, cancellationToken);
|
||||
return res;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Stream> proxyAsync(string host, SocketsHttpConnectionContext context, CancellationToken cancellation)
|
||||
{
|
||||
WebInfo web;
|
||||
if (!fastTunnelServer.WebList.TryGetValue(host, out web))
|
||||
{
|
||||
// 客户端已离线
|
||||
return await OfflinePage(host, context);
|
||||
}
|
||||
|
||||
var msgId = Guid.NewGuid().ToString().Replace("-", "");
|
||||
|
||||
TaskCompletionSource<Stream> tcs = new();
|
||||
logger.LogDebug($"[Http]Swap开始 {msgId}|{host}=>{web.WebConfig.LocalIp}:{web.WebConfig.LocalPort}");
|
||||
tcs.SetTimeOut(10000, () => { logger.LogDebug($"[Proxy TimeOut]:{msgId}"); });
|
||||
|
||||
fastTunnelServer.ResponseTasks.TryAdd(msgId, tcs);
|
||||
|
||||
try
|
||||
{
|
||||
// 发送指令给客户端,等待建立隧道
|
||||
await web.Socket.SendCmdAsync(MessageType.SwapMsg, $"{msgId}|{web.WebConfig.LocalIp}:{web.WebConfig.LocalPort}", cancellation);
|
||||
var res = await tcs.Task;
|
||||
|
||||
logger.LogDebug($"[Http]Swap OK {msgId}");
|
||||
return res;
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{
|
||||
// 通讯异常,返回客户端离线
|
||||
return await OfflinePage(host, context);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fastTunnelServer.ResponseTasks.TryRemove(msgId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async ValueTask<Stream> OfflinePage(string host, SocketsHttpConnectionContext context)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(
|
||||
$"HTTP/1.1 200 OK\r\nContent-Type:text/html; charset=utf-8\r\n\r\n{TunnelResource.Page_Offline}\r\n");
|
||||
|
||||
return await Task.FromResult(new ResponseStream(bytes));
|
||||
}
|
||||
}
|
||||
}
|
180
FastTunnel.Core/Forwarder/MiddleWare/DuplexPipeStream.cs
Normal file
180
FastTunnel.Core/Forwarder/MiddleWare/DuplexPipeStream.cs
Normal file
|
@ -0,0 +1,180 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License").
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://github.com/FastTunnel/FastTunnel/edit/v2/LICENSE
|
||||
// Copyright (c) 2019 Gui.H
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FastTunnel.Core.Extensions;
|
||||
|
||||
namespace FastTunnel.Core.Forwarder.MiddleWare;
|
||||
|
||||
internal class DuplexPipeStream : Stream
|
||||
{
|
||||
private readonly PipeReader _input;
|
||||
private readonly PipeWriter _output;
|
||||
private readonly bool _throwOnCancelled;
|
||||
private volatile bool _cancelCalled;
|
||||
|
||||
public DuplexPipeStream(PipeReader input, PipeWriter output, bool throwOnCancelled = false)
|
||||
{
|
||||
_input = input;
|
||||
_output = output;
|
||||
_throwOnCancelled = throwOnCancelled;
|
||||
}
|
||||
|
||||
public void CancelPendingRead()
|
||||
{
|
||||
_cancelCalled = true;
|
||||
_input.CancelPendingRead();
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
ValueTask<int> vt = ReadAsyncInternal(new Memory<byte>(buffer, offset, count), default);
|
||||
return vt.IsCompleted ?
|
||||
vt.Result :
|
||||
vt.AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ReadAsyncInternal(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ReadAsyncInternal(destination, cancellationToken);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
WriteAsync(buffer, offset, count).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return _output.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).GetAsTask();
|
||||
}
|
||||
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _output.WriteAsync(source, cancellationToken).GetAsValueTask();
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _output.FlushAsync(cancellationToken).GetAsTask();
|
||||
}
|
||||
|
||||
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
|
||||
private async ValueTask<int> ReadAsyncInternal(Memory<byte> destination, CancellationToken cancellationToken)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var result = await _input.ReadAsync(cancellationToken);
|
||||
var readableBuffer = result.Buffer;
|
||||
try
|
||||
{
|
||||
if (_throwOnCancelled && result.IsCanceled && _cancelCalled)
|
||||
{
|
||||
// Reset the bool
|
||||
_cancelCalled = false;
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
|
||||
if (!readableBuffer.IsEmpty)
|
||||
{
|
||||
// buffer.Count is int
|
||||
var count = (int)Math.Min(readableBuffer.Length, destination.Length);
|
||||
readableBuffer = readableBuffer.Slice(0, count);
|
||||
|
||||
var str = Encoding.UTF8.GetString(readableBuffer);
|
||||
|
||||
Console.WriteLine($"[ReadAsyncInternal]:{str}");
|
||||
readableBuffer.CopyTo(destination.Span);
|
||||
return count;
|
||||
}
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_input.AdvanceTo(readableBuffer.End, readableBuffer.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return TaskToApm.End<int>(asyncResult);
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
TaskToApm.End(asyncResult);
|
||||
}
|
||||
}
|
236
FastTunnel.Core/Forwarder/MiddleWare/LoggingStream.cs
Normal file
236
FastTunnel.Core/Forwarder/MiddleWare/LoggingStream.cs
Normal file
|
@ -0,0 +1,236 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License").
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://github.com/FastTunnel/FastTunnel/edit/v2/LICENSE
|
||||
// Copyright (c) 2019 Gui.H
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FastTunnel.Core.Forwarder.MiddleWare
|
||||
{
|
||||
internal sealed class LoggingStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LoggingStream(Stream inner, ILogger logger)
|
||||
{
|
||||
_inner = inner;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.CanRead;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanSeek
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.CanSeek;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.CanWrite;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.Position;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_inner.Position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_inner.Flush();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _inner.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int read = _inner.Read(buffer, offset, count);
|
||||
Log("[Read]", new ReadOnlySpan<byte>(buffer, offset, read));
|
||||
return read;
|
||||
}
|
||||
|
||||
public override int Read(Span<byte> destination)
|
||||
{
|
||||
int read = _inner.Read(destination);
|
||||
Log("[Read]", destination.Slice(0, read));
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
int read = await _inner.ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
|
||||
Log("[ReadAsync]", new ReadOnlySpan<byte>(buffer, offset, read));
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int read = await _inner.ReadAsync(destination, cancellationToken);
|
||||
Log("[ReadAsync]", destination.Span.Slice(0, read));
|
||||
return read;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
return _inner.Seek(offset, origin);
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
_inner.SetLength(value);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
Log("[Write]", new ReadOnlySpan<byte>(buffer, offset, count));
|
||||
_inner.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override void Write(ReadOnlySpan<byte> source)
|
||||
{
|
||||
Log("[Write]", source);
|
||||
_inner.Write(source);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
Log("WriteAsync", new ReadOnlySpan<byte>(buffer, offset, count));
|
||||
return _inner.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Log("WriteAsync", source.Span);
|
||||
return _inner.WriteAsync(source, cancellationToken);
|
||||
}
|
||||
|
||||
private void Log(string method, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(method);
|
||||
builder.Append('[');
|
||||
builder.Append(buffer.Length);
|
||||
builder.Append(']');
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
var charBuilder = new StringBuilder();
|
||||
|
||||
// Write the hex
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
builder.Append(buffer[i].ToString("X2", CultureInfo.InvariantCulture));
|
||||
builder.Append(' ');
|
||||
|
||||
var bufferChar = (char)buffer[i];
|
||||
if (char.IsControl(bufferChar))
|
||||
{
|
||||
charBuilder.Append('.');
|
||||
}
|
||||
else
|
||||
{
|
||||
charBuilder.Append(bufferChar);
|
||||
}
|
||||
|
||||
if ((i + 1) % 16 == 0)
|
||||
{
|
||||
builder.Append(" ");
|
||||
builder.Append(charBuilder);
|
||||
if (i != buffer.Length - 1)
|
||||
{
|
||||
builder.AppendLine();
|
||||
}
|
||||
charBuilder.Clear();
|
||||
}
|
||||
else if ((i + 1) % 8 == 0)
|
||||
{
|
||||
builder.Append(' ');
|
||||
charBuilder.Append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Different than charBuffer.Length since charBuffer contains an extra " " after the 8th byte.
|
||||
var numBytesInLastLine = buffer.Length % 16;
|
||||
|
||||
if (numBytesInLastLine > 0)
|
||||
{
|
||||
// 2 (between hex and char blocks) + num bytes left (3 per byte)
|
||||
var padLength = 2 + (3 * (16 - numBytesInLastLine));
|
||||
// extra for space after 8th byte
|
||||
if (numBytesInLastLine < 8)
|
||||
{
|
||||
padLength++;
|
||||
}
|
||||
|
||||
builder.Append(new string(' ', padLength));
|
||||
builder.Append(charBuilder);
|
||||
}
|
||||
|
||||
_logger.LogInformation(builder.ToString());
|
||||
}
|
||||
|
||||
// The below APM methods call the underlying Read/WriteAsync methods which will still be logged.
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return TaskToApm.End<int>(asyncResult);
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
|
||||
{
|
||||
return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
TaskToApm.End(asyncResult);
|
||||
}
|
||||
}
|
||||
}
|
139
FastTunnel.Core/Forwarder/MiddleWare/SwapConnectionMiddleware.cs
Normal file
139
FastTunnel.Core/Forwarder/MiddleWare/SwapConnectionMiddleware.cs
Normal file
|
@ -0,0 +1,139 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License").
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://github.com/FastTunnel/FastTunnel/edit/v2/LICENSE
|
||||
// Copyright (c) 2019 Gui.H
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FastTunnel.Core.Client;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FastTunnel.Core.Forwarder.MiddleWare;
|
||||
|
||||
internal class SwapConnectionMiddleware
|
||||
{
|
||||
readonly ConnectionDelegate next;
|
||||
readonly ILogger<SwapConnectionMiddleware> logger;
|
||||
FastTunnelServer fastTunnelServer;
|
||||
|
||||
public SwapConnectionMiddleware(ConnectionDelegate next, ILogger<SwapConnectionMiddleware> logger, FastTunnelServer fastTunnelServer)
|
||||
{
|
||||
this.next = next;
|
||||
this.logger = logger;
|
||||
this.fastTunnelServer = fastTunnelServer;
|
||||
}
|
||||
|
||||
internal async Task OnConnectionAsync(ConnectionContext context)
|
||||
{
|
||||
var oldTransport = context.Transport;
|
||||
|
||||
try
|
||||
{
|
||||
if (!await ReadPipeAsync(context))
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Transport = oldTransport;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<bool> ReadPipeAsync(ConnectionContext context)
|
||||
{
|
||||
var reader = context.Transport.Input;
|
||||
|
||||
bool isProxy = false;
|
||||
while (true)
|
||||
{
|
||||
ReadResult result = await reader.ReadAsync();
|
||||
ReadOnlySequence<byte> buffer = result.Buffer;
|
||||
SequencePosition? position = null;
|
||||
|
||||
do
|
||||
{
|
||||
position = buffer.PositionOf((byte)'\n');
|
||||
|
||||
if (position != null)
|
||||
{
|
||||
isProxy = ProcessProxyLine(buffer.Slice(0, position.Value));
|
||||
if (isProxy)
|
||||
{
|
||||
await Swap(buffer, position.Value, context);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Transport.Input.AdvanceTo(buffer.Start, buffer.Start);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (position != null);
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task Swap(ReadOnlySequence<byte> buffer, SequencePosition position, ConnectionContext context)
|
||||
{
|
||||
var firstLineBuffer = buffer.Slice(0, position);
|
||||
var firstLine = Encoding.UTF8.GetString(firstLineBuffer);
|
||||
|
||||
// PROXY /c74eb488a0f54d888e63d85c67428b52 HTTP/1.1
|
||||
var endIndex = firstLine.IndexOf(" ", 7);
|
||||
var requestId = firstLine.Substring(7, endIndex - 7);
|
||||
Console.WriteLine($"[开始进行Swap操作] {requestId}");
|
||||
|
||||
context.Transport.Input.AdvanceTo(buffer.GetPosition(1, position), buffer.GetPosition(1, position));
|
||||
|
||||
if (!fastTunnelServer.ResponseTasks.TryRemove(requestId, out var responseForYarp))
|
||||
{
|
||||
logger.LogError($"[PROXY]:RequestId不存在 {requestId}");
|
||||
return;
|
||||
};
|
||||
|
||||
using var reverseConnection = new DuplexPipeStream(context.Transport.Input, context.Transport.Output, true);
|
||||
responseForYarp.TrySetResult(reverseConnection);
|
||||
|
||||
var lifetime = context.Features.Get<IConnectionLifetimeFeature>();
|
||||
|
||||
var closedAwaiter = new TaskCompletionSource<object>();
|
||||
|
||||
lifetime.ConnectionClosed.Register((task) =>
|
||||
{
|
||||
(task as TaskCompletionSource<object>).SetResult(null);
|
||||
}, closedAwaiter);
|
||||
|
||||
await closedAwaiter.Task;
|
||||
|
||||
logger.LogDebug($"[PROXY]:Closed {requestId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="readOnlySequence"></param>
|
||||
private bool ProcessProxyLine(ReadOnlySequence<byte> readOnlySequence)
|
||||
{
|
||||
var str = Encoding.UTF8.GetString(readOnlySequence);
|
||||
|
||||
return str.StartsWith("PROXY");
|
||||
}
|
||||
}
|
115
FastTunnel.Core/Forwarder/MiddleWare/TaskToApm.cs
Normal file
115
FastTunnel.Core/Forwarder/MiddleWare/TaskToApm.cs
Normal file
|
@ -0,0 +1,115 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License").
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://github.com/FastTunnel/FastTunnel/edit/v2/LICENSE
|
||||
// Copyright (c) 2019 Gui.H
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FastTunnel.Core.Forwarder.MiddleWare
|
||||
{
|
||||
internal static class TaskToApm
|
||||
{
|
||||
/// <summary>
|
||||
/// Marshals the Task as an IAsyncResult, using the supplied callback and state
|
||||
/// to implement the APM pattern.
|
||||
/// </summary>
|
||||
/// <param name="task">The Task to be marshaled.</param>
|
||||
/// <param name="callback">The callback to be invoked upon completion.</param>
|
||||
/// <param name="state">The state to be stored in the IAsyncResult.</param>
|
||||
/// <returns>An IAsyncResult to represent the task's asynchronous operation.</returns>
|
||||
public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) =>
|
||||
new TaskAsyncResult(task, state, callback);
|
||||
|
||||
/// <summary>Processes an IAsyncResult returned by Begin.</summary>
|
||||
/// <param name="asyncResult">The IAsyncResult to unwrap.</param>
|
||||
public static void End(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult is TaskAsyncResult twar)
|
||||
{
|
||||
twar._task.GetAwaiter().GetResult();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentNullException(nameof(asyncResult));
|
||||
}
|
||||
|
||||
/// <summary>Processes an IAsyncResult returned by Begin.</summary>
|
||||
/// <param name="asyncResult">The IAsyncResult to unwrap.</param>
|
||||
public static TResult End<TResult>(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult is TaskAsyncResult twar && twar._task is Task<TResult> task)
|
||||
{
|
||||
return task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
throw new ArgumentNullException(nameof(asyncResult));
|
||||
}
|
||||
|
||||
/// <summary>Provides a simple IAsyncResult that wraps a Task.</summary>
|
||||
/// <remarks>
|
||||
/// We could use the Task as the IAsyncResult if the Task's AsyncState is the same as the object state,
|
||||
/// but that's very rare, in particular in a situation where someone cares about allocation, and always
|
||||
/// using TaskAsyncResult simplifies things and enables additional optimizations.
|
||||
/// </remarks>
|
||||
internal sealed class TaskAsyncResult : IAsyncResult
|
||||
{
|
||||
/// <summary>The wrapped Task.</summary>
|
||||
internal readonly Task _task;
|
||||
/// <summary>Callback to invoke when the wrapped task completes.</summary>
|
||||
private readonly AsyncCallback? _callback;
|
||||
|
||||
/// <summary>Initializes the IAsyncResult with the Task to wrap and the associated object state.</summary>
|
||||
/// <param name="task">The Task to wrap.</param>
|
||||
/// <param name="state">The new AsyncState value.</param>
|
||||
/// <param name="callback">Callback to invoke when the wrapped task completes.</param>
|
||||
internal TaskAsyncResult(Task task, object? state, AsyncCallback? callback)
|
||||
{
|
||||
Debug.Assert(task != null);
|
||||
_task = task;
|
||||
AsyncState = state;
|
||||
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
// Synchronous completion. Invoke the callback. No need to store it.
|
||||
CompletedSynchronously = true;
|
||||
callback?.Invoke(this);
|
||||
}
|
||||
else if (callback != null)
|
||||
{
|
||||
// Asynchronous completion, and we have a callback; schedule it. We use OnCompleted rather than ContinueWith in
|
||||
// order to avoid running synchronously if the task has already completed by the time we get here but still run
|
||||
// synchronously as part of the task's completion if the task completes after (the more common case).
|
||||
_callback = callback;
|
||||
_task.ConfigureAwait(continueOnCapturedContext: false)
|
||||
.GetAwaiter()
|
||||
.OnCompleted(InvokeCallback); // allocates a delegate, but avoids a closure
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invokes the callback.</summary>
|
||||
private void InvokeCallback()
|
||||
{
|
||||
Debug.Assert(!CompletedSynchronously);
|
||||
Debug.Assert(_callback != null);
|
||||
_callback.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>Gets a user-defined object that qualifies or contains information about an asynchronous operation.</summary>
|
||||
public object? AsyncState { get; }
|
||||
/// <summary>Gets a value that indicates whether the asynchronous operation completed synchronously.</summary>
|
||||
/// <remarks>This is set lazily based on whether the <see cref="_task"/> has completed by the time this object is created.</remarks>
|
||||
public bool CompletedSynchronously { get; }
|
||||
/// <summary>Gets a value that indicates whether the asynchronous operation has completed.</summary>
|
||||
public bool IsCompleted => _task.IsCompleted;
|
||||
/// <summary>Gets a <see cref="WaitHandle"/> that is used to wait for an asynchronous operation to complete.</summary>
|
||||
public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2019-2022 Gui.H. https://github.com/FastTunnel/FastTunnel
|
||||
// Copyright (c) 2019-2022 Gui.H. https://github.com/FastTunnel/FastTunnel
|
||||
// The FastTunnel licenses this file to you under the Apache License Version 2.0.
|
||||
// For more details,You may obtain License file at: https://github.com/FastTunnel/FastTunnel/blob/v2/LICENSE
|
||||
|
||||
|
@ -65,7 +65,7 @@ namespace FastTunnel.Core.Handlers.Client
|
|||
serverStream = sslStream;
|
||||
}
|
||||
|
||||
var reverse = $"PROXY /{requestId} HTTP/1.1\r\nHost: {cleint.Server.ServerAddr}:{cleint.Server.ServerPort}\r\n\r\n";
|
||||
var reverse = $"PROXY /{requestId} HTTP/1.1\r\n";
|
||||
var requestMsg = Encoding.UTF8.GetBytes(reverse);
|
||||
await serverStream.WriteAsync(requestMsg, cancellationToken);
|
||||
return serverStream;
|
||||
|
|
76
FastTunnel.Server/ClientCertBufferingFeature.cs
Normal file
76
FastTunnel.Server/ClientCertBufferingFeature.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace FastTunnel.Server;
|
||||
|
||||
internal static class ClientCertBufferingExtensions
|
||||
{
|
||||
// Buffers HTTP/1.x request bodies received over TLS (https) if a client certificate needs to be negotiated.
|
||||
// This avoids the issue where POST data is received during the certificate negotiation:
|
||||
// InvalidOperationException: Received data during renegotiation.
|
||||
public static IApplicationBuilder UseClientCertBuffering(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.Use((context, next) =>
|
||||
{
|
||||
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
|
||||
var bodyFeature = context.Features.Get<IHttpRequestBodyDetectionFeature>();
|
||||
var connectionItems = context.Features.Get<IConnectionItemsFeature>();
|
||||
|
||||
// Look for TLS connections that don't already have a client cert, and requests that could have a body.
|
||||
if (tlsFeature != null && tlsFeature.ClientCertificate == null && bodyFeature.CanHaveBody
|
||||
&& !connectionItems.Items.TryGetValue("tls.clientcert.negotiated", out var _))
|
||||
{
|
||||
context.Features.Set<ITlsConnectionFeature>(new ClientCertBufferingFeature(tlsFeature, context));
|
||||
}
|
||||
|
||||
return next(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class ClientCertBufferingFeature : ITlsConnectionFeature
|
||||
{
|
||||
private readonly ITlsConnectionFeature _tlsFeature;
|
||||
private readonly HttpContext _context;
|
||||
|
||||
public ClientCertBufferingFeature(ITlsConnectionFeature tlsFeature, HttpContext context)
|
||||
{
|
||||
_tlsFeature = tlsFeature;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public X509Certificate2 ClientCertificate
|
||||
{
|
||||
get => _tlsFeature.ClientCertificate;
|
||||
set => _tlsFeature.ClientCertificate = value;
|
||||
}
|
||||
|
||||
public async Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Note: This doesn't set its own size limit for the buffering or draining, it relies on the server's
|
||||
// 30mb default request size limit.
|
||||
if (!_context.Request.Body.CanSeek)
|
||||
{
|
||||
_context.Request.EnableBuffering();
|
||||
}
|
||||
|
||||
var body = _context.Request.Body;
|
||||
await body.DrainAsync(cancellationToken);
|
||||
body.Position = 0;
|
||||
|
||||
// Negative caching, prevent buffering on future requests even if the client does not give a cert when prompted.
|
||||
var connectionItems = _context.Features.Get<IConnectionItemsFeature>();
|
||||
connectionItems.Items["tls.clientcert.negotiated"] = true;
|
||||
|
||||
return await _tlsFeature.GetClientCertificateAsync(cancellationToken);
|
||||
}
|
||||
}
|
|
@ -17,13 +17,17 @@
|
|||
<None Remove="logs\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="FastTunnelConnectionMiddleware.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0-preview.3.22175.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0-preview.3.22175.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="7.0.0-preview.3.22175.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)'=='Debug'">
|
||||
|
|
112
FastTunnel.Server/FastTunnelConnectionMiddleware.cs
Normal file
112
FastTunnel.Server/FastTunnelConnectionMiddleware.cs
Normal file
|
@ -0,0 +1,112 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License").
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://github.com/FastTunnel/FastTunnel/edit/v2/LICENSE
|
||||
// Copyright (c) 2019 Gui.H
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
|
||||
namespace FastTunnel.Server
|
||||
{
|
||||
public class FastTunnelConnectionMiddleware
|
||||
{
|
||||
private ConnectionDelegate next;
|
||||
private int index;
|
||||
|
||||
public FastTunnelConnectionMiddleware(ConnectionDelegate next, int index)
|
||||
{
|
||||
this.next = next;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
PipeReader _input;
|
||||
internal async Task OnConnectionAsync(ConnectionContext context)
|
||||
{
|
||||
var oldTransport = context.Transport;
|
||||
_input = oldTransport.Input;
|
||||
|
||||
await ReadPipeAsync(_input);
|
||||
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Transport = oldTransport;
|
||||
}
|
||||
}
|
||||
|
||||
async Task ReadPipeAsync(PipeReader reader)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
ReadResult result = await reader.ReadAsync();
|
||||
|
||||
ReadOnlySequence<byte> buffer = result.Buffer;
|
||||
SequencePosition? position = null;
|
||||
|
||||
do
|
||||
{
|
||||
// 在缓冲数据中查找找一个行末尾
|
||||
position = buffer.PositionOf((byte)'\r\n');
|
||||
|
||||
if (position != null)
|
||||
{
|
||||
// 处理这一行
|
||||
ProcessLine(buffer.Slice(0, position.Value));
|
||||
|
||||
// 跳过 这一行+\n (basically position 主要位置?)
|
||||
buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
|
||||
}
|
||||
}
|
||||
while (position != null);
|
||||
|
||||
// 告诉PipeReader我们以及处理多少缓冲
|
||||
reader.AdvanceTo(buffer.Start, buffer.End);
|
||||
|
||||
// 如果没有更多的数据,停止都去
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 将PipeReader标记为完成
|
||||
reader.Complete();
|
||||
}
|
||||
|
||||
private void ProcessLine(ReadOnlySequence<byte> readOnlySequence)
|
||||
{
|
||||
var str = Encoding.UTF8.GetString(readOnlySequence);
|
||||
|
||||
Console.WriteLine($"[Handle] {str}");
|
||||
}
|
||||
|
||||
public class TestDuplexPipe : IDuplexPipe, IDisposable
|
||||
{
|
||||
public TestDuplexPipe(IDuplexPipe Transport)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public PipeReader Input => throw new NotImplementedException();
|
||||
|
||||
public PipeWriter Output => throw new NotImplementedException();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Input.CompleteAsync();
|
||||
Output.CompleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,19 +8,49 @@ using Microsoft.AspNetCore.Hosting;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FastTunnel.Core.Extensions;
|
||||
using Serilog;
|
||||
using System;
|
||||
|
||||
namespace FastTunnel.Server;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
// The initial "bootstrap" logger is able to log errors during start-up. It's completely replaced by the
|
||||
// logger configured in `UseSerilog()` below, once configuration and dependency-injection have both been
|
||||
// set up successfully.
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console()
|
||||
.CreateBootstrapLogger();
|
||||
|
||||
Log.Information("Starting up!");
|
||||
|
||||
try
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
|
||||
Log.Information("Stopped cleanly");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "An unhandled exception occured during bootstrapping");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseWindowsService()
|
||||
.UseSerilog((context, services, configuration) => configuration
|
||||
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
|
||||
.WriteTo.Console())
|
||||
.ConfigureWebHost(webHostBuilder =>
|
||||
{
|
||||
webHostBuilder.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
|
@ -29,19 +59,17 @@ public class Program
|
|||
config.AddJsonFile("config/appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"config/appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
});
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
})
|
||||
.ConfigureLogging((HostBuilderContext context, ILoggingBuilder logging) =>
|
||||
{
|
||||
var enableFileLog = (bool)context.Configuration.GetSection("EnableFileLog").Get(typeof(bool));
|
||||
if (enableFileLog)
|
||||
|
||||
webHostBuilder.UseKestrel((context, options) =>
|
||||
{
|
||||
logging.ClearProviders();
|
||||
logging.SetMinimumLevel(LogLevel.Trace);
|
||||
logging.AddLog4Net();
|
||||
}
|
||||
var basePort = context.Configuration.GetValue<int?>("BASE_PORT") ?? 1270;
|
||||
options.ListenAnyIP(basePort, listenOptions =>
|
||||
{
|
||||
//listenOptions.UseConnectionLogging();
|
||||
listenOptions.UseFastTunnelSwap();
|
||||
});
|
||||
});
|
||||
|
||||
webHostBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
|
|
12
FastTunnel.Server/appsettings.Development.json
Normal file
12
FastTunnel.Server/appsettings.Development.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
// Trace Debug Information Warning Error
|
||||
"Default": "Debug",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"EnableFileLog": false
|
||||
}
|
48
FastTunnel.Server/appsettings.json
Normal file
48
FastTunnel.Server/appsettings.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
// Trace Debug Information Warning Error
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
// Http&客户端通讯端口
|
||||
"BASE_PORT": 1270,
|
||||
// 是否启用文件日志输出
|
||||
"EnableFileLog": false,
|
||||
"FastTunnel": {
|
||||
// 可选,绑定的根域名,
|
||||
// 客户端需配置SubDomain,实现 ${SubDomain}.${WebDomain}访问内网的站点,注意:需要通过域名访问网站时必选。
|
||||
"WebDomain": "test.cc",
|
||||
|
||||
// 可选,访问白名单,为空时:所有人有权限访问,不为空时:不在白名单的ip拒绝。
|
||||
"WebAllowAccessIps": [ "192.168.0.101" ],
|
||||
|
||||
// 可选,是否开启端口转发代理,禁用后不处理Forward类型端口转发.默认false。
|
||||
"EnableForward": true,
|
||||
|
||||
// 可选,当不为空时,客户端也必须携带Tokens中的任意一个token,否则拒绝登录。
|
||||
"Tokens": [ "TOKEN_FOR_CLIENT_AUTHENTICATION" ],
|
||||
|
||||
/**
|
||||
* 访问api接口的JWT配置
|
||||
*/
|
||||
"Api": {
|
||||
"JWT": {
|
||||
"ClockSkew": 10,
|
||||
"ValidAudience": "https://suidao.io",
|
||||
"ValidIssuer": "FastTunnel",
|
||||
"IssuerSigningKey": "This is IssuerSigningKey",
|
||||
"Expires": 120
|
||||
},
|
||||
"Accounts": [
|
||||
{
|
||||
"Name": "admin",
|
||||
"Password": "admin123"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
12
FastTunnel.Server/cmd/install.bat
Normal file
12
FastTunnel.Server/cmd/install.bat
Normal file
|
@ -0,0 +1,12 @@
|
|||
CHCP 65001
|
||||
@echo off
|
||||
color 0e
|
||||
@echo ==================================
|
||||
@echo 提醒:请右键本文件,用管理员方式打开。
|
||||
@echo ==================================
|
||||
@echo Start Install FastTunnel.Server
|
||||
|
||||
sc create FastTunnel.Server binPath=%~dp0\FastTunnel.Server.exe start= auto
|
||||
sc description FastTunnel.Server "FastTunnel-开源内网穿透服务,仓库地址:https://github.com/SpringHgui/FastTunnel star项目以支持作者"
|
||||
Net Start FastTunnel.Server
|
||||
pause
|
11
FastTunnel.Server/cmd/uninstall.bat
Normal file
11
FastTunnel.Server/cmd/uninstall.bat
Normal file
|
@ -0,0 +1,11 @@
|
|||
CHCP 65001
|
||||
@echo off
|
||||
color 0e
|
||||
@echo ==================================
|
||||
@echo 提醒:请右键本文件,用管理员方式打开。
|
||||
@echo ==================================
|
||||
@echo Start Remove FastTunnel.Server
|
||||
|
||||
Net stop FastTunnel.Server
|
||||
sc delete FastTunnel.Server
|
||||
pause
|
|
@ -1,72 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<log4net>
|
||||
<!-- If you are looking here and want more output, first thing to do is change root/priority/@value to "INFO" or "ALL". -->
|
||||
<root>
|
||||
Value of priority may be ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF.
|
||||
<priority value="ALL" />
|
||||
<appender-ref ref="error-file" />
|
||||
<appender-ref ref="debug-file" />
|
||||
<appender-ref ref="info-console" />
|
||||
</root>
|
||||
|
||||
<!-- Example of turning on the output from a component or namespace. -->
|
||||
<logger name="Common">
|
||||
<appender-ref ref="debugger"/>
|
||||
<priority value="DEBUG" />
|
||||
</logger>
|
||||
|
||||
<appender name="debugger" type="log4net.Appender.DebugAppender">
|
||||
<!-- Sends log messages to Visual Studio if attached. -->
|
||||
<immediateFlush value="true" />
|
||||
<layout type="log4net.Layout.SimpleLayout" />
|
||||
</appender>
|
||||
|
||||
<appender name="info-console" type="log4net.Appender.ConsoleAppender">
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date |%level%| %message%newline" />
|
||||
</layout>
|
||||
|
||||
<filter type="log4net.Filter.LevelRangeFilter">
|
||||
<param name="LevelMin" value="Info"/>
|
||||
<param name="LevelMax" value="Fatal"/>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<appender name="debug-file" type="log4net.Appender.RollingFileAppender">
|
||||
<param name="Encoding" value="utf-8" />
|
||||
<file value="Logs/debug" />
|
||||
<appendToFile value="true" />
|
||||
<!-- Immediate flush on error log, to avoid data loss with sudden termination. -->
|
||||
<immediateFlush value="true" />
|
||||
<staticLogFileName value="false" />
|
||||
<rollingStyle value="Date" />
|
||||
<datepattern value="-yyyy.MM.dd'.log'" />
|
||||
<!-- Prevents Orchard.exe from displaying locking debug messages. -->
|
||||
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date %level% [%thread] %logger - %P{Tenant} - %message%newline" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<appender name="error-file" type="log4net.Appender.RollingFileAppender">
|
||||
<param name="Encoding" value="utf-8" />
|
||||
<file value="Logs/error" />
|
||||
<appendToFile value="true" />
|
||||
<!-- Immediate flush on error log, to avoid data loss with sudden termination. -->
|
||||
<immediateFlush value="true" />
|
||||
<staticLogFileName value="false" />
|
||||
<rollingStyle value="Date" />
|
||||
<datepattern value="-yyyy.MM.dd'.log'" />
|
||||
<!-- Prevents Orchard.exe from displaying locking debug messages. -->
|
||||
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
|
||||
<filter type="log4net.Filter.LevelRangeFilter">
|
||||
<!-- Only ERROR and FATAL log messages end up in this target, even if child loggers accept lower priority. -->
|
||||
<param name="LevelMin" value="Error"/>
|
||||
<param name="LevelMax" value="Fatal"/>
|
||||
</filter>
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<conversionPattern value="%date [%thread] %logger - %P{Tenant} - %message [%P{Url}]%newline" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
</log4net>
|
Loading…
Reference in New Issue
Block a user