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
+
+
+
+
-
+
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.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-