From d22084f6ce6e5d09d80c732f131ad14ea8a740ea Mon Sep 17 00:00:00 2001 From: "Gui.H" Date: Mon, 2 May 2022 22:08:56 +0800 Subject: [PATCH] replace log4net by serilog use kestrelMiddleware --- FastTunnel.Client/FastTunnel.Client.csproj | 4 +- FastTunnel.Client/Program.cs | 41 +-- FastTunnel.Client/log4net.config | 72 ------ .../Extensions/ListenOptionsSwapExtensions.cs | 31 +++ .../Extensions/ValueTaskExtensions.cs | 51 ++++ .../Forwarder/ForwarderClientFactory.cs | 107 ++++++++ .../Forwarder/MiddleWare/DuplexPipeStream.cs | 180 +++++++++++++ .../Forwarder/MiddleWare/LoggingStream.cs | 236 ++++++++++++++++++ .../MiddleWare/SwapConnectionMiddleware.cs | 139 +++++++++++ .../Forwarder/MiddleWare/TaskToApm.cs | 115 +++++++++ .../Handlers/Client/SwapHandler.cs | 4 +- .../ClientCertBufferingFeature.cs | 76 ++++++ FastTunnel.Server/FastTunnel.Server.csproj | 6 +- .../FastTunnelConnectionMiddleware.cs | 112 +++++++++ FastTunnel.Server/Program.cs | 58 +++-- .../appsettings.Development.json | 12 + FastTunnel.Server/appsettings.json | 48 ++++ FastTunnel.Server/cmd/install.bat | 12 + FastTunnel.Server/cmd/uninstall.bat | 11 + FastTunnel.Server/log4net.config | 72 ------ 20 files changed, 1207 insertions(+), 180 deletions(-) delete mode 100644 FastTunnel.Client/log4net.config create mode 100644 FastTunnel.Core/Extensions/ListenOptionsSwapExtensions.cs create mode 100644 FastTunnel.Core/Extensions/ValueTaskExtensions.cs create mode 100644 FastTunnel.Core/Forwarder/ForwarderClientFactory.cs create mode 100644 FastTunnel.Core/Forwarder/MiddleWare/DuplexPipeStream.cs create mode 100644 FastTunnel.Core/Forwarder/MiddleWare/LoggingStream.cs create mode 100644 FastTunnel.Core/Forwarder/MiddleWare/SwapConnectionMiddleware.cs create mode 100644 FastTunnel.Core/Forwarder/MiddleWare/TaskToApm.cs create mode 100644 FastTunnel.Server/ClientCertBufferingFeature.cs create mode 100644 FastTunnel.Server/FastTunnelConnectionMiddleware.cs create mode 100644 FastTunnel.Server/appsettings.Development.json create mode 100644 FastTunnel.Server/appsettings.json create mode 100644 FastTunnel.Server/cmd/install.bat create mode 100644 FastTunnel.Server/cmd/uninstall.bat delete mode 100644 FastTunnel.Server/log4net.config diff --git a/FastTunnel.Client/FastTunnel.Client.csproj b/FastTunnel.Client/FastTunnel.Client.csproj index d4c55c0..6919d74 100644 --- a/FastTunnel.Client/FastTunnel.Client.csproj +++ b/FastTunnel.Client/FastTunnel.Client.csproj @@ -7,6 +7,7 @@ + @@ -20,9 +21,6 @@ Always - - Always - Always diff --git a/FastTunnel.Client/Program.cs b/FastTunnel.Client/Program.cs index fdce01e..01ac75b 100644 --- a/FastTunnel.Client/Program.cs +++ b/FastTunnel.Client/Program.cs @@ -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-------------------- }); } diff --git a/FastTunnel.Client/log4net.config b/FastTunnel.Client/log4net.config deleted file mode 100644 index 86e9d65..0000000 --- a/FastTunnel.Client/log4net.config +++ /dev/null @@ -1,72 +0,0 @@ - - - - - Value of priority may be ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FastTunnel.Core/Extensions/ListenOptionsSwapExtensions.cs b/FastTunnel.Core/Extensions/ListenOptionsSwapExtensions.cs new file mode 100644 index 0000000..36016fc --- /dev/null +++ b/FastTunnel.Core/Extensions/ListenOptionsSwapExtensions.cs @@ -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(); + var logger = loggerFactory.CreateLogger(); + var fastTunnelServer = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + + listenOptions.Use(next => new SwapConnectionMiddleware(next, logger, fastTunnelServer).OnConnectionAsync); + return listenOptions; + } +} diff --git a/FastTunnel.Core/Extensions/ValueTaskExtensions.cs b/FastTunnel.Core/Extensions/ValueTaskExtensions.cs new file mode 100644 index 0000000..de597cb --- /dev/null +++ b/FastTunnel.Core/Extensions/ValueTaskExtensions.cs @@ -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 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 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()); + } + } + } +} diff --git a/FastTunnel.Core/Forwarder/ForwarderClientFactory.cs b/FastTunnel.Core/Forwarder/ForwarderClientFactory.cs new file mode 100644 index 0000000..c298726 --- /dev/null +++ b/FastTunnel.Core/Forwarder/ForwarderClientFactory.cs @@ -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 logger; + readonly FastTunnelServer fastTunnelServer; + + public ForwarderClientFactory(ILogger 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 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 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 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 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)); + } + } +} diff --git a/FastTunnel.Core/Forwarder/MiddleWare/DuplexPipeStream.cs b/FastTunnel.Core/Forwarder/MiddleWare/DuplexPipeStream.cs new file mode 100644 index 0000000..8e515ca --- /dev/null +++ b/FastTunnel.Core/Forwarder/MiddleWare/DuplexPipeStream.cs @@ -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 vt = ReadAsyncInternal(new Memory(buffer, offset, count), default); + return vt.IsCompleted ? + vt.Result : + vt.AsTask().GetAwaiter().GetResult(); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + { + return ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override ValueTask ReadAsync(Memory 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 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 ReadAsyncInternal(Memory 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(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); + } +} diff --git a/FastTunnel.Core/Forwarder/MiddleWare/LoggingStream.cs b/FastTunnel.Core/Forwarder/MiddleWare/LoggingStream.cs new file mode 100644 index 0000000..1a99ebc --- /dev/null +++ b/FastTunnel.Core/Forwarder/MiddleWare/LoggingStream.cs @@ -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(buffer, offset, read)); + return read; + } + + public override int Read(Span destination) + { + int read = _inner.Read(destination); + Log("[Read]", destination.Slice(0, read)); + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = await _inner.ReadAsync(buffer.AsMemory(offset, count), cancellationToken); + Log("[ReadAsync]", new ReadOnlySpan(buffer, offset, read)); + return read; + } + + public override async ValueTask ReadAsync(Memory 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(buffer, offset, count)); + _inner.Write(buffer, offset, count); + } + + public override void Write(ReadOnlySpan source) + { + Log("[Write]", source); + _inner.Write(source); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Log("WriteAsync", new ReadOnlySpan(buffer, offset, count)); + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + Log("WriteAsync", source.Span); + return _inner.WriteAsync(source, cancellationToken); + } + + private void Log(string method, ReadOnlySpan 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(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); + } + } +} diff --git a/FastTunnel.Core/Forwarder/MiddleWare/SwapConnectionMiddleware.cs b/FastTunnel.Core/Forwarder/MiddleWare/SwapConnectionMiddleware.cs new file mode 100644 index 0000000..d4d7a05 --- /dev/null +++ b/FastTunnel.Core/Forwarder/MiddleWare/SwapConnectionMiddleware.cs @@ -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 logger; + FastTunnelServer fastTunnelServer; + + public SwapConnectionMiddleware(ConnectionDelegate next, ILogger 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 ReadPipeAsync(ConnectionContext context) + { + var reader = context.Transport.Input; + + bool isProxy = false; + while (true) + { + ReadResult result = await reader.ReadAsync(); + ReadOnlySequence 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 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(); + + var closedAwaiter = new TaskCompletionSource(); + + lifetime.ConnectionClosed.Register((task) => + { + (task as TaskCompletionSource).SetResult(null); + }, closedAwaiter); + + await closedAwaiter.Task; + + logger.LogDebug($"[PROXY]:Closed {requestId}"); + } + + /// + /// + /// + /// + private bool ProcessProxyLine(ReadOnlySequence readOnlySequence) + { + var str = Encoding.UTF8.GetString(readOnlySequence); + + return str.StartsWith("PROXY"); + } +} diff --git a/FastTunnel.Core/Forwarder/MiddleWare/TaskToApm.cs b/FastTunnel.Core/Forwarder/MiddleWare/TaskToApm.cs new file mode 100644 index 0000000..5b2e09d --- /dev/null +++ b/FastTunnel.Core/Forwarder/MiddleWare/TaskToApm.cs @@ -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 + { + /// + /// Marshals the Task as an IAsyncResult, using the supplied callback and state + /// to implement the APM pattern. + /// + /// The Task to be marshaled. + /// The callback to be invoked upon completion. + /// The state to be stored in the IAsyncResult. + /// An IAsyncResult to represent the task's asynchronous operation. + public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) => + new TaskAsyncResult(task, state, callback); + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static void End(IAsyncResult asyncResult) + { + if (asyncResult is TaskAsyncResult twar) + { + twar._task.GetAwaiter().GetResult(); + return; + } + + throw new ArgumentNullException(nameof(asyncResult)); + } + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static TResult End(IAsyncResult asyncResult) + { + if (asyncResult is TaskAsyncResult twar && twar._task is Task task) + { + return task.GetAwaiter().GetResult(); + } + + throw new ArgumentNullException(nameof(asyncResult)); + } + + /// Provides a simple IAsyncResult that wraps a Task. + /// + /// 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. + /// + internal sealed class TaskAsyncResult : IAsyncResult + { + /// The wrapped Task. + internal readonly Task _task; + /// Callback to invoke when the wrapped task completes. + private readonly AsyncCallback? _callback; + + /// Initializes the IAsyncResult with the Task to wrap and the associated object state. + /// The Task to wrap. + /// The new AsyncState value. + /// Callback to invoke when the wrapped task completes. + 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 + } + } + + /// Invokes the callback. + private void InvokeCallback() + { + Debug.Assert(!CompletedSynchronously); + Debug.Assert(_callback != null); + _callback.Invoke(this); + } + + /// Gets a user-defined object that qualifies or contains information about an asynchronous operation. + public object? AsyncState { get; } + /// Gets a value that indicates whether the asynchronous operation completed synchronously. + /// This is set lazily based on whether the has completed by the time this object is created. + public bool CompletedSynchronously { get; } + /// Gets a value that indicates whether the asynchronous operation has completed. + public bool IsCompleted => _task.IsCompleted; + /// Gets a that is used to wait for an asynchronous operation to complete. + public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle; + } + } +} diff --git a/FastTunnel.Core/Handlers/Client/SwapHandler.cs b/FastTunnel.Core/Handlers/Client/SwapHandler.cs index 224f492..d082ebe 100644 --- a/FastTunnel.Core/Handlers/Client/SwapHandler.cs +++ b/FastTunnel.Core/Handlers/Client/SwapHandler.cs @@ -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; diff --git a/FastTunnel.Server/ClientCertBufferingFeature.cs b/FastTunnel.Server/ClientCertBufferingFeature.cs new file mode 100644 index 0000000..f056626 --- /dev/null +++ b/FastTunnel.Server/ClientCertBufferingFeature.cs @@ -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(); + var bodyFeature = context.Features.Get(); + var connectionItems = context.Features.Get(); + + // 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(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 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(); + connectionItems.Items["tls.clientcert.negotiated"] = true; + + return await _tlsFeature.GetClientCertificateAsync(cancellationToken); + } +} diff --git a/FastTunnel.Server/FastTunnel.Server.csproj b/FastTunnel.Server/FastTunnel.Server.csproj index 4b081a7..21d4e78 100644 --- a/FastTunnel.Server/FastTunnel.Server.csproj +++ b/FastTunnel.Server/FastTunnel.Server.csproj @@ -17,13 +17,17 @@ + + + + - + diff --git a/FastTunnel.Server/FastTunnelConnectionMiddleware.cs b/FastTunnel.Server/FastTunnelConnectionMiddleware.cs new file mode 100644 index 0000000..da4315d --- /dev/null +++ b/FastTunnel.Server/FastTunnelConnectionMiddleware.cs @@ -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 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 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(); + } + } + } +} diff --git a/FastTunnel.Server/Program.cs b/FastTunnel.Server/Program.cs index 0a8e82a..e18dd90 100644 --- a/FastTunnel.Server/Program.cs +++ b/FastTunnel.Server/Program.cs @@ -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(); - }) - .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("BASE_PORT") ?? 1270; + options.ListenAnyIP(basePort, listenOptions => + { + //listenOptions.UseConnectionLogging(); + listenOptions.UseFastTunnelSwap(); + }); + }); + + webHostBuilder.UseStartup(); }); } diff --git a/FastTunnel.Server/appsettings.Development.json b/FastTunnel.Server/appsettings.Development.json new file mode 100644 index 0000000..834acb9 --- /dev/null +++ b/FastTunnel.Server/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + // Trace Debug Information Warning Error + "Default": "Debug", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "EnableFileLog": false +} diff --git a/FastTunnel.Server/appsettings.json b/FastTunnel.Server/appsettings.json new file mode 100644 index 0000000..1e0b4e5 --- /dev/null +++ b/FastTunnel.Server/appsettings.json @@ -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" + } + ] + } + } +} diff --git a/FastTunnel.Server/cmd/install.bat b/FastTunnel.Server/cmd/install.bat new file mode 100644 index 0000000..9099578 --- /dev/null +++ b/FastTunnel.Server/cmd/install.bat @@ -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 \ No newline at end of file diff --git a/FastTunnel.Server/cmd/uninstall.bat b/FastTunnel.Server/cmd/uninstall.bat new file mode 100644 index 0000000..27a65bc --- /dev/null +++ b/FastTunnel.Server/cmd/uninstall.bat @@ -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 \ No newline at end of file diff --git a/FastTunnel.Server/log4net.config b/FastTunnel.Server/log4net.config deleted file mode 100644 index 86e9d65..0000000 --- a/FastTunnel.Server/log4net.config +++ /dev/null @@ -1,72 +0,0 @@ - - - - - Value of priority may be ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -