diff --git a/WindowsDevicePortalWrapper/UnitTestProject/UnitTestProject.csproj b/WindowsDevicePortalWrapper/UnitTestProject/UnitTestProject.csproj index 7ff5def3..abcf4fd4 100644 --- a/WindowsDevicePortalWrapper/UnitTestProject/UnitTestProject.csproj +++ b/WindowsDevicePortalWrapper/UnitTestProject/UnitTestProject.csproj @@ -60,6 +60,9 @@ + + WDPMockImplementations\HttpMultipartFileContent.cs + diff --git a/WindowsDevicePortalWrapper/UnitTestProject/WDPMockImplementations/RestPost.cs b/WindowsDevicePortalWrapper/UnitTestProject/WDPMockImplementations/RestPost.cs index 87423035..75f872a5 100644 --- a/WindowsDevicePortalWrapper/UnitTestProject/WDPMockImplementations/RestPost.cs +++ b/WindowsDevicePortalWrapper/UnitTestProject/WDPMockImplementations/RestPost.cs @@ -24,21 +24,25 @@ public partial class DevicePortal /// Optional stream containing data for the request body. /// The type of that request body data. /// Task tracking the completion of the POST request - private async Task PostAsync( + private Task PostAsync( Uri uri, Stream requestStream = null, string requestStreamContentType = null) { StreamContent requestContent = null; - MemoryStream dataStream = null; - if (requestStream != null) { requestContent = new StreamContent(requestStream); requestContent.Headers.Remove(ContentTypeHeaderName); requestContent.Headers.TryAddWithoutValidation(ContentTypeHeaderName, requestStreamContentType); } - + return PostAsync(uri, requestContent); + } + private async Task PostAsync( + Uri uri, + HttpContent requestContent) + { + MemoryStream dataStream = null; WebRequestHandler requestSettings = new WebRequestHandler(); requestSettings.UseDefaultCredentials = false; requestSettings.Credentials = this.deviceConnection.Credentials; diff --git a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.Shared/Core/AppDeployment.cs b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.Shared/Core/AppDeployment.cs index be904419..a2024d77 100644 --- a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.Shared/Core/AppDeployment.cs +++ b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.Shared/Core/AppDeployment.cs @@ -17,6 +17,7 @@ #if WINDOWS_UWP using Windows.Foundation; using Windows.Security.Credentials; +using Windows.Storage.Streams; using Windows.Web.Http; using Windows.Web.Http.Filters; using Windows.Web.Http.Headers; @@ -110,7 +111,7 @@ public async Task InstallApplicationAsync( } } } - + // Create the API endpoint and generate a unique boundary string. Uri uri; string boundaryString; @@ -119,59 +120,17 @@ public async Task InstallApplicationAsync( out uri, out boundaryString); - using (MemoryStream dataStream = new MemoryStream()) - { - byte[] data; - - // Copy the application package. - installPhaseDescription = string.Format("Copying: {0}", packageFile.Name); - this.SendAppInstallStatus( - ApplicationInstallStatus.InProgress, - ApplicationInstallPhase.CopyingFile, - installPhaseDescription); - data = Encoding.ASCII.GetBytes(string.Format("--{0}\r\n", boundaryString)); - dataStream.Write(data, 0, data.Length); - CopyFileToRequestStream(packageFile, dataStream); - - // Copy dependency files, if any. - foreach (string dependencyFile in dependencyFileNames) - { - FileInfo fi = new FileInfo(dependencyFile); - installPhaseDescription = string.Format("Copying: {0}", fi.Name); - this.SendAppInstallStatus( - ApplicationInstallStatus.InProgress, - ApplicationInstallPhase.CopyingFile, - installPhaseDescription); - data = Encoding.ASCII.GetBytes(string.Format("\r\n--{0}\r\n", boundaryString)); - dataStream.Write(data, 0, data.Length); - CopyFileToRequestStream(fi, dataStream); - } - - // Copy the certificate file, if provided. - if (!string.IsNullOrEmpty(certificateFileName)) - { - FileInfo fi = new FileInfo(certificateFileName); - installPhaseDescription = string.Format("Copying: {0}", fi.Name); - this.SendAppInstallStatus( - ApplicationInstallStatus.InProgress, - ApplicationInstallPhase.CopyingFile, - installPhaseDescription); - data = Encoding.ASCII.GetBytes(string.Format("\r\n--{0}\r\n", boundaryString)); - dataStream.Write(data, 0, data.Length); - CopyFileToRequestStream(fi, dataStream); - } - - // Close the installation request data. - data = Encoding.ASCII.GetBytes(string.Format("\r\n--{0}--\r\n", boundaryString)); - dataStream.Write(data, 0, data.Length); - - dataStream.Position = 0; - - string contentType = string.Format("multipart/form-data; boundary={0}", boundaryString); + installPhaseDescription = string.Format("Copying: {0}", packageFile.Name); + this.SendAppInstallStatus( + ApplicationInstallStatus.InProgress, + ApplicationInstallPhase.CopyingFile, + installPhaseDescription); - // Make the HTTP request. - await this.PostAsync(uri, dataStream, contentType); - } + var content = new HttpMultipartFileContent(); + content.Add(packageFile.FullName); + content.AddRange(dependencyFileNames); + content.Add(certificateFileName); + await this.PostAsync(uri, content); // Poll the status until complete. ApplicationInstallStatus status = ApplicationInstallStatus.InProgress; diff --git a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/HttpRest/HttpMultipartFileContent.cs b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/HttpRest/HttpMultipartFileContent.cs new file mode 100644 index 00000000..72b728d3 --- /dev/null +++ b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/HttpRest/HttpMultipartFileContent.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Storage.Streams; +using Windows.Web.Http; +using Windows.Web.Http.Headers; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// This class mimicks , with two main differences + /// 1. Simplifies posting files by taking file names instead of managing streams. + /// 2. Does not quote the boundaries, due to a bug in the device portal: + /// https://insider.windows.com/FeedbackHub/fb?contextid=519&feedbackid=19a5af49-38f4-409a-b464-e66f80679545&form=1 + /// + internal sealed class HttpMultipartFileContent : IHttpContent + { + private List items = new List(); + private string boundaryString; + + public HttpMultipartFileContent() : this(Guid.NewGuid().ToString()) { } + + public HttpMultipartFileContent(string boundary) + { + boundaryString = boundary; + Headers.ContentType = new HttpMediaTypeHeaderValue(string.Format("multipart/form-data; boundary={0}", boundaryString)); + } + + public void Add(string filename) + { + if (filename != null) + items.Add(filename); + } + + public void AddRange(IEnumerable filenames) + { + if (filenames != null) + items.AddRange(filenames); + } + + public HttpContentHeaderCollection Headers { get; } = new HttpContentHeaderCollection(); + + IAsyncOperationWithProgress IHttpContent.BufferAllAsync() + { + throw new NotImplementedException(); + } + + void IDisposable.Dispose() + { + items.Clear(); + } + + IAsyncOperationWithProgress IHttpContent.ReadAsBufferAsync() + { + throw new NotImplementedException(); + } + + IAsyncOperationWithProgress IHttpContent.ReadAsInputStreamAsync() + { + throw new NotImplementedException(); + } + + IAsyncOperationWithProgress IHttpContent.ReadAsStringAsync() + { + throw new NotImplementedException(); + } + + bool IHttpContent.TryComputeLength(out ulong length) + { + length = 0; + var boundaryLength = Encoding.ASCII.GetBytes(string.Format("--{0}\r\n", boundaryString)).Length; + foreach (var item in items) + { + var headerdata = GetFileHeader(new FileInfo(item)); + length += (ulong)(boundaryLength + headerdata.Length + new FileInfo(item).Length + 2); + } + length += (ulong)(boundaryLength + 2); + return true; + } + + IAsyncOperationWithProgress IHttpContent.WriteToStreamAsync(IOutputStream outputStream) + { + return System.Runtime.InteropServices.WindowsRuntime.AsyncInfo.Run((token, progress) => + { + return WriteToStreamAsyncTask(outputStream, (ulong p) => progress.Report(p)); + }); + } + + private async Task WriteToStreamAsyncTask(IOutputStream outputStream, Action progress) + { + ulong bytesWritten = 0; + var outStream = outputStream.AsStreamForWrite(); + var boundary = Encoding.ASCII.GetBytes($"--{boundaryString}\r\n"); + var newline = Encoding.ASCII.GetBytes("\r\n"); + foreach (var item in items) + { + outStream.Write(boundary, 0, boundary.Length); + bytesWritten += (ulong)boundary.Length; + var headerdata = GetFileHeader(new FileInfo(item)); + outStream.Write(headerdata, 0, headerdata.Length); + bytesWritten += (ulong)headerdata.Length; + using (var file = File.OpenRead(item)) + { + await file.CopyToAsync(outStream); + bytesWritten += (ulong)file.Position; + } + outStream.Write(newline, 0, newline.Length); + bytesWritten += (ulong)newline.Length; + await outStream.FlushAsync(); + progress(bytesWritten); + } + // Close the installation request data. + boundary = Encoding.ASCII.GetBytes($"--{boundaryString}--\r\n"); + outStream.Write(boundary, 0, boundary.Length); + await outStream.FlushAsync(); + bytesWritten += (ulong)boundary.Length; + return bytesWritten; + } + private static byte[] GetFileHeader(FileInfo info) + { + string contentType = "application/octet-stream"; + if (info.Extension.ToLower() == ".cer") + contentType = "application/x-x509-ca-cert"; + + return Encoding.ASCII.GetBytes(string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", info.Name, contentType)); + } + } +} diff --git a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/HttpRest/RestPost.cs b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/HttpRest/RestPost.cs index b8eabb84..b5380083 100644 --- a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/HttpRest/RestPost.cs +++ b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/HttpRest/RestPost.cs @@ -30,21 +30,26 @@ public partial class DevicePortal /// The type of that request body data. /// Task tracking the completion of the POST request #pragma warning disable 1998 - private async Task PostAsync( + private Task PostAsync( Uri uri, Stream requestStream = null, string requestStreamContentType = null) { HttpStreamContent requestContent = null; - IBuffer dataBuffer = null; - + if (requestStream != null) { requestContent = new HttpStreamContent(requestStream.AsInputStream()); requestContent.Headers.Remove(ContentTypeHeaderName); requestContent.Headers.TryAppendWithoutValidation(ContentTypeHeaderName, requestStreamContentType); } - + return PostAsync(uri, requestContent); + } + private async Task PostAsync( + Uri uri, + IHttpContent requestContent) + { + IBuffer dataBuffer = null; HttpBaseProtocolFilter httpFilter = new HttpBaseProtocolFilter(); httpFilter.AllowUI = false; diff --git a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/WindowsDevicePortalWrapper.UniversalWindows.csproj b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/WindowsDevicePortalWrapper.UniversalWindows.csproj index 52acc73d..8065ceda 100644 --- a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/WindowsDevicePortalWrapper.UniversalWindows.csproj +++ b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.UniversalWindows/WindowsDevicePortalWrapper.UniversalWindows.csproj @@ -112,6 +112,7 @@ + diff --git a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/HttpRest/HttpMultipartFileContent.cs b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/HttpRest/HttpMultipartFileContent.cs new file mode 100644 index 00000000..0c5da580 --- /dev/null +++ b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/HttpRest/HttpMultipartFileContent.cs @@ -0,0 +1,89 @@ +using System; +using System.Globalization; +using System.Diagnostics; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Tools.WindowsDevicePortal +{ + /// + /// This class mimicks , with two main differences + /// 1. Simplifies posting files by taking file names instead of managing streams. + /// 2. Does not quote the boundaries, due to a bug in the device portal: + /// https://insider.windows.com/FeedbackHub/fb?contextid=519&feedbackid=19a5af49-38f4-409a-b464-e66f80679545&form=1 + /// + internal sealed class HttpMultipartFileContent : HttpContent + { + private List items = new List(); + private string boundaryString; + + public HttpMultipartFileContent() : this(Guid.NewGuid().ToString()) { } + + public HttpMultipartFileContent(string boundary) + { + boundaryString = boundary; + Headers.TryAddWithoutValidation("Content-Type", string.Format("multipart/form-data; boundary={0}", boundaryString)); + } + + public void Add(string filename) + { + if (filename != null) + items.Add(filename); + } + + public void AddRange(IEnumerable filenames) + { + if (filenames != null) + items.AddRange(filenames); + } + + protected override async Task SerializeToStreamAsync(Stream outStream, TransportContext context) + { + var boundary = Encoding.ASCII.GetBytes($"--{boundaryString}\r\n"); + var newline = Encoding.ASCII.GetBytes("\r\n"); + foreach (var item in items) + { + outStream.Write(boundary, 0, boundary.Length); + var headerdata = GetFileHeader(new FileInfo(item)); + outStream.Write(headerdata, 0, headerdata.Length); + + using (var file = File.OpenRead(item)) + { + await file.CopyToAsync(outStream); + } + outStream.Write(newline, 0, newline.Length); + await outStream.FlushAsync(); + } + // Close the installation request data. + boundary = Encoding.ASCII.GetBytes($"--{boundaryString}--\r\n"); + outStream.Write(boundary, 0, boundary.Length); + await outStream.FlushAsync(); + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + var boundaryLength = Encoding.ASCII.GetBytes(string.Format("--{0}\r\n", boundaryString)).Length; + foreach (var item in items) + { + var headerdata = GetFileHeader(new FileInfo(item)); + length += boundaryLength + headerdata.Length + new FileInfo(item).Length + 2; + } + length += (boundaryLength + 2); + return true; + } + private static byte[] GetFileHeader(FileInfo info) + { + string contentType = "application/octet-stream"; + if (info.Extension.ToLower() == ".cer") + contentType = "application/x-x509-ca-cert"; + + return Encoding.ASCII.GetBytes(string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{0}\"\r\nContent-Type: {1}\r\n\r\n", info.Name, contentType)); + } + + } +} \ No newline at end of file diff --git a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/HttpRest/RestPost.cs b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/HttpRest/RestPost.cs index 13f5a015..802f89a8 100644 --- a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/HttpRest/RestPost.cs +++ b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/HttpRest/RestPost.cs @@ -24,20 +24,27 @@ public partial class DevicePortal /// Optional stream containing data for the request body. /// The type of that request body data. /// Task tracking the completion of the POST request - private async Task PostAsync( + private Task PostAsync( Uri uri, Stream requestStream = null, string requestStreamContentType = null) { StreamContent requestContent = null; - MemoryStream responseDataStream = null; - + if (requestStream != null) { requestContent = new StreamContent(requestStream); requestContent.Headers.Remove(ContentTypeHeaderName); requestContent.Headers.TryAddWithoutValidation(ContentTypeHeaderName, requestStreamContentType); } + return PostAsync(uri, requestContent); + } + + private async Task PostAsync( + Uri uri, + HttpContent requestContent) + { + MemoryStream responseDataStream = null; WebRequestHandler requestSettings = new WebRequestHandler(); requestSettings.UseDefaultCredentials = false; diff --git a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.csproj b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.csproj index cfcf6357..1c353a81 100644 --- a/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.csproj +++ b/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper/WindowsDevicePortalWrapper.csproj @@ -47,6 +47,7 @@ +