예제 #1
0
        /// <summary>
        /// This is here purely as an example of how a host page needs to be rendered for it to be able to first post to Collabora and then have it
        /// relay back to this WOPI host to serve up and manage the actual file
        /// </summary>
        /// <param name="httpContext"></param>
        /// <returns></returns>
        private static async Task GetCollaboraHostPageAsync(HttpContext httpContext)
        {
            var cancellationToken = httpContext.RequestAborted;

            // Try to get the discovery document from the Collabora server.  This tells us what document types are supported, but more importantly
            // the encrption keys it is using to sign the callback requests it sends to us.  We will use these keys to assure non-reupidation/tampering
            // If we fail to load this file, we cannot continue to build the page we are return

            var wopiDiscoveryDocumentFactory = httpContext.RequestServices.GetRequiredService <IWopiDiscoveryDocumentFactory>();

            var wopiDiscoveryDocument = await wopiDiscoveryDocumentFactory.CreateDocumentAsync(cancellationToken);

            if (wopiDiscoveryDocument.IsEmpty)
            {
                throw new ApplicationException("Unable to download the WOPI Discovery Document - remote host is either unavailable or returned non-success status code");
            }

            // Identify the file that we want to view/edit by inspecting the incoming request for an id.

            var fileId = httpContext.Request.Query["file_id"].FirstOrDefault()?.Trim();

            if (string.IsNullOrWhiteSpace(fileId))
            {
                fileId = File.With("DF796179-DB2F-4A06-B4D5-AD7F012CC2CC", "2021-08-09T18:15:02.4214747Z");
            }

            var wopiConfiguration = httpContext.RequestServices.GetRequiredService <IOptionsSnapshot <WopiConfiguration> >().Value;

            var hostFilesUrl = wopiConfiguration.HostFilesUrl;

            if (string.IsNullOrWhiteSpace(hostFilesUrl))
            {
                return;
            }

            Debug.Assert(fileId is object);

            var wopiHostFileEndpointUrl = new Uri(Path.Combine(hostFilesUrl, fileId), UriKind.Absolute);

            var fileRepository = httpContext.RequestServices.GetRequiredService <IFileRepository>();

            var fileMetadata = await fileRepository.GetMetadataAsync(fileId, cancellationToken);

            var fileExtension = fileMetadata.Extension;

            if (string.IsNullOrWhiteSpace(fileExtension))
            {
                return;              // TODO - Return appropriate status code to caller
            }
            var fileAction = "view"; // edit | view | etc (see comments in discoveryDoc.GetEndpointForAsync)

            var collaboraOnlineEndpoint = wopiDiscoveryDocument.GetEndpointForFileExtension(fileExtension, fileAction, wopiHostFileEndpointUrl);

            if (collaboraOnlineEndpoint is null || !collaboraOnlineEndpoint.IsAbsoluteUri)
            {
                return;                                                                             // TODO - Return appropriate status code to caller
            }
            var sb = new StringBuilder();

            // https://wopi.readthedocs.io/projects/wopirest/en/latest/concepts.html#term-wopisrc

            // When running locally in DEBUG ...
            // https://127.0.0.1:9980/loleaflet/4aa2794/loleaflet.html? is the Collabora endpoint for this document type, pulled out of the discovery xml file hosted by Collabora
            // https://127.0.0.1:44355/wopi/files/<FILE_ID> is the url Collabora uses to callback to us to get the file information and contents

            // In Azure, the container will be mapped to port 80 in docker run command

            // TODO - Generate a token with a set TTL that is specific to the current user and file combination
            //        This token will be sent back to us by Collabora by way of it verifying the request (it will be signed so we know it
            //        comes from them and hasn't been tampered with outside of the servers)
            //        For now, we'll just use a Guid

            var accessToken = Guid.NewGuid().ToString().Replace("-", string.Empty);

            // TODO - This is either going to have to be generated by MVCForum or somehow injected by it after a call to our API,
            //        but given the need for input elements, it might be more appropriate for us to just generate the token and
            //        return both it and the collabora endpoint that needs to be used, or MVCForum gets the discovery document itself
            //        and generates a token we can later understand

            httpContext.Response.StatusCode = StatusCodes.Status200OK;

            sb.AppendLine($"<!doctype html>");
            sb.AppendLine($"<html>");
            sb.AppendLine($"  <body>");
            sb.AppendLine($"    <form action=\"{collaboraOnlineEndpoint.AbsoluteUri}\" enctype =\"multipart/form-data\" method=\"post\">");
            sb.AppendLine($"      <input name=\"access_token\" value=\"{ accessToken }\" type=\"hidden\">");
            sb.AppendLine($"      <input type=\"submit\" value=\"Load Document (Collabora)\">");
            sb.AppendLine($"    </form>");
            sb.AppendLine($"  </body>");
            sb.AppendLine($"</html>");

            await httpContext.Response.WriteAsync(sb.ToString());
        }
예제 #2
0
        public async Task HandleAsync_ResolvesAndWritesFileCorrectlyToGivenStream(string fileName)
        {
            var cancellationToken = new CancellationToken();

            var httpContext = new DefaultHttpContext();

            var contentRootPath = Environment.CurrentDirectory;

            var filePath = Path.Combine(contentRootPath, "Files", fileName);

            Assert.IsTrue(System.IO.File.Exists(filePath), $"Expected the {fileName} file to be accessible in the test environment");

            var fileInfo = new FileInfo(filePath);

            var fileBuffer = await System.IO.File.ReadAllBytesAsync(filePath, cancellationToken);

            using var responseBodyStream = new MemoryStream(fileBuffer.Length);

            httpContext.Response.Body = responseBodyStream;

            var fileRepository = new Moq.Mock <IFileRepository>();

            var fileRepositoryInvoked = false;

            var services = new ServiceCollection();

            services.AddScoped(sp => fileRepository.Object);

            httpContext.RequestServices = services.BuildServiceProvider();

            var fileVersion = Guid.NewGuid().ToString();

            using var algo = MD5.Create();

            var contentHash = algo.ComputeHash(fileBuffer);

            var fileMetadata = new FileMetadata("title", "description", "group-name", fileVersion, "owner", fileName, fileInfo.Extension, (ulong)fileInfo.Length, "blobName", DateTimeOffset.UtcNow, Convert.ToBase64String(contentHash), FileStatus.Verified);

            var fileWriteDetails = new FileWriteDetails(fileVersion, "content-type", contentHash, (ulong)fileBuffer.Length, "content-encoding", "content-language", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, fileMetadata);

            fileRepository.
            Setup(x => x.WriteToStreamAsync(Moq.It.IsAny <FileMetadata>(), Moq.It.IsAny <Stream>(), Moq.It.IsAny <CancellationToken>())).
            Callback(async(FileMetadata givenFileMetadata, Stream givenStream, CancellationToken givenCancellationToken) => {
                Assert.IsFalse(givenFileMetadata.IsEmpty);
                Assert.IsNotNull(givenStream);

                Assert.IsFalse(givenCancellationToken.IsCancellationRequested, "Expected the cancellation token to not be cancelled");

                Assert.AreSame(responseBodyStream, givenStream, "Expected the SUT to as the repository to write the file to the stream it was asked to");
                Assert.AreSame(fileName, givenFileMetadata.Name, "Expected the SUT to request the file from the repository whose name it was provided with");
                Assert.AreSame(fileVersion, givenFileMetadata.Version, "Expected the SUT to request the file version from the repository that it was provided with");
                Assert.AreEqual(cancellationToken, givenCancellationToken, "Expected the same cancellation token to propagate between service interfaces");

                await givenStream.WriteAsync(fileBuffer, cancellationToken);
                await givenStream.FlushAsync(cancellationToken);

                fileRepositoryInvoked = true;
            }).
            Returns(Task.FromResult(fileWriteDetails));

            fileRepository.Setup(x => x.GetMetadataAsync(Moq.It.IsAny <File>(), Moq.It.IsAny <CancellationToken>())).Returns(Task.FromResult(fileMetadata));

            var accessToken = Guid.NewGuid().ToString();

            var file = File.With(fileName, fileVersion);

            var getFileWopiRequest = GetFileWopiRequest.With(file, accessToken);

            await getFileWopiRequest.HandleAsync(httpContext, cancellationToken);

            Assert.IsTrue(fileRepositoryInvoked, "Expected the SUT to defer to the file repository with the correct parameters");

            Assert.AreEqual(fileBuffer.Length, responseBodyStream.Length, "All bytes in the file should be written to the target stream");

            Assert.IsTrue(httpContext.Response.Headers.ContainsKey("X-WOPI-ItemVersion"), "Expected the X-WOPI-ItemVersion header to have been written to the response");

            Assert.IsNotNull(httpContext.Response.Headers["X-WOPI-ItemVersion"], "Expected the X-WOPI-ItemVersion header in the response to not be null");
        }
예제 #3
0
        public void With_DoesNotThrowIfAccessTokenIsNeitherNullNorWhitespace()
        {
            var file = File.With("file-name", "file-version");

            GetFileWopiRequest.With(file, accessToken: "access-token");
        }
예제 #4
0
        public void With_ThrowsIfAccessTokenIsWhitespace()
        {
            var file = File.With("file-name", "file-version");

            GetFileWopiRequest.With(file, accessToken: " ");
        }
예제 #5
0
        public void With_ThrowsIfAccessTokenIsEmpty()
        {
            var file = File.With("file-name", "file-version");

            GetFileWopiRequest.With(file, accessToken: string.Empty);
        }
예제 #6
0
        public void With_ThrowsIfAccessTokenIsNull()
        {
            var file = File.With("file-name", "file-version");

            GetFileWopiRequest.With(file, accessToken: default);
        }