Commit e8eff8be authored by David Kline's avatar David Kline Committed by GitHub
Browse files

Merge pull request #57 from Microsoft/HymanMatt

Add UWP web socket support
parents b1d7e7f4 fa153f2b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -71,6 +71,7 @@
    <Compile Include="Core\OsInformationTests.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="TestHelpers.cs" />
    <Compile Include="WDPMockImplementations\WebSocket.cs" />
    <Compile Include="Xbox\UserManagementTests.cs" />
    <Compile Include="Xbox\XboxAppDeploymentTests.cs" />
    <Compile Include="Xbox\XboxSettingsTests.cs" />
+50 −0
Original line number Diff line number Diff line
//----------------------------------------------------------------------------------------------
// <copyright file="WebSocket.cs" company="Microsoft Corporation">
//     Licensed under the MIT License. See LICENSE.TXT in the project root license information.
// </copyright>
//----------------------------------------------------------------------------------------------

using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace Microsoft.Tools.WindowsDevicePortal
{
    /// <summary>
    /// MOCK implementation of HTTP web socket wrapper
    /// </summary>
    /// <typeparam name="T">Return type for the websocket messages.</typeparam>
    internal partial class WebSocket<T>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="WebSocket{T}" /> class.
        /// </summary>
        /// <param name="connection">Implementation of a connection object.</param>
        /// <param name="serverCertificateValidationHandler">Server certificate handler.</param>
        public WebSocket(IDevicePortalConnection connection, Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> serverCertificateValidationHandler)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Stops listneing for messages from the websocket and closes the connection to the websocket.
        /// </summary>
        /// <returns>The task of closing the websocket connection.</returns>
        private async Task StopListeningForMessagesInternal()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Connects to the websocket and starts listening for messages from the websocket.
        /// Once they are received they are parsed and the WebSocketMessageReceived event is raised.
        /// </summary>
        /// <param name="endpoint">The uri that the weboscket should connect to</param>
        /// <returns>The task of listening for messages from the websocket.</returns>
        private async Task StartListeningForMessagesInternal(Uri endpoint)
        {
            throw new NotImplementedException();
        }
    }
}
+14 −19
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ namespace Microsoft.Tools.WindowsDevicePortal
        /// </summary>
        private static readonly string SystemPerfApi = "api/resourcemanager/systemperf";

#if !WINDOWS_UWP
        /// <summary>
        /// Web socket to get running processes.
        /// </summary>
@@ -55,8 +54,6 @@ namespace Microsoft.Tools.WindowsDevicePortal
            set;
        }

#endif // !WINDOWS_UWP

        /// <summary>
        /// Gets the collection of processes running on the device.
        /// </summary>
@@ -66,7 +63,6 @@ namespace Microsoft.Tools.WindowsDevicePortal
            return await this.Get<RunningProcesses>(RunningProcessApi);
        }

#if !WINDOWS_UWP
        /// <summary>
        /// Starts listening for the running processes on the device with them being returned via the RunningProcessesMessageReceived event handler.
        /// </summary>
@@ -75,7 +71,11 @@ namespace Microsoft.Tools.WindowsDevicePortal
        {
            if (this.deviceProcessesWebSocket == null)
            {
#if WINDOWS_UWP
                this.deviceProcessesWebSocket = new WebSocket<RunningProcesses>(this.deviceConnection);
#else
                this.deviceProcessesWebSocket = new WebSocket<RunningProcesses>(this.deviceConnection, this.ServerCertificateValidation);
#endif

                this.deviceProcessesWebSocket.WebSocketMessageReceived += this.RunningProcessesReceivedHandler;
            }
@@ -87,10 +87,7 @@ namespace Microsoft.Tools.WindowsDevicePortal
                }
            }

            await this.deviceProcessesWebSocket.OpenConnection(RunningProcessApi);

            // Do not await on the actual listening.
            Task listenTask = this.deviceProcessesWebSocket.StartListeningForMessages();
            await this.deviceProcessesWebSocket.StartListeningForMessages(RunningProcessApi);
        }

        /// <summary>
@@ -104,9 +101,8 @@ namespace Microsoft.Tools.WindowsDevicePortal
                return;
            }

            await this.deviceProcessesWebSocket.CloseConnection();
            await this.deviceProcessesWebSocket.StopListeningForMessages();
        }
#endif // !WINDOWS_UWP

        /// <summary>
        /// Gets system performance information for the device.
@@ -117,7 +113,6 @@ namespace Microsoft.Tools.WindowsDevicePortal
            return await this.Get<SystemPerformanceInformation>(SystemPerfApi);
        }

#if !WINDOWS_UWP
        /// <summary>
        /// Starts listening for the system performance information for the device with it being returned via the SystemPerfMessageReceived event handler.
        /// </summary>
@@ -126,7 +121,11 @@ namespace Microsoft.Tools.WindowsDevicePortal
        {
            if (this.systemPerfWebSocket == null)
            {
#if WINDOWS_UWP
                this.systemPerfWebSocket = new WebSocket<SystemPerformanceInformation>(this.deviceConnection);
#else
                this.systemPerfWebSocket = new WebSocket<SystemPerformanceInformation>(this.deviceConnection, this.ServerCertificateValidation);
#endif

                this.systemPerfWebSocket.WebSocketMessageReceived += this.SystemPerfMessageReceived;
            }
@@ -138,10 +137,7 @@ namespace Microsoft.Tools.WindowsDevicePortal
                }
            }

            await this.systemPerfWebSocket.OpenConnection(SystemPerfApi);

            // Do not await on the actual listening.
            Task listenTask = this.systemPerfWebSocket.StartListeningForMessages();
            await this.systemPerfWebSocket.StartListeningForMessages(SystemPerfApi);
        }

        /// <summary>
@@ -155,7 +151,7 @@ namespace Microsoft.Tools.WindowsDevicePortal
                return;
            }

            await this.systemPerfWebSocket.CloseConnection();
            await this.systemPerfWebSocket.StopListeningForMessages();
        }

        /// <summary>
@@ -192,7 +188,6 @@ namespace Microsoft.Tools.WindowsDevicePortal
            }
        }

#endif // !WINDOWS_UWP
#region Device contract

        /// <summary>
+31 −153
Original line number Diff line number Diff line
@@ -4,17 +4,9 @@
// </copyright>
//----------------------------------------------------------------------------------------------

// TODO: enable this once the rest of the code is UWP friendly
#if !WINDOWS_UWP 
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Runtime.Serialization.Json;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Tools.WindowsDevicePortal
@@ -23,50 +15,13 @@ namespace Microsoft.Tools.WindowsDevicePortal
    /// HTTP Websocket Wrapper
    /// </summary>
    /// <typeparam name="T">Return type for the websocket messages.</typeparam>
    internal class WebSocket<T>
    internal partial class WebSocket<T>
    {
        /// <summary>
        /// The hresult for the connection being reset by the peer.
        /// </summary>
        private const int WSAECONNRESET = 0x2746;

        /// <summary>
        /// The maximum number of bytes that can be received in a single chunk.
        /// </summary>
        private static readonly uint MaxChunkSizeInBytes = 1024;

        /// <summary>
        /// The device that the <see cref="WebSocket{T}" /> is connected to.
        /// </summary>
        private IDevicePortalConnection deviceConnection;

        /// <summary>
        /// The <see cref="ClientWebSocket" /> that is being wrapped.
        /// </summary>
        private ClientWebSocket websocket = new ClientWebSocket();

        /// <summary>
        /// <see cref="ManualResetEvent" /> used to indicate that the <see cref="WebSocket{T}" /> has stopped receiving messages.
        /// </summary>
        private ManualResetEvent stoppedReceivingMessages = new ManualResetEvent(false);

        /// <summary>
        /// The handler used to validate server certificates.
        /// </summary>
        private Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> serverCertificateValidationHandler;

        /// <summary>
        /// Initializes a new instance of the <see cref="WebSocket{T}" /> class.
        /// </summary>
        /// <param name="connection">Implementation of a connection object.</param>
        /// <param name="serverCertificateValidationHandler">Server certificate handler.</param>
        public WebSocket(IDevicePortalConnection connection, Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> serverCertificateValidationHandler)
        {
            this.deviceConnection = connection;
            this.IsListeningForMessages = false;
            this.serverCertificateValidationHandler = serverCertificateValidationHandler;
        }

        /// <summary>
        /// Gets a value indicating whether the web socket is listening for messages.
        /// </summary>
@@ -86,105 +41,50 @@ namespace Microsoft.Tools.WindowsDevicePortal
        }

        /// <summary>
        /// Opens a connection to the specified websocket API with the provided payload.
        /// </summary>
        /// <param name="apiPath">The relative portion of the uri path that specifies the API to call.</param>
        /// <param name="payload">The query string portion of the uri path that provides the parameterized data.</param>
        /// <returns>The task of opening a connection to the websocket.</returns>
        internal async Task OpenConnection(
            string apiPath,
            string payload = null)
        {
            Uri uri = Utilities.BuildEndpoint(
                this.deviceConnection.WebSocketConnection,
                apiPath,
                payload);

            this.websocket.Options.UseDefaultCredentials = false;
            this.websocket.Options.Credentials = this.deviceConnection.Credentials;
            this.websocket.Options.SetRequestHeader("Origin", this.deviceConnection.Connection.AbsoluteUri);

            // There is no way to set a ServerCertificateValidationCallback for a single web socket, hence the workaround.
            ServicePointManager.ServerCertificateValidationCallback = delegate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors policyErrors)
            {
                return this.serverCertificateValidationHandler(sender, cert, chain, policyErrors);
            };

            await this.websocket.ConnectAsync(uri, CancellationToken.None);
        }

        /// <summary>
        /// Closes the connection to the websocket.
        /// Closes the connection to the websocket and stop listening for messages.
        /// </summary>
        /// <returns>The task of closing the websocket connection.</returns>
        internal async Task CloseConnection()
        {
            if (this.websocket.State != WebSocketState.Closed)
        internal async Task StopListeningForMessages()
        {
                await this.websocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);

                // Wait for web socket to no longer be receiving messages.
            if (this.IsListeningForMessages)
            {
                    this.stoppedReceivingMessages.WaitOne();
                    this.stoppedReceivingMessages.Reset();
                }

                // Reset websocket as WDP will abort the connection once it receives the close message.
                this.websocket = new ClientWebSocket();
                await this.StopListeningForMessagesInternal();
            }
        }

        /// <summary>
        /// Starts listening for messages from the websocket. Once they are received they are parsed and the WebSocketMessageReceived event is raised.
        /// </summary>
        /// <param name="apiPath">The relative portion of the uri path that specifies the API to call.</param>
        /// <param name="payload">The query string portion of the uri path that provides the parameterized data.</param>
        /// <returns>The task of listening for messages from the websocket.</returns>
        internal async Task StartListeningForMessages()
        {
            if (this.websocket.State != WebSocketState.Open || this.IsListeningForMessages)
            {
                return;
            }

            this.IsListeningForMessages = true;

            try
            {
                ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[MaxChunkSizeInBytes]);

                // Once close message is sent do not try to get any more messages as WDP will abort the web socket connection.
                while (this.websocket.State == WebSocketState.Open)
                {
                    // Receive single message in chunks.
                    using (var ms = new MemoryStream())
                    {
                        WebSocketReceiveResult result;

                        do
        internal async Task StartListeningForMessages(
            string apiPath,
            string payload = null)
        {
                            result = await this.websocket.ReceiveAsync(buffer, CancellationToken.None);

                            if (result.MessageType == WebSocketMessageType.Close)
            if (!this.IsListeningForMessages)
            {
                                await this.websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                                return;
                            }
                Uri uri = Utilities.BuildEndpoint(
                    this.deviceConnection.WebSocketConnection,
                    apiPath,
                    payload);

                            if (result.Count > MaxChunkSizeInBytes)
                            {
                                throw new InvalidOperationException("Buffer not large enough");
                await this.StartListeningForMessagesInternal(uri);
            }

                            ms.Write(buffer.Array, buffer.Offset, result.Count);
        }
                        while (!result.EndOfMessage);

                        ms.Seek(0, SeekOrigin.Begin);

                        if (result.MessageType == WebSocketMessageType.Text)
        /// <summary>
        /// Converts received stream to a parsed message and passes it to the WebSocketMessageReceived handler.
        /// </summary>
        /// <param name="stream">The received stream.</param>
        private void ConvertStreamToMessage(Stream stream)
        {
            if (stream != null && stream.Length != 0)
            {
                using (stream)
                {
                    DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
                            T message = (T)serializer.ReadObject(ms);
                    T message = (T)serializer.ReadObject(stream);

                    this.WebSocketMessageReceived?.Invoke(
                        this,
@@ -193,26 +93,4 @@ namespace Microsoft.Tools.WindowsDevicePortal
            }
        }
    }
            catch (WebSocketException e)
            {
                // If WDP aborted the web socket connection ignore the exception.
                SocketException socketException = e.InnerException?.InnerException as SocketException;
                if (socketException != null)
                {
                    if (socketException.NativeErrorCode == WSAECONNRESET)
                    {
                        return;
                    }
                }

                throw;
            }
            finally
            {
                this.stoppedReceivingMessages.Set();
                this.IsListeningForMessages = false;
            }
        }
    }
}
 No newline at end of file
#endif // !WINDOWS_UWP
 No newline at end of file
+138 −0
Original line number Diff line number Diff line
//----------------------------------------------------------------------------------------------
// <copyright file="WebSocket.cs" company="Microsoft Corporation">
//     Licensed under the MIT License. See LICENSE.TXT in the project root license information.
// </copyright>
//----------------------------------------------------------------------------------------------

using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Networking.Sockets;
using Windows.Security.Credentials;
using Windows.Storage.Streams;

namespace Microsoft.Tools.WindowsDevicePortal
{
    /// <summary>
    /// HTTP Websocket Wrapper
    /// </summary>
    /// <typeparam name="T">Return type for the websocket messages.</typeparam>
    internal partial class WebSocket<T>
    {
        /// <summary>
        /// The <see cref="MessageWebSocket" /> that is being wrapped.
        /// </summary>
        private MessageWebSocket websocket = null;

        /// <summary>
        /// Initializes a new instance of the <see cref="WebSocket{T}" /> class.
        /// </summary>
        /// <param name="connection">Implementation of a connection object.</param>
        public WebSocket(IDevicePortalConnection connection)
        {
            this.deviceConnection = connection;
            this.IsListeningForMessages = false;
        }

        /// <summary>
        /// Opens a connection to the specified websocket API and starts listening for messages.
        /// </summary>
        /// <param name="endpoint">The uri that the weboscket should connect to</param>
        /// <returns>The task of opening a connection to the websocket.</returns>
        private async Task StartListeningForMessagesInternal(
            Uri endpoint)
        {
            this.websocket = new MessageWebSocket();

            this.websocket.Control.MessageType = SocketMessageType.Utf8;

            this.websocket.MessageReceived += this.MessageReceived;

            this.websocket.Closed += (senderSocket, args) =>
            {
                if (this.websocket != null)
                {
                    this.websocket.Dispose();
                    this.websocket = null;
                    this.IsListeningForMessages = false;
                }
            };

            PasswordCredential cred = new PasswordCredential();
            cred.UserName = this.deviceConnection.Credentials.UserName;
            cred.Password = this.deviceConnection.Credentials.Password;

            this.websocket.Control.ServerCredential = cred;
            this.websocket.SetRequestHeader("Origin", this.deviceConnection.Connection.AbsoluteUri);

            // Do not wait on receiving messages.
            Task connectTask = this.ConnectAsync(endpoint);
        }

        /// <summary>
        /// Opens a connection to the specified websocket API.
        /// </summary>
        /// <param name="endpoint">The uri that the weboscket should connect to</param>
        /// <returns>The task of opening a connection to the websocket.</returns>
        private async Task ConnectAsync(
            Uri endpoint)
        {
            bool connecting = true;
            try
            {
                await this.websocket.ConnectAsync(endpoint);
                connecting = false;
                this.IsListeningForMessages = true;
            }
            catch (Exception)
            {
                // Error happened during connect operation.
                if (connecting && this.websocket != null)
                {
                    this.websocket.Dispose();
                    this.websocket = null;
                    this.IsListeningForMessages = false;
                }
            }
        }

        /// <summary>
        /// Converts received stream to a parsed message and passes it to the WebSocketMessageReceived handler.
        /// </summary>
        /// <param name="sender">The  <see cref="MessageWebSocket" /> that sent the message.</param>
        /// <param name="args">The message from the web socket.</param>
        private void MessageReceived(MessageWebSocket sender, MessageWebSocketMessageReceivedEventArgs args)
        {
            using (IInputStream inputStream = args.GetDataStream())
            {
                Stream stream = new MemoryStream();

                Task copyTask = inputStream.AsStreamForRead().CopyToAsync(stream);
                copyTask.Wait();

                // Ensure we return with the stream pointed at the origin.
                stream.Position = 0;

                this.ConvertStreamToMessage(stream);
            }
        }

        /// <summary>
        /// Closes the connection to the websocket.
        /// </summary>
        /// <returns>The task of closing the websocket connection.</returns>
        private async Task StopListeningForMessagesInternal()
        {
            if (this.IsListeningForMessages)
            {
                if (this.websocket != null)
                {
                    // Code 1000 indicates that the purpose of the connection has been fulfilled and the connection is no longer needed.
                    this.websocket.Close(1000, "Closed due to user request.");
                    this.websocket = null;
                    this.IsListeningForMessages = false;
                }
            }
        }
    }
}
Loading