Skip to content

Commit 7c5ef4e

Browse files
feat: Start-OpenXML ( Fixes #28 )
1 parent a635a42 commit 7c5ef4e

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed

Commands/Start-OpenXML.ps1

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
function Start-OpenXML {
2+
<#
3+
.SYNOPSIS
4+
Starts an OpenXML Server
5+
.DESCRIPTION
6+
Starts a read only server, using one or more OpenXML files as the storage.
7+
.NOTES
8+
If a URI in the package is requested, that URI will be returned.
9+
10+
If a path does not have an extension, it will search for an .index.html.
11+
12+
If the file was not found, a 404 code will be returned.
13+
14+
If the OpenXML archive contains a `/404.html`, the content in this file will be returned with the 404
15+
16+
If another method than GET or HEAD is used, a 405 code will be returned.
17+
18+
If the OpenXML archive contains a `/405.html`, then content in this file will be returned with the 405.
19+
20+
### Security Implications
21+
22+
By default, this only serves content locally.
23+
24+
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.
25+
26+
If the file contained PII, this could be problematic.
27+
28+
If you see this command running in an administrative process, please contact your network infrastructure and security teams
29+
.EXAMPLE
30+
$openXmlServer = Get-OpenXML ./Examples/Blank.docx |
31+
Set-OpenXML -Uri '/index.html' -ContentType text/html -Content "
32+
<h1>Hello World</h1>
33+
" |
34+
Start-OpenXML
35+
36+
Start-Process $openXmlServer.Name
37+
.EXAMPLE
38+
$openXmlServer = Get-OpenXML ./Examples/Blank.docx |
39+
Set-OpenXML -Uri '/index.html' -ContentType text/html -Content "
40+
<html>
41+
<head>
42+
<title>Hello World</title>
43+
<link rel='stylesheet' href='/css/style.css' />
44+
</head>
45+
<body>
46+
<h1>Hello World</h1>
47+
</body>
48+
</html>
49+
" |
50+
Set-OpenXML -Uri '/css/style.css' -ContentType text/css -Content "
51+
body { background-color: #000000; color: #4488ff }
52+
" |
53+
Set-OpenXML -Uri '/404.html' -ContentType text/html -Content "
54+
<html>
55+
<head>
56+
<title>Hello World</title>
57+
<link rel='stylesheet' href='/css/style.css' />
58+
</head>
59+
<body>
60+
<h1>File Not Found</h1>
61+
</body>
62+
</html>
63+
" |
64+
Start-OpenXML
65+
66+
Start-Process $openXmlServer.Name
67+
.EXAMPLE
68+
$openXmlUpdate = Get-OpenXML ./Examples/Blank.docx |
69+
Set-OpenXML -Uri '/index.html' -ContentType text/html -Content "<h1>Hello World</h1>" |
70+
Export-OpenXML ./Examples/Server.docx -Force
71+
72+
$openXmlServer = Start-OpenXML -FilePath $openXmlUpdate.FilePath
73+
74+
Start-Process $openXmlServer.Name
75+
#>
76+
param(
77+
# The path to an OpenXML file, or a glob that matches multiple OpenXML files.
78+
[Parameter(Mandatory,ValueFromPipelineByPropertyName)]
79+
[string]
80+
$FilePath,
81+
82+
# The Root
83+
[Parameter(ValueFromPipelineByPropertyName)]
84+
[string]
85+
$RootUrl = "http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/",
86+
87+
# The input object. This can be provided to avoid loading a file from disk.
88+
[Parameter(ValueFromPipeline)]
89+
[PSObject]
90+
$InputObject
91+
)
92+
93+
begin {
94+
if ($PSVersionTable.PSVersion -lt '7.0') {
95+
Write-Error "This feature requires thread jobs, which are part of PowerShell Core"
96+
return
97+
}
98+
}
99+
process {
100+
# Get our OpenXML
101+
$openXml =
102+
if ($inputObject -is [IO.Packaging.Package]) {
103+
$inputObject
104+
} else {
105+
Get-OpenXML -FilePath $FilePath
106+
}
107+
108+
# and return if we could not
109+
if (-not $openXml) { return }
110+
111+
# Create a listener
112+
$httpListener = [Net.HttpListener]::new()
113+
$httpListener.Prefixes.Add($RootUrl)
114+
$httpListener.Start()
115+
116+
117+
# Create an IO object to populate the background runspace
118+
$IO = [Ordered]@{
119+
HttpListener = $httpListener
120+
OpenXML = $openXml
121+
}
122+
123+
# Because this function exposes a server, we want to fire some events.
124+
# First is an approve event: `Approve-Start-OpenXML`
125+
# By using Register-EngineEvent, this can be handled
126+
$beforeEvent =
127+
New-Event -SourceIdentifier "Approve-Start-OpenXML" -MessageData $IO -Sender $MyInvocation.MyCommand -EventArguments $IO
128+
129+
# We will just wait almost no time, so that the handler can run.
130+
Start-Sleep -Milliseconds 0
131+
132+
# To reject the event, the handler can put one of three values in the `$event.MessageData`
133+
134+
if ($beforeEvent.MessageData.Rejected -or
135+
$beforeEvent.MessageData.Reject -or
136+
$beforeEvent.MessageData.No) {
137+
}
138+
139+
# Start a thread job
140+
$startedJob = Start-ThreadJob -ScriptBlock {
141+
param([Collections.IDictionary]$IO)
142+
143+
# unpack our IO into local variables
144+
foreach ($variableName in $IO.Keys) {
145+
$ExecutionContext.SessionState.PSVariable.Set($variableName, $IO[$variableName])
146+
}
147+
148+
# declare a little filter to serve a part
149+
150+
filter servePart {
151+
$uriPart = $_
152+
$packagePart = $package.GetPart($uriPart)
153+
$response.ContentType = $packagePart.ContentType
154+
$partStream = $packagePart.GetStream()
155+
$partStream.CopyTo($response.OutputStream)
156+
$partStream.Close()
157+
$response.Close()
158+
}
159+
160+
# and start listening
161+
:nextRequest while ($httpListener.IsListening) {
162+
$getContextAsync = $httpListener.GetContextAsync()
163+
# wait in short increments to minimize CPU impact and stay snappy
164+
while (-not $getContextAsync.Wait(7)) {
165+
166+
}
167+
# Get our listener context
168+
$context = $getContextAsync.Result
169+
# and break that into a result and response
170+
$request, $response = $context.Request, $context.Response
171+
172+
# If they asked for an inappropriate method
173+
if ($request.HttpMethod -notin 'GET', 'HEAD') {
174+
# use the appropriate status code
175+
$response.StatusCode = 405
176+
foreach ($package in $openXml) {
177+
# and serve any /405.html we find.
178+
if ($package.PartExists("/405.html")) {
179+
"/405.html" | servePart
180+
continue nextRequest
181+
}
182+
}
183+
# close out
184+
$response.close()
185+
# and continue to the next request
186+
continue nextRequest
187+
}
188+
189+
# Get the local path
190+
$localPath = $request.Url.LocalPath
191+
# if it lacks an extension, look for an index.
192+
$uriPart = if ($localPath -notmatch '\..+?$') {
193+
($localPath -replace '/$') + '/index.html'
194+
} else {
195+
$localPath
196+
}
197+
198+
# If we find the part
199+
foreach ($package in $openXml) {
200+
if ($package.PartExists($uriPart)) {
201+
# serve it and continue
202+
$uriPart | servePart
203+
continue nextRequest
204+
}
205+
}
206+
# If we did not find a part, set the appropriate status code
207+
$response.StatusCode = 404
208+
foreach ($package in $openXml) {
209+
# look for a 404 to serve
210+
if ($package.PartExists("/404.html")) {
211+
"/404.html" | servePart
212+
continue nextRequest
213+
}
214+
}
215+
# and close the respons
216+
$response.Close()
217+
}
218+
} -ArgumentList $IO -Name $RootUrl -ThrottleLimit 100 |
219+
Add-Member NoteProperty IO $IO -Force -PassThru |
220+
Add-Member NoteProperty HttpListener $httpListener -Force -PassThru |
221+
Add-Member NoteProperty OpenXML $openXml -Force -PassThru
222+
223+
$null = New-Event -SourceIdentifier Start-OpenXML -Sender $MyInvocation.MyCommand -EventArguments $startedJob -MessageData $IO
224+
225+
$startedJob
226+
227+
}
228+
}

0 commit comments

Comments
 (0)