>";
96 | if (frame.MessageType == WebSocketMessageType.Text)
97 | {
98 | content = Encoding.UTF8.GetString(buffer, 0, frame.Count);
99 | }
100 | message = $"{frame.MessageType}: Len={frame.Count}, Fin={frame.EndOfMessage}: {content}";
101 | }
102 | logger.LogDebug("Received Frame " + message);
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/samples/EchoApp/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 | WebSocket Test Page
15 | Ready to connect...
16 |
17 | WebSocket Server URL:
18 |
19 | Connect
20 |
21 |
22 | Message to send:
23 |
24 | Send
25 | Close Socket
26 |
27 |
28 | Note: When connected to the default server (i.e. the server in the address bar ;)), the message "ServerClose" will cause the server to close the connection. Similarly, the message "ServerAbort" will cause the server to forcibly terminate the connection without a closing handshake
29 |
30 | Communication Log
31 |
32 |
33 |
34 | From
35 | To
36 | Data
37 |
38 |
39 |
40 |
41 |
42 |
43 |
150 |
151 |
--------------------------------------------------------------------------------
/samples/TestServer/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/samples/TestServer/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Net.WebSockets;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace TestServer
11 | {
12 | class Program
13 | {
14 | static void Main(string[] args)
15 | {
16 | RunEchoServer().Wait();
17 | }
18 |
19 | private static async Task RunEchoServer()
20 | {
21 | HttpListener listener = new HttpListener();
22 | listener.Prefixes.Add("http://localhost:12345/");
23 | listener.Start();
24 | Console.WriteLine("Started");
25 |
26 | while (true)
27 | {
28 | HttpListenerContext context = listener.GetContext();
29 | if (!context.Request.IsWebSocketRequest)
30 | {
31 | context.Response.Close();
32 | continue;
33 | }
34 | Console.WriteLine("Accepted");
35 |
36 | var wsContext = await context.AcceptWebSocketAsync(null);
37 | var webSocket = wsContext.WebSocket;
38 |
39 | byte[] buffer = new byte[1024];
40 | WebSocketReceiveResult received = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
41 |
42 | while (received.MessageType != WebSocketMessageType.Close)
43 | {
44 | Console.WriteLine($"Echoing {received.Count} bytes received in a {received.MessageType} message; Fin={received.EndOfMessage}");
45 | // Echo anything we receive
46 | await webSocket.SendAsync(new ArraySegment(buffer, 0, received.Count), received.MessageType, received.EndOfMessage, CancellationToken.None);
47 |
48 | received = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
49 | }
50 |
51 | await webSocket.CloseAsync(received.CloseStatus.Value, received.CloseStatusDescription, CancellationToken.None);
52 |
53 | webSocket.Dispose();
54 | Console.WriteLine("Finished");
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/samples/TestServer/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("TestServer")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("TestServer")]
13 | [assembly: AssemblyCopyright("Copyright © 2014")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("ffe69337-e3b4-4625-8244-36bd609742ba")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/samples/TestServer/TestServer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}
8 | Exe
9 | Properties
10 | TestServer
11 | TestServer
12 | v4.6.1
13 | 512
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
58 |
59 |
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/ExtendedWebSocketAcceptContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using Microsoft.AspNetCore.Http;
6 |
7 | namespace Microsoft.AspNetCore.WebSockets
8 | {
9 | public class ExtendedWebSocketAcceptContext : WebSocketAcceptContext
10 | {
11 | public override string SubProtocol { get; set; }
12 | public int? ReceiveBufferSize { get; set; }
13 | public TimeSpan? KeepAliveInterval { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/Internal/Constants.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.AspNetCore.WebSockets.Internal
5 | {
6 | public static class Constants
7 | {
8 | public static class Headers
9 | {
10 | public const string Upgrade = "Upgrade";
11 | public const string UpgradeWebSocket = "websocket";
12 | public const string Connection = "Connection";
13 | public const string ConnectionUpgrade = "Upgrade";
14 | public const string SecWebSocketKey = "Sec-WebSocket-Key";
15 | public const string SecWebSocketVersion = "Sec-WebSocket-Version";
16 | public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
17 | public const string SecWebSocketAccept = "Sec-WebSocket-Accept";
18 | public const string SupportedVersion = "13";
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/Internal/HandshakeHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Security.Cryptography;
7 | using System.Text;
8 | using Microsoft.AspNetCore.Http;
9 |
10 | namespace Microsoft.AspNetCore.WebSockets.Internal
11 | {
12 | internal static class HandshakeHelpers
13 | {
14 | ///
15 | /// Gets request headers needed process the handshake on the server.
16 | ///
17 | public static readonly IEnumerable NeededHeaders = new[]
18 | {
19 | Constants.Headers.Upgrade,
20 | Constants.Headers.Connection,
21 | Constants.Headers.SecWebSocketKey,
22 | Constants.Headers.SecWebSocketVersion
23 | };
24 |
25 | // Verify Method, Upgrade, Connection, version, key, etc..
26 | public static bool CheckSupportedWebSocketRequest(string method, IEnumerable> headers)
27 | {
28 | bool validUpgrade = false, validConnection = false, validKey = false, validVersion = false;
29 |
30 | if (!string.Equals("GET", method, StringComparison.OrdinalIgnoreCase))
31 | {
32 | return false;
33 | }
34 |
35 | foreach (var pair in headers)
36 | {
37 | if (string.Equals(Constants.Headers.Connection, pair.Key, StringComparison.OrdinalIgnoreCase))
38 | {
39 | if (string.Equals(Constants.Headers.ConnectionUpgrade, pair.Value, StringComparison.OrdinalIgnoreCase))
40 | {
41 | validConnection = true;
42 | }
43 | }
44 | else if (string.Equals(Constants.Headers.Upgrade, pair.Key, StringComparison.OrdinalIgnoreCase))
45 | {
46 | if (string.Equals(Constants.Headers.UpgradeWebSocket, pair.Value, StringComparison.OrdinalIgnoreCase))
47 | {
48 | validUpgrade = true;
49 | }
50 | }
51 | else if (string.Equals(Constants.Headers.SecWebSocketVersion, pair.Key, StringComparison.OrdinalIgnoreCase))
52 | {
53 | if (string.Equals(Constants.Headers.SupportedVersion, pair.Value, StringComparison.OrdinalIgnoreCase))
54 | {
55 | validVersion = true;
56 | }
57 | }
58 | else if (string.Equals(Constants.Headers.SecWebSocketKey, pair.Key, StringComparison.OrdinalIgnoreCase))
59 | {
60 | validKey = IsRequestKeyValid(pair.Value);
61 | }
62 | }
63 |
64 | return validConnection && validUpgrade && validVersion && validKey;
65 | }
66 |
67 | public static void GenerateResponseHeaders(string key, string subProtocol, IHeaderDictionary headers)
68 | {
69 | headers[Constants.Headers.Connection] = Constants.Headers.ConnectionUpgrade;
70 | headers[Constants.Headers.Upgrade] = Constants.Headers.UpgradeWebSocket;
71 | headers[Constants.Headers.SecWebSocketAccept] = CreateResponseKey(key);
72 | if (!string.IsNullOrWhiteSpace(subProtocol))
73 | {
74 | headers[Constants.Headers.SecWebSocketProtocol] = subProtocol;
75 | }
76 | }
77 |
78 | ///
79 | /// Validates the Sec-WebSocket-Key request header
80 | ///
81 | ///
82 | ///
83 | public static bool IsRequestKeyValid(string value)
84 | {
85 | if (string.IsNullOrWhiteSpace(value))
86 | {
87 | return false;
88 | }
89 | try
90 | {
91 | byte[] data = Convert.FromBase64String(value);
92 | return data.Length == 16;
93 | }
94 | catch (Exception)
95 | {
96 | return false;
97 | }
98 | }
99 |
100 | public static string CreateResponseKey(string requestKey)
101 | {
102 | // "The value of this header field is constructed by concatenating /key/, defined above in step 4
103 | // in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of
104 | // this concatenated value to obtain a 20-byte value and base64-encoding"
105 | // https://tools.ietf.org/html/rfc6455#section-4.2.2
106 |
107 | if (requestKey == null)
108 | {
109 | throw new ArgumentNullException(nameof(requestKey));
110 | }
111 |
112 | using (var algorithm = SHA1.Create())
113 | {
114 | string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
115 | byte[] mergedBytes = Encoding.UTF8.GetBytes(merged);
116 | byte[] hashedBytes = algorithm.ComputeHash(mergedBytes);
117 | return Convert.ToBase64String(hashedBytes);
118 | }
119 | }
120 | }
121 | }
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/Microsoft.AspNetCore.WebSockets.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ASP.NET Core web socket middleware for use on top of opaque servers.
5 | netstandard2.0
6 | $(NoWarn);CS1591
7 | true
8 | true
9 | aspnetcore
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddleware.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Net.WebSockets;
9 | using System.Threading.Tasks;
10 | using Microsoft.AspNetCore.Builder;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.AspNetCore.Http.Features;
13 | using Microsoft.AspNetCore.WebSockets.Internal;
14 | using Microsoft.Extensions.Logging;
15 | using Microsoft.Extensions.Logging.Abstractions;
16 | using Microsoft.Extensions.Options;
17 | using Microsoft.Extensions.Primitives;
18 | using Microsoft.Net.Http.Headers;
19 |
20 | namespace Microsoft.AspNetCore.WebSockets
21 | {
22 | public class WebSocketMiddleware
23 | {
24 | private readonly RequestDelegate _next;
25 | private readonly WebSocketOptions _options;
26 | private readonly ILogger _logger;
27 | private readonly bool _anyOriginAllowed;
28 | private readonly List _allowedOrigins;
29 |
30 | public WebSocketMiddleware(RequestDelegate next, IOptions options, ILoggerFactory loggerFactory)
31 | {
32 | if (next == null)
33 | {
34 | throw new ArgumentNullException(nameof(next));
35 | }
36 | if (options == null)
37 | {
38 | throw new ArgumentNullException(nameof(options));
39 | }
40 |
41 | _next = next;
42 | _options = options.Value;
43 | _allowedOrigins = _options.AllowedOrigins.Select(o => o.ToLowerInvariant()).ToList();
44 | _anyOriginAllowed = _options.AllowedOrigins.Count == 0 || _options.AllowedOrigins.Contains("*", StringComparer.Ordinal);
45 |
46 | _logger = loggerFactory.CreateLogger();
47 |
48 | // TODO: validate options.
49 | }
50 |
51 | [Obsolete("This constructor has been replaced with an equivalent constructor which requires an ILoggerFactory.")]
52 | public WebSocketMiddleware(RequestDelegate next, IOptions options)
53 | : this(next, options, NullLoggerFactory.Instance)
54 | {
55 | }
56 |
57 | public Task Invoke(HttpContext context)
58 | {
59 | // Detect if an opaque upgrade is available. If so, add a websocket upgrade.
60 | var upgradeFeature = context.Features.Get();
61 | if (upgradeFeature != null && context.Features.Get() == null)
62 | {
63 | var webSocketFeature = new UpgradeHandshake(context, upgradeFeature, _options);
64 | context.Features.Set(webSocketFeature);
65 |
66 | if (!_anyOriginAllowed)
67 | {
68 | // Check for Origin header
69 | var originHeader = context.Request.Headers[HeaderNames.Origin];
70 |
71 | if (!StringValues.IsNullOrEmpty(originHeader) && webSocketFeature.IsWebSocketRequest)
72 | {
73 | // Check allowed origins to see if request is allowed
74 | if (!_allowedOrigins.Contains(originHeader.ToString(), StringComparer.Ordinal))
75 | {
76 | _logger.LogDebug("Request origin {Origin} is not in the list of allowed origins.", originHeader);
77 | context.Response.StatusCode = StatusCodes.Status403Forbidden;
78 | return Task.CompletedTask;
79 | }
80 | }
81 | }
82 | }
83 |
84 | return _next(context);
85 | }
86 |
87 | private class UpgradeHandshake : IHttpWebSocketFeature
88 | {
89 | private readonly HttpContext _context;
90 | private readonly IHttpUpgradeFeature _upgradeFeature;
91 | private readonly WebSocketOptions _options;
92 | private bool? _isWebSocketRequest;
93 |
94 | public UpgradeHandshake(HttpContext context, IHttpUpgradeFeature upgradeFeature, WebSocketOptions options)
95 | {
96 | _context = context;
97 | _upgradeFeature = upgradeFeature;
98 | _options = options;
99 | }
100 |
101 | public bool IsWebSocketRequest
102 | {
103 | get
104 | {
105 | if (_isWebSocketRequest == null)
106 | {
107 | if (!_upgradeFeature.IsUpgradableRequest)
108 | {
109 | _isWebSocketRequest = false;
110 | }
111 | else
112 | {
113 | var headers = new List>();
114 | foreach (string headerName in HandshakeHelpers.NeededHeaders)
115 | {
116 | foreach (var value in _context.Request.Headers.GetCommaSeparatedValues(headerName))
117 | {
118 | headers.Add(new KeyValuePair(headerName, value));
119 | }
120 | }
121 | _isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, headers);
122 | }
123 | }
124 | return _isWebSocketRequest.Value;
125 | }
126 | }
127 |
128 | public async Task AcceptAsync(WebSocketAcceptContext acceptContext)
129 | {
130 | if (!IsWebSocketRequest)
131 | {
132 | throw new InvalidOperationException("Not a WebSocket request."); // TODO: LOC
133 | }
134 |
135 | string subProtocol = null;
136 | if (acceptContext != null)
137 | {
138 | subProtocol = acceptContext.SubProtocol;
139 | }
140 |
141 | TimeSpan keepAliveInterval = _options.KeepAliveInterval;
142 | int receiveBufferSize = _options.ReceiveBufferSize;
143 | var advancedAcceptContext = acceptContext as ExtendedWebSocketAcceptContext;
144 | if (advancedAcceptContext != null)
145 | {
146 | if (advancedAcceptContext.ReceiveBufferSize.HasValue)
147 | {
148 | receiveBufferSize = advancedAcceptContext.ReceiveBufferSize.Value;
149 | }
150 | if (advancedAcceptContext.KeepAliveInterval.HasValue)
151 | {
152 | keepAliveInterval = advancedAcceptContext.KeepAliveInterval.Value;
153 | }
154 | }
155 |
156 | string key = string.Join(", ", _context.Request.Headers[Constants.Headers.SecWebSocketKey]);
157 |
158 | HandshakeHelpers.GenerateResponseHeaders(key, subProtocol, _context.Response.Headers);
159 |
160 | Stream opaqueTransport = await _upgradeFeature.UpgradeAsync(); // Sets status code to 101
161 |
162 | return WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: subProtocol, keepAliveInterval: keepAliveInterval);
163 | }
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddlewareExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using Microsoft.AspNetCore.WebSockets;
6 | using Microsoft.Extensions.Options;
7 |
8 | namespace Microsoft.AspNetCore.Builder
9 | {
10 | public static class WebSocketMiddlewareExtensions
11 | {
12 | public static IApplicationBuilder UseWebSockets(this IApplicationBuilder app)
13 | {
14 | if (app == null)
15 | {
16 | throw new ArgumentNullException(nameof(app));
17 | }
18 |
19 | return app.UseMiddleware();
20 | }
21 |
22 | public static IApplicationBuilder UseWebSockets(this IApplicationBuilder app, WebSocketOptions options)
23 | {
24 | if (app == null)
25 | {
26 | throw new ArgumentNullException(nameof(app));
27 | }
28 | if (options == null)
29 | {
30 | throw new ArgumentNullException(nameof(options));
31 | }
32 |
33 | return app.UseMiddleware(Options.Create(options));
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/WebSocketOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | namespace Microsoft.AspNetCore.Builder
8 | {
9 | ///
10 | /// Configuration options for the WebSocketMiddleware
11 | ///
12 | public class WebSocketOptions
13 | {
14 | public WebSocketOptions()
15 | {
16 | KeepAliveInterval = TimeSpan.FromMinutes(2);
17 | ReceiveBufferSize = 4 * 1024;
18 | AllowedOrigins = new List();
19 | }
20 |
21 | ///
22 | /// Gets or sets the frequency at which to send Ping/Pong keep-alive control frames.
23 | /// The default is two minutes.
24 | ///
25 | public TimeSpan KeepAliveInterval { get; set; }
26 |
27 | ///
28 | /// Gets or sets the size of the protocol buffer used to receive and parse frames.
29 | /// The default is 4kb.
30 | ///
31 | public int ReceiveBufferSize { get; set; }
32 |
33 | ///
34 | /// Set the Origin header values allowed for WebSocket requests to prevent Cross-Site WebSocket Hijacking.
35 | /// By default all Origins are allowed.
36 | ///
37 | public IList AllowedOrigins { get; }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/WebSocketsDependencyInjectionExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | namespace Microsoft.AspNetCore.WebSockets
9 | {
10 | public static class WebSocketsDependencyInjectionExtensions
11 | {
12 | public static IServiceCollection AddWebSockets(this IServiceCollection services, Action configure)
13 | {
14 | return services.Configure(configure);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Microsoft.AspNetCore.WebSockets/baseline.netcore.json:
--------------------------------------------------------------------------------
1 | {
2 | "AssemblyIdentity": "Microsoft.AspNetCore.WebSockets, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
3 | "Types": [
4 | {
5 | "Name": "Microsoft.AspNetCore.Builder.WebSocketMiddlewareExtensions",
6 | "Visibility": "Public",
7 | "Kind": "Class",
8 | "Abstract": true,
9 | "Static": true,
10 | "Sealed": true,
11 | "ImplementedInterfaces": [],
12 | "Members": [
13 | {
14 | "Kind": "Method",
15 | "Name": "UseWebSockets",
16 | "Parameters": [
17 | {
18 | "Name": "app",
19 | "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
20 | }
21 | ],
22 | "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
23 | "Static": true,
24 | "Extension": true,
25 | "Visibility": "Public",
26 | "GenericParameter": []
27 | },
28 | {
29 | "Kind": "Method",
30 | "Name": "UseWebSockets",
31 | "Parameters": [
32 | {
33 | "Name": "app",
34 | "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
35 | },
36 | {
37 | "Name": "options",
38 | "Type": "Microsoft.AspNetCore.Builder.WebSocketOptions"
39 | }
40 | ],
41 | "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
42 | "Static": true,
43 | "Extension": true,
44 | "Visibility": "Public",
45 | "GenericParameter": []
46 | }
47 | ],
48 | "GenericParameters": []
49 | },
50 | {
51 | "Name": "Microsoft.AspNetCore.Builder.WebSocketOptions",
52 | "Visibility": "Public",
53 | "Kind": "Class",
54 | "ImplementedInterfaces": [],
55 | "Members": [
56 | {
57 | "Kind": "Method",
58 | "Name": "get_KeepAliveInterval",
59 | "Parameters": [],
60 | "ReturnType": "System.TimeSpan",
61 | "Visibility": "Public",
62 | "GenericParameter": []
63 | },
64 | {
65 | "Kind": "Method",
66 | "Name": "set_KeepAliveInterval",
67 | "Parameters": [
68 | {
69 | "Name": "value",
70 | "Type": "System.TimeSpan"
71 | }
72 | ],
73 | "ReturnType": "System.Void",
74 | "Visibility": "Public",
75 | "GenericParameter": []
76 | },
77 | {
78 | "Kind": "Method",
79 | "Name": "get_ReceiveBufferSize",
80 | "Parameters": [],
81 | "ReturnType": "System.Int32",
82 | "Visibility": "Public",
83 | "GenericParameter": []
84 | },
85 | {
86 | "Kind": "Method",
87 | "Name": "set_ReceiveBufferSize",
88 | "Parameters": [
89 | {
90 | "Name": "value",
91 | "Type": "System.Int32"
92 | }
93 | ],
94 | "ReturnType": "System.Void",
95 | "Visibility": "Public",
96 | "GenericParameter": []
97 | },
98 | {
99 | "Kind": "Constructor",
100 | "Name": ".ctor",
101 | "Parameters": [],
102 | "Visibility": "Public",
103 | "GenericParameter": []
104 | }
105 | ],
106 | "GenericParameters": []
107 | },
108 | {
109 | "Name": "Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext",
110 | "Visibility": "Public",
111 | "Kind": "Class",
112 | "BaseType": "Microsoft.AspNetCore.Http.WebSocketAcceptContext",
113 | "ImplementedInterfaces": [],
114 | "Members": [
115 | {
116 | "Kind": "Method",
117 | "Name": "get_SubProtocol",
118 | "Parameters": [],
119 | "ReturnType": "System.String",
120 | "Virtual": true,
121 | "Override": true,
122 | "Visibility": "Public",
123 | "GenericParameter": []
124 | },
125 | {
126 | "Kind": "Method",
127 | "Name": "set_SubProtocol",
128 | "Parameters": [
129 | {
130 | "Name": "value",
131 | "Type": "System.String"
132 | }
133 | ],
134 | "ReturnType": "System.Void",
135 | "Virtual": true,
136 | "Override": true,
137 | "Visibility": "Public",
138 | "GenericParameter": []
139 | },
140 | {
141 | "Kind": "Method",
142 | "Name": "get_ReceiveBufferSize",
143 | "Parameters": [],
144 | "ReturnType": "System.Nullable",
145 | "Visibility": "Public",
146 | "GenericParameter": []
147 | },
148 | {
149 | "Kind": "Method",
150 | "Name": "set_ReceiveBufferSize",
151 | "Parameters": [
152 | {
153 | "Name": "value",
154 | "Type": "System.Nullable"
155 | }
156 | ],
157 | "ReturnType": "System.Void",
158 | "Visibility": "Public",
159 | "GenericParameter": []
160 | },
161 | {
162 | "Kind": "Method",
163 | "Name": "get_KeepAliveInterval",
164 | "Parameters": [],
165 | "ReturnType": "System.Nullable",
166 | "Visibility": "Public",
167 | "GenericParameter": []
168 | },
169 | {
170 | "Kind": "Method",
171 | "Name": "set_KeepAliveInterval",
172 | "Parameters": [
173 | {
174 | "Name": "value",
175 | "Type": "System.Nullable"
176 | }
177 | ],
178 | "ReturnType": "System.Void",
179 | "Visibility": "Public",
180 | "GenericParameter": []
181 | },
182 | {
183 | "Kind": "Constructor",
184 | "Name": ".ctor",
185 | "Parameters": [],
186 | "Visibility": "Public",
187 | "GenericParameter": []
188 | }
189 | ],
190 | "GenericParameters": []
191 | },
192 | {
193 | "Name": "Microsoft.AspNetCore.WebSockets.WebSocketMiddleware",
194 | "Visibility": "Public",
195 | "Kind": "Class",
196 | "ImplementedInterfaces": [],
197 | "Members": [
198 | {
199 | "Kind": "Method",
200 | "Name": "Invoke",
201 | "Parameters": [
202 | {
203 | "Name": "context",
204 | "Type": "Microsoft.AspNetCore.Http.HttpContext"
205 | }
206 | ],
207 | "ReturnType": "System.Threading.Tasks.Task",
208 | "Visibility": "Public",
209 | "GenericParameter": []
210 | },
211 | {
212 | "Kind": "Constructor",
213 | "Name": ".ctor",
214 | "Parameters": [
215 | {
216 | "Name": "next",
217 | "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
218 | },
219 | {
220 | "Name": "options",
221 | "Type": "Microsoft.Extensions.Options.IOptions"
222 | }
223 | ],
224 | "Visibility": "Public",
225 | "GenericParameter": []
226 | }
227 | ],
228 | "GenericParameters": []
229 | }
230 | ]
231 | }
--------------------------------------------------------------------------------
/test/AutobahnTestApp/AutobahnTestApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/AutobahnTestApp/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net;
4 | using System.Runtime.Loader;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace AutobahnTestApp
10 | {
11 | public class Program
12 | {
13 | public static void Main(string[] args)
14 | {
15 | var scenarioName = "Unknown";
16 | var config = new ConfigurationBuilder()
17 | .AddCommandLine(args)
18 | .Build();
19 |
20 | var builder = new WebHostBuilder()
21 | .ConfigureLogging(loggingBuilder => loggingBuilder.AddConsole())
22 | .UseConfiguration(config)
23 | .UseContentRoot(Directory.GetCurrentDirectory())
24 | .UseIISIntegration()
25 | .UseStartup();
26 |
27 | if (string.Equals(builder.GetSetting("server"), "Microsoft.AspNetCore.Server.HttpSys", System.StringComparison.Ordinal))
28 | {
29 | scenarioName = "HttpSysServer";
30 | Console.WriteLine("Using HttpSys server");
31 | builder.UseHttpSys();
32 | }
33 | else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_PORT")))
34 | {
35 | // ANCM is hosting the process.
36 | // The port will not yet be configured at this point, but will also not require HTTPS.
37 | scenarioName = "AspNetCoreModule";
38 | Console.WriteLine("Detected ANCM, using Kestrel");
39 | builder.UseKestrel();
40 | }
41 | else
42 | {
43 | // Also check "server.urls" for back-compat.
44 | var urls = builder.GetSetting(WebHostDefaults.ServerUrlsKey) ?? builder.GetSetting("server.urls");
45 | builder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Empty);
46 |
47 | Console.WriteLine($"Using Kestrel, URL: {urls}");
48 |
49 | if (urls.Contains(";"))
50 | {
51 | throw new NotSupportedException("This test app does not support multiple endpoints.");
52 | }
53 |
54 | var uri = new Uri(urls);
55 |
56 | builder.UseKestrel(options =>
57 | {
58 | options.Listen(IPAddress.Loopback, uri.Port, listenOptions =>
59 | {
60 | if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
61 | {
62 | scenarioName = "Kestrel(SSL)";
63 | var certPath = Path.Combine(AppContext.BaseDirectory, "TestResources", "testCert.pfx");
64 | Console.WriteLine($"Using SSL with certificate: {certPath}");
65 | listenOptions.UseHttps(certPath, "testPassword");
66 | }
67 | else
68 | {
69 | scenarioName = "Kestrel(NonSSL)";
70 | }
71 | });
72 | });
73 | }
74 |
75 | var host = builder.Build();
76 |
77 | AppDomain.CurrentDomain.UnhandledException += (_, a) =>
78 | {
79 | Console.WriteLine($"Unhandled exception (Scenario: {scenarioName}): {a.ExceptionObject.ToString()}");
80 | };
81 |
82 | Console.WriteLine($"Starting Server for Scenario: {scenarioName}");
83 | host.Run();
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/AutobahnTestApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:6155/",
7 | "sslPort": 44371
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets"
16 | }
17 | },
18 | "AutobahnTestApp": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "launchUrl": "http://localhost:5000",
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets"
24 | }
25 | },
26 | "AutobahnTestApp (SSL)": {
27 | "commandName": "Project",
28 | "launchBrowser": true,
29 | "launchUrl": "https://localhost:5443",
30 | "environmentVariables": {
31 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets"
32 | }
33 | },
34 | "WebListener": {
35 | "commandName": "Project",
36 | "commandLineArgs": "--server Microsoft.AspNetCore.Server.HttpSys",
37 | "environmentVariables": {
38 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets"
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/test/AutobahnTestApp/README.md:
--------------------------------------------------------------------------------
1 | # Autobahn Testing
2 |
3 | This application is used to provide the server for the [Autobahn Test Suite](http://autobahn.ws/testsuite) 'fuzzingclient' mode to test. It is a simple echo server that echos each frame received back to the client.
4 |
5 | In order to run these tests you must install CPython 2.7, Pip, and the test suite modules. You must also have
6 | the `wstest` executable provided by the Autobahn Suite on the `PATH`. See http://autobahn.ws/testsuite/installation.html#installation for more info
7 |
8 | Once Autobahn is installed, launch this application in the desired configuration (in IIS Express, or using Kestrel directly) from Visual Studio and get the WebSocket URL from the HTTP response. Use that URL in place of `ws://server:1234` and invoke the `scripts\RunAutobahnTests.ps1` script in this project like so:
9 |
10 | ```
11 | > .\scripts\RunAutobahnTests.ps1 -ServerUrl ws://server:1234
12 | ```
13 |
14 | By default, all cases are run and the report is written to the `autobahnreports` sub-directory of the directory in which you run the script. You can change either by using the `-Cases` and `-OutputDir` switches, use `.\script\RunAutobahnTests.ps1 -?` for help.
15 |
--------------------------------------------------------------------------------
/test/AutobahnTestApp/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.WebSockets;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace AutobahnTestApp
11 | {
12 | public class Startup
13 | {
14 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
15 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
16 | {
17 | app.UseWebSockets();
18 |
19 | var logger = loggerFactory.CreateLogger();
20 | app.Use(async (context, next) =>
21 | {
22 | if (context.WebSockets.IsWebSocketRequest)
23 | {
24 | logger.LogInformation("Received WebSocket request");
25 | using (var webSocket = await context.WebSockets.AcceptWebSocketAsync())
26 | {
27 | await Echo(webSocket, context.RequestAborted);
28 | }
29 | }
30 | else
31 | {
32 | var wsScheme = context.Request.IsHttps ? "wss" : "ws";
33 | var wsUrl = $"{wsScheme}://{context.Request.Host.Host}:{context.Request.Host.Port}{context.Request.Path}";
34 | await context.Response.WriteAsync($"Ready to accept a WebSocket request at: {wsUrl}");
35 | }
36 | });
37 |
38 | }
39 |
40 | private async Task Echo(WebSocket webSocket, CancellationToken cancellationToken)
41 | {
42 | var buffer = new byte[1024 * 4];
43 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken);
44 | while (!result.CloseStatus.HasValue)
45 | {
46 | await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
47 | result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken);
48 | }
49 | await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancellationToken);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/test/AutobahnTestApp/TestResources/testCert.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aspnet/WebSockets/afeb7fe49bf1652108e4e0f9e0e3b1eeec84bfd4/test/AutobahnTestApp/TestResources/testCert.pfx
--------------------------------------------------------------------------------
/test/AutobahnTestApp/TestResources/testCert.txt:
--------------------------------------------------------------------------------
1 | The password for this is 'testPassword'
2 |
3 | DO NOT EVER TRUST THIS CERT. The private key for it is publicly released.
--------------------------------------------------------------------------------
/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1:
--------------------------------------------------------------------------------
1 | #
2 | # RunAutobahnTests.ps1
3 | #
4 | param([Parameter(Mandatory=$true)][string]$ServerUrl, [string[]]$Cases = @("*"), [string]$OutputDir, [int]$Iterations = 1)
5 |
6 | if(!(Get-Command wstest -ErrorAction SilentlyContinue)) {
7 | throw "Missing required command 'wstest'. See README.md in Microsoft.AspNetCore.WebSockets.Server.Test project for information on installing Autobahn Test Suite."
8 | }
9 |
10 | if(!$OutputDir) {
11 | $OutputDir = Convert-Path "."
12 | $OutputDir = Join-Path $OutputDir "autobahnreports"
13 | }
14 |
15 | Write-Host "Launching Autobahn Test Suite ($Iterations iteration(s))..."
16 |
17 | 0..($Iterations-1) | % {
18 | $iteration = $_
19 |
20 | $Spec = Convert-Path (Join-Path $PSScriptRoot "autobahn.spec.json")
21 |
22 | $CasesArray = [string]::Join(",", @($Cases | ForEach-Object { "`"$_`"" }))
23 |
24 | $SpecJson = [IO.File]::ReadAllText($Spec).Replace("OUTPUTDIR", $OutputDir.Replace("\", "\\")).Replace("WEBSOCKETURL", $ServerUrl).Replace("`"CASES`"", $CasesArray)
25 |
26 | $TempFile = [IO.Path]::GetTempFileName()
27 |
28 | try {
29 | [IO.File]::WriteAllText($TempFile, $SpecJson)
30 | $wstestOutput = & wstest -m fuzzingclient -s $TempFile
31 | } finally {
32 | if(Test-Path $TempFile) {
33 | rm $TempFile
34 | }
35 | }
36 |
37 | $report = ConvertFrom-Json ([IO.File]::ReadAllText((Convert-Path (Join-Path $OutputDir "index.json"))))
38 |
39 | $report.Server | gm | ? { $_.MemberType -eq "NoteProperty" } | % {
40 | $case = $report.Server."$($_.Name)"
41 | Write-Host "[#$($iteration.ToString().PadRight(2))] [$($case.behavior.PadRight(6))] Case $($_.Name)"
42 | }
43 | }
--------------------------------------------------------------------------------
/test/AutobahnTestApp/scripts/autobahn.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "options": { "failByDrop": false },
3 | "outdir": "OUTPUTDIR",
4 | "servers": [
5 | {
6 | "agent": "Server",
7 | "url": "WEBSOCKETURL",
8 | "options": { "version": 18 }
9 | }
10 | ],
11 | "cases": ["CASES"],
12 | "exclude-cases": ["12.*", "13.*"],
13 | "exclude-agent-cases": {}
14 | }
15 |
--------------------------------------------------------------------------------
/test/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | netcoreapp2.2
6 | $(DeveloperBuildTestTfms)
7 |
8 | $(StandardTestTfms);net461
9 |
10 |
11 |
12 |
16 | true
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnCaseResult.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Newtonsoft.Json.Linq;
4 |
5 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
6 | {
7 | public class AutobahnCaseResult
8 | {
9 | public string Name { get; }
10 | public string ActualBehavior { get; }
11 |
12 | public AutobahnCaseResult(string name, string actualBehavior)
13 | {
14 | Name = name;
15 | ActualBehavior = actualBehavior;
16 | }
17 |
18 | public static AutobahnCaseResult FromJson(JProperty prop)
19 | {
20 | var caseObj = (JObject)prop.Value;
21 | var actualBehavior = (string)caseObj["behavior"];
22 | return new AutobahnCaseResult(prop.Name, actualBehavior);
23 | }
24 |
25 | public bool BehaviorIs(params string[] behaviors)
26 | {
27 | return behaviors.Any(b => string.Equals(b, ActualBehavior, StringComparison.Ordinal));
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnExpectations.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Microsoft.AspNetCore.Server.IntegrationTesting;
5 |
6 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
7 | {
8 | public class AutobahnExpectations
9 | {
10 | private Dictionary _expectations = new Dictionary();
11 | public bool Ssl { get; }
12 | public ServerType Server { get; }
13 | public string Environment { get; }
14 |
15 | public AutobahnExpectations(ServerType server, bool ssl, string environment)
16 | {
17 | Server = server;
18 | Ssl = ssl;
19 | Environment = environment;
20 | }
21 |
22 | public AutobahnExpectations Fail(params string[] caseSpecs) => Expect(Expectation.Fail, caseSpecs);
23 | public AutobahnExpectations NonStrict(params string[] caseSpecs) => Expect(Expectation.NonStrict, caseSpecs);
24 | public AutobahnExpectations OkOrFail(params string[] caseSpecs) => Expect(Expectation.OkOrFail, caseSpecs);
25 |
26 | public AutobahnExpectations Expect(Expectation expectation, params string[] caseSpecs)
27 | {
28 | foreach (var caseSpec in caseSpecs)
29 | {
30 | _expectations[caseSpec] = expectation;
31 | }
32 | return this;
33 | }
34 |
35 | internal void Verify(AutobahnServerResult serverResult, StringBuilder failures)
36 | {
37 | foreach (var caseResult in serverResult.Cases)
38 | {
39 | // If this is an informational test result, we can't compare it to anything
40 | if (!string.Equals(caseResult.ActualBehavior, "INFORMATIONAL", StringComparison.Ordinal))
41 | {
42 | Expectation expectation;
43 | if (!_expectations.TryGetValue(caseResult.Name, out expectation))
44 | {
45 | expectation = Expectation.Ok;
46 | }
47 |
48 | switch (expectation)
49 | {
50 | case Expectation.Fail:
51 | if (!caseResult.BehaviorIs("FAILED"))
52 | {
53 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'FAILED', but got '{caseResult.ActualBehavior}'");
54 | }
55 | break;
56 | case Expectation.NonStrict:
57 | if (!caseResult.BehaviorIs("NON-STRICT"))
58 | {
59 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'NON-STRICT', but got '{caseResult.ActualBehavior}'");
60 | }
61 | break;
62 | case Expectation.Ok:
63 | if (!caseResult.BehaviorIs("NON-STRICT") && !caseResult.BehaviorIs("OK"))
64 | {
65 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'NON-STRICT' or 'OK', but got '{caseResult.ActualBehavior}'");
66 | }
67 | break;
68 | case Expectation.OkOrFail:
69 | if (!caseResult.BehaviorIs("NON-STRICT") && !caseResult.BehaviorIs("FAILED") && !caseResult.BehaviorIs("OK"))
70 | {
71 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'FAILED', 'NON-STRICT' or 'OK', but got '{caseResult.ActualBehavior}'");
72 | }
73 | break;
74 | default:
75 | break;
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnResult.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Newtonsoft.Json.Linq;
4 |
5 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
6 | {
7 | public class AutobahnResult
8 | {
9 | public IEnumerable Servers { get; }
10 |
11 | public AutobahnResult(IEnumerable servers)
12 | {
13 | Servers = servers;
14 | }
15 |
16 | public static AutobahnResult FromReportJson(JObject indexJson)
17 | {
18 | // Load the report
19 | return new AutobahnResult(indexJson.Properties().Select(AutobahnServerResult.FromJson));
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnServerResult.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Microsoft.AspNetCore.Server.IntegrationTesting;
5 | using Newtonsoft.Json.Linq;
6 |
7 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
8 | {
9 | public class AutobahnServerResult
10 | {
11 | public ServerType Server { get; }
12 | public bool Ssl { get; }
13 | public string Environment { get; }
14 | public string Name { get; }
15 | public IEnumerable Cases { get; }
16 |
17 | public AutobahnServerResult(string name, IEnumerable cases)
18 | {
19 | Name = name;
20 |
21 | var splat = name.Split('|');
22 | if (splat.Length < 3)
23 | {
24 | throw new FormatException("Results incorrectly formatted");
25 | }
26 |
27 | Server = (ServerType)Enum.Parse(typeof(ServerType), splat[0]);
28 | Ssl = string.Equals(splat[1], "SSL", StringComparison.Ordinal);
29 | Environment = splat[2];
30 | Cases = cases;
31 | }
32 |
33 | public static AutobahnServerResult FromJson(JProperty prop)
34 | {
35 | var valueObj = ((JObject)prop.Value);
36 | return new AutobahnServerResult(prop.Name, valueObj.Properties().Select(AutobahnCaseResult.FromJson));
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnSpec.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Linq;
4 | using Newtonsoft.Json;
5 | using Newtonsoft.Json.Linq;
6 |
7 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
8 | {
9 | public class AutobahnSpec
10 | {
11 | public string OutputDirectory { get; }
12 | public IList Servers { get; } = new List();
13 | public IList Cases { get; } = new List();
14 | public IList ExcludedCases { get; } = new List();
15 |
16 | public AutobahnSpec(string outputDirectory)
17 | {
18 | OutputDirectory = outputDirectory;
19 | }
20 |
21 | public AutobahnSpec WithServer(string name, string url)
22 | {
23 | Servers.Add(new ServerSpec(name, url));
24 | return this;
25 | }
26 |
27 | public AutobahnSpec IncludeCase(params string[] caseSpecs)
28 | {
29 | foreach (var caseSpec in caseSpecs)
30 | {
31 | Cases.Add(caseSpec);
32 | }
33 | return this;
34 | }
35 |
36 | public AutobahnSpec ExcludeCase(params string[] caseSpecs)
37 | {
38 | foreach (var caseSpec in caseSpecs)
39 | {
40 | ExcludedCases.Add(caseSpec);
41 | }
42 | return this;
43 | }
44 |
45 | public void WriteJson(string file)
46 | {
47 | File.WriteAllText(file, GetJson().ToString(Formatting.Indented));
48 | }
49 |
50 | public JObject GetJson() => new JObject(
51 | new JProperty("options", new JObject(
52 | new JProperty("failByDrop", false))),
53 | new JProperty("outdir", OutputDirectory),
54 | new JProperty("servers", new JArray(Servers.Select(s => s.GetJson()).ToArray())),
55 | new JProperty("cases", new JArray(Cases.ToArray())),
56 | new JProperty("exclude-cases", new JArray(ExcludedCases.ToArray())),
57 | new JProperty("exclude-agent-cases", new JObject()));
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnTester.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net.Http;
6 | using System.Security.Authentication;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Microsoft.AspNetCore.Server.IntegrationTesting;
11 | using Microsoft.Extensions.Logging;
12 | using Newtonsoft.Json.Linq;
13 | using Xunit;
14 |
15 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
16 | {
17 | public class AutobahnTester : IDisposable
18 | {
19 | private readonly List _deployers = new List();
20 | private readonly List _deployments = new List();
21 | private readonly List _expectations = new List();
22 | private readonly ILoggerFactory _loggerFactory;
23 | private readonly ILogger _logger;
24 |
25 | public AutobahnSpec Spec { get; }
26 |
27 | public AutobahnTester(ILoggerFactory loggerFactory, AutobahnSpec baseSpec)
28 | {
29 | _loggerFactory = loggerFactory;
30 | _logger = _loggerFactory.CreateLogger("AutobahnTester");
31 |
32 | Spec = baseSpec;
33 | }
34 |
35 | public async Task Run(CancellationToken cancellationToken)
36 | {
37 | var specFile = Path.GetTempFileName();
38 | try
39 | {
40 | // Start pinging the servers to see that they're still running
41 | var pingCts = new CancellationTokenSource();
42 | var pinger = new Timer(state => Pinger((CancellationToken)state), pingCts.Token, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
43 |
44 | Spec.WriteJson(specFile);
45 |
46 | // Run the test (write something to the console so people know this will take a while...)
47 | _logger.LogInformation("Using 'wstest' from: {WsTestPath}", Wstest.Default.Location);
48 | _logger.LogInformation("Now launching Autobahn Test Suite. This will take a while.");
49 | var exitCode = await Wstest.Default.ExecAsync("-m fuzzingclient -s " + specFile, cancellationToken, _loggerFactory.CreateLogger("wstest"));
50 | if (exitCode != 0)
51 | {
52 | throw new Exception("wstest failed");
53 | }
54 |
55 | pingCts.Cancel();
56 | }
57 | finally
58 | {
59 | if (File.Exists(specFile))
60 | {
61 | File.Delete(specFile);
62 | }
63 | }
64 |
65 | cancellationToken.ThrowIfCancellationRequested();
66 |
67 | // Parse the output.
68 | var outputFile = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", Spec.OutputDirectory, "index.json");
69 | using (var reader = new StreamReader(File.OpenRead(outputFile)))
70 | {
71 | return AutobahnResult.FromReportJson(JObject.Parse(await reader.ReadToEndAsync()));
72 | }
73 | }
74 |
75 | // Async void! It's OK here because we are running in a timer. We're just using async void to chain continuations.
76 | // There's nobody to await this, hence async void.
77 | private async void Pinger(CancellationToken token)
78 | {
79 | try
80 | {
81 | while (!token.IsCancellationRequested)
82 | {
83 | try
84 | {
85 | foreach (var deployment in _deployments)
86 | {
87 | if (token.IsCancellationRequested)
88 | {
89 | return;
90 | }
91 |
92 | var resp = await deployment.HttpClient.GetAsync("/ping", token);
93 | if (!resp.IsSuccessStatusCode)
94 | {
95 | _logger.LogWarning("Non-successful response when pinging {url}: {statusCode} {reasonPhrase}", deployment.ApplicationBaseUri, resp.StatusCode, resp.ReasonPhrase);
96 | }
97 | }
98 | }
99 | catch (OperationCanceledException)
100 | {
101 | // We don't want to throw when the token fires, just stop.
102 | }
103 | }
104 | }
105 | catch (Exception ex)
106 | {
107 | _logger.LogError(ex, "Error while pinging servers");
108 | }
109 | }
110 |
111 | public void Verify(AutobahnResult result)
112 | {
113 | var failures = new StringBuilder();
114 | foreach (var serverResult in result.Servers)
115 | {
116 | var serverExpectation = _expectations.FirstOrDefault(e => e.Server == serverResult.Server && e.Ssl == serverResult.Ssl);
117 | if (serverExpectation == null)
118 | {
119 | failures.AppendLine($"Expected no results for server: {serverResult.Name} but found results!");
120 | }
121 | else
122 | {
123 | serverExpectation.Verify(serverResult, failures);
124 | }
125 | }
126 |
127 | Assert.True(failures.Length == 0, "Autobahn results did not meet expectations:" + Environment.NewLine + failures.ToString());
128 | }
129 |
130 | public async Task DeployTestAndAddToSpec(ServerType server, bool ssl, string environment, CancellationToken cancellationToken, Action expectationConfig = null)
131 | {
132 | var sslNamePart = ssl ? "SSL" : "NoSSL";
133 | var name = $"{server}|{sslNamePart}|{environment}";
134 | var logger = _loggerFactory.CreateLogger($"AutobahnTestApp:{server}:{sslNamePart}:{environment}");
135 |
136 | var appPath = Helpers.GetApplicationPath("AutobahnTestApp");
137 | var configPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Http.config");
138 | var targetFramework =
139 | #if NETCOREAPP2_2
140 | "netcoreapp2.2";
141 | #else
142 | #error Target frameworks need to be updated
143 | #endif
144 | var parameters = new DeploymentParameters(appPath, server, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64)
145 | {
146 | Scheme = (ssl ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
147 | ApplicationType = ApplicationType.Portable,
148 | TargetFramework = targetFramework,
149 | EnvironmentName = environment,
150 | SiteName = "HttpTestSite", // This is configured in the Http.config
151 | ServerConfigTemplateContent = (server == ServerType.IISExpress) ? File.ReadAllText(configPath) : null,
152 | };
153 |
154 | var deployer = ApplicationDeployerFactory.Create(parameters, _loggerFactory);
155 | var result = await deployer.DeployAsync();
156 | _deployers.Add(deployer);
157 | _deployments.Add(result);
158 | cancellationToken.ThrowIfCancellationRequested();
159 |
160 | var handler = new HttpClientHandler();
161 | // Win7 HttpClient on NetCoreApp2.2 defaults to TLS 1.0 and won't connect to Kestrel. https://github.com/dotnet/corefx/issues/28733
162 | // Mac HttpClient on NetCoreApp2.0 doesn't alow you to set some combinations.
163 | // https://github.com/dotnet/corefx/blob/586cffcdfdf23ad6c193a4bf37fce88a1bf69508/src/System.Net.Http/src/System/Net/Http/CurlHandler/CurlHandler.SslProvider.OSX.cs#L104-L106
164 | handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
165 | handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
166 | var client = result.CreateHttpClient(handler);
167 |
168 | // Make sure the server works
169 | var resp = await RetryHelper.RetryRequest(() =>
170 | {
171 | cancellationToken.ThrowIfCancellationRequested();
172 | return client.GetAsync(result.ApplicationBaseUri);
173 | }, logger, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, result.HostShutdownToken).Token);
174 | resp.EnsureSuccessStatusCode();
175 |
176 | cancellationToken.ThrowIfCancellationRequested();
177 |
178 | // Add to the current spec
179 | var wsUrl = result.ApplicationBaseUri.Replace("https://", "wss://").Replace("http://", "ws://");
180 | Spec.WithServer(name, wsUrl);
181 |
182 | var expectations = new AutobahnExpectations(server, ssl, environment);
183 | expectationConfig?.Invoke(expectations);
184 | _expectations.Add(expectations);
185 |
186 | cancellationToken.ThrowIfCancellationRequested();
187 | }
188 |
189 | public void Dispose()
190 | {
191 | foreach (var deployer in _deployers)
192 | {
193 | deployer.Dispose();
194 | }
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Executable.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Runtime.InteropServices;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
10 | {
11 | public class Executable
12 | {
13 | private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
14 |
15 | public string Location { get; }
16 |
17 | protected Executable(string path)
18 | {
19 | Location = path;
20 | }
21 |
22 | public static string Locate(string name)
23 | {
24 | foreach (var dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator))
25 | {
26 | var candidate = Path.Combine(dir, name + _exeSuffix);
27 | if (File.Exists(candidate))
28 | {
29 | return candidate;
30 | }
31 | }
32 | return null;
33 | }
34 |
35 | public async Task ExecAsync(string args, CancellationToken cancellationToken, ILogger logger)
36 | {
37 | var process = new Process()
38 | {
39 | StartInfo = new ProcessStartInfo()
40 | {
41 | FileName = Location,
42 | Arguments = args,
43 | UseShellExecute = false,
44 | RedirectStandardError = true,
45 | RedirectStandardOutput = true
46 | },
47 | EnableRaisingEvents = true
48 | };
49 | var tcs = new TaskCompletionSource();
50 |
51 | using (cancellationToken.Register(() => Cancel(process, tcs)))
52 | {
53 | process.Exited += (_, __) => tcs.TrySetResult(process.ExitCode);
54 | process.OutputDataReceived += (_, a) => LogIfNotNull(logger.LogInformation, "stdout: {0}", a.Data);
55 | process.ErrorDataReceived += (_, a) => LogIfNotNull(logger.LogError, "stderr: {0}", a.Data);
56 |
57 | process.Start();
58 |
59 | process.BeginErrorReadLine();
60 | process.BeginOutputReadLine();
61 |
62 | return await tcs.Task;
63 | }
64 | }
65 |
66 | private void LogIfNotNull(Action logger, string message, string data)
67 | {
68 | if (!string.IsNullOrEmpty(data))
69 | {
70 | logger(message, new[] { data });
71 | }
72 | }
73 |
74 | private static void Cancel(Process process, TaskCompletionSource tcs)
75 | {
76 | if (process != null && !process.HasExited)
77 | {
78 | process.Kill();
79 | }
80 | tcs.TrySetCanceled();
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Expectation.cs:
--------------------------------------------------------------------------------
1 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
2 | {
3 | public enum Expectation
4 | {
5 | Fail,
6 | NonStrict,
7 | OkOrFail,
8 | Ok
9 | }
10 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/ServerSpec.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json.Linq;
2 |
3 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
4 | {
5 | public class ServerSpec
6 | {
7 | public string Name { get; }
8 | public string Url { get; }
9 |
10 | public ServerSpec(string name, string url)
11 | {
12 | Name = name;
13 | Url = url;
14 | }
15 |
16 | public JObject GetJson() => new JObject(
17 | new JProperty("agent", Name),
18 | new JProperty("url", Url),
19 | new JProperty("options", new JObject(
20 | new JProperty("version", 18))));
21 | }
22 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Wstest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn
5 | {
6 | ///
7 | /// Wrapper around the Autobahn Test Suite's "wstest" app.
8 | ///
9 | public class Wstest : Executable
10 | {
11 | private static Lazy _instance = new Lazy(Create);
12 |
13 | public static readonly string DefaultLocation = LocateWstest();
14 |
15 | public static Wstest Default => _instance.Value;
16 |
17 | public Wstest(string path) : base(path) { }
18 |
19 | private static Wstest Create()
20 | {
21 | var location = LocateWstest();
22 |
23 | return (location == null || !File.Exists(location)) ? null : new Wstest(location);
24 | }
25 |
26 | private static string LocateWstest()
27 | {
28 | var location = Environment.GetEnvironmentVariable("ASPNETCORE_WSTEST_PATH");
29 | if (string.IsNullOrEmpty(location))
30 | {
31 | location = Locate("wstest");
32 | }
33 |
34 | return location;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/AutobahnTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Runtime.InteropServices;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Server.IntegrationTesting;
8 | using Microsoft.AspNetCore.Testing.xunit;
9 | using Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn;
10 | using Microsoft.Extensions.Logging;
11 | using Microsoft.Extensions.Logging.Testing;
12 | using Xunit;
13 | using Xunit.Abstractions;
14 |
15 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest
16 | {
17 | public class AutobahnTests : LoggedTest
18 | {
19 | private static readonly TimeSpan TestTimeout = TimeSpan.FromMinutes(3);
20 |
21 | public AutobahnTests(ITestOutputHelper output) : base(output)
22 | {
23 | }
24 |
25 | // Skip if wstest is not installed for now, see https://github.com/aspnet/WebSockets/issues/95
26 | // We will enable Wstest on every build once we've gotten the necessary infrastructure sorted out :).
27 | [ConditionalFact]
28 | [SkipIfWsTestNotPresent]
29 | public async Task AutobahnTestSuite()
30 | {
31 | // If we're on CI, we want to actually fail if WsTest isn't installed, rather than just skipping the test
32 | // The SkipIfWsTestNotPresent attribute ensures that this test isn't skipped on CI, so we just need to check that Wstest is present
33 | // And we use Assert.True to provide an error message
34 | Assert.True(Wstest.Default != null, $"The 'wstest' executable (Autobahn WebSockets Test Suite) could not be found at '{Wstest.DefaultLocation}'. Run the Build Agent setup scripts to install it or see https://github.com/crossbario/autobahn-testsuite for instructions on manual installation.");
35 |
36 | using (StartLog(out var loggerFactory))
37 | {
38 | var logger = loggerFactory.CreateLogger();
39 | var reportDir = Environment.GetEnvironmentVariable("AUTOBAHN_SUITES_REPORT_DIR");
40 | var outDir = !string.IsNullOrEmpty(reportDir) ?
41 | reportDir :
42 | Path.Combine(AppContext.BaseDirectory, "autobahnreports");
43 |
44 | if (Directory.Exists(outDir))
45 | {
46 | Directory.Delete(outDir, recursive: true);
47 | }
48 |
49 | outDir = outDir.Replace("\\", "\\\\");
50 |
51 | // 9.* is Limits/Performance which is VERY SLOW; 12.*/13.* are compression which we don't implement
52 | var spec = new AutobahnSpec(outDir)
53 | .IncludeCase("*")
54 | .ExcludeCase("9.*", "12.*", "13.*");
55 |
56 | var cts = new CancellationTokenSource();
57 | cts.CancelAfter(TestTimeout); // These tests generally complete in just over 1 minute.
58 |
59 | using (cts.Token.Register(() => logger.LogError("Test run is taking longer than maximum duration of {timeoutMinutes:0.00} minutes. Aborting...", TestTimeout.TotalMinutes)))
60 | {
61 | AutobahnResult result;
62 | using (var tester = new AutobahnTester(loggerFactory, spec))
63 | {
64 | await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: false, environment: "ManagedSockets", cancellationToken: cts.Token);
65 | await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: true, environment: "ManagedSockets", cancellationToken: cts.Token);
66 |
67 | // Windows-only WebListener tests
68 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
69 | {
70 | if (IsWindows8OrHigher())
71 | {
72 | // WebListener occasionally gives a non-strict response on 3.2. IIS Express seems to have the same behavior. Wonder if it's related to HttpSys?
73 | // For now, just allow the non-strict response, it's not a failure.
74 | await tester.DeployTestAndAddToSpec(ServerType.HttpSys, ssl: false, environment: "ManagedSockets", cancellationToken: cts.Token);
75 | }
76 | }
77 |
78 | result = await tester.Run(cts.Token);
79 | tester.Verify(result);
80 | }
81 | }
82 |
83 | // If it hasn't been cancelled yet, cancel the token just to be sure
84 | cts.Cancel();
85 | }
86 | }
87 |
88 | private bool IsWindows8OrHigher()
89 | {
90 | const string WindowsName = "Microsoft Windows ";
91 | const int VersionOffset = 18;
92 |
93 | if (RuntimeInformation.OSDescription.StartsWith(WindowsName))
94 | {
95 | var versionStr = RuntimeInformation.OSDescription.Substring(VersionOffset);
96 | Version version;
97 | if (Version.TryParse(versionStr, out version))
98 | {
99 | return version.Major > 6 || (version.Major == 6 && version.Minor >= 2);
100 | }
101 | }
102 |
103 | return false;
104 | }
105 |
106 | private bool IsIISExpress10Installed()
107 | {
108 | var pf = Environment.GetEnvironmentVariable("PROGRAMFILES");
109 | var iisExpressExe = Path.Combine(pf, "IIS Express", "iisexpress.exe");
110 | return File.Exists(iisExpressExe) && FileVersionInfo.GetVersionInfo(iisExpressExe).FileMajorPart >= 10;
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Helpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest
5 | {
6 | public class Helpers
7 | {
8 | public static string GetApplicationPath(string projectName)
9 | {
10 | var applicationBasePath = AppContext.BaseDirectory;
11 |
12 | var directoryInfo = new DirectoryInfo(applicationBasePath);
13 | do
14 | {
15 | var solutionFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, "WebSockets.sln"));
16 | if (solutionFileInfo.Exists)
17 | {
18 | return Path.GetFullPath(Path.Combine(directoryInfo.FullName, "test", projectName));
19 | }
20 |
21 | directoryInfo = directoryInfo.Parent;
22 | }
23 | while (directoryInfo.Parent != null);
24 |
25 | throw new Exception($"Solution root could not be found using {applicationBasePath}");
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Microsoft.AspNetCore.WebSockets.ConformanceTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/SkipIfWsTestNotPresentAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Testing.xunit;
3 | using Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn;
4 |
5 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest
6 | {
7 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
8 | public class SkipIfWsTestNotPresentAttribute : Attribute, ITestCondition
9 | {
10 | public bool IsMet => IsOnCi || Wstest.Default != null;
11 | public string SkipReason => "Autobahn Test Suite is not installed on the host machine.";
12 |
13 | private static bool IsOnCi =>
14 | !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TEAMCITY_VERSION")) ||
15 | string.Equals(Environment.GetEnvironmentVariable("TRAVIS"), "true", StringComparison.OrdinalIgnoreCase) ||
16 | string.Equals(Environment.GetEnvironmentVariable("APPVEYOR"), "true", StringComparison.OrdinalIgnoreCase);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/AddWebSocketsTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Options;
8 | using Xunit;
9 |
10 | namespace Microsoft.AspNetCore.WebSockets.Test
11 | {
12 | public class AddWebSocketsTests
13 | {
14 | [Fact]
15 | public void AddWebSocketsConfiguresOptions()
16 | {
17 | var serviceCollection = new ServiceCollection();
18 |
19 | serviceCollection.AddWebSockets(o =>
20 | {
21 | o.KeepAliveInterval = TimeSpan.FromSeconds(1000);
22 | o.AllowedOrigins.Add("someString");
23 | });
24 |
25 | var services = serviceCollection.BuildServiceProvider();
26 | var socketOptions = services.GetRequiredService>().Value;
27 |
28 | Assert.Equal(TimeSpan.FromSeconds(1000), socketOptions.KeepAliveInterval);
29 | Assert.Single(socketOptions.AllowedOrigins);
30 | Assert.Equal("someString", socketOptions.AllowedOrigins[0]);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/BufferStream.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
2 |
3 | using System;
4 | using System.Collections.Concurrent;
5 | using System.Diagnostics.CodeAnalysis;
6 | using System.Diagnostics.Contracts;
7 | using System.IO;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace Microsoft.AspNetCore.WebSockets.Test
12 | {
13 | // This steam accepts writes from one side, buffers them internally, and returns the data via Reads
14 | // when requested on the other side.
15 | public class BufferStream : Stream
16 | {
17 | private bool _disposed;
18 | private bool _aborted;
19 | private bool _terminated;
20 | private Exception _abortException;
21 | private ConcurrentQueue _bufferedData;
22 | private ArraySegment _topBuffer;
23 | private SemaphoreSlim _readLock;
24 | private SemaphoreSlim _writeLock;
25 | private TaskCompletionSource _readWaitingForData;
26 |
27 | internal BufferStream()
28 | {
29 | _readLock = new SemaphoreSlim(1, 1);
30 | _writeLock = new SemaphoreSlim(1, 1);
31 | _bufferedData = new ConcurrentQueue();
32 | _readWaitingForData = new TaskCompletionSource();
33 | }
34 |
35 | public override bool CanRead
36 | {
37 | get { return true; }
38 | }
39 |
40 | public override bool CanSeek
41 | {
42 | get { return false; }
43 | }
44 |
45 | public override bool CanWrite
46 | {
47 | get { return true; }
48 | }
49 |
50 | #region NotSupported
51 |
52 | public override long Length
53 | {
54 | get { throw new NotSupportedException(); }
55 | }
56 |
57 | public override long Position
58 | {
59 | get { throw new NotSupportedException(); }
60 | set { throw new NotSupportedException(); }
61 | }
62 |
63 | public override long Seek(long offset, SeekOrigin origin)
64 | {
65 | throw new NotSupportedException();
66 | }
67 |
68 | public override void SetLength(long value)
69 | {
70 | throw new NotSupportedException();
71 | }
72 |
73 | #endregion NotSupported
74 |
75 | ///
76 | /// Ends the stream, meaning all future reads will return '0'.
77 | ///
78 | public void End()
79 | {
80 | _terminated = true;
81 | }
82 |
83 | public override void Flush()
84 | {
85 | CheckDisposed();
86 | // TODO: Wait for data to drain?
87 | }
88 |
89 | public override Task FlushAsync(CancellationToken cancellationToken)
90 | {
91 | if (cancellationToken.IsCancellationRequested)
92 | {
93 | TaskCompletionSource tcs = new TaskCompletionSource();
94 | tcs.TrySetCanceled();
95 | return tcs.Task;
96 | }
97 |
98 | Flush();
99 |
100 | // TODO: Wait for data to drain?
101 |
102 | return Task.FromResult(0);
103 | }
104 |
105 | public override int Read(byte[] buffer, int offset, int count)
106 | {
107 | if(_terminated)
108 | {
109 | return 0;
110 | }
111 |
112 | VerifyBuffer(buffer, offset, count, allowEmpty: false);
113 | _readLock.Wait();
114 | try
115 | {
116 | int totalRead = 0;
117 | do
118 | {
119 | // Don't drain buffered data when signaling an abort.
120 | CheckAborted();
121 | if (_topBuffer.Count <= 0)
122 | {
123 | byte[] topBuffer = null;
124 | while (!_bufferedData.TryDequeue(out topBuffer))
125 | {
126 | if (_disposed)
127 | {
128 | CheckAborted();
129 | // Graceful close
130 | return totalRead;
131 | }
132 | WaitForDataAsync().Wait();
133 | }
134 | _topBuffer = new ArraySegment(topBuffer);
135 | }
136 | int actualCount = Math.Min(count, _topBuffer.Count);
137 | Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount);
138 | _topBuffer = new ArraySegment(_topBuffer.Array,
139 | _topBuffer.Offset + actualCount,
140 | _topBuffer.Count - actualCount);
141 | totalRead += actualCount;
142 | offset += actualCount;
143 | count -= actualCount;
144 | }
145 | while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0));
146 | // Keep reading while there is more data available and we have more space to put it in.
147 | return totalRead;
148 | }
149 | finally
150 | {
151 | _readLock.Release();
152 | }
153 | }
154 |
155 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
156 | {
157 | // TODO: This option doesn't preserve the state object.
158 | // return ReadAsync(buffer, offset, count);
159 | return base.BeginRead(buffer, offset, count, callback, state);
160 | }
161 |
162 | public override int EndRead(IAsyncResult asyncResult)
163 | {
164 | // return ((Task)asyncResult).Result;
165 | return base.EndRead(asyncResult);
166 | }
167 |
168 | public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
169 | {
170 | if (_terminated)
171 | {
172 | return 0;
173 | }
174 |
175 | VerifyBuffer(buffer, offset, count, allowEmpty: false);
176 | var registration = cancellationToken.Register(Abort);
177 | await _readLock.WaitAsync(cancellationToken);
178 | try
179 | {
180 | int totalRead = 0;
181 | do
182 | {
183 | // Don't drained buffered data on abort.
184 | CheckAborted();
185 | if (_topBuffer.Count <= 0)
186 | {
187 | byte[] topBuffer = null;
188 | while (!_bufferedData.TryDequeue(out topBuffer))
189 | {
190 | if (_disposed)
191 | {
192 | CheckAborted();
193 | // Graceful close
194 | return totalRead;
195 | }
196 | await WaitForDataAsync();
197 | }
198 | _topBuffer = new ArraySegment(topBuffer);
199 | }
200 | var actualCount = Math.Min(count, _topBuffer.Count);
201 | Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount);
202 | _topBuffer = new ArraySegment(_topBuffer.Array,
203 | _topBuffer.Offset + actualCount,
204 | _topBuffer.Count - actualCount);
205 | totalRead += actualCount;
206 | offset += actualCount;
207 | count -= actualCount;
208 | }
209 | while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0));
210 | // Keep reading while there is more data available and we have more space to put it in.
211 | return totalRead;
212 | }
213 | finally
214 | {
215 | registration.Dispose();
216 | _readLock.Release();
217 | }
218 | }
219 |
220 | // Write with count 0 will still trigger OnFirstWrite
221 | public override void Write(byte[] buffer, int offset, int count)
222 | {
223 | VerifyBuffer(buffer, offset, count, allowEmpty: true);
224 | CheckDisposed();
225 |
226 | _writeLock.Wait();
227 | try
228 | {
229 | if (count == 0)
230 | {
231 | return;
232 | }
233 | // Copies are necessary because we don't know what the caller is going to do with the buffer afterwards.
234 | var internalBuffer = new byte[count];
235 | Buffer.BlockCopy(buffer, offset, internalBuffer, 0, count);
236 | _bufferedData.Enqueue(internalBuffer);
237 |
238 | SignalDataAvailable();
239 | }
240 | finally
241 | {
242 | _writeLock.Release();
243 | }
244 | }
245 |
246 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
247 | {
248 | Write(buffer, offset, count);
249 | var tcs = new TaskCompletionSource(state);
250 | tcs.TrySetResult(null);
251 | var result = tcs.Task;
252 | if (callback != null)
253 | {
254 | callback(result);
255 | }
256 | return result;
257 | }
258 |
259 | public override void EndWrite(IAsyncResult asyncResult) { }
260 |
261 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
262 | {
263 | VerifyBuffer(buffer, offset, count, allowEmpty: true);
264 | if (cancellationToken.IsCancellationRequested)
265 | {
266 | var tcs = new TaskCompletionSource();
267 | tcs.TrySetCanceled();
268 | return tcs.Task;
269 | }
270 |
271 | Write(buffer, offset, count);
272 | return Task.FromResult(null);
273 | }
274 |
275 | private static void VerifyBuffer(byte[] buffer, int offset, int count, bool allowEmpty)
276 | {
277 | if (offset < 0 || offset > buffer.Length)
278 | {
279 | throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
280 | }
281 | if (count < 0 || count > buffer.Length - offset
282 | || (!allowEmpty && count == 0))
283 | {
284 | throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
285 | }
286 | }
287 |
288 | private void SignalDataAvailable()
289 | {
290 | // Dispatch, as TrySetResult will synchronously execute the waiters callback and block our Write.
291 | Task.Factory.StartNew(() => _readWaitingForData.TrySetResult(null));
292 | }
293 |
294 | private Task WaitForDataAsync()
295 | {
296 | _readWaitingForData = new TaskCompletionSource();
297 |
298 | if (!_bufferedData.IsEmpty || _disposed)
299 | {
300 | // Race, data could have arrived before we created the TCS.
301 | _readWaitingForData.TrySetResult(null);
302 | }
303 |
304 | return _readWaitingForData.Task;
305 | }
306 |
307 | internal void Abort()
308 | {
309 | Abort(new OperationCanceledException());
310 | }
311 |
312 | internal void Abort(Exception innerException)
313 | {
314 | Contract.Requires(innerException != null);
315 | _aborted = true;
316 | _abortException = innerException;
317 | Dispose();
318 | }
319 |
320 | private void CheckAborted()
321 | {
322 | if (_aborted)
323 | {
324 | throw new IOException(string.Empty, _abortException);
325 | }
326 | }
327 |
328 | [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_writeLock", Justification = "ODEs from the locks would mask IOEs from abort.")]
329 | [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_readLock", Justification = "Data can still be read unless we get aborted.")]
330 | protected override void Dispose(bool disposing)
331 | {
332 | if (disposing)
333 | {
334 | // Throw for further writes, but not reads. Allow reads to drain the buffered data and then return 0 for further reads.
335 | _disposed = true;
336 | _readWaitingForData.TrySetResult(null);
337 | }
338 |
339 | base.Dispose(disposing);
340 | }
341 |
342 | private void CheckDisposed()
343 | {
344 | if (_disposed)
345 | {
346 | throw new ObjectDisposedException(GetType().FullName);
347 | }
348 | }
349 | }
350 | }
351 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/DuplexStream.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
2 |
3 | using System;
4 | using System.IO;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace Microsoft.AspNetCore.WebSockets.Test
9 | {
10 | // A duplex wrapper around a read and write stream.
11 | public class DuplexStream : Stream
12 | {
13 | public BufferStream ReadStream { get; }
14 | public BufferStream WriteStream { get; }
15 |
16 | public DuplexStream()
17 | : this (new BufferStream(), new BufferStream())
18 | {
19 | }
20 |
21 | public DuplexStream(BufferStream readStream, BufferStream writeStream)
22 | {
23 | ReadStream = readStream;
24 | WriteStream = writeStream;
25 | }
26 |
27 | public DuplexStream CreateReverseDuplexStream()
28 | {
29 | return new DuplexStream(WriteStream, ReadStream);
30 | }
31 |
32 |
33 | #region Properties
34 |
35 | public override bool CanRead
36 | {
37 | get { return ReadStream.CanRead; }
38 | }
39 |
40 | public override bool CanSeek
41 | {
42 | get { return false; }
43 | }
44 |
45 | public override bool CanTimeout
46 | {
47 | get { return ReadStream.CanTimeout || WriteStream.CanTimeout; }
48 | }
49 |
50 | public override bool CanWrite
51 | {
52 | get { return WriteStream.CanWrite; }
53 | }
54 |
55 | public override long Length
56 | {
57 | get { throw new NotSupportedException(); }
58 | }
59 |
60 | public override long Position
61 | {
62 | get { throw new NotSupportedException(); }
63 | set { throw new NotSupportedException(); }
64 | }
65 |
66 | public override int ReadTimeout
67 | {
68 | get { return ReadStream.ReadTimeout; }
69 | set { ReadStream.ReadTimeout = value; }
70 | }
71 |
72 | public override int WriteTimeout
73 | {
74 | get { return WriteStream.WriteTimeout; }
75 | set { WriteStream.WriteTimeout = value; }
76 | }
77 |
78 | #endregion Properties
79 |
80 | public override long Seek(long offset, SeekOrigin origin)
81 | {
82 | throw new NotSupportedException();
83 | }
84 |
85 | public override void SetLength(long value)
86 | {
87 | throw new NotSupportedException();
88 | }
89 |
90 | public override int ReadByte()
91 | {
92 | return ReadStream.ReadByte();
93 | }
94 |
95 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
96 | {
97 | return ReadStream.BeginRead(buffer, offset, count, callback, state);
98 | }
99 |
100 | public override int EndRead(IAsyncResult asyncResult)
101 | {
102 | return ReadStream.EndRead(asyncResult);
103 | }
104 |
105 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
106 | {
107 | return ReadStream.ReadAsync(buffer, offset, count, cancellationToken);
108 | }
109 |
110 | public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
111 | {
112 | return ReadStream.CopyToAsync(destination, bufferSize, cancellationToken);
113 | }
114 |
115 | #region Read
116 |
117 | public override int Read(byte[] buffer, int offset, int count)
118 | {
119 | return ReadStream.Read(buffer, offset, count);
120 | }
121 |
122 | #endregion Read
123 |
124 | #region Write
125 |
126 | public override void Write(byte[] buffer, int offset, int count)
127 | {
128 | WriteStream.Write(buffer, offset, count);
129 | }
130 | public override void WriteByte(byte value)
131 | {
132 | WriteStream.WriteByte(value);
133 | }
134 |
135 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
136 | {
137 | return WriteStream.BeginWrite(buffer, offset, count, callback, state);
138 | }
139 |
140 | public override void EndWrite(IAsyncResult asyncResult)
141 | {
142 | WriteStream.EndWrite(asyncResult);
143 | }
144 |
145 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
146 | {
147 | return WriteStream.WriteAsync(buffer, offset, count, cancellationToken);
148 | }
149 |
150 | public override Task FlushAsync(CancellationToken cancellationToken)
151 | {
152 | return WriteStream.FlushAsync(cancellationToken);
153 | }
154 |
155 | public override void Flush()
156 | {
157 | WriteStream.Flush();
158 | }
159 |
160 | #endregion Write
161 |
162 | protected override void Dispose(bool disposing)
163 | {
164 | if (disposing)
165 | {
166 | ReadStream.Dispose();
167 | WriteStream.Dispose();
168 | }
169 | base.Dispose(disposing);
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/IWebHostPortExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using Microsoft.AspNetCore.Hosting.Server.Features;
8 |
9 | namespace Microsoft.AspNetCore.Hosting
10 | {
11 | public static class IWebHostPortExtensions
12 | {
13 | public static int GetPort(this IWebHost host)
14 | {
15 | return host.GetPorts().First();
16 | }
17 |
18 | public static IEnumerable GetPorts(this IWebHost host)
19 | {
20 | return host.GetUris()
21 | .Select(u => u.Port);
22 | }
23 |
24 | public static IEnumerable GetUris(this IWebHost host)
25 | {
26 | return host.ServerFeatures.Get().Addresses
27 | .Select(a => new Uri(a));
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/KestrelWebSocketHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.Logging;
11 | using Microsoft.Extensions.DependencyInjection;
12 |
13 | namespace Microsoft.AspNetCore.WebSockets.Test
14 | {
15 | public class KestrelWebSocketHelpers
16 | {
17 | public static IDisposable CreateServer(ILoggerFactory loggerFactory, out int port, Func app, Action configure = null)
18 | {
19 | configure = configure ?? (o => { });
20 | Action startup = builder =>
21 | {
22 | builder.Use(async (ct, next) =>
23 | {
24 | try
25 | {
26 | // Kestrel does not return proper error responses:
27 | // https://github.com/aspnet/KestrelHttpServer/issues/43
28 | await next();
29 | }
30 | catch (Exception ex)
31 | {
32 | if (ct.Response.HasStarted)
33 | {
34 | throw;
35 | }
36 |
37 | ct.Response.StatusCode = 500;
38 | ct.Response.Headers.Clear();
39 | await ct.Response.WriteAsync(ex.ToString());
40 | }
41 | });
42 | builder.UseWebSockets();
43 | builder.Run(c => app(c));
44 | };
45 |
46 | var configBuilder = new ConfigurationBuilder();
47 | configBuilder.AddInMemoryCollection();
48 | var config = configBuilder.Build();
49 | config["server.urls"] = $"http://127.0.0.1:0";
50 |
51 | var host = new WebHostBuilder()
52 | .ConfigureServices(s =>
53 | {
54 | s.AddWebSockets(configure);
55 | s.AddSingleton(loggerFactory);
56 | })
57 | .UseConfiguration(config)
58 | .UseKestrel()
59 | .Configure(startup)
60 | .Build();
61 |
62 | host.Start();
63 | port = host.GetPort();
64 |
65 | return host;
66 | }
67 | }
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/Microsoft.AspNetCore.WebSockets.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(StandardTestTfms)
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/SendReceiveTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Net.WebSockets;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Xunit;
8 |
9 | namespace Microsoft.AspNetCore.WebSockets.Test
10 | {
11 | public class SendReceiveTests
12 | {
13 | [Fact]
14 | public async Task ClientToServerTextMessage()
15 | {
16 | const string message = "Hello, World!";
17 |
18 | var pair = WebSocketPair.Create();
19 | var sendBuffer = Encoding.UTF8.GetBytes(message);
20 |
21 | await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None);
22 |
23 | var receiveBuffer = new byte[32];
24 | var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None);
25 |
26 | Assert.Equal(WebSocketMessageType.Text, result.MessageType);
27 | Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count));
28 | }
29 |
30 | [Fact]
31 | public async Task ServerToClientTextMessage()
32 | {
33 | const string message = "Hello, World!";
34 |
35 | var pair = WebSocketPair.Create();
36 | var sendBuffer = Encoding.UTF8.GetBytes(message);
37 |
38 | await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None);
39 |
40 | var receiveBuffer = new byte[32];
41 | var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None);
42 |
43 | Assert.Equal(WebSocketMessageType.Text, result.MessageType);
44 | Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count));
45 | }
46 |
47 | [Fact]
48 | public async Task ClientToServerBinaryMessage()
49 | {
50 | var pair = WebSocketPair.Create();
51 | var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef };
52 |
53 | await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None);
54 |
55 | var receiveBuffer = new byte[32];
56 | var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None);
57 |
58 | Assert.Equal(WebSocketMessageType.Binary, result.MessageType);
59 | Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray());
60 | }
61 |
62 | [Fact]
63 | public async Task ServerToClientBinaryMessage()
64 | {
65 | var pair = WebSocketPair.Create();
66 | var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef };
67 |
68 | await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None);
69 |
70 | var receiveBuffer = new byte[32];
71 | var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None);
72 |
73 | Assert.Equal(WebSocketMessageType.Binary, result.MessageType);
74 | Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray());
75 | }
76 |
77 | [Fact]
78 | public async Task ThrowsWhenUnderlyingStreamClosed()
79 | {
80 | var pair = WebSocketPair.Create();
81 | var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef };
82 |
83 | await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None);
84 |
85 | var receiveBuffer = new byte[32];
86 | var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None);
87 |
88 | Assert.Equal(WebSocketMessageType.Binary, result.MessageType);
89 |
90 | // Close the client socket's read end
91 | pair.ClientStream.ReadStream.End();
92 |
93 | // Assert.Throws doesn't support async :(
94 | try
95 | {
96 | await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None);
97 |
98 | // The exception should prevent this line from running
99 | Assert.False(true, "Expected an exception to be thrown!");
100 | }
101 | catch (WebSocketException ex)
102 | {
103 | Assert.Equal(WebSocketError.ConnectionClosedPrematurely, ex.WebSocketErrorCode);
104 | }
105 | }
106 | }
107 | }
--------------------------------------------------------------------------------
/test/Microsoft.AspNetCore.WebSockets.Test/WebSocketPair.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.WebSockets;
3 | using Microsoft.AspNetCore.WebSockets.Internal;
4 |
5 | namespace Microsoft.AspNetCore.WebSockets.Test
6 | {
7 | internal class WebSocketPair
8 | {
9 | public WebSocket ClientSocket { get; }
10 | public WebSocket ServerSocket { get; }
11 | public DuplexStream ServerStream { get; }
12 | public DuplexStream ClientStream { get; }
13 |
14 | public WebSocketPair(DuplexStream serverStream, DuplexStream clientStream, WebSocket clientSocket, WebSocket serverSocket)
15 | {
16 | ClientStream = clientStream;
17 | ServerStream = serverStream;
18 | ClientSocket = clientSocket;
19 | ServerSocket = serverSocket;
20 | }
21 |
22 | public static WebSocketPair Create()
23 | {
24 | // Create streams
25 | var serverStream = new DuplexStream();
26 | var clientStream = serverStream.CreateReverseDuplexStream();
27 |
28 | return new WebSocketPair(
29 | serverStream,
30 | clientStream,
31 | clientSocket: WebSocketProtocol.CreateFromStream(clientStream, isServer: false, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)),
32 | serverSocket: WebSocketProtocol.CreateFromStream(serverStream, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)));
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/version.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | 3.0.0
4 | alpha1
5 | $(VersionPrefix)
6 | $(VersionPrefix)-$(VersionSuffix)-final
7 | t000
8 | a-
9 | $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))
10 | $(VersionSuffix)-$(BuildNumber)
11 |
12 |
13 |
--------------------------------------------------------------------------------