diff --git a/docs/Hosting/SmtpServer.md b/docs/Hosting/SmtpServer.md index 96916bfb2..68ddd5c7c 100644 --- a/docs/Hosting/SmtpServer.md +++ b/docs/Hosting/SmtpServer.md @@ -1,9 +1,14 @@ # SMTP Server -Pode has an inbuilt SMTP server which automatically creates a TCP listener on port 25 (unless you specify a different port via the [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint) function). +Pode has an inbuilt SMTP server for receiving Email which automatically creates a TCP listener on port 25 - unless you specify a different port via the [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint) function. Unlike with web servers that use the Route functions, SMTP servers use the Handler functions, which let you specify logic for handling responses from TCP streams. +!!! tip + You can setup multiple different Handlers to run different logic for one Email. + +## Usage + To create a Handler for the inbuilt SMTP server you can use the following example: ```powershell @@ -18,7 +23,31 @@ Start-PodeServer { } ``` -The SMTP Handler will be passed the a `$SmtpEvent` object, that contains te Request, Response, and Email: +Starting this server will listen for incoming email on `localhost:25`. The Handler will have access to the `$SmtpEvent` object (see below), which contains information about the Email. + +An example of sending Email to the above server via `Send-MailMessage`: + +```powershell +Send-MailMessage -SmtpServer localhost -To 'to@example.com' -From 'from@example.com' -Body 'Hello' -Subject 'Hi there' -Port 25 +``` + +## Attachments + +The SMTP server also accepts attachments, which are available in a Handler via `$SmtpEvent.Email.Attachments`. This property contains a list of available attachments on the Email, each attachment has a `Name` and `Bytes` properties - the latter being the raw byte content of the attachment. + +An attachment also has a `.Save()` method. For example, if the Email has an a single attachment: an `example.png` file, and you wish to save it, then the following will save the file to `C:\temp\example.png`: + +```powershell +Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { + $SmtpEvent.Email.Attachments[0].Save('C:\temp') +} +``` + +## Objects + +### SmtpEvent + +The SMTP Handler will be passed the `$SmtpEvent` object, that contains the Request, Response, and Email properties: | Name | Type | Description | | ---- | ---- | ----------- | @@ -27,7 +56,9 @@ The SMTP Handler will be passed the a `$SmtpEvent` object, that contains te Requ | Lockable | hashtable | A synchronized hashtable that can be used with `Lock-PodeObject` | | Email | hashtable | An object containing data from the email, as seen below | -The `Email` property contains the following: +### Email + +The `Email` property contains the following properties: | Name | Type | Description | | ---- | ---- | ----------- | @@ -40,3 +71,4 @@ The `Email` property contains the following: | ContentEncoding | string | The content encoding of the original email body | | Headers | hashtable | A list of all the headers received for the email | | Data | string | The full raw data of the email | +| Attachments | PodeSmtpAttachment[] | An list of SMTP attachments, containing the Name and Bytes of the attachment | diff --git a/docs/Tutorials/Endpoints/External.md b/docs/Tutorials/Endpoints/External.md index 1cf49ae44..e19e6adba 100644 --- a/docs/Tutorials/Endpoints/External.md +++ b/docs/Tutorials/Endpoints/External.md @@ -7,7 +7,7 @@ At most times you'll possibly be accessing your Pode server locally. However, yo ## All Addresses -The default and common approach is to set your Pode server to listen on all IP addresses: +The default and common approach is to set your Pode server to listen on all IP addresses; this approach does require administrator privileges: ```powershell Add-PodeEndpoint -Address * -Port 8080 -Protocol Http @@ -21,7 +21,7 @@ Invoke-RestMethod -Uri 'http://:8080' ## IP Address -The other way to expose your server externally is to create an endpoint using the server's Private/Public IP address. For example, assuming the the server's IP is `10.10.1.5`: +The other way to expose your server externally is to create an endpoint using the server's Private/Public IP address; this approach does require administrator privileges. For example, assuming the the server's IP is `10.10.1.5`: ```powershell Add-PodeEndpoint -Address 10.10.1.5 -Port 8080 -Protocol Http @@ -35,7 +35,7 @@ Invoke-RestMethod -Uri 'http://10.10.1.5:8080' ## Hostnames -The final way to expose your server externally is to allow only specific hostnames bound to the server's Private/Public IP address - something like SNI in IIS. +Another way to expose your server externally is to allow only specific hostnames bound to the server's Private/Public IP address - something like SNI in IIS. This approach does require administrator privileges. To do this, let's say you want to allow only `one.pode.com` and `two.pode.com` on a server with IP `10.10.1.5`. There are two way of doing this: @@ -68,3 +68,37 @@ With these set, you can access your endpoint using only the `one.pode.com` and ` Invoke-RestMethod -Uri 'http://one.pode.com:8080' Invoke-RestMethod -Uri 'http://two.pode.com:8080' ``` + +## Netsh + +This next way allows you to access your server external, but be able to run the server without administrator privileges. The initial setup does require administrator privileges, but running the server does not. + +To do this, let's say you want to access your server on `10.10.1.5`, you can use the following steps: + +1. You server should be listening on localhost and then any port you wish: + +```powershell +Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http +``` + +2. Next, you can run the following command as an administrator where the `` can be any port that's not the port in your [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint) (such as port+1): + +```bash +netsh interface portproxy add v4tov4 listenport= connectaddress=127.0.0.1 connectport= +``` + +For example, the above endpoint could be: + +```bash +netsh interface portproxy add v4tov4 listenport=8081 connectaddress=127.0.0.1 connectport=8080 +``` + +3. Run your Pode server as a non-admin user. + +With this done, you can access your endpoint on `10.10.1.5:8081`: + +```powershell +Invoke-RestMethod -Uri 'http://10.10.1.5:8081' +``` + +This works by having `netsh interface portproxy` redirect traffic to the local port which your Pode server is listening on. diff --git a/docs/Tutorials/Events.md b/docs/Tutorials/Events.md new file mode 100644 index 000000000..22c081a5a --- /dev/null +++ b/docs/Tutorials/Events.md @@ -0,0 +1,50 @@ +# Events + +Pode lets you register scripts to be run when certain server events are triggered. The following types of events can have scripts registered: + +* Start +* Terminate +* Restart +* Browser + +## Overview + +You can use [`Register-PodeEvent`](../../Functions/Events/Register-PodeEvent) to register a script that can be run when an event within Pode is triggered. Each event can have multiple scripts registered, and you can unregister a script at any point using [`Unregister-PodeEvent`](../../Functions/Events/Unregister-PodeEvent): + +```powershell +# register: +Register-PodeEvent -Type Start -Name '' -ScriptBlock { + # inform a portal, write a log, etc +} + +# unregister: +Unregister-PodeEvent -Type Start -Name '' +``` + +The scriptblock supplied to `Register-PodeEvent` also supports `$using:` variables. You can retrieve a registered script using [`Get-PodeEvent`](../../Functions/Events/Get-PodeEvent): + +```powershell +$evt = Get-PodeEvent -Type Start -Name '' +``` + +## Types + +### Start + +Scripts registered to the `Start` event will all be invoked just after the server's main scriptblock has been invoked - ie: the `-ScriptBlock` supplied to [`Start-PodeServer`](../../Functions/Core/Start-PodeServer). + +These scripts will also be re-invoked after a server restart has occurred. + +### Terminate + +Scripts registered to the `Terminate` event will all be invoked just before the server terminates. Ie, when the `Terminating...` message usually appears in the terminal, the script will run just after this and just before the `Done` message. + +These script *will not* run when a Restart is triggered. + +### Restart + +Scripts registered to the `Restart` event will all be invoked whenever an internal server restart occurs. This could be due to file monitoring, auto-restarting, `Ctrl+R`, or [`Restart-PodeServer`](../../Functions/Core/Restart-PodeServer). They will be invoked just after the `Restarting...` message appears in the terminal, and just before the `Done` message. + +### Browser + +Scripts registered to the `Browser` event will all be invoked whenever the server is told to open a browser, ie: when `Ctrl+B` is pressed. diff --git a/docs/Tutorials/Middleware/Types/Sessions.md b/docs/Tutorials/Middleware/Types/Sessions.md index c9ec63fb7..12dc8f4d3 100644 --- a/docs/Tutorials/Middleware/Types/Sessions.md +++ b/docs/Tutorials/Middleware/Types/Sessions.md @@ -1,8 +1,8 @@ # Sessions -Session Middleware is supported on web requests and responses in the form of signed-cookies/headers and server-side data storage. When configured, the middleware will check for a session-cookie/header on the request; if a cookie/header is not found on the request, or the session is not in the store, then a new session is created and attached to the response. If there is a session, then the appropriate data is loaded from the store. +Session Middleware is supported on web requests and responses in the form of signed-cookies/headers and server-side data storage. When configured, the middleware will check for a session-cookie/header on the request; if a cookie/header is not found on the request, or the session is not in storage, then a new session is created and attached to the response. If there is a session, then the appropriate data for that session is loaded from storage. -The duration of the session-cookie/header can be specified, as well as whether to extend the duration each time on each request. A secret-key to sign sessions can be supplied, as well as the ability to specify custom data stores - the default is in-mem, custom could be anything like Redis/MongoDB. +The duration of the session-cookie/header can be specified, as well as whether to extend the duration each time on each request. A secret-key to sign sessions can be supplied (default is a random GUID), as well as the ability to specify custom data stores - the default is in-memory, but custom storage could be anything like Redis/MongoDB/etc. !!! note Using sessions via headers is best used with REST APIs and the CLI. It's not advised to use them for normal websites, as browsers don't send back response headers in new requests - unlike cookies. @@ -11,13 +11,15 @@ The duration of the session-cookie/header can be specified, as well as whether t To initialise sessions in Pode you use the [`Enable-PodeSessionMiddleware`](../../../../Functions/Middleware/Enable-PodeSessionMiddleware) function. This function will configure and automatically create Middleware to enable sessions. By default sessions are set using cookies, but support is also available for headers. +Sessions are automatically signed using a random GUID. For Pode running on a single server using the default in-memory storage this is OK. However if you're running Pode on multiple servers, or if you're defining a custom storage then a `-Secret` is required - this is so that sessions from different servers, or after a server restart, don't become corrupt and unusable. + ### Cookies The following is an example of how to setup session middleware using cookies: ```powershell Start-PodeServer { - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend } ``` @@ -29,7 +31,7 @@ Sessions are also supported using headers - useful for CLI requests. The followi ```powershell Start-PodeServer { - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend -UseHeaders + Enable-PodeSessionMiddleware -Duration 120 -Extend -UseHeaders } ``` @@ -47,13 +49,13 @@ Within a route, or middleware, you can get the current authenticated sessionId u You can flag sessions as being strict using the `-Strict` switch. Strict sessions will extend the signing process by also using the client's UserAgent and RemoteIPAddress, to help prevent session sharing on different browsers/consoles. -You can supply the secret value as normal, Pode will automatically extend it for you. +Pode will automatically extend the Secret for signing for you, whether you're using the default GUID, or supplying a specific `-Secret` value. ## Storage -The inbuilt storage for sessions is a simple In-Memory store - with auto-cleanup for expired sessions. +The inbuilt storage for sessions is a simple in-memory store - with auto-cleanup for expired sessions. -If supplied, the `-Storage` parameter is a `psobject` with the following required `NoteProperty` scriptblock members: +You can define a custom storage by supplying a `psobject` to the `-Storage` parameter, and also note that a `-Secret` will be required. The `psobject` supplied should have the following `NoteProperty` scriptblock members: ```powershell [hashtable] Get([string] $sessionId) @@ -64,22 +66,29 @@ If supplied, the `-Storage` parameter is a `psobject` with the following require For example, the following is a mock up of a Storage for Redis (note that the functions are fake): ```powershell +# create the object $store = New-Object -TypeName psobject +# add a Get property for retreiving a session's data by SessionId $store | Add-Member -MemberType NoteProperty -Name Get -Value { param($sessionId) return (Get-RedisKey -Key $sessionId) } +# add a Set property to save a session's data $store | Add-Member -MemberType NoteProperty -Name Set -Value { param($sessionId, $data, $expiry) Set-RedisKey -Key $sessionId -Value $data -TimeToLive $expiry } +# add a Delete property to delete a session's data by SessionId $store | Add-Member -MemberType NoteProperty -Name Delete -Value { param($sessionId) Remove-RedisKey -Key $sessionId } + +# enable session middleware - a secret is required +Enable-PodeSessionMiddleware -Duration 120 -Storage $store -Secret 'schwifty' ``` ## Session Data @@ -92,7 +101,7 @@ An example of using sessions in a Route to increment a views counter could be do ```powershell Start-PodeServer { - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 + Enable-PodeSessionMiddleware -Duration 120 Add-PodeRoute -Method Get -Path '/' -ScriptBlock { $WebEvent.Session.Data.Views++ diff --git a/docs/Tutorials/Restarting/Overview.md b/docs/Tutorials/Restarting/Overview.md index 79b7a251b..ba3c965b8 100644 --- a/docs/Tutorials/Restarting/Overview.md +++ b/docs/Tutorials/Restarting/Overview.md @@ -1,10 +1,11 @@ # Overview -There are 3 ways to restart a running Pode server: +There are 4 ways to restart a running Pode server: 1. **Ctrl+R**: If you press `Ctrl+R` on a running server, it will trigger a restart to take place. 1a. On Unix you can use `Shift+R`. 2. [**File Monitoring**](../Types/FileMonitoring): This will watch for file changes, and if enabled will trigger the server to restart. 3. [**Auto-Restarting**](../Types/AutoRestarting): Defined within the `server.psd1` configuration file, you can set schedules for the server to automatically restart. +4. [`Restart-PodeServer`](../../../Functions/Core/Restart-PodeServer): This function lets you manually restart Pode from within the server. When the server restarts, it will re-invoke the `-ScriptBlock` supplied to the [`Start-PodeServer`](../../../Functions/Core/Start-PodeServer) function. This means the best approach to reload new modules/scripts it to dot-source/[`Use-PodeScript`](../../../Functions/Utilities/Use-PodeScript) your scripts into your server, as any changes to the main `scriptblock` will **not** take place. diff --git a/docs/Tutorials/Routes/Examples/LoginPage.md b/docs/Tutorials/Routes/Examples/LoginPage.md index 09c79fc04..f18165db8 100644 --- a/docs/Tutorials/Routes/Examples/LoginPage.md +++ b/docs/Tutorials/Routes/Examples/LoginPage.md @@ -1,6 +1,6 @@ # Creating a Login Page -This is an example of having a website with a login and home page - with a logout button. The pages will all be done using `.pode` files, and authentication will be done using Form authentication with Sessions. +This is an example of having a website with a login and home page - with a logout button. The pages will all be done using `.pode` files, and authentication will be done using [Form authentication](../../../Authentication/Methods/Form) with [Sessions]((../../../Middleware/Types/Sessions)). !!! info The full example can be seen on GitHub in [`examples/web-auth-form.ps1`](https://github.com/Badgerati/Pode/blob/develop/examples/web-auth-form.ps1). @@ -38,7 +38,7 @@ Set-PodeViewEngine -Type Pode To use sessions for our authentication (so we can stay logged in), we need to setup Session Middleware using the [`Enable-PodeSessionMiddleware`](../../../../Functions/Middleware/Enable-PodeSessionMiddleware) function. Here our sessions will last for 2 minutes, and will be extended on each request: ```powershell -Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend +Enable-PodeSessionMiddleware -Duration 120 -Extend ``` Once we have the Session Middleware initialised, we need to setup Form authentication - the username/password here are hard-coded, but normally you would validate against some database. We also specify a `-FailureUrl`, which is the URL to redirect a user to if they try to access a page un-authenticated. The `-SuccessUrl` is the URL to redirect to on successful authentication. @@ -110,7 +110,7 @@ Start-PodeServer -Thread 2 { Set-PodeViewEngine -Type Pode # setup session middleware - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form authentication New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { diff --git a/docs/Tutorials/Routes/Examples/RestApiSessions.md b/docs/Tutorials/Routes/Examples/RestApiSessions.md index 97194dbf4..d63fab54e 100644 --- a/docs/Tutorials/Routes/Examples/RestApiSessions.md +++ b/docs/Tutorials/Routes/Examples/RestApiSessions.md @@ -1,6 +1,6 @@ # REST APIs and Sessions -Sessions in Pode are normally done using cookies, but you can also use them via headers as well. This way you can have two endpoints for authentication login/logout, and the rest of your routes depend on a valid SessionId. +[Sessions](../../../Middleware/Types/Sessions) in Pode are normally done using cookies, but you can also use them via headers as well. This way you can have two endpoints for authentication login/logout, and the rest of your routes depend on a valid SessionId. !!! info The full example can be seen on GitHub in `examples/web-auth-basic-header.ps1`. @@ -26,7 +26,7 @@ Add-PodeEndpoint -Address * -Port 8080 -Protocol Http To use sessions with headers for our authentication, we need to setup Session Middleware using the [`Enable-PodeSessionMiddleware`](../../../../Functions/Middleware/Enable-PodeSessionMiddleware) function. Here our sessions will last for 2 minutes, and will be extended on each request: ```powershell -Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend -UseHeaders +Enable-PodeSessionMiddleware -Duration 120 -Extend -UseHeaders ``` ## Authentication diff --git a/docs/Tutorials/SharedState.md b/docs/Tutorials/SharedState.md index de05e71bf..d97bc8ffe 100644 --- a/docs/Tutorials/SharedState.md +++ b/docs/Tutorials/SharedState.md @@ -14,6 +14,8 @@ You can also use the State in combination with the [`Lock-PodeObject`](../../Fun ## Usage +Where possible use the same casing for the `-Name` of state keys. When using [`Restore-PodeState`](../../Functions/State/Restore-PodeState) the state will become case-sensitive due to the nature of how `ConvertFrom-Json` works. + ### Set The [`Set-PodeState`](../../Functions/State/Set-PodeState) function will create/update a variable in the state. You need to supply a name and a value to set on the state, there's also an optional scope that can be supplied - which lets you save specific state objects with a certain scope. @@ -23,7 +25,7 @@ An example of setting a hashtable variable in the state is as follows: ```powershell Start-PodeServer { Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock { - Lock-PodeObject -Object $TimerEvent.Lockable { + Lock-PodeObject -Object $TimerEvent.Lockable -ScriptBlock { Set-PodeState -Name 'data' -Value @{ 'Name' = 'Rick Sanchez' } | Out-Null } } @@ -41,7 +43,7 @@ Start-PodeServer { Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock { $value = $null - Lock-PodeObject -Object $TimerEvent.Lockable { + Lock-PodeObject -Object $TimerEvent.Lockable -ScriptBlock { $value = (Get-PodeState -Name 'data') } @@ -59,7 +61,7 @@ An example of removing a variable from the state is as follows: ```powershell Start-PodeServer { Add-PodeTimer -Name 'do-something' -Interval 5 -ScriptBlock { - Lock-PodeObject -Object $TimerEvent.Lockable { + Lock-PodeObject -Object $TimerEvent.Lockable -ScriptBlock { Remove-PodeState -Name 'data' | Out-Null } } @@ -75,7 +77,7 @@ An example of saving the current state every hour is as follows: ```powershell Start-PodeServer { Add-PodeSchedule -Name 'save-state' -Cron '@hourly' -ScriptBlock { - Lock-PodeObject -Object $lockable { + Lock-PodeObject -Object $lockable -ScriptBlock { Save-PodeState -Path './state.json' } } @@ -115,7 +117,7 @@ Start-PodeServer { # timer to add a random number to the shared state Add-PodeTimer -Name 'forever' -Interval 2 -ScriptBlock { # ensure we're thread safe - Lock-PodeObject -Object $TimerEvent.Lockable { + Lock-PodeObject -Object $TimerEvent.Lockable -ScriptBlock { # attempt to get the hashtable from the state $hash = (Get-PodeState -Name 'hash') @@ -131,7 +133,7 @@ Start-PodeServer { # route to return the value of the hashtable from shared state Add-PodeRoute -Method Get -Path '/' -ScriptBlock { # again, ensure we're thread safe - Lock-PodeObject -Object $WebEvent.Lockable { + Lock-PodeObject -Object $WebEvent.Lockable -ScriptBlock { # get the hashtable from the state and return it $hash = (Get-PodeState -Name 'hash') @@ -142,7 +144,7 @@ Start-PodeServer { # route to remove the hashtable from shared state Add-PodeRoute -Method Delete -Path '/' -ScriptBlock { # ensure we're thread safe - Lock-PodeObject -Object $WebEvent.Lockable { + Lock-PodeObject -Object $WebEvent.Lockable -ScriptBlock { # remove the hashtable from the state Remove-PodeState -Name 'hash' | Out-Null diff --git a/docs/Tutorials/Threading.md b/docs/Tutorials/Threading.md index 43b97a26f..cfc8072bf 100644 --- a/docs/Tutorials/Threading.md +++ b/docs/Tutorials/Threading.md @@ -1,6 +1,6 @@ # Threading -By default Pode deals with incoming requests synchronously in a single thread. You can increase the number of threads/runspaces that Pode uses to handle requests by using the `-Threads` parameter on the [`Start-PodeServer`](../../Functions/Core/Start-PodeServer) function: +By default Pode deals with incoming requests synchronously in a single thread. You can increase the number of threads/runspaces that Pode uses to handle requests by using the `-Threads` parameter on [`Start-PodeServer`](../../Functions/Core/Start-PodeServer): ```powershell Start-PodeServer -Threads 2 { @@ -9,3 +9,62 @@ Start-PodeServer -Threads 2 { ``` The number of threads supplied only applies to Web, SMTP, and TCP servers. If `-Threads` is not supplied, or is <=0 then the number of threads is forced to the default of 1. + +## Locking + +When using multi-threading in Pode at times you'll want to ensure certain functions run thread-safe. To do this you can use [`Lock-PodeObject`](../../Functions/Utilities/Lock-PodeObject) which will lock an object cross-thread. + +### Global + +In event objects, like `$WebEvent`, there is a global `Lockable` object that you can use - this object is synchronized across every thread, so locking it on one will lock it on all: + +```powershell +Add-PodeRoute -Method Get -Path '/save' -ScriptBlock { + Lock-PodeObject -Object $WebEvent.Lockable -ScriptBlock { + Save-PodeState -Path './state.json' + } +} +``` + +### Custom + +The global lockable is good, but at times you will have separate processes where they can use different lockables objects - rather than sharing a global one and locking each other out needlessly. + +To create a custom lockable object you can use [`New-PodeLockable`](../../Functions/Utilities/New-PodeLockable), and this will create a synchronized object across all threads that you can use. You cna then use this object via [`Get-PodeLockable`](../../Functions/Utilities/Get-PodeLockable) and pipe it into [`Lock-PodeObject`](../../Functions/Utilities/Lock-PodeObject): + +```powershell +New-PodeLockable -Name 'Lock1' + +Add-PodeRoute -Method Get -Path '/save' -ScriptBlock { + Get-PodeLockable -Name 'Lock1' | Lock-PodeObject -ScriptBlock { + Save-PodeState -Path './state.json' + } +} +``` + +On [`Lock-PodeObject`](../../Functions/Utilities/Lock-PodeObject) there's also a `-CheckGlobal` switch. This switch will check if the global lockable is locked, and wait for it to free up before locking the specified object and running the script. This is useful if you have a number of custom lockables, and then when saving the current state you lock the global. Every other process could lock their custom lockables, but then also check the global lockable and block until saving state is finished. + +For example, the following has two routes. The first route locks the global lockable and sleeps for 10 seconds, whereas the second route locks the custom object but checks the global for locking. Calling the first route then the second straight after, they will both return after 10 seconds: + +```powershell +Start-PodeServer -Threads 2 { + Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + + New-PodeLockable -Name 'TestLock' + + # lock global, sleep for 10secs + Add-PodeRoute -Method Get -Path '/route1' -ScriptBlock { + Lock-PodeObject -Object $WebEvent.Lockable -ScriptBlock { + Start-Sleep -Seconds 10 + } + + Write-PodeJsonResponse -Value @{ Route = 1; Thread = $ThreadId } + } + + # lock custom, but check global + Add-PodeRoute -Method Get -Path '/route2' -ScriptBlock { + Get-PodeLockable -Name 'TestLock' | Lock-PodeObject -CheckGlobal -ScriptBlock {} + Write-PodeJsonResponse -Value @{ Route = 2; Thread = $ThreadId } + } +} +``` diff --git a/docs/release-notes.md b/docs/release-notes.md index 51069c318..da22d7c64 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,29 @@ # Release Notes +## v2.4.0 + +```plain +### Features +* #766: Add support for server Event Hooks, to run scripts on events like terminating the server +* #769: Add support for custom Lockable objects + +### Enhancements +* #763: Add support for SMTP attachments +* #765: Use a random secure GUID for Session `-Secret` if not supplied +* #767: Add new `Restart-PodeServer` to manually restart the server internally +* #779: Replace uses of `Join-Path` with `[System.IO.Path]::Combine` +* #786: Add new `Get-PodeStateNames` to get array of current Names with shared state + +### Bugs +* #768: Fix for a rare multithreading bug when serialising session data +* #770: `-SuccessUseOrigin` should only work for GET requests +* #776: Fix for the PodeResponse class and the handling of AggregateExceptions + +### Documentation +* #757: Add information about using `netsh interface portproxy` for external access as non-admin +* #762: Update `Add-PodeMiddleware` function summary to reference returning a boolean value +``` + ## v2.3.0 ```plain diff --git a/examples/lockables.ps1 b/examples/lockables.ps1 new file mode 100644 index 000000000..782b4485b --- /dev/null +++ b/examples/lockables.ps1 @@ -0,0 +1,41 @@ +$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) +Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop + +# or just: +# Import-Module Pode + +# create a basic server +Start-PodeServer -Threads 2 { + + Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + New-PodeLockable -Name 'TestLock' + + Add-PodeRoute -Method Get -Path '/custom-route1' -ScriptBlock { + Get-PodeLockable -Name 'TestLock' | Lock-PodeObject -ScriptBlock { + Start-Sleep -Seconds 10 + } + + Write-PodeJsonResponse -Value @{ Route = 1; Thread = $ThreadId } + } + + Add-PodeRoute -Method Get -Path '/custom-route2' -ScriptBlock { + Get-PodeLockable -Name 'TestLock' | Lock-PodeObject -ScriptBlock {} + Write-PodeJsonResponse -Value @{ Route = 2; Thread = $ThreadId } + } + + Add-PodeRoute -Method Get -Path '/global-route1' -ScriptBlock { + Lock-PodeObject -Object $WebEvent.Lockable -ScriptBlock { + Start-Sleep -Seconds 10 + } + + Write-PodeJsonResponse -Value @{ Route = 1; Thread = $ThreadId } + } + + Add-PodeRoute -Method Get -Path '/global-route2' -ScriptBlock { + Get-PodeLockable -Name 'TestLock' | Lock-PodeObject -CheckGlobal -ScriptBlock {} + Write-PodeJsonResponse -Value @{ Route = 2; Thread = $ThreadId } + } + +} \ No newline at end of file diff --git a/examples/looping-service.ps1 b/examples/looping-service.ps1 index 2e31a83cb..841b56d19 100644 --- a/examples/looping-service.ps1 +++ b/examples/looping-service.ps1 @@ -9,7 +9,7 @@ Start-PodeServer -Interval 3 { Add-PodeHandler -Type Service -Name 'Hello' -ScriptBlock { Write-Host 'hello, world!' - Lock-PodeObject -Object $ServiceEvent.Lockable { + Lock-PodeObject -Object $ServiceEvent.Lockable -ScriptBlock { "Look I'm locked!" | Out-PodeHost } } diff --git a/examples/mail-server.ps1 b/examples/mail-server.ps1 index c160aced6..e77fdef72 100644 --- a/examples/mail-server.ps1 +++ b/examples/mail-server.ps1 @@ -28,9 +28,15 @@ Start-PodeServer -Threads 2 { Write-Host '|' Write-Host $SmtpEvent.Email.Body Write-Host '|' - Write-Host $SmtpEvent.Email.Data + # Write-Host $SmtpEvent.Email.Data + # Write-Host '|' + $SmtpEvent.Email.Attachments | Out-Default + if ($SmtpEvent.Email.Attachments.Length -gt 0) { + #$SmtpEvent.Email.Attachments[0].Save('C:\temp') + } Write-Host '|' $SmtpEvent.Email | Out-Default + $SmtpEvent.Request | out-default Write-Host '- - - - - - - - - - - - - - - - - -' } diff --git a/examples/shared-state.ps1 b/examples/shared-state.ps1 index 181afc8be..8e7c1c443 100644 --- a/examples/shared-state.ps1 +++ b/examples/shared-state.ps1 @@ -29,7 +29,7 @@ Start-PodeServer { Add-PodeTimer -Name 'forever' -Interval 2 -ScriptBlock { $hash = $null - Lock-PodeObject -Object $WebEvent.Lockable { + Lock-PodeObject -Object $TimerEvent.Lockable -ScriptBlock { $hash = (Get-PodeState -Name 'hash1') $hash.values += (Get-Random -Minimum 0 -Maximum 10) Save-PodeState -Path './state.json' -Scope Scope1 #-Exclude 'hash1' @@ -38,7 +38,7 @@ Start-PodeServer { # route to retrieve and return the value of the hashtable from global state Add-PodeRoute -Method Get -Path '/array' -ScriptBlock { - Lock-PodeObject -Object $WebEvent.Lockable { + Lock-PodeObject -Object $WebEvent.Lockable -ScriptBlock { $hash = (Get-PodeState 'hash1') Write-PodeJsonResponse -Value $hash } @@ -46,8 +46,8 @@ Start-PodeServer { # route to remove the hashtable from global state Add-PodeRoute -Method Delete -Path '/array' -ScriptBlock { - Lock-PodeObject -Object $WebEvent.Lockable { - $hash = (Set-PodeState -Name 'hash' -Value @{}) + Lock-PodeObject -Object $WebEvent.Lockable -ScriptBlock { + $hash = (Set-PodeState -Name 'hash1' -Value @{}) $hash.values = @() } } diff --git a/examples/timers.ps1 b/examples/timers.ps1 index 8ba80e571..3d4a5039e 100644 --- a/examples/timers.ps1 +++ b/examples/timers.ps1 @@ -14,7 +14,7 @@ Start-PodeServer { Add-PodeTimer -Name 'forever' -Interval 5 -ScriptBlock { '- - -' | Out-PodeHost $using:message | Out-PodeHost - Lock-PodeObject -Object $TimerEvent.Lockable { + Lock-PodeObject -Object $TimerEvent.Lockable -ScriptBlock { "Look I'm locked!" | Out-PodeHost } '- - -' | Out-PodeHost diff --git a/examples/web-auth-basic-header.ps1 b/examples/web-auth-basic-header.ps1 index 54ad5a15e..8f11208d9 100644 --- a/examples/web-auth-basic-header.ps1 +++ b/examples/web-auth-basic-header.ps1 @@ -28,7 +28,7 @@ Start-PodeServer -Threads 2 { New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend -UseHeaders -Strict + Enable-PodeSessionMiddleware -Duration 120 -Extend -UseHeaders -Strict # setup basic auth (base64> username:password in header) New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Login' -ScriptBlock { diff --git a/examples/web-auth-form-ad.ps1 b/examples/web-auth-form-ad.ps1 index 8cf3d648b..f91e73a93 100644 --- a/examples/web-auth-form-ad.ps1 +++ b/examples/web-auth-form-ad.ps1 @@ -25,7 +25,7 @@ Start-PodeServer -Threads 2 { Set-PodeViewEngine -Type Pode # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth against windows AD (
in HTML) New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'Login' -Groups @() -Users @() -FailureUrl '/login' -SuccessUrl '/' diff --git a/examples/web-auth-form-creds.ps1 b/examples/web-auth-form-creds.ps1 index 951a8616b..aac22042c 100644 --- a/examples/web-auth-form-creds.ps1 +++ b/examples/web-auth-form-creds.ps1 @@ -28,7 +28,7 @@ Start-PodeServer -Threads 2 { New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth ( in HTML) New-PodeAuthScheme -Form -AsCredential | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { diff --git a/examples/web-auth-form-file.ps1 b/examples/web-auth-form-file.ps1 index 7b7773d80..207a4ca98 100644 --- a/examples/web-auth-form-file.ps1 +++ b/examples/web-auth-form-file.ps1 @@ -28,7 +28,7 @@ Start-PodeServer -Threads 2 { Set-PodeViewEngine -Type Pode # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth against user file ( in HTML) New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' -FilePath './users/users.json' -FailureUrl '/login' -SuccessUrl '/' diff --git a/examples/web-auth-form-local.ps1 b/examples/web-auth-form-local.ps1 index a706d290a..066c40fa3 100644 --- a/examples/web-auth-form-local.ps1 +++ b/examples/web-auth-form-local.ps1 @@ -25,7 +25,7 @@ Start-PodeServer -Threads 2 { Set-PodeViewEngine -Type Pode # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth against windows local users ( in HTML) New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'Login' -Groups @() -Users @() -FailureUrl '/login' -SuccessUrl '/' diff --git a/examples/web-auth-form.ps1 b/examples/web-auth-form.ps1 index 2aa9aba53..71174e674 100644 --- a/examples/web-auth-form.ps1 +++ b/examples/web-auth-form.ps1 @@ -27,7 +27,7 @@ Start-PodeServer -Threads 2 { New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth ( in HTML) New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { diff --git a/examples/web-auth-oauth2-form.ps1 b/examples/web-auth-oauth2-form.ps1 index 06447cc53..4bf3a0b75 100644 --- a/examples/web-auth-oauth2-form.ps1 +++ b/examples/web-auth-oauth2-form.ps1 @@ -25,7 +25,7 @@ Start-PodeServer -Threads 2 { Set-PodeViewEngine -Type Pode # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth against Azure AD (the following are from registering an app in the portal) $clientId = '' diff --git a/examples/web-auth-oauth2.ps1 b/examples/web-auth-oauth2.ps1 index e820e4ca7..6c5602c95 100644 --- a/examples/web-auth-oauth2.ps1 +++ b/examples/web-auth-oauth2.ps1 @@ -25,7 +25,7 @@ Start-PodeServer -Threads 2 { Set-PodeViewEngine -Type Pode # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend + Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth against Azure AD (the following are from registering an app in the portal) $clientId = '' diff --git a/examples/web-csrf.ps1 b/examples/web-csrf.ps1 index 465483d17..85561a927 100644 --- a/examples/web-csrf.ps1 +++ b/examples/web-csrf.ps1 @@ -28,7 +28,7 @@ Start-PodeServer -Threads 2 { } 'session' { - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 + Enable-PodeSessionMiddleware -Duration 120 Enable-PodeCsrfMiddleware } } diff --git a/examples/web-sessions.ps1 b/examples/web-sessions.ps1 index cd408110e..833427c01 100644 --- a/examples/web-sessions.ps1 +++ b/examples/web-sessions.ps1 @@ -14,7 +14,7 @@ Start-PodeServer { Set-PodeViewEngine -Type Pode # setup session details - Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend -Generator { + Enable-PodeSessionMiddleware -Duration 120 -Extend -Generator { return [System.IO.Path]::GetRandomFileName() } diff --git a/src/Listener/PodeForm.cs b/src/Listener/PodeForm.cs index 39e94d6aa..783f9729e 100644 --- a/src/Listener/PodeForm.cs +++ b/src/Listener/PodeForm.cs @@ -48,6 +48,11 @@ public static PodeForm Parse(byte[] bytes, string contentType, Encoding contentE } } + return ParseHttp(form, lines, boundaryLineIndexes, contentEncoding); + } + + private static PodeForm ParseHttp(PodeForm form, List lines, List boundaryLineIndexes, Encoding contentEncoding) + { var boundaryLineIndex = 0; var disposition = string.Empty; var fields = new Dictionary(); @@ -129,5 +134,15 @@ private static bool IsLineBoundary(byte[] bytes, string boundary, Encoding conte return (contentEncoding.GetString(bytes).StartsWith(boundary)); } + + public static bool IsLineBoundary(string line, string boundary) + { + if (string.IsNullOrEmpty(line)) + { + return false; + } + + return line.StartsWith(boundary); + } } } \ No newline at end of file diff --git a/src/Listener/PodeHelpers.cs b/src/Listener/PodeHelpers.cs index e760623d0..e4ec97157 100644 --- a/src/Listener/PodeHelpers.cs +++ b/src/Listener/PodeHelpers.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Security.Cryptography; namespace Pode @@ -30,10 +31,24 @@ public class PodeHelpers return; } - Console.WriteLine(ex.Message); + Console.WriteLine($"{ex.GetType().Name}: {ex.Message}"); Console.WriteLine(ex.StackTrace); } + public static void HandleAggregateException(AggregateException aex, PodeListener listener = default(PodeListener)) + { + aex.Handle((ex) => + { + if (ex is IOException || ex is OperationCanceledException) + { + return true; + } + + PodeHelpers.WriteException(ex, listener); + return false; + }); + } + public static string NewGuid(int length = 16) { using (var rnd = RandomNumberGenerator.Create()) @@ -98,5 +113,18 @@ public static List ConvertToByteLines(byte[] bytes) return lines; } + + public static T[] Subset(T[] array, int startIndex, int endIndex) + { + var count = endIndex - startIndex; + var newArray = new T[count]; + Array.Copy(array, startIndex, newArray, 0, count); + return newArray; + } + + public static List Subset(List list, int startIndex, int endIndex) + { + return Subset(list.ToArray(), startIndex, endIndex).ToList(); + } } } \ No newline at end of file diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index 77075f56e..68f9720d3 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -111,6 +111,10 @@ public void Send() } catch (OperationCanceledException) {} catch (IOException) {} + catch (AggregateException aex) + { + PodeHelpers.HandleAggregateException(aex); + } catch (Exception ex) { PodeHelpers.WriteException(ex); @@ -156,6 +160,10 @@ public void SendTimeout() } catch (OperationCanceledException) {} catch (IOException) {} + catch (AggregateException aex) + { + PodeHelpers.HandleAggregateException(aex); + } catch (Exception ex) { PodeHelpers.WriteException(ex); @@ -238,6 +246,10 @@ public void Write(byte[] buffer, bool flush = false) } catch (OperationCanceledException) {} catch (IOException) {} + catch (AggregateException aex) + { + PodeHelpers.HandleAggregateException(aex); + } catch (Exception ex) { PodeHelpers.WriteException(ex); diff --git a/src/Listener/PodeSmtpAttachment.cs b/src/Listener/PodeSmtpAttachment.cs new file mode 100644 index 000000000..d828d1f8f --- /dev/null +++ b/src/Listener/PodeSmtpAttachment.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; + +namespace Pode +{ + public class PodeSmtpAttachment : IDisposable + { + public string Name { get; private set; } + public string ContentType { get; private set; } + public string ContentEncoding { get; private set; } + public byte[] Bytes => _stream.ToArray(); + + private MemoryStream _stream; + + public PodeSmtpAttachment(string name, MemoryStream stream, string contentType, string contentEncoding) + { + Name = name; + ContentType = contentType; + ContentEncoding = contentEncoding; + _stream = stream; + } + + public void Save(string path, bool addNameToPath = true) + { + if (addNameToPath) + { + path = Path.Combine(path, Name); + } + + using (var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) + { + _stream.WriteTo(file); + } + } + + public void Dispose() + { + _stream.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Listener/PodeSmtpRequest.cs b/src/Listener/PodeSmtpRequest.cs index 546d5aa10..0b45b7c09 100644 --- a/src/Listener/PodeSmtpRequest.cs +++ b/src/Listener/PodeSmtpRequest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Globalization; using _Encoding = System.Text.Encoding; +using System.IO; namespace Pode { @@ -14,9 +15,11 @@ public class PodeSmtpRequest : PodeRequest { public string ContentType { get; private set; } public string ContentEncoding { get; private set; } + public string Boundary { get; private set; } public Hashtable Headers { get; private set; } + public List Attachments { get; private set; } public string Body { get; private set; } - public string RawBody { get; private set; } + public byte[] RawBody { get; private set; } public string Subject { get; private set; } public bool IsUrgent { get; private set; } public string From { get; private set; } @@ -34,11 +37,17 @@ public PodeSmtpRequest(Socket socket) { CanProcess = false; IsKeepAlive = true; + Command = string.Empty; To = new List(); } private bool IsCommand(string content, string command) { + if (string.IsNullOrWhiteSpace(content)) + { + return false; + } + return content.StartsWith(command, true, CultureInfo.InvariantCulture); } @@ -47,6 +56,30 @@ public void SendAck() Context.Response.WriteLine($"220 {Context.PodeSocket.Hostnames[0]} -- Pode Proxy Server", true); } + protected override bool ValidateInput(byte[] bytes) + { + // we need more bytes! + if (bytes.Length == 0) + { + return false; + } + + // for data, we need to wait till it ends with a '.' + if (IsCommand(Command, "DATA")) + { + if (bytes.Length < 3) + { + return false; + } + + return (bytes[bytes.Length - 3] == (byte)46 + && bytes[bytes.Length - 2] == (byte)13 + && bytes[bytes.Length - 1] == (byte)10); + } + + return true; + } + protected override bool Parse(byte[] bytes) { // if there are no bytes, return (0 bytes read means we can close the socket) @@ -112,17 +145,35 @@ protected override bool Parse(byte[] bytes) switch (Command.ToUpperInvariant()) { case "DATA": + CanProcess = true; Context.Response.WriteLine("250 OK", true); - RawBody = content; - ParseHeaders(content); + RawBody = bytes; + Attachments = new List(); + + // parse the headers + Headers = ParseHeaders(content); Subject = $"{Headers["Subject"]}"; IsUrgent = ($"{Headers["Priority"]}".Equals("urgent", StringComparison.InvariantCultureIgnoreCase) || $"{Headers["Importance"]}".Equals("high", StringComparison.InvariantCultureIgnoreCase)); - ContentType = $"{Headers["Content-Type"]}"; ContentEncoding = $"{Headers["Content-Transfer-Encoding"]}"; + ContentType = $"{Headers["Content-Type"]}"; + if (!string.IsNullOrEmpty(Boundary) && !ContentType.Contains("boundary=")) + { + ContentType = ContentType.TrimEnd(';'); + ContentType += $"; boundary={Boundary}"; + } + + // check if body is valid and parse, else error if (IsBodyValid(content)) { - ParseBody(content); + // parse the body + Body = ConvertBodyType(ConvertBodyEncoding(ParseBody(content), ContentEncoding), ContentType); + + // if we have a boundary, get attachments/body + if (!string.IsNullOrWhiteSpace(Boundary)) + { + ParseBoundary(); + } } else { @@ -130,12 +181,10 @@ protected override bool Parse(byte[] bytes) Context.Response.WriteLine("501 Invalid DATA received", true); return true; } - - CanProcess = true; break; default: - throw new HttpRequestException(); + throw new HttpRequestException("Invalid SMTP command"); } return true; @@ -148,12 +197,22 @@ public void Reset() From = string.Empty; To = new List(); Body = string.Empty; - RawBody = string.Empty; + RawBody = default(byte[]); Command = string.Empty; ContentType = string.Empty; ContentEncoding = string.Empty; Subject = string.Empty; IsUrgent = false; + + if (Attachments != default(List)) + { + foreach (var attachment in Attachments) + { + attachment.Dispose(); + } + } + + Attachments = new List(); } private string ParseEmail(string value) @@ -167,9 +226,9 @@ private string ParseEmail(string value) return string.Empty; } - private void ParseHeaders(string value) + private Hashtable ParseHeaders(string value) { - Headers = new Hashtable(StringComparer.InvariantCultureIgnoreCase); + var headers = new Hashtable(StringComparer.InvariantCultureIgnoreCase); var lines = value.Split(new string[] { PodeHelpers.NEW_LINE }, StringSplitOptions.None); var match = default(Match); @@ -181,12 +240,22 @@ private void ParseHeaders(string value) break; } + // header match = Regex.Match(line, "^(?.*?)\\:\\s+(?.*?)$"); if (match.Success) { - Headers.Add(match.Groups["name"].Value, match.Groups["value"].Value); + headers.Add(match.Groups["name"].Value, match.Groups["value"].Value); + } + + // boundary line + match = Regex.Match(line, "^\\s*boundary=(?.+?)$"); + if (match.Success) + { + Boundary = match.Groups["boundary"].Value; } } + + return headers; } private bool IsBodyValid(string value) @@ -195,36 +264,97 @@ private bool IsBodyValid(string value) return (Array.LastIndexOf(lines, ".") > -1); } - private void ParseBody(string value) + private void ParseBoundary() { - Body = string.Empty; + var lines = Body.Split(new string[] { PodeHelpers.NEW_LINE }, StringSplitOptions.None); + var boundaryStart = $"--{Boundary}"; + var boundaryEnd = $"{boundaryStart}--"; + + var boundaryLineIndexes = new List(); + for (var i = 0; i < lines.Length; i++) + { + if (PodeForm.IsLineBoundary(lines[i], boundaryStart) || PodeForm.IsLineBoundary(lines[i], boundaryEnd)) + { + boundaryLineIndexes.Add(i); + } + } + + var boundaryIndex = 0; + var nextBoundaryIndex = 0; + + for (var i = 0; i < (boundaryLineIndexes.Count - 1); i++) + { + boundaryIndex = boundaryLineIndexes[i]; + nextBoundaryIndex = boundaryLineIndexes[i + 1]; + + // get the boundary headers + var boundaryBody = string.Join(PodeHelpers.NEW_LINE, PodeHelpers.Subset(lines, boundaryIndex + 1, nextBoundaryIndex + 1)); + var headers = ParseHeaders(boundaryBody); + + var contentType = $"{headers["Content-Type"]}"; + var contentEncoding = $"{headers["Content-Transfer-Encoding"]}"; + // get the boundary + var body = ParseBody(boundaryBody, Boundary); + var bodyBytes = ConvertBodyEncoding(body, contentEncoding); + + // file or main body? + var contentDisposition = $"{headers["Content-Disposition"]}"; + if (!string.IsNullOrEmpty(contentDisposition) && contentDisposition.Equals("attachment", StringComparison.InvariantCultureIgnoreCase)) + { + var match = Regex.Match(contentType, "name=(?.+)"); + var name = match.Groups["name"].Value; + + var stream = new MemoryStream(); + stream.Write(bodyBytes, 0, bodyBytes.Length); + var attachment = new PodeSmtpAttachment(name, stream, contentType, contentEncoding); + Attachments.Add(attachment); + } + else + { + Body = ConvertBodyType(bodyBytes, contentType); + } + } + } + + private string ParseBody(string value, string boundary = null) + { // split the message up var lines = value.Split(new string[] { PodeHelpers.NEW_LINE }, StringSplitOptions.None); + // what's the end char? + var useBoundary = !string.IsNullOrEmpty(boundary); + var endChar = useBoundary ? $"--{boundary}" : "."; + var trimCount = useBoundary ? 1 : 2; + // get the index of the first blank line, and last dot var indexOfBlankLine = Array.IndexOf(lines, string.Empty); - var indexOfLastDot = Array.LastIndexOf(lines, "."); + + var indexOfLastDot = Array.LastIndexOf(lines, endChar); + if (indexOfLastDot == -1 && useBoundary) + { + indexOfLastDot = Array.LastIndexOf(lines, $"{endChar}--"); + } // get the body - var bodyLines = lines.Skip(indexOfBlankLine + 1).Take(indexOfLastDot - indexOfBlankLine - 2); + var bodyLines = lines.Skip(indexOfBlankLine + 1).Take(indexOfLastDot - indexOfBlankLine - trimCount); var body = string.Join(PodeHelpers.NEW_LINE, bodyLines); // if there's no body, return if (indexOfLastDot == -1 || string.IsNullOrWhiteSpace(body)) { - Body = body; - return; + return string.Empty; } - // decode body based on encoding - var bodyBytes = default(byte[]); + return body; + } - switch (ContentEncoding.ToLowerInvariant()) + private byte[] ConvertBodyEncoding(string body, string contentEncoding) + { + switch (contentEncoding.ToLowerInvariant()) { case "base64": - bodyBytes = Convert.FromBase64String(body); - break; + return Convert.FromBase64String(body); case "quoted-printable": var match = default(Match); @@ -232,47 +362,68 @@ private void ParseBody(string value) { body = (body.Replace(match.Groups["code"].Value, $"{(char)Convert.ToInt32(match.Groups["hex"].Value, 16)}")); } - break; + + return _Encoding.UTF8.GetBytes(body); + + default: + return _Encoding.UTF8.GetBytes(body); } + } - // if body bytes set, convert to string based on type - if (bodyBytes != default(byte[])) + private string ConvertBodyType(byte[] bytes, string contentType) + { + if (bytes == default(byte[]) || bytes.Length == 0) { - var type = ContentType.ToLowerInvariant(); + return string.Empty; + } - // utf-7 - if (type.Contains("utf-7")) - { - body = _Encoding.UTF7.GetString(bodyBytes); - } + contentType = contentType.ToLowerInvariant(); - // utf-8 - else if (type.Contains("utf-8")) - { - body = _Encoding.UTF8.GetString(bodyBytes); - } + // utf-7 + if (contentType.Contains("utf-7")) + { + return _Encoding.UTF7.GetString(bytes); + } - // utf-16 - else if (type.Contains("utf-16")) - { - body = _Encoding.Unicode.GetString(bodyBytes); - } + // utf-8 + else if (contentType.Contains("utf-8")) + { + return _Encoding.UTF8.GetString(bytes); + } - // utf-32 - else if (type.Contains("utf32")) - { - body = _Encoding.UTF32.GetString(bodyBytes); - } + // utf-16 + else if (contentType.Contains("utf-16")) + { + return _Encoding.Unicode.GetString(bytes); + } - // default (ascii) - else + // utf-32 + else if (contentType.Contains("utf32")) + { + return _Encoding.UTF32.GetString(bytes); + } + + // default (ascii) + else + { + return _Encoding.ASCII.GetString(bytes); + } + } + + public override void Dispose() + { + RawBody = default(byte[]); + Body = string.Empty; + + if (Attachments != default(List)) + { + foreach (var attachment in Attachments) { - body = _Encoding.ASCII.GetString(bodyBytes); + attachment.Dispose(); } } - // set body - Body = body; + base.Dispose(); } } } \ No newline at end of file diff --git a/src/Listener/PodeSocket.cs b/src/Listener/PodeSocket.cs index 9e69af334..c5b61559d 100644 --- a/src/Listener/PodeSocket.cs +++ b/src/Listener/PodeSocket.cs @@ -7,6 +7,7 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; +using System.IO; namespace Pode { @@ -212,6 +213,11 @@ private void ProcessReceive(SocketAsyncEventArgs args) Task.Factory.StartNew(() => context.Receive(), context.ContextTimeoutToken.Token); } catch (OperationCanceledException) {} + catch (IOException) {} + catch (AggregateException aex) + { + PodeHelpers.HandleAggregateException(aex, Listener); + } catch (Exception ex) { PodeHelpers.WriteException(ex, Listener); diff --git a/src/Pode.psd1 b/src/Pode.psd1 index d5c6e1ae0..dc2584508 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -70,6 +70,7 @@ 'Save-PodeState', 'Restore-PodeState', 'Test-PodeState', + 'Get-PodeStateNames', # response helpers 'Set-PodeResponseAttachment', @@ -114,6 +115,10 @@ 'Write-PodeHost', 'Test-PodeIsIIS', 'Test-PodeIsHeroku', + 'New-PodeLockable', + 'Remove-PodeLockable', + 'Get-PodeLockable', + 'Test-PodeLockable', # routes 'Add-PodeRoute', @@ -200,6 +205,7 @@ # core 'Start-PodeServer', 'Close-PodeServer', + 'Restart-PodeServer', 'Start-PodeStaticServer', 'Show-PodeGui', 'Add-PodeEndpoint', @@ -236,7 +242,14 @@ # AutoImport 'Export-PodeModule', 'Export-PodeSnapin', - 'Export-PodeFunction' + 'Export-PodeFunction', + + # Events + 'Register-PodeEvent', + 'Unregister-PodeEvent', + 'Test-PodeEvent', + 'Get-PodeEvent', + 'Clear-PodeEvent' ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index 99697a939..c166f9cf2 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1251,7 +1251,7 @@ function Set-PodeAuthStatus # check if we have a failure url redirect if (![string]::IsNullOrWhiteSpace($Failure.Url)) { - if ($Success.UseOrigin) { + if ($Success.UseOrigin -and ($WebEvent.Method -ieq 'get')) { Set-PodeCookie -Name 'pode.redirecturl' -Value $WebEvent.Request.Url.PathAndQuery } @@ -1267,7 +1267,7 @@ function Set-PodeAuthStatus # if no statuscode, success, so check if we have a success url redirect (but only for auto-login routes) if ((!$NoSuccessRedirect -or $LoginRoute) -and ![string]::IsNullOrWhiteSpace($Success.Url)) { $url = $Success.Url - if ($Success.UseOrigin) { + if ($Success.UseOrigin -and ($WebEvent.Method -ieq 'get')) { $tmpUrl = Get-PodeCookieValue -Name 'pode.redirecturl' Remove-PodeCookie -Name 'pode.redirecturl' diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 4259aef75..3e80108d5 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -68,7 +68,7 @@ function New-PodeContext Add-Member -MemberType NoteProperty -Name RunspaceState -Value $null -PassThru | Add-Member -MemberType NoteProperty -Name Tokens -Value @{} -PassThru | Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name Lockable -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name Lockables -Value $null -PassThru | Add-Member -MemberType NoteProperty -Name Server -Value @{} -PassThru | Add-Member -MemberType NoteProperty -Name Metrics -Value @{} -PassThru | Add-Member -MemberType NoteProperty -Name Listeners -Value @() -PassThru @@ -323,11 +323,22 @@ function New-PodeContext } # session state - $ctx.Lockable = [hashtable]::Synchronized(@{}) + $ctx.Lockables = @{ + Global = [hashtable]::Synchronized(@{}) + Custom = @{} + } # setup runspaces $ctx.Runspaces = @() + # setup events + $ctx.Server.Events = @{ + Start = [ordered]@{} + Terminate = [ordered]@{} + Restart = [ordered]@{} + Browser = [ordered]@{} + } + # return the new context return $ctx } @@ -545,7 +556,7 @@ function New-PodeStateContext Add-Member -MemberType NoteProperty -Name Tokens -Value $Context.Tokens -PassThru | Add-Member -MemberType NoteProperty -Name Metrics -Value $Context.Metrics -PassThru | Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $Context.LogsToProcess -PassThru | - Add-Member -MemberType NoteProperty -Name Lockable -Value $Context.Lockable -PassThru | + Add-Member -MemberType NoteProperty -Name Lockables -Value $Context.Lockables -PassThru | Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru) } diff --git a/src/Private/Events.ps1 b/src/Private/Events.ps1 new file mode 100644 index 000000000..489087276 --- /dev/null +++ b/src/Private/Events.ps1 @@ -0,0 +1,37 @@ +function Invoke-PodeEvent +{ + param( + [Parameter(Mandatory=$true)] + [ValidateSet('Start', 'Terminate', 'Restart', 'Browser')] + [string] + $Type + ) + + # do nothing if no events + if ($PodeContext.Server.Events[$Type].Count -eq 0) { + return + } + + # invoke each event's scriptblock + foreach ($evt in $PodeContext.Server.Events[$Type].Values) { + if (($null -eq $evt) -or ($null -eq $evt.ScriptBlock)) { + continue + } + + try { + $_args = @($evt.Arguments) + if ($null -ne $evt.UsingVariables) { + $_vars = @() + foreach ($_var in $evt.UsingVariables) { + $_vars += ,$_var.Value + } + $_args = $_vars + $_args + } + + Invoke-PodeScriptBlock -ScriptBlock $evt.ScriptBlock -Arguments $_args -Scoped -Splat | Out-Null + } + catch { + $_ | Write-PodeErrorLog + } + } +} \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 522a059ff..a10f37122 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -750,7 +750,7 @@ function New-PodePSDrive $PodeContext.Server.Drives[$drive.Name] = $Path } - return "$($drive.Name):" + return "$($drive.Name):$([System.IO.Path]::DirectorySeparatorChar)" } function Add-PodePSDrives @@ -809,7 +809,7 @@ function Join-PodeServerRoot } # join the folder/file to the root path - return (Join-PodePaths @($Root, $Folder, $FilePath)) + return [System.IO.Path]::Combine($Root, $Folder, $FilePath) } function Remove-PodeEmptyItemsFromArray @@ -849,17 +849,6 @@ function Remove-PodeNullKeysFromHashtable } } -function Join-PodePaths -{ - param ( - [Parameter()] - [string[]] - $Paths - ) - - return [System.IO.Path]::Combine($Paths) -} - function Get-PodeFileExtension { param ( @@ -1657,7 +1646,7 @@ function Get-PodeModuleRootPath function Get-PodeModuleMiscPath { - return (Join-Path (Get-PodeModuleRootPath) 'Misc') + return [System.IO.Path]::Combine((Get-PodeModuleRootPath), 'Misc') } function Get-PodeUrl @@ -1831,7 +1820,7 @@ function Find-PodeFileForContentType ) # get all files at the path that start with the name - $files = @(Get-ChildItem -Path (Join-Path $Path "$($Name).*")) + $files = @(Get-ChildItem -Path ([System.IO.Path]::Combine($Path, "$($Name).*"))) # if there are no files, return if ($null -eq $files -or $files.Length -eq 0) { @@ -1951,7 +1940,7 @@ function Get-PodeRelativePath $RootPath = $PodeContext.Server.Root } - $Path = Join-Path $RootPath $Path + $Path = [System.IO.Path]::Combine($RootPath, $Path) } # if flagged, resolve the path @@ -1986,7 +1975,7 @@ function Get-PodeWildcardFiles # if the OriginalPath is a directory, add wildcard if (Test-PodePathIsDirectory -Path $Path) { - $Path = (Join-Path $Path $Wildcard) + $Path = [System.IO.Path]::Combine($Path, $Wildcard) } # if path has a *, assume wildcard diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index e7a9dfbbe..2d5646b98 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -42,7 +42,7 @@ function Get-PodeLoggingFileMethod # get the fileId if ($options.FileId -eq 0) { - $path = (Join-Path $options.Path "$($options.Name)_$($date)_*.log") + $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_*.log") $options.FileId = (@(Get-ChildItem -Path $path)).Length if ($options.FileId -eq 0) { $options.FileId = 1 @@ -51,7 +51,7 @@ function Get-PodeLoggingFileMethod $id = "$($options.FileId)".PadLeft(3, '0') if ($options.MaxSize -gt 0) { - $path = (Join-Path $options.Path "$($options.Name)_$($date)_$($id).log") + $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_$($id).log") if ((Get-Item -Path $path -Force).Length -ge $options.MaxSize) { $options.FileId++ $id = "$($options.FileId)".PadLeft(3, '0') @@ -59,7 +59,7 @@ function Get-PodeLoggingFileMethod } # get the file to write to - $path = (Join-Path $options.Path "$($options.Name)_$($date)_$($id).log") + $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_$($id).log") # write the item to the file $item.ToString() | Out-File -FilePath $path -Encoding utf8 -Append -Force diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 7484adb9d..394b756fe 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -102,7 +102,7 @@ function Start-PodeWebServer Auth = @{} Response = $Response Request = $Request - Lockable = $PodeContext.Lockable + Lockable = $PodeContext.Lockables.Global Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath) Method = $Request.HttpMethod.ToLowerInvariant() Query = $null diff --git a/src/Private/Routes.ps1 b/src/Private/Routes.ps1 index d70576a64..53f1bc30a 100644 --- a/src/Private/Routes.ps1 +++ b/src/Private/Routes.ps1 @@ -109,7 +109,7 @@ function Find-PodePublicRoute # use the public static directory (but only if path is a file, and a public dir is present) if (Test-PodePathIsFile $Path) { - $source = (Join-Path $publicPath $Path) + $source = [System.IO.Path]::Combine($publicPath, $Path.TrimStart('/', '\')) if (!(Test-PodePath -Path $source -NoStatus)) { $source = $null } @@ -151,19 +151,19 @@ function Find-PodeStaticRoute if (!$found.Download -and !(Test-PodePathIsFile $file) -and (Get-PodeCount @($found.Defaults)) -gt 0) { if ((Get-PodeCount @($found.Defaults)) -eq 1) { - $file = Join-PodePaths @($file, @($found.Defaults)[0]) + $file = [System.IO.Path]::Combine($file, @($found.Defaults)[0]) } else { foreach ($def in $found.Defaults) { - if (Test-PodePath (Join-Path $found.Source $def) -NoStatus) { - $file = Join-PodePaths @($file, $def) + if (Test-PodePath ([System.IO.Path]::Combine($found.Source, $def)) -NoStatus) { + $file = [System.IO.Path]::Combine($file, $def) break } } } } - $source = (Join-Path $found.Source $file) + $source = [System.IO.Path]::Combine($found.Source, $file) } # check public, if flagged diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index ce7ef7a31..190e61f6c 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -122,7 +122,7 @@ function Invoke-PodeInternalScheduleLogic # setup event param $parameters = @{ Event = @{ - Lockable = $PodeContext.Lockable + Lockable = $PodeContext.Lockables.Global } } diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 9a3f5c4ba..9f5e8c032 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -38,6 +38,9 @@ function Start-PodeInternalServer New-PodeRunspacePools Open-PodeRunspacePools + # run start event hooks + Invoke-PodeEvent -Type Start + # create timer/schedules for auto-restarting New-PodeAutoRestartServer @@ -125,6 +128,9 @@ function Restart-PodeInternalServer # inform restart Write-PodeHost 'Restarting server...' -NoNewline -ForegroundColor Cyan + # run restart event hooks + Invoke-PodeEvent -Type Restart + # cancel the session token $PodeContext.Tokens.Cancellation.Cancel() @@ -143,6 +149,10 @@ function Restart-PodeInternalServer $PodeContext.Server.Handlers[$_].Clear() } + $PodeContext.Server.Events.Keys.Clone() | ForEach-Object { + $PodeContext.Server.Events[$_].Clear() + } + $PodeContext.Server.Views.Clear() $PodeContext.Timers.Clear() $PodeContext.Schedules.Clear() diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index 2e3213319..98f8f0f07 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -33,7 +33,7 @@ function Start-PodeAzFuncServer Auth = @{} Response = $response Request = $request - Lockable = $PodeContext.Lockable + Lockable = $PodeContext.Lockables.Global Path = [string]::Empty Method = $request.Method.ToLowerInvariant() Query = $request.Query @@ -167,7 +167,7 @@ function Start-PodeAwsLambdaServer Auth = @{} Response = $response Request = $request - Lockable = $PodeContext.Lockable + Lockable = $PodeContext.Lockables.Global Path = [System.Web.HttpUtility]::UrlDecode($request.path) Method = $request.httpMethod.ToLowerInvariant() Query = $request.queryStringParameters diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index 238233e75..55972f479 100644 --- a/src/Private/ServiceServer.ps1 +++ b/src/Private/ServiceServer.ps1 @@ -16,7 +16,7 @@ function Start-PodeServiceServer { # the event object $ServiceEvent = @{ - Lockable = $PodeContext.Lockable + Lockable = $PodeContext.Lockables.Global } # invoke the service handlers diff --git a/src/Private/Sessions.ps1 b/src/Private/Sessions.ps1 index bcfdd2df9..cadb7a21d 100644 --- a/src/Private/Sessions.ps1 +++ b/src/Private/Sessions.ps1 @@ -160,7 +160,7 @@ function Set-PodeSessionDataHash $Session.Data = @{} } - $Session.DataHash = (Invoke-PodeSHA256Hash -Value ($Session.Data | ConvertTo-Json -Depth 10 -Compress)) + $Session.DataHash = (Invoke-PodeSHA256Hash -Value ($Session.Data.Clone() | ConvertTo-Json -Depth 10 -Compress)) } function Test-PodeSessionDataHash diff --git a/src/Private/Setup.ps1 b/src/Private/Setup.ps1 index f92888272..743711c4d 100644 --- a/src/Private/Setup.ps1 +++ b/src/Private/Setup.ps1 @@ -48,10 +48,10 @@ function Install-PodeLocalModules Write-Host "=> Downloading $($_name)@$($_version) from $($_repository)... " -NoNewline -ForegroundColor Cyan # if the current version exists, do nothing - if (!(Test-Path (Join-Path $psModules "$($_name)/$($_version)"))) { + if (!(Test-Path ([System.IO.Path]::Combine($psModules, "$($_name)/$($_version)")))) { # remove other versions - if (Test-Path (Join-Path $psModules "$($_name)")) { - Remove-Item -Path (Join-Path $psModules "$($_name)") -Force -Recurse | Out-Null + if (Test-Path ([System.IO.Path]::Combine($psModules, "$($_name)"))) { + Remove-Item -Path ([System.IO.Path]::Combine($psModules, "$($_name)")) -Force -Recurse | Out-Null } # download the module diff --git a/src/Private/SignalServer.ps1 b/src/Private/SignalServer.ps1 index 7d6a2a8a6..6453dd976 100644 --- a/src/Private/SignalServer.ps1 +++ b/src/Private/SignalServer.ps1 @@ -141,7 +141,7 @@ function Start-PodeSignalServer $SignalEvent = @{ Response = $Response Request = $Request - Lockable = $PodeContext.Lockable + Lockable = $PodeContext.Lockables.Global Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath) Data = @{ Path = [System.Web.HttpUtility]::UrlDecode($payload.path) diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index b5e96e062..239fe2d6f 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -62,57 +62,71 @@ function Start-PodeSmtpServer try { - $Request = $context.Request - $Response = $context.Response - - $SmtpEvent = @{ - Response = $Response - Request = $Request - Lockable = $PodeContext.Lockable - Email = @{ - From = $Request.From - To = $Request.To - Data = $Request.RawBody - Headers = $Request.Headers - Subject = $Request.Subject - IsUrgent = $Request.IsUrgent - ContentType = $Request.ContentType - ContentEncoding = $Request.ContentEncoding - Body = $Request.Body + try + { + $Request = $context.Request + $Response = $context.Response + + $SmtpEvent = @{ + Response = $Response + Request = $Request + Lockable = $PodeContext.Lockables.Global + Email = @{ + From = $Request.From + To = $Request.To + Data = $Request.RawBody + Headers = $Request.Headers + Subject = $Request.Subject + IsUrgent = $Request.IsUrgent + ContentType = $Request.ContentType + ContentEncoding = $Request.ContentEncoding + Attachments = $Request.Attachments + Body = $Request.Body + } } - } - # convert the ip - $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint) + # stop now if the request has an error + if ($Request.IsAborted) { + throw $Request.Error + } - # ensure the request ip is allowed - if (!(Test-PodeIPAccess -IP $ip)) { - $Response.WriteLine('554 Your IP address was rejected', $true) - } + # convert the ip + $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint) - # has the ip hit the rate limit? - elseif (!(Test-PodeIPLimit -IP $ip)) { - $Response.WriteLine('554 Your IP address has hit the rate limit', $true) - } + # ensure the request ip is allowed + if (!(Test-PodeIPAccess -IP $ip)) { + $Response.WriteLine('554 Your IP address was rejected', $true) + } - # deal with smtp call - else { - $handlers = Get-PodeHandler -Type Smtp - foreach ($name in $handlers.Keys) { - $handler = $handlers[$name] - - $_args = @($handler.Arguments) - if ($null -ne $handler.UsingVariables) { - $_vars = @() - foreach ($_var in $handler.UsingVariables) { - $_vars += ,$_var.Value + # has the ip hit the rate limit? + elseif (!(Test-PodeIPLimit -IP $ip)) { + $Response.WriteLine('554 Your IP address has hit the rate limit', $true) + } + + # deal with smtp call + else { + $handlers = Get-PodeHandler -Type Smtp + foreach ($name in $handlers.Keys) { + $handler = $handlers[$name] + + $_args = @($handler.Arguments) + if ($null -ne $handler.UsingVariables) { + $_vars = @() + foreach ($_var in $handler.UsingVariables) { + $_vars += ,$_var.Value + } + $_args = $_vars + $_args } - $_args = $_vars + $_args - } - Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $_args -Scoped -Splat + } } } + catch [System.OperationCanceledException] {} + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + } } finally { Close-PodeDisposable -Disposable $context diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 2b6a3188e..c51ac4c33 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -61,7 +61,7 @@ function Start-PodeTcpServer if ((Test-PodeIPAccess -IP $ip) -and (Test-PodeIPLimit -IP $ip)) { $TcpEvent = @{ Client = $client - Lockable = $PodeContext.Lockable + Lockable = $PodeContext.Lockables.Global } # invoke the tcp handlers diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index 61b33e392..4070e75b9 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -60,7 +60,7 @@ function Invoke-PodeInternalTimer ) try { - $global:TimerEvent = @{ Lockable = $PodeContext.Lockable } + $global:TimerEvent = @{ Lockable = $PodeContext.Lockables.Global } $_args = @($Timer.Arguments) if ($null -ne $Timer.UsingVariables) { diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 60dae7300..514694633 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -182,6 +182,7 @@ function Start-PodeServer # check for open browser if (Test-PodeOpenBrowserPressed -Key $key) { + Invoke-PodeEvent -Type Browser Start-Process (Get-PodeEndpointUrl) } } @@ -191,6 +192,7 @@ function Start-PodeServer } Write-PodeHost 'Terminating...' -NoNewline -ForegroundColor Yellow + Invoke-PodeEvent -Type Terminate $PodeContext.Tokens.Cancellation.Cancel() } catch { @@ -224,6 +226,24 @@ function Close-PodeServer $PodeContext.Tokens.Cancellation.Cancel() } +<# +.SYNOPSIS +Restarts the Pode server. + +.DESCRIPTION +Restarts the Pode server. + +.EXAMPLE +Restart-PodeServer +#> +function Restart-PodeServer +{ + [CmdletBinding()] + param() + + $PodeContext.Tokens.Restart.Cancel() +} + <# .SYNOPSIS Helper wrapper function to start a Pode web server for a static website at the current directory. @@ -1917,13 +1937,13 @@ function Get-PodeScheduleNextTrigger Adds a new Middleware to be invoked before every Route, or certain Routes. .DESCRIPTION -Adds a new Middleware to be invoked before every Route, or certain Routes. +Adds a new Middleware to be invoked before every Route, or certain Routes. ScriptBlock should return $true to continue execution, or $false to stop. .PARAMETER Name The Name of the Middleware. .PARAMETER ScriptBlock -The Script defining the logic of the Middleware. +The Script defining the logic of the Middleware. Should return $true to continue execution, or $false to stop. .PARAMETER InputObject A Middleware HashTable from New-PodeMiddleware, or from certain other functions that return Middleware as a HashTable. @@ -1934,6 +1954,9 @@ A Route path for which Routes this Middleware should only be invoked against. .PARAMETER ArgumentList An array of arguments to supply to the Middleware's ScriptBlock. +.OUTPUTS +Boolean. ScriptBlock should return $true to continue to the next middleware/route, or return $false to stop execution. + .EXAMPLE Add-PodeMiddleware -Name 'BlockAgents' -ScriptBlock { /* logic */ } @@ -2001,10 +2024,10 @@ function Add-PodeMiddleware Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes. .DESCRIPTION -Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes. +Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes. ScriptBlock should return $true to continue execution, or $false to stop. .PARAMETER ScriptBlock -The Script that defines the logic of the Middleware. +The Script that defines the logic of the Middleware. Should return $true to continue execution, or $false to stop. .PARAMETER Route A Route path for which Routes this Middleware should only be invoked against. @@ -2012,6 +2035,9 @@ A Route path for which Routes this Middleware should only be invoked against. .PARAMETER ArgumentList An array of arguments to supply to the Middleware's ScriptBlock. +.OUTPUTS +Boolean. ScriptBlock should return $true to continue to the next middleware/route, or return $false to stop execution. + .EXAMPLE New-PodeMiddleware -ScriptBlock { /* logic */ } -ArgumentList 'Email' | Add-PodeMiddleware -Name 'CheckEmail' #> diff --git a/src/Public/Events.ps1 b/src/Public/Events.ps1 new file mode 100644 index 000000000..668297475 --- /dev/null +++ b/src/Public/Events.ps1 @@ -0,0 +1,191 @@ +<# +.SYNOPSIS +Registers a script to be run when a certain server event occurs within Pode + +.DESCRIPTION +Registers a script to be run when a certain server event occurs within Pode, such as Start, Terminate, and Restart. + +.PARAMETER Type +The Type of event to be registered. + +.PARAMETER Name +A unique Name for the registered event. + +.PARAMETER ScriptBlock +A ScriptBlock to invoke when the event is triggered. + +.PARAMETER ArgumentList +An array of arguments to supply to the ScriptBlock. + +.EXAMPLE +Register-PodeEvent -Type Start -Name 'Event1' -ScriptBlock { } +#> +function Register-PodeEvent +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateSet('Start', 'Terminate', 'Restart', 'Browser')] + [string] + $Type, + + [Parameter(Mandatory=$true)] + [string] + $Name, + + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [object[]] + $ArgumentList + ) + + # error if already registered + if (Test-PodeEvent -Type $Type -Name $Name) { + throw "$($Type) event already registered: $($Name)" + } + + # check if the scriptblock has any using vars + $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + + # add event + $PodeContext.Server.Events[$Type][$Name] = @{ + Name = $Name + ScriptBlock = $ScriptBlock + UsingVariables = $usingVars + Arguments = $ArgumentList + } +} + +<# +.SYNOPSIS +Unregisters an event that has been registered with the specified Name. + +.DESCRIPTION +Unregisters an event that has been registered with the specified Name. + +.PARAMETER Type +The Type of the event to unregister. + +.PARAMETER Name +The Name of the event to unregister. + +.EXAMPLE +Unregister-PodeEvent -Type Start -Name 'Event1' +#> +function Unregister-PodeEvent +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateSet('Start', 'Terminate', 'Restart', 'Browser')] + [string] + $Type, + + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + # error if not registered + if (!(Test-PodeEvent -Type $Type -Name $Name)) { + throw "No $($Type) event registered: $($Name)" + } + + # remove event + $PodeContext.Server.Events[$Type].Remove($Name) | Out-Null +} + +<# +.SYNOPSIS +Tests if an event has been registered with the specified Name. + +.DESCRIPTION +Tests if an event has been registered with the specified Name. + +.PARAMETER Type +The Type of the event to test. + +.PARAMETER Name +The Name of the event to test. + +.EXAMPLE +Test-PodeEvent -Type Start -Name 'Event1' +#> +function Test-PodeEvent +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateSet('Start', 'Terminate', 'Restart', 'Browser')] + [string] + $Type, + + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + return $PodeContext.Server.Events[$Type].Contains($Name) +} + +<# +.SYNOPSIS +Retrieves an event. + +.DESCRIPTION +Retrieves an event. + +.PARAMETER Type +The Type of event to retrieve. + +.PARAMETER Name +The Name of the event to retrieve. + +.EXAMPLE +Get-PodeEvent -Type Start -Name 'Event1' +#> +function Get-PodeEvent +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateSet('Start', 'Terminate', 'Restart', 'Browser')] + [string] + $Type, + + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + return $PodeContext.Server.Events[$Type][$Name] +} + +<# +.SYNOPSIS +Clears an event of all registered scripts. + +.DESCRIPTION +Clears an event of all registered scripts. + +.PARAMETER Type +The Type of event to clear. + +.EXAMPLE +Clear-PodeEvent -Type Start +#> +function Clear-PodeEvent +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateSet('Start', 'Terminate', 'Restart', 'Browser')] + [string] + $Type + ) + + $PodeContext.Server.Events[$Type].Clear() | Out-Null +} \ No newline at end of file diff --git a/src/Public/Middleware.ps1 b/src/Public/Middleware.ps1 index 71dc0fb83..35a451f5f 100644 --- a/src/Public/Middleware.ps1 +++ b/src/Public/Middleware.ps1 @@ -135,10 +135,11 @@ function Add-PodeLimitRule Enables Middleware for creating, retrieving and using Sessions within Pode. .DESCRIPTION -Enables Middleware for creating, retrieving and using Sessions within Pode. With support for defining Session duration, and custom Storage. +Enables Middleware for creating, retrieving and using Sessions within Pode; with support for defining Session duration, and custom Storage. +If you're storing sessions outside of Pode, you must supply a Secret value so sessions aren't corrupted. .PARAMETER Secret -A secret to use when signing Sessions. +An optional Secret to use when signing Sessions (Default: random GUID). .PARAMETER Name The name of the cookie/header used for the Session. @@ -150,7 +151,7 @@ The duration a Session should last for, before being expired. A custom ScriptBlock to generate a random unique SessionId. The value returned must be a String. .PARAMETER Storage -A custom PSObject that defines methods for Delete, Get, and Set. This allow you to store Sessions in custom Storage such as Redis. +A custom PSObject that defines methods for Delete, Get, and Set. This allow you to store Sessions in custom Storage such as Redis. A Secret is required. .PARAMETER Extend If supplied, the Sessions will have their durations extended on each successful Request. @@ -162,16 +163,16 @@ If supplied, the Session cookie will only be accessible to browsers. If supplied, the Session cookie will only be accessible over HTTPS Requests. .PARAMETER Strict -If supplied, the supplie Secret will be extended using the client request's UserAgent and RemoteIPAddress. +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. .PARAMETER UseHeaders If supplied, Sessions will be sent back in a header on the Response with the Name supplied. .EXAMPLE -Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 +Enable-PodeSessionMiddleware -Duration 120 .EXAMPLE -Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend -Generator { return [System.IO.Path]::GetRandomFileName() } +Enable-PodeSessionMiddleware -Duration 120 -Extend -Generator { return [System.IO.Path]::GetRandomFileName() } .EXAMPLE Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -UseHeaders -Strict @@ -180,7 +181,7 @@ function Enable-PodeSessionMiddleware { [CmdletBinding(DefaultParameterSetName='Cookies')] param ( - [Parameter(Mandatory=$true)] + [Parameter()] [string] $Secret, @@ -242,6 +243,15 @@ function Enable-PodeSessionMiddleware } } + # verify the secret, set to guid if not supplied, or error if none and we have a storage + if ([string]::IsNullOrEmpty($Secret)) { + if (!(Test-PodeIsEmpty $Storage)) { + throw "A Secret is required when using custom session storage" + } + + $Secret = New-PodeGuid -Secure + } + # if no custom storage, use the inmem one if (Test-PodeIsEmpty $Storage) { $Storage = (Get-PodeSessionInMemStore) diff --git a/src/Public/OpenApi.ps1 b/src/Public/OpenApi.ps1 index 4f74667f0..956400f1f 100644 --- a/src/Public/OpenApi.ps1 +++ b/src/Public/OpenApi.ps1 @@ -1522,7 +1522,7 @@ function Enable-PodeOpenApiViewer Add-PodeRoute -Method Get -Path $Path -Middleware $Middleware -ArgumentList $meta -ScriptBlock { param($meta) $podeRoot = Get-PodeModuleMiscPath - Write-PodeFileResponse -Path (Join-Path $podeRoot "default-$($meta.Type).html.pode") -Data @{ + Write-PodeFileResponse -Path ([System.IO.Path]::Combine($podeRoot, "default-$($meta.Type).html.pode")) -Data @{ Title = $meta.Title OpenApi = $meta.OpenApi DarkMode = $meta.DarkMode diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index 6883a66ff..478d3fa7b 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -868,7 +868,7 @@ function Write-PodeViewResponse $viewFolder = $PodeContext.Server.Views[$Folder] } - $Path = (Join-Path $viewFolder $Path) + $Path = [System.IO.Path]::Combine($viewFolder, $Path) # test the file path, and set status accordingly if (!(Test-PodePath $Path)) { @@ -1227,7 +1227,7 @@ function Save-PodeRequestFile # if the path is a directory, add the filename if (Test-PodePathIsDirectory -Path $Path) { - $Path = Join-Path $Path $fileName + $Path = [System.IO.Path]::Combine($Path, $fileName) } # save the file @@ -1352,7 +1352,7 @@ function Use-PodePartialView $viewFolder = $PodeContext.Server.Views[$Folder] } - $Path = (Join-Path $viewFolder $Path) + $Path = [System.IO.Path]::Combine($viewFolder, $Path) # test the file path, and set status accordingly if (!(Test-PodePath $Path -NoStatus)) { diff --git a/src/Public/State.ps1 b/src/Public/State.ps1 index 368a1de6e..0abf95e15 100644 --- a/src/Public/State.ps1 +++ b/src/Public/State.ps1 @@ -94,6 +94,68 @@ function Get-PodeState } } +<# +.SYNOPSIS +Returns the current names of state variables. + +.DESCRIPTION +Returns the current names of state variables that have been set. You can filter the result using Scope or a Pattern. + +.PARAMETER Pattern +An optional regex Pattern to filter the state names. + +.PARAMETER Scope +An optional Scope to filter the state names. + +.EXAMPLE +$names = Get-PodeStateNames -Scope '' + +.EXAMPLE +$names = Get-PodeStateNames -Pattern '^\w+[0-9]{0,2}$' +#> +function Get-PodeStateNames +{ + [CmdletBinding()] + param( + [Parameter()] + [string] + $Pattern, + + [Parameter()] + [string[]] + $Scope + ) + + if ($null -eq $PodeContext.Server.State) { + throw "Pode has not been initialised" + } + + if ($null -eq $Scope) { + $Scope = @() + } + + $tempState = $PodeContext.Server.State.Clone() + $keys = $tempState.Keys + + if ($Scope.Length -gt 0) { + $keys = @(foreach($key in $keys) { + if ($tempState[$key].Scope -iin $Scope) { + $key + } + }) + } + + if (![string]::IsNullOrWhiteSpace($Pattern)) { + $keys = @(foreach($key in $keys) { + if ($key -imatch $Pattern) { + $key + } + }) + } + + return $keys +} + <# .SYNOPSIS Removes some state object from the shared state. diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 8d38aa2e0..a149f741a 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -133,6 +133,9 @@ The ScriptBlock to invoke. .PARAMETER Return If supplied, any values from the ScriptBlock will be returned. +.PARAMETER CheckGlobal +If supplied, will check the global Lockable object and wait until it's freed-up before locking the passed object. + .EXAMPLE Lock-PodeObject -Object $SomeArray -ScriptBlock { /* logic */ } @@ -153,7 +156,10 @@ function Lock-PodeObject $ScriptBlock, [switch] - $Return + $Return, + + [switch] + $CheckGlobal ) if ($null -eq $Object) { @@ -167,6 +173,10 @@ function Lock-PodeObject $locked = $false try { + if ($CheckGlobal) { + Lock-PodeObject -Object $PodeContext.Lockables.Global -ScriptBlock {} + } + [System.Threading.Monitor]::Enter($Object.SyncRoot) $locked = $true @@ -444,9 +454,9 @@ function Import-PodeModule # get the path of a module, or import modules on mass switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { 'name' { - $modulePath = Join-PodeServerRoot -Folder (Join-PodePaths @('ps_modules', $Name)) -Root $rootPath + $modulePath = Join-PodeServerRoot -Folder ([System.IO.Path]::Combine('ps_modules', $Name)) -Root $rootPath if (Test-PodePath -Path $modulePath -NoStatus) { - $Path = (Get-ChildItem (Join-PodePaths @($modulePath, '*', "$($Name).ps*1")) -Recurse -Force | Select-Object -First 1).FullName + $Path = (Get-ChildItem ([System.IO.Path]::Combine($modulePath, '*', "$($Name).ps*1")) -Recurse -Force | Select-Object -First 1).FullName } else { $Path = (Get-Module -Name $Name -ListAvailable | Select-Object -First 1).Path @@ -889,4 +899,110 @@ function Test-PodeIsHeroku param() return $PodeContext.Server.IsHeroku +} + +<# +.SYNOPSIS +Creates a new custom lockable object for use with Lock-PodeObject. + +.DESCRIPTION +Creates a new custom lockable object for use with Lock-PodeObject. + +.PARAMETER Name +The Name of the lockable object. + +.EXAMPLE +New-PodeLockable -Name 'Lock1' +#> +function New-PodeLockable +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + if (Test-PodeLockable -Name $Name) { + return + } + + $PodeContext.Lockables.Custom[$Name] = [hashtable]::Synchronized(@{}) +} + +<# +.SYNOPSIS +Removes a custom lockable object. + +.DESCRIPTION +Removes a custom lockable object. + +.PARAMETER Name +The Name of the lockable object to remove. + +.EXAMPLE +Remove-PodeLockable -Name 'Lock1' +#> +function Remove-PodeLockable +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + if (Test-PodeLockable -Name $Name) { + $PodeContext.Lockables.Custom.Remove($Name) + } +} + +<# +.SYNOPSIS +Get a custom lockable object for use with Lock-PodeObject. + +.DESCRIPTION +Get a custom lockable object for use with Lock-PodeObject. + +.PARAMETER Name +The Name of the lockable object. + +.EXAMPLE +Get-PodeLockable -Name 'Lock1' | Lock-PodeObject -ScriptBlock {} +#> +function Get-PodeLockable +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + return $PodeContext.Lockables.Custom[$Name] +} + +<# +.SYNOPSIS +Test if a custom lockable object exists. + +.DESCRIPTION +Test if a custom lockable object exists. + +.PARAMETER Name +The Name of the lockable object. + +.EXAMPLE +Test-PodeLockable -Name 'Lock1' +#> +function Test-PodeLockable +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + return $PodeContext.Lockables.Custom.ContainsKey($Name) } \ No newline at end of file diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index 151e8efc2..b53aa7821 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -757,25 +757,6 @@ Describe 'Remove-PodeEmptyItemsFromArray' { } } -Describe 'Join-PodePaths' { - It 'Returns valid for 0 items' { - Join-PodePaths @() | Should Be ([string]::Empty) - } - - It 'Returns valid for 1 item' { - Join-PodePaths @('this') | Should Be 'this' - } - - It 'Returns valid for 2 items' { - Join-PodePaths @('this', 'is') | Should Be (Join-Path 'this' 'is') - } - - It 'Returns valid for 2+ items' { - $result = (Join-Path (Join-Path (Join-Path 'this' 'is') 'a') 'path') - Join-PodePaths @('this', 'is', 'a', 'path') | Should Be $result - } -} - Describe 'Get-PodeEndpointInfo' { It 'Returns null for no endpoint' { Get-PodeEndpointInfo -Address ([string]::Empty) | Should Be $null @@ -1093,21 +1074,18 @@ Describe 'Get-PodeRelativePath' { It 'Returns path for a relative path joined to default root' { Mock Test-PodePathIsRelative { return $true } - Mock Join-Path { return 'c:/path' } - Get-PodeRelativePath -Path './path' -JoinRoot | Should Be 'c:/path' + Get-PodeRelativePath -Path './path' -JoinRoot | Should Be 'c:/./path' } It 'Returns resolved path for a relative path joined to default root when resolving' { Mock Test-PodePathIsRelative { return $true } - Mock Join-Path { return 'c:/path' } Mock Resolve-Path { return @{ 'Path' = 'c:/path' } } Get-PodeRelativePath -Path './path' -JoinRoot -Resolve | Should Be 'c:/path' } It 'Returns path for a relative path joined to passed root' { Mock Test-PodePathIsRelative { return $true } - Mock Join-Path { return 'e:/path' } - Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should Be 'e:/path' + Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should Be 'e:/./path' } It 'Throws error for path ot existing' { diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index db47305bc..7f0abbd12 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -25,6 +25,7 @@ Describe 'Start-PodeInternalServer' { Mock Import-PodeModulesIntoRunspaceState { } Mock Import-PodeSnapinsIntoRunspaceState { } Mock Import-PodeFunctionsIntoRunspaceState { } + Mock Invoke-PodeEvent { } It 'Calls one-off script logic' { $PodeContext.Server = @{ Types = ([string]::Empty); Logic = {} } @@ -91,6 +92,7 @@ Describe 'Restart-PodeInternalServer' { Mock Start-PodeInternalServer { } Mock Write-PodeErrorLog { } Mock Close-PodeDisposable { } + Mock Invoke-PodeEvent { } It 'Resetting the server values' { $PodeContext = @{ @@ -99,7 +101,7 @@ Describe 'Restart-PodeInternalServer' { Restart = New-Object System.Threading.CancellationTokenSource; }; Server = @{ - Routes =@{ + Routes = @{ GET = @{ 'key' = 'value' }; POST = @{ 'key' = 'value' }; }; @@ -145,6 +147,9 @@ Describe 'Restart-PodeInternalServer' { Functions = @{ Exported = @() } } Views = @{ 'key' = 'value' }; + Events = @{ + Start = @{} + } }; Metrics = @{ Server = @{