Nice little directory browser :D
1<#
2 This file is part of Utatane.
3
4 Utatane is free software: you can redistribute it and/or modify it under the
5 terms of the GNU Affero General Public License as published by the Free
6 Software Foundation, either version 3 of the License, or (at your option)
7 any later version.
8
9 Utatane is distributed in the hope that it will be useful, but WITHOUT ANY
10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
12 more details.
13
14 You should have received a copy of the GNU Affero General Public License
15 along with Utatane. If not, see <http://www.gnu.org/licenses/>.
16#>
17
18Import-Module Mizumiya
19Import-Module PSParseHTML -Function Optimize-HTML
20
21$Root = (Get-PodeConfig).Utatane.Root
22
23if (-not (Get-Command tailwindcss -ErrorAction SilentlyContinue)) {
24 _warn "The tailwindcss binary is missing. Source styles will not be synced with the public style.css."
25 _warn "This is only needed during development, don't worry about it in production"
26} else {
27 Add-PodeFileWatcher -Path $PSScriptRoot -Exclude "$PSScriptRoot/public" -ScriptBlock {
28 tailwindcss --input $PSScriptRoot/tailwind/style.tw.css --output $PSScriptRoot/public/style.css
29 }
30}
31
32_info "Serving root directory $Root"
33
34# Use-PodeScript -Path functions.ps1 # https://github.com/Badgerati/Pode/issues/1582
35Use-PodeScript -Path $PSScriptRoot/functions.ps1
36
37New-PodeLoggingMethod -Custom -ScriptBlock {
38 param ($Item)
39
40 $Date = $Item.UtcDate | Get-Date -Format s
41 $Query = $Item.Request.Query -eq '-' ? '' : '?' + $Item.Request.Query
42
43 $PrevCol = $PSStyle.Reset + $PSStyle.Foreground.BrightBlack
44 switch ($Item.Response.StatusCode) {
45 {$_ -ge 400} { $Col = $PSStyle.Foreground.BrightRed }
46 {$_ -ge 500} { $Col = $PSStyle.Foreground.White + $PSStyle.Background.Red }
47 default { $Col = '' }
48 }
49
50 $RequestLine = "$($Item.Host) $($Item.Request.Method) $($PSStyle.Foreground.White)$($Item.Request.Resource)$Query$PrevCol $($Item.Request.Protocol) by ""$($Item.Request.Agent)"""
51 $ResponseLine = "$Col$($Item.Response.StatusCode) $($Item.Response.StatusDescription)$PrevCol $(_format_size $Item.Response.Size)"
52
53 _request "$Date | $RequestLine >>> $ResponseLine"
54} | Enable-PodeRequestLogging -Raw
55
56New-PodeLoggingMethod -Custom -ScriptBlock {
57 param ($Item)
58
59 _warn "$($Item.Date | Get-Date -Format s) | $($Item.Level) ($($Item.Server):$($Item.ThreadId)) | $($Item.Category): $($Item.Message)"
60 $Item.StackTrace -split "`n" | % { _warn $_ }
61} | Enable-PodeErrorLogging -Raw -Levels Error, Warning, Informational
62
63Add-PodeEndpoint -Address * -Port 8080 -Protocol HTTP
64
65Set-PodeViewEngine -Type Mizumiya -Extension ps1 -ScriptBlock {
66 param ($Path, $Data)
67
68 . ./functions.ps1
69
70 <#
71 FOR JSON: Output any object and ConvertTo-Json will handle it
72 FOR XMLs and HTML: Your script MUST output a well-formed string in your
73 target format; PowerShell's ConvertTo-Xml is not built for this
74 purpose
75 #>
76 switch -Wildcard ($Path) {
77 '*.html.ps1' {
78 Set-PodeHeader -Name Content-Type -Value 'text/html; charset=utf-8'
79 return ([String] (. $Path -Data $Data)) | Optimize-HTML
80 }
81
82 '*.json.ps1' {
83 Set-PodeHeader -Name Content-Type -Value 'application/json; charset=utf-8'
84 return [PSCustomObject]((. $Path -Data $Data) ?? @{}) | ConvertTo-Json -Compress -Depth 8
85 }
86
87 '*.xml.ps1' {
88 Set-PodeHeader -Name Content-Type -Value 'application/xml; charset=utf-8'
89 return ([String] (. $Path -Data $Data))
90 }
91
92 '*.xhtml.ps1' {
93 Set-PodeHeader -Name Content-Type -Value 'application/xhtml+xml; charset=utf-8'
94 return ([String] (. $Path -Data $Data))
95 }
96
97 '*.atom.ps1' {
98 Set-PodeHeader -Name Content-Type -Value 'application/atom+xml; charset=utf-8'
99 return ([String] (. $Path -Data $Data))
100 }
101 }
102}
103
104$Middleware_DirsHaveTrailingSlash = {
105 $Root = $using:Root
106 $Path = $WebEvent.Path
107 $JoinedPath = Get-Item -Lit (Join-Path $Root $Path)
108
109 if (-not (_is_file $JoinedPath) -and $Path -ne '/' -and $Path -notmatch '/$') {
110 Move-PodeResponseUrl -Url ("$Path/")
111 return $false
112 }
113
114 return $true
115}
116
117Add-PodeMiddleware -Name CheckRootIsAvailable -ScriptBlock {
118 $Root = $using:Root
119
120 if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
121 _log "Root $Root not available!" -Type Warning
122 Set-PodeResponseStatus -Code 503 -Description 'The root directory is not available.'
123 return $false
124 }
125
126 return $true
127}
128
129Add-PodeMiddleware -Name CreateAcceptInWebEvent -ScriptBlock {
130 $WebEvent.RequestAccept = ((Get-PodeHeader 'Accept') ?? 'text/html' | ConvertFrom-AcceptHeader)
131
132 return $true
133}
134
135Add-PodeMiddleware -Name __pode_mw_static_content__ -ScriptBlock {
136 if ($WebEvent.Path -eq '/favicon.ico') {
137 $pubRoute = Find-PodePublicRoute -Path '/favicon.ico'
138
139 if ($null -eq $pubRoute) {
140 Set-PodeResponseStatus -Code 404
141 return $false
142 }
143
144 $cachable = Test-PodeRouteValidForCaching -Path '/favicon.ico'
145
146 Write-PodeFileResponse -FileInfo $pubRoute.FileInfo -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable
147 return $false
148 }
149
150 if (-not ($WebEvent.Path -like '/.nhnd/*') -or $WebEvent.Path.Length -lt 6) {
151 return $true
152 }
153
154 if ($WebEvent.Path -in ('/.nhnd', '/.nhnd/')) {
155 Set-PodeResponseStatus -Code 404
156 return $false
157 }
158
159 $_path = $WebEvent.Path.SubString(6) # strip /.nhnd
160 $pubRoute = Find-PodePublicRoute -Path $_path
161
162 if ($null -eq $pubRoute) {
163 # using our own directory so we can just blanket deny
164 Set-PodeResponseStatus -Code 404
165 return $false
166 }
167
168 # check current state of caching
169 $cachable = Test-PodeRouteValidForCaching -Path $_path
170
171 # write the file to the response
172 Write-PodeFileResponse -FileInfo $pubRoute.FileInfo -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable
173 return $false
174}
175
176Enable-PodeSessionMiddleware -Duration 120 -Extend
177
178Add-PodeRouteGroup -Path /api -Routes {
179 Add-PodeRoute -Path /files -Method GET -ScriptBlock {
180 $Root = $using:Root
181 $Path = $WebEvent.Query.Path
182 $JoinedPath = Join-Path $Root $Path
183
184 foreach ($Redirect in (Get-PodeConfig).Utatane.RedirectMap.GetEnumerator()) {
185 if (-not ($Path -eq $Redirect.Key -or ($Path + '/') -eq $Redirect.Key)) {
186 continue
187 }
188
189 $Url = ("/api/files?path=" + (_encode $Redirect.Value))
190
191 # api doesn't get a message since they probably aren't keeping the
192 # cookie needed for session tracking
193 Move-PodeResponseUrl -Url $Url
194 return
195 }
196
197 if (-not (_check_path $Root $JoinedPath)) {
198 _warn "/api/files: query for $JoinedPath ($Path) refused!"
199 Set-PodeResponseStatus -Code 404
200 return;
201 }
202
203 $Data = @{ Root=$Root; Path=$Path; JoinedPath=$JoinedPath}
204 _render_view -Page api/files -Data $Data
205 }
206
207 Add-PodeRoute -Path /* -Method GET -ScriptBlock {
208 Set-PodeResponseStatus -Code 404
209 }
210}
211
212Add-PodeRoute -Method GET -Path /about -ScriptBlock {
213 _render_view about
214}
215
216Add-PodeRoute -Method GET -Path /* -Middleware $Middleware_DirsHaveTrailingSlash -ScriptBlock {
217 $Root = $using:Root
218 $Path = $WebEvent.Path ?? '/'
219 $JoinedPath = Get-Item -Lit (Join-Path $Root $Path)
220
221 foreach ($Redirect in (Get-PodeConfig).Utatane.RedirectMap.GetEnumerator()) {
222 if ($Path -ne $Redirect.Key) {
223 continue;
224 }
225
226 Add-PodeFlashMessage -name redirect-notice -Message @{
227 From = $Path
228 To = $Redirect.Value
229 }
230
231 Move-PodeResponseUrl -Url $Redirect.Value
232 return;
233 }
234
235 if ($null -eq $JoinedPath) {
236 _warn "1failed: '$(Join-Path $Root $Path)' for query $($WebEvent.Query|convertto-json -compress)"
237 Set-PodeResponseStatus -Code 404
238 return
239 }
240
241 # No path traversal, no blacklist!
242 if (-not (_check_path $Root $JoinedPath)) {
243 _warn "denied $Path ($JoinedPath)"
244 Set-PodeResponseStatus -Code 404
245 return
246 }
247
248 $IsFile = _is_file $JoinedPath
249
250 if ($IsFile) {
251 Set-PodeResponseAttachment -path ([WildcardPattern]::Escape($JoinedPath))
252 return
253 } else {
254 # also don't let the server serve itself
255 if ("$JoinedPath/".StartsWith($using:PSScriptRoot)) {
256 _warn 'failed: refusing to serve the server root'
257 Set-PodeResponseStatus -Code 404
258 }
259
260 _render_view -Page index -Data @{ Root=$Root ; Path=$Path }
261 return
262 }
263
264 _warn "failed: '$Root$Rath' for query $($WebEvent.Query|convertto-json -compress)"
265 Set-PodeResponseStatus -Code 404
266 return
267}