diff --git a/Discord.Net.sln b/Discord.Net.sln index 25b6e0386..6308b4444 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26014.0 @@ -25,6 +25,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E304-46E0-9C3A-294F340DB37D}" +EndProject +Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -143,6 +147,18 @@ Global {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|x64 {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|x86 {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|x86 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.ActiveCfg = Debug|x64 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.Build.0 = Debug|x64 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.ActiveCfg = Debug|x86 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.Build.0 = Debug|x86 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.Build.0 = Release|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.ActiveCfg = Release|x64 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|x64 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -154,5 +170,6 @@ Global {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs index 8563c4035..c75729acf 100644 --- a/src/Discord.Net.Core/AssemblyInfo.cs +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Discord.Net.Relay")] [assembly: InternalsVisibleTo("Discord.Net.Rest")] [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] diff --git a/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs b/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs new file mode 100644 index 000000000..2a1e759c0 --- /dev/null +++ b/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Builder; +using System; + +namespace Discord.Relay +{ + public static class ApplicationBuilderExtensions + { + public static void UseDiscordRelay(this IApplicationBuilder app, Action configAction = null) + { + var server = new RelayServer(configAction); + server.StartAsync(); + app.Use(async (context, next) => + { + if (context.WebSockets.IsWebSocketRequest) + await server.AcceptAsync(context); + await next(); + }); + } + } +} diff --git a/src/Discord.Net.Relay/AssemblyInfo.cs b/src/Discord.Net.Relay/AssemblyInfo.cs new file mode 100644 index 000000000..5e9efa5bc --- /dev/null +++ b/src/Discord.Net.Relay/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Relay/Discord.Net.Relay.csproj b/src/Discord.Net.Relay/Discord.Net.Relay.csproj new file mode 100644 index 000000000..8fee12d14 --- /dev/null +++ b/src/Discord.Net.Relay/Discord.Net.Relay.csproj @@ -0,0 +1,32 @@ + + + 1.0.0 + rc-dev + rc-$(BuildNumber) + netstandard1.3 + Discord.Net.Relay + RogueException + A core Discord.Net library containing the Relay server. + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + Discord.Relay + true + + + + + + + + + + + + $(NoWarn);CS1573;CS1591 + true + true + + \ No newline at end of file diff --git a/src/Discord.Net.Relay/RelayConnection.cs b/src/Discord.Net.Relay/RelayConnection.cs new file mode 100644 index 000000000..ffce74f9c --- /dev/null +++ b/src/Discord.Net.Relay/RelayConnection.cs @@ -0,0 +1,79 @@ +using Discord.API; +using Discord.API.Gateway; +using Discord.Logging; +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using WebSocketClient = System.Net.WebSockets.WebSocket; + +namespace Discord.Relay +{ + public class RelayConnection + { + private readonly RelayServer _server; + private readonly WebSocketClient _socket; + private readonly CancellationTokenSource _cancelToken; + private readonly byte[] _inBuffer, _outBuffer; + private readonly Logger _logger; + + internal RelayConnection(RelayServer server, WebSocketClient socket, int id) + { + _server = server; + _socket = socket; + _cancelToken = new CancellationTokenSource(); + _inBuffer = new byte[4000]; + _outBuffer = new byte[4000]; + _logger = server.LogManager.CreateLogger($"Client #{id}"); + } + + internal async Task RunAsync() + { + await _logger.InfoAsync($"Connected"); + var token = _cancelToken.Token; + try + { + var segment = new ArraySegment(_inBuffer); + + //Send HELLO + await SendAsync(GatewayOpCode.Hello, new HelloEvent { HeartbeatInterval = 15000 }).ConfigureAwait(false); + + while (_socket.State == WebSocketState.Open) + { + var result = await _socket.ReceiveAsync(segment, token).ConfigureAwait(false); + if (result.MessageType == WebSocketMessageType.Close) + await _logger.WarningAsync($"Received Close {result.CloseStatus} ({result.CloseStatusDescription ?? "No Reason"})").ConfigureAwait(false); + else + await _logger.InfoAsync($"Received {result.Count} bytes"); + } + } + catch (OperationCanceledException) + { + try { await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); } + catch { } + } + catch (Exception ex) + { + try { await _socket.CloseAsync(WebSocketCloseStatus.InternalServerError, ex.Message, CancellationToken.None).ConfigureAwait(false); } + catch { } + } + finally + { + await _logger.InfoAsync($"Disconnected"); + } + } + + internal void Stop() + { + _cancelToken.Cancel(); + } + + private async Task SendAsync(GatewayOpCode opCode, object payload) + { + var frame = new SocketFrame { Operation = (int)opCode, Payload = payload }; + var bytes = _server.Serialize(frame, _outBuffer); + var segment = new ArraySegment(_outBuffer, 0, bytes); + await _socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.Relay/RelayServer.cs b/src/Discord.Net.Relay/RelayServer.cs new file mode 100644 index 000000000..4082191fb --- /dev/null +++ b/src/Discord.Net.Relay/RelayServer.cs @@ -0,0 +1,103 @@ +using Discord.API; +using Discord.Logging; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Discord.Rest; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using WebSocketClient = System.Net.WebSockets.WebSocket; + +namespace Discord.Relay +{ + public class RelayServer + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + private readonly HashSet _connections; + private readonly SemaphoreSlim _lock; + private readonly JsonSerializer _serializer; + private readonly DiscordSocketApiClient _discord; + private int _nextId; + + internal LogManager LogManager { get; } + + internal RelayServer(Action configAction) + { + _connections = new HashSet(); + _lock = new SemaphoreSlim(1, 1); + _serializer = new JsonSerializer(); + _discord = new DiscordSocketApiClient( + DefaultRestClientProvider.Instance, + DefaultWebSocketProvider.Instance, + DiscordRestConfig.UserAgent); + configAction?.Invoke(this); + + LogManager = new LogManager(LogSeverity.Debug); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + } + + internal async Task AcceptAsync(HttpContext context) + { + WebSocketClient socket; + try + { + socket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + } + catch { return; } + + var _ = Task.Run(async () => + { + var conn = new RelayConnection(this, socket, Interlocked.Increment(ref _nextId)); + await AddConnection(conn).ConfigureAwait(false); + try + { + await conn.RunAsync().ConfigureAwait(false); + } + finally { await RemoveConnection(conn).ConfigureAwait(false); } + }); + } + + internal void StartAsync() + { + Task.Run(async () => + { + await _discord.ConnectAsync().ConfigureAwait(false); + }); + } + + internal async Task AddConnection(RelayConnection conn) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + _connections.Add(conn); + } + finally { _lock.Release(); } + } + internal async Task RemoveConnection(RelayConnection conn) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + _connections.Remove(conn); + } + finally { _lock.Release(); } + } + + internal int Serialize(object obj, byte[] buffer) + { + using (var stream = new MemoryStream(buffer)) + using (var writer = new StreamWriter(stream)) + { + _serializer.Serialize(writer, obj); + return (int)stream.Position; + } + } + } +} diff --git a/src/Discord.Net.WebSocket/AssemblyInfo.cs b/src/Discord.Net.WebSocket/AssemblyInfo.cs index c6b5997b4..ca3e05e2f 100644 --- a/src/Discord.Net.WebSocket/AssemblyInfo.cs +++ b/src/Discord.Net.WebSocket/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Discord.Net.Relay")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index fcfa76653..cbefd795c 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -33,10 +33,11 @@ namespace Discord.API public ConnectionState ConnectionState { get; private set; } - public DiscordSocketApiClient(RestClientProvider restClientProvider, string userAgent, WebSocketProvider webSocketProvider, - RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) + public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, + string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) : base(restClientProvider, userAgent, defaultRetryMode, serializer) - { + { + _gatewayUrl = url; WebSocketClient = webSocketProvider(); //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) WebSocketClient.BinaryMessage += async (data, index, count) => @@ -115,9 +116,9 @@ namespace Discord.API ConnectionState = ConnectionState.Connected; } - catch (Exception) + catch { - _gatewayUrl = null; //Uncache in case the gateway url changed + _gatewayUrl = null; //Uncache in case the gateway url changed await DisconnectInternalAsync().ConfigureAwait(false); throw; } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index ea0250c9b..c0608a868 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -142,7 +142,7 @@ namespace Discord.WebSocket _largeGuilds = new ConcurrentQueue(); } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) - => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost); protected override async Task OnLoginAsync(TokenType tokenType, string token) { diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index b47f62dca..78a637d0e 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -2,7 +2,6 @@ using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; -using System; namespace Discord.WebSocket { @@ -10,6 +9,9 @@ namespace Discord.WebSocket { public const string GatewayEncoding = "json"; + /// Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint. + public string GatewayHost { get; set; } = null; + /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. public int ConnectionTimeout { get; set; } = 30000; @@ -38,41 +40,8 @@ namespace Discord.WebSocket public DiscordSocketConfig() { -#if NETSTANDARD1_3 - WebSocketProvider = () => - { - try - { - return new DefaultWebSocketClient(); - } - catch (PlatformNotSupportedException ex) - { - throw new PlatformNotSupportedException("The default websocket provider is not supported on this platform.", ex); - } - }; - UdpSocketProvider = () => - { - try - { - return new DefaultUdpSocket(); - } - catch (PlatformNotSupportedException ex) - { - throw new PlatformNotSupportedException("The default UDP provider is not supported on this platform.", ex); - } - }; -#else - WebSocketProvider = () => - { - throw new PlatformNotSupportedException("The default websocket provider is not supported on this platform.\n" + - "You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); - }; - UdpSocketProvider = () => - { - throw new PlatformNotSupportedException("The default UDP provider is not supported on this platform.\n" + - "You must specify a UdpSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); - }; -#endif + WebSocketProvider = DefaultWebSocketProvider.Instance; + UdpSocketProvider = DefaultUdpSocketProvider.Instance; } internal DiscordSocketConfig Clone() => MemberwiseClone() as DiscordSocketConfig;