Private/Sessions.ps1
function New-PodeSession { # sessionId $sessionId = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.GenerateId -Return # tabId $tabId = $null if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser) { $tabId = Get-PodeSessionTabId } # return new session data return @{ Name = $PodeContext.Server.Sessions.Name Id = $sessionId TabId = $tabId FullId = (Get-PodeSessionFullId -SessionId $sessionId -TabId $tabId) Extend = $PodeContext.Server.Sessions.Info.Extend TimeStamp = [datetime]::UtcNow Data = @{} } } function Get-PodeSessionFullId { param( [Parameter()] [string] $SessionId, [Parameter()] [string] $TabId ) if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and ![string]::IsNullOrEmpty($TabId)) { return "$($SessionId)-$($TabId)" } return $SessionId } function Set-PodeSession { if ($null -eq $WebEvent.Session) { # There is no session available to set on the response throw ($PodeLocale.noSessionToSetOnResponseExceptionMessage) } # convert secret to strict mode $strict = $PodeContext.Server.Sessions.Info.Strict $secret = $PodeContext.Server.Sessions.Secret # set session on header if ($PodeContext.Server.Sessions.Info.UseHeaders) { Set-PodeHeader -Name $WebEvent.Session.Name -Value $WebEvent.Session.Id -Secret $secret -Strict:$strict } # set session as cookie else { $null = Set-PodeCookie ` -Name $WebEvent.Session.Name ` -Value $WebEvent.Session.Id ` -Secret $secret ` -Strict:$strict ` -ExpiryDate (Get-PodeSessionExpiry) ` -HttpOnly:$PodeContext.Server.Sessions.Info.HttpOnly ` -Secure:$PodeContext.Server.Sessions.Info.Secure } } function Get-PodeSession { $secret = $PodeContext.Server.Sessions.Secret $sessionId = $null $tabId = Get-PodeSessionTabId $name = $PodeContext.Server.Sessions.Name # convert secret to strict mode if ($PodeContext.Server.Sessions.Info.Strict) { $secret = ConvertTo-PodeStrictSecret -Secret $secret } # session from header if ($PodeContext.Server.Sessions.Info.UseHeaders) { # check that the header is validly signed if (!(Test-PodeHeaderSigned -Name $PodeContext.Server.Sessions.Name -Secret $secret)) { return $null } # get the header from the request $sessionId = Get-PodeHeader -Name $PodeContext.Server.Sessions.Name -Secret $secret if ([string]::IsNullOrEmpty($sessionId)) { return $null } } # session from cookie else { # check that the cookie is validly signed if (!(Test-PodeCookieSigned -Name $PodeContext.Server.Sessions.Name -Secret $secret)) { return $null } # get the cookie from the request $cookie = Get-PodeCookie -Name $PodeContext.Server.Sessions.Name -Secret $secret if ([string]::IsNullOrEmpty($cookie)) { return $null } # get details from cookie $name = $cookie.Name $sessionId = $cookie.Value } # generate the session data return @{ Name = $name Id = $sessionId TabId = $tabId FullId = (Get-PodeSessionFullId -SessionId $sessionId -TabId $tabId) Extend = $PodeContext.Server.Sessions.Info.Extend TimeStamp = $null Data = @{} } } function Revoke-PodeSession { # do nothing if no current session if ($null -eq $WebEvent.Session) { return } # remove from cookie if being used if (!$PodeContext.Server.Sessions.Info.UseHeaders) { Remove-PodeCookie -Name $WebEvent.Session.Name } # remove session from store Remove-PodeSessionInternal } function Set-PodeSessionDataHash { if ($null -eq $WebEvent.Session) { # No session available to calculate data hash throw ($PodeLocale.noSessionToCalculateDataHashExceptionMessage) } if (($null -eq $WebEvent.Session.Data) -or ($WebEvent.Session.Data.Count -eq 0)) { $WebEvent.Session.Data = @{} } $WebEvent.Session.DataHash = (Invoke-PodeSHA256Hash -Value (ConvertTo-Json -InputObject $WebEvent.Session.Data.Clone() -Depth 10 -Compress)) } function Test-PodeSessionDataHash { if ($null -eq $WebEvent.Session) { return $false } if ([string]::IsNullOrWhiteSpace($WebEvent.Session.DataHash)) { return $false } if (($null -eq $WebEvent.Session.Data) -or ($WebEvent.Session.Data.Count -eq 0)) { $WebEvent.Session.Data = @{} } $hash = (Invoke-PodeSHA256Hash -Value (ConvertTo-Json -InputObject $WebEvent.Session.Data -Depth 10 -Compress)) return ($WebEvent.Session.DataHash -eq $hash) } function Save-PodeSessionInternal { param( [switch] $Force ) # do nothing if session has no ID if ([string]::IsNullOrEmpty($WebEvent.Session.FullId)) { return } # only save if check and hashes different, but not if extending expiry or updated if (!$WebEvent.Session.Extend -and $Force -and (Test-PodeSessionDataHash)) { return } # generate the expiry $expiry = Get-PodeSessionExpiry # the data to save - which will be the data, and some extra metadata like timestamp $data = @{ Version = 3 Metadata = @{ TimeStamp = $WebEvent.Session.TimeStamp } Data = $WebEvent.Session.Data } # save base session data to store if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and $WebEvent.Session.TabId) { $authData = @{ Version = 3 Metadata = @{ TimeStamp = $WebEvent.Session.TimeStamp Tabbed = $true } Data = @{ Auth = $WebEvent.Session.Data.Auth } } $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.Id, $authData, $expiry) -Splat $data.Metadata['Parent'] = $WebEvent.Session.Id } # save session data to store $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.FullId, $data, $expiry) -Splat # update session's data hash Set-PodeSessionDataHash } function Remove-PodeSessionInternal { if ($null -eq $WebEvent.Session) { return } # remove data from store $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $WebEvent.Session.Id # clear session $WebEvent.Session.Clear() $WebEvent.Session = $null } function Get-PodeSessionInMemStore { $store = [psobject]::new() # add in-mem storage $store | Add-Member -MemberType NoteProperty -Name Memory -Value @{} # delete a sessionId and data $store | Add-Member -MemberType NoteProperty -Name Delete -Value { param($sessionId) $null = $PodeContext.Server.Sessions.Store.Memory.Remove($sessionId) if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser) { Invoke-PodeSchedule -Name '__pode_session_inmem_cleanup__' } } # get a sessionId's data $store | Add-Member -MemberType NoteProperty -Name Get -Value { param($sessionId) $s = $PodeContext.Server.Sessions.Store.Memory[$sessionId] # if expire, remove if (($null -ne $s) -and ($s.Expiry -lt [DateTime]::UtcNow)) { $null = $PodeContext.Server.Sessions.Store.Memory.Remove($sessionId) return $null } return $s.Data } # update/insert a sessionId and data $store | Add-Member -MemberType NoteProperty -Name Set -Value { param($sessionId, $data, $expiry) $PodeContext.Server.Sessions.Store.Memory[$sessionId] = @{ Data = $data Expiry = $expiry } } return $store } function Set-PodeSessionInMemClearDown { # don't setup if serverless - as memory is short lived anyway if ($PodeContext.Server.IsServerless) { return } # cleardown expired inmem session every 10 minutes Add-PodeSchedule -Name '__pode_session_inmem_cleanup__' -Cron '0/10 * * * *' -ScriptBlock { # do nothing if no sessions $store = $PodeContext.Server.Sessions.Store if (($null -eq $store.Memory) -or ($store.Memory.Count -eq 0)) { return } # remove sessions that have expired, or where the parent is gone $now = [DateTime]::UtcNow foreach ($key in $store.Memory.Keys) { # expired if ($store.Memory[$key].Expiry -lt $now) { $null = $store.Memory.Remove($key) continue } # parent check - gone/expired $parentKey = $store.Memory[$key].Data.Metadata.Parent if ($parentKey -and (!$store.Memory.ContainsKey($parentKey) -or ($store.Memory[$parentKey].Expiry -lt $now))) { $null = $store.Memory.Remove($key) } } } } function Test-PodeSessionsInUse { return (($null -ne $WebEvent.Session) -and ($WebEvent.Session.Count -gt 0)) } function Get-PodeSessionData { param( [Parameter()] [string] $SessionId, [Parameter()] [string] $TabId = $null ) $data = $null # try and get Tab session if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and ![string]::IsNullOrEmpty($TabId)) { $data = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments "$($SessionId)-$($TabId)" -Return # now get the parent - but fail if it doesn't exist if ($data.Metadata.Parent) { $parent = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments $data.Metadata.Parent -Return if (!$parent) { return $null } if (!$data.Data.Auth) { $data.Data.Auth = $parent.Data.Auth } } } # try and get normal session if (($null -eq $data) -and ![string]::IsNullOrEmpty($SessionId)) { $data = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments $SessionId -Return } return $data } function Get-PodeSessionMiddleware { return { # if session already set, return if ($WebEvent.Session) { return $true } try { # retrieve the current session from cookie/header $WebEvent.Session = Get-PodeSession # if no session found, create a new one on the current web event if (!$WebEvent.Session) { $WebEvent.Session = New-PodeSession $new = $true } # get the session's data from store elseif ($null -ne ($data = (Get-PodeSessionData -SessionId $WebEvent.Session.Id -TabId $WebEvent.Session.TabId))) { if ($data.Version -lt 3) { $WebEvent.Session.Data = $data $WebEvent.Session.TimeStamp = [datetime]::UtcNow } else { $WebEvent.Session.Data = $data.Data if ($data.Metadata.Tabbed) { $WebEvent.Session.TimeStamp = [datetime]::UtcNow } else { $WebEvent.Session.TimeStamp = $data.Metadata.TimeStamp } } } # session not in store, create a new one else { $WebEvent.Session = New-PodeSession $new = $true } # set data hash Set-PodeSessionDataHash # add session to response if it's new or extendible if ($new -or $WebEvent.Session.Extend) { Set-PodeSession } # assign endware for session to set cookie/header $WebEvent.OnEnd += @{ Logic = { if ($null -ne $WebEvent.Session) { Save-PodeSession -Force } } } } catch { $_ | Write-PodeErrorLog return $false } # move along return $true } } |