replace log4net by serilog

use kestrelMiddleware
This commit is contained in:
Gui.H 2022-05-02 22:08:56 +08:00
parent f352be9004
commit d22084f6ce
20 changed files with 1207 additions and 180 deletions

View File

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

View File

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

View File

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

View 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;
}
}

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

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

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

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

View 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");
}
}

View 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;
}
}
}

View File

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

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

View File

@ -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'">

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

View File

@ -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)
{
// 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 =>
webHostBuilder.UseKestrel((context, options) =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureLogging((HostBuilderContext context, ILoggingBuilder logging) =>
var basePort = context.Configuration.GetValue<int?>("BASE_PORT") ?? 1270;
options.ListenAnyIP(basePort, listenOptions =>
{
var enableFileLog = (bool)context.Configuration.GetSection("EnableFileLog").Get(typeof(bool));
if (enableFileLog)
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.Trace);
logging.AddLog4Net();
}
//listenOptions.UseConnectionLogging();
listenOptions.UseFastTunnelSwap();
});
});
webHostBuilder.UseStartup<Startup>();
});
}

View File

@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
// Trace Debug Information Warning Error
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"EnableFileLog": false
}

View 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,
// Tokenstoken
"Tokens": [ "TOKEN_FOR_CLIENT_AUTHENTICATION" ],
/**
* 访apiJWT
*/
"Api": {
"JWT": {
"ClockSkew": 10,
"ValidAudience": "https://suidao.io",
"ValidIssuer": "FastTunnel",
"IssuerSigningKey": "This is IssuerSigningKey",
"Expires": 120
},
"Accounts": [
{
"Name": "admin",
"Password": "admin123"
}
]
}
}
}

View 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

View 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

View File

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