Commands/Start-OpenXML.ps1
function Start-OpenXML { <# .SYNOPSIS Starts an OpenXML Server .DESCRIPTION Starts a read only server, using one or more OpenXML files as the storage. .NOTES If a URI in the package is requested, that URI will be returned. If a path does not have an extension, it will search for an .index.html. If the file was not found, a 404 code will be returned. If the OpenXML archive contains a `/404.html`, the content in this file will be returned with the 404 If another method than GET or HEAD is used, a 405 code will be returned. If the OpenXML archive contains a `/405.html`, then content in this file will be returned with the 405. ### Security Implications By default, this only serves content locally. If this serves content to any other machine, the script will need to be running as administrator, and all of the content within the file will be exposed. If the file contained PII, this could be problematic. If you see this command running in an administrative process, please contact your network infrastructure and security teams .EXAMPLE $openXmlServer = Get-OpenXML ./Examples/Blank.docx | Set-OpenXML -Uri '/index.html' -ContentType text/html -Content " <h1>Hello World</h1> " | Start-OpenXML Start-Process $openXmlServer.Name .EXAMPLE $openXmlServer = Get-OpenXML ./Examples/Blank.docx | Set-OpenXML -Uri '/index.html' -ContentType text/html -Content " <html> <head> <title>Hello World</title> <link rel='stylesheet' href='/css/style.css' /> </head> <body> <h1>Hello World</h1> </body> </html> " | Set-OpenXML -Uri '/css/style.css' -ContentType text/css -Content " body { background-color: #000000; color: #4488ff } " | Set-OpenXML -Uri '/404.html' -ContentType text/html -Content " <html> <head> <title>Hello World</title> <link rel='stylesheet' href='/css/style.css' /> </head> <body> <h1>File Not Found</h1> </body> </html> " | Start-OpenXML Start-Process $openXmlServer.Name .EXAMPLE $openXmlUpdate = Get-OpenXML ./Examples/Blank.docx | Set-OpenXML -Uri '/index.html' -ContentType text/html -Content "<h1>Hello World</h1>" | Export-OpenXML ./Examples/Server.docx -Force $openXmlServer = Start-OpenXML -FilePath $openXmlUpdate.FilePath Start-Process $openXmlServer.Name #> param( # The path to an OpenXML file, or a glob that matches multiple OpenXML files. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [string] $FilePath, # The Root [Parameter(ValueFromPipelineByPropertyName)] [string] $RootUrl = "http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/", # The input object. This can be provided to avoid loading a file from disk. [Parameter(ValueFromPipeline)] [PSObject] $InputObject ) begin { if ($PSVersionTable.PSVersion -lt '7.0') { Write-Error "This feature requires thread jobs, which are part of PowerShell Core" return } } process { # Get our OpenXML $openXml = if ($inputObject -is [IO.Packaging.Package]) { $inputObject } else { Get-OpenXML -FilePath $FilePath } # and return if we could not if (-not $openXml) { return } # Create a listener $httpListener = [Net.HttpListener]::new() $httpListener.Prefixes.Add($RootUrl) $httpListener.Start() # Create an IO object to populate the background runspace $IO = [Ordered]@{ HttpListener = $httpListener OpenXML = $openXml } # Because this function exposes a server, we want to fire some events. # First is an approve event: `Approve-Start-OpenXML` # By using Register-EngineEvent, this can be handled $beforeEvent = New-Event -SourceIdentifier "Approve-Start-OpenXML" -MessageData $IO -Sender $MyInvocation.MyCommand -EventArguments $IO # We will just wait almost no time, so that the handler can run. Start-Sleep -Milliseconds 0 # To reject the event, the handler can put one of three values in the `$event.MessageData` if ($beforeEvent.MessageData.Rejected -or $beforeEvent.MessageData.Reject -or $beforeEvent.MessageData.No) { } # Start a thread job $startedJob = Start-ThreadJob -ScriptBlock { param([Collections.IDictionary]$IO) # unpack our IO into local variables foreach ($variableName in $IO.Keys) { $ExecutionContext.SessionState.PSVariable.Set($variableName, $IO[$variableName]) } # declare a little filter to serve a part filter servePart { $uriPart = $_ $packagePart = $package.GetPart($uriPart) $response.ContentType = $packagePart.ContentType $partStream = $packagePart.GetStream() $partStream.CopyTo($response.OutputStream) $partStream.Close() $response.Close() } # and start listening :nextRequest while ($httpListener.IsListening) { $getContextAsync = $httpListener.GetContextAsync() # wait in short increments to minimize CPU impact and stay snappy while (-not $getContextAsync.Wait(7)) { } # Get our listener context $context = $getContextAsync.Result # and break that into a result and response $request, $response = $context.Request, $context.Response # If they asked for an inappropriate method if ($request.HttpMethod -notin 'GET', 'HEAD') { # use the appropriate status code $response.StatusCode = 405 foreach ($package in $openXml) { # and serve any /405.html we find. if ($package.PartExists("/405.html")) { "/405.html" | servePart continue nextRequest } } # close out $response.close() # and continue to the next request continue nextRequest } # Get the local path $localPath = $request.Url.LocalPath # if it lacks an extension, look for an index. $uriPart = if ($localPath -notmatch '\..+?$') { ($localPath -replace '/$') + '/index.html' } else { $localPath } # If we find the part foreach ($package in $openXml) { if ($package.PartExists($uriPart)) { # serve it and continue $uriPart | servePart continue nextRequest } } # If we did not find a part, set the appropriate status code $response.StatusCode = 404 foreach ($package in $openXml) { # look for a 404 to serve if ($package.PartExists("/404.html")) { "/404.html" | servePart continue nextRequest } } # and close the respons $response.Close() } } -ArgumentList $IO -Name $RootUrl -ThrottleLimit 100 | Add-Member NoteProperty IO $IO -Force -PassThru | Add-Member NoteProperty HttpListener $httpListener -Force -PassThru | Add-Member NoteProperty OpenXML $openXml -Force -PassThru $null = New-Event -SourceIdentifier Start-OpenXML -Sender $MyInvocation.MyCommand -EventArguments $startedJob -MessageData $IO $startedJob } } |