Downloads:
16,330
Downloads of v 2.10.1:
95
Last Update:
27 May 2024
Package Maintainer(s):
Software Author(s):
- Badgerati
Tags:
powershell web server rest api smtp unix cross-platform file-monitoring multithreaded schedule middleware authentication aws azure websockets openapi- Software Specific:
- Software Site
- Software License
- Software Docs
- Software Issues
- Package Specific:
- Package Source
- Package outdated?
- Package broken?
- Contact Maintainers
- Contact Site Admins
- Software Vendor?
- Report Abuse
- Download
Pode
- 1
- 2
- 3
2.10.1 | Updated: 27 May 2024
- Software Specific:
- Software Site
- Software License
- Software Docs
- Software Issues
- Package Specific:
- Package Source
- Package outdated?
- Package broken?
- Contact Maintainers
- Contact Site Admins
- Software Vendor?
- Report Abuse
- Download
Downloads:
16,330
Downloads of v 2.10.1:
95
Maintainer(s):
Software Author(s):
- Badgerati
Pode 2.10.1
- 1
- 2
- 3
All Checks are Passing
3 Passing Tests
Deployment Method: Individual Install, Upgrade, & Uninstall
To install Pode, run the following command from the command line or from PowerShell:
To upgrade Pode, run the following command from the command line or from PowerShell:
To uninstall Pode, run the following command from the command line or from PowerShell:
Deployment Method:
This applies to both open source and commercial editions of Chocolatey.
1. Enter Your Internal Repository Url
(this should look similar to https://community.chocolatey.org/api/v2/)
2. Setup Your Environment
1. Ensure you are set for organizational deployment
Please see the organizational deployment guide
2. Get the package into your environment
Option 1: Cached Package (Unreliable, Requires Internet - Same As Community)-
Open Source or Commercial:
- Proxy Repository - Create a proxy nuget repository on Nexus, Artifactory Pro, or a proxy Chocolatey repository on ProGet. Point your upstream to https://community.chocolatey.org/api/v2/. Packages cache on first access automatically. Make sure your choco clients are using your proxy repository as a source and NOT the default community repository. See source command for more information.
- You can also just download the package and push it to a repository Download
-
Open Source
-
Download the package:
Download - Follow manual internalization instructions
-
-
Package Internalizer (C4B)
-
Run: (additional options)
choco download pode --internalize --source=https://community.chocolatey.org/api/v2/
-
For package and dependencies run:
choco push --source="'INTERNAL REPO URL'"
- Automate package internalization
-
Run: (additional options)
3. Copy Your Script
choco upgrade pode -y --source="'INTERNAL REPO URL'" [other options]
See options you can pass to upgrade.
See best practices for scripting.
Add this to a PowerShell script or use a Batch script with tools and in places where you are calling directly to Chocolatey. If you are integrating, keep in mind enhanced exit codes.
If you do use a PowerShell script, use the following to ensure bad exit codes are shown as failures:
choco upgrade pode -y --source="'INTERNAL REPO URL'"
$exitCode = $LASTEXITCODE
Write-Verbose "Exit code was $exitCode"
$validExitCodes = @(0, 1605, 1614, 1641, 3010)
if ($validExitCodes -contains $exitCode) {
Exit 0
}
Exit $exitCode
- name: Install pode
win_chocolatey:
name: pode
version: '2.10.1'
source: INTERNAL REPO URL
state: present
See docs at https://docs.ansible.com/ansible/latest/modules/win_chocolatey_module.html.
chocolatey_package 'pode' do
action :install
source 'INTERNAL REPO URL'
version '2.10.1'
end
See docs at https://docs.chef.io/resource_chocolatey_package.html.
cChocoPackageInstaller pode
{
Name = "pode"
Version = "2.10.1"
Source = "INTERNAL REPO URL"
}
Requires cChoco DSC Resource. See docs at https://github.com/chocolatey/cChoco.
package { 'pode':
ensure => '2.10.1',
provider => 'chocolatey',
source => 'INTERNAL REPO URL',
}
Requires Puppet Chocolatey Provider module. See docs at https://forge.puppet.com/puppetlabs/chocolatey.
4. If applicable - Chocolatey configuration/installation
See infrastructure management matrix for Chocolatey configuration elements and examples.
This package was approved as a trusted package on 27 Jun 2024.
Pode is a Cross-Platform framework for creating web servers to host REST APIs and Websites. Pode also has support for being used in Azure Functions and AWS Lambda.
Features
- Cross-platform using PowerShell Core (with support for PS5)
- Docker support, including images for ARM/Raspberry Pi
- Azure Functions, AWS Lambda, and IIS support
- OpenAPI, Swagger, and ReDoc support
- Listen on a single or multiple IP address/hostnames
- Cross-platform support for HTTP, HTTPS, TCP and SMTP
- Cross-platform support for WebSockets, including secure WebSockets
- Host REST APIs, Web Pages, and Static Content (with caching)
- Support for custom error pages
- Request and Response compression using GZip/Deflate
- Multi-thread support for incoming requests
- Inbuilt template engine, with support for third-parties
- Async timers for short-running repeatable processes
- Async scheduled tasks using cron expressions for short/long-running processes
- Supports logging to CLI, Files, and custom logic for other services like LogStash
- Cross-state variable access across multiple runspaces
- Restart the server via file monitoring, or defined periods/times
- Ability to allow/deny requests from certain IP addresses and subnets
- Basic rate limiting for IP addresses and subnets
- Middleware and Sessions on web servers, with Flash message and CSRF support
- Authentication on requests, such as Basic, Windows and Azure AD
- Support for dynamically building Routes from Functions and Modules
- Generate/bind self-signed certificates
- Secret management support to load secrets from vaults
- Support for File Watchers
- (Windows) Open the hosted server as a desktop application
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v6.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v6.0": {
"Pode/2.10.1": {
"runtime": {
"Pode.dll": {}
}
}
}
},
"libraries": {
"Pode/2.10.1": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}
md5: 77E4DC0CEDD10A75F28FD6895987A203 | sha1: FE419F6E8357666BB26D893B1430CA00BF369BD4 | sha256: 906EBF752DF09438AAF79894E3F6402E0CA09E2C1CC2BAF12662CA2510C9EEF8 | sha512: 98591AB6771601DD7C8179E1A497645DD69A75A079A75E735F3E63B20D6D4398CECAE84845A5A22BC530D7BCAD0046D5705276A7B1900CDF9BB2AD5B41EA3C13
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"Pode/2.10.1": {
"runtime": {
"Pode.dll": {}
}
}
}
},
"libraries": {
"Pode/2.10.1": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}
md5: 0A3627CC6B534F61A0D84B04A0BA4A3E | sha1: 404F0539C15F12A7D916D00DC792F098AED690C9 | sha256: A3A8A13B21F310DE46E83171AD23F1636FBBCFEF87DDF9813A00FA9E7D0DE363 | sha512: 57292FC93000F76356B3DB3F7A9F6F9472C3EFD895DD596BC9E3EFF55D2427BCB5FF99F3D0E43B53A9F5B9CA859CCA4B956BF72314483D0D59AFBF1C8F6E8978
{
"runtimeTarget": {
"name": ".NETStandard,Version=v2.0/",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETStandard,Version=v2.0": {},
".NETStandard,Version=v2.0/": {
"Pode/2.10.1": {
"dependencies": {
"NETStandard.Library": "2.0.3"
},
"runtime": {
"Pode.dll": {}
}
},
"Microsoft.NETCore.Platforms/1.1.0": {},
"NETStandard.Library/2.0.3": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
}
}
}
},
"libraries": {
"Pode/2.10.1": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Microsoft.NETCore.Platforms/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
"path": "microsoft.netcore.platforms/1.1.0",
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
},
"NETStandard.Library/2.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
"path": "netstandard.library/2.0.3",
"hashPath": "netstandard.library.2.0.3.nupkg.sha512"
}
}
}
md5: 2DD97B738FD9077E6158E16BDA0648D8 | sha1: 9C82331895539C09C035CB9986BBB0C78CC24842 | sha256: D204933FEECADD6C99EFD1EA30404D3E23A88B5B602B6F5451CAD802DC60822A | sha512: 4A4F682B307B4FD62C2278505549129BE2A4D4F4626E0815C5FB1AC124B9D1E641FC49E03579DF0795E161A9E3C07726E98A1ABE6C159D3C95682FCDB2E8AB82
The MIT License (MIT)
Copyright (c) [2017-2024] [Matthew Kelly (Badgerati)]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Project URL: https://github.com/twbs/bootstrap
The MIT License (MIT)
Copyright (c) 2011-2024 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Project URL: https://github.com/FortAwesome/Font-Awesome
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**
Project URL: https://github.com/highlightjs/highlight.js/
BSD 3-Clause License
Copyright (c) 2006, Ivan Sagalaev.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Project URL: https://github.com/Authress-Engineering/openapi-explorer
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Project URL: https://github.com/cloudbase/powershell-yaml
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016-2023 Cloudbase Solutions SRL
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Project URL: https://github.com/Phil-Factor/PSYaml
The MIT License (MIT)
Copyright (c) 2016 Jakku Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Project URL: https://github.com/rapi-doc/RapiDoc
MIT License
Copyright (c) 2022 Mrinmoy Majumdar
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Project URL: https://github.com/mrin9/RapiPdf
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Project URL: https://github.com/Redocly/redoc
The MIT License (MIT)
Copyright (c) 2015-present, Rebilly, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Project URL: https://github.com/PowerShell/SecretManagement
Copyright (c) Microsoft Corporation.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Project URL: https://github.com/stoplightio/elements
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2018 Stoplight, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Project URL: https://github.com/swagger-api/swagger-editor
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Project URL: https://github.com/swagger-api/swagger-ui
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#
# Internal module manifest for module 'Pode'
#
# Generated by: Matthew Kelly (Badgerati)
#
# Generated on: 24/01/2023
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'Pode.Internal.psm1'
# Version number of this module.
ModuleVersion = '2.10.1'
# ID used to uniquely identify this module
GUID = '86b48c1c-8b59-4f3c-80bb-936d6b3218f6'
# Author of this module
Author = 'Matthew Kelly (Badgerati)'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.1'
}
# root path
$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
# import everything
$sysfuncs = Get-ChildItem Function:
# load private functions
Get-ChildItem "$($root)/Private/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }
# load public functions
Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }
# get functions from memory and compare to existing to find new functions added
$funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }
# export the module's public functions
Export-ModuleMember -Function ($funcs.Name)
#
# Module manifest for module 'Pode'
#
# Generated by: Matthew Kelly (Badgerati)
#
# Generated on: 28/11/2017
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'Pode.psm1'
# Version number of this module.
ModuleVersion = '2.10.1'
# ID used to uniquely identify this module
GUID = 'e3ea217c-fc3d-406b-95d5-4304ab06c6af'
# Author of this module
Author = 'Matthew Kelly (Badgerati)'
# Copyright statement for this module
Copyright = 'Copyright (c) 2017-2024 Matthew Kelly (Badgerati), licensed under the MIT License.'
# Description of the functionality provided by this module
Description = 'A Cross-Platform PowerShell framework for creating web servers to host REST APIs and Websites. Pode also has support for being used in Azure Functions and AWS Lambda.'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.1'
# Functions to export from this Module
FunctionsToExport = @(
# cookies
'Get-PodeCookie',
'Get-PodeCookieSecret',
'Remove-PodeCookie',
'Set-PodeCookie',
'Set-PodeCookieSecret',
'Test-PodeCookie',
'Test-PodeCookieSigned',
'Update-PodeCookieExpiry',
'Get-PodeCookieValue',
# flash
'Add-PodeFlashMessage',
'Clear-PodeFlashMessages',
'Get-PodeFlashMessage',
'Get-PodeFlashMessageNames',
'Remove-PodeFlashMessage',
'Test-PodeFlashMessage',
# headers
'Add-PodeHeader',
'Add-PodeHeaderBulk',
'Test-PodeHeader',
'Get-PodeHeader',
'Set-PodeHeader',
'Set-PodeHeaderBulk',
'Test-PodeHeaderSigned',
# state
'Set-PodeState',
'Get-PodeState',
'Remove-PodeState',
'Save-PodeState',
'Restore-PodeState',
'Test-PodeState',
'Get-PodeStateNames',
# response helpers
'Set-PodeResponseAttachment',
'Write-PodeTextResponse',
'Write-PodeFileResponse',
'Write-PodeCsvResponse',
'Write-PodeHtmlResponse',
'Write-PodeMarkdownResponse',
'Write-PodeJsonResponse',
'Write-PodeYamlResponse',
'Write-PodeXmlResponse',
'Write-PodeViewResponse',
'Write-PodeDirectoryResponse',
'Set-PodeResponseStatus',
'Move-PodeResponseUrl',
'Write-PodeTcpClient',
'Read-PodeTcpClient',
'Close-PodeTcpClient',
'Save-PodeRequestFile',
'Test-PodeRequestFile',
'Set-PodeViewEngine',
'Use-PodePartialView',
'Send-PodeSignal',
'Add-PodeViewFolder',
'Send-PodeResponse',
# sse
'ConvertTo-PodeSseConnection',
'Send-PodeSseEvent',
'Close-PodeSseConnection',
'Test-PodeSseClientIdSigned',
'Test-PodeSseClientIdValid',
'New-PodeSseClientId',
'Enable-PodeSseSigning',
'Disable-PodeSseSigning',
'Set-PodeSseBroadcastLevel',
'Get-PodeSseBroadcastLevel',
'Test-PodeSseBroadcastLevel',
'Set-PodeSseDefaultScope',
'Get-PodeSseDefaultScope',
'Test-PodeSseName',
'Test-PodeSseClientId',
# utility helpers
'Close-PodeDisposable',
'Get-PodeServerPath',
'Start-PodeStopwatch',
'Use-PodeStream',
'Use-PodeScript',
'Get-PodeConfig',
'Add-PodeEndware',
'Use-PodeEndware',
'Import-PodeModule',
'Import-PodeSnapIn',
'Protect-PodeValue',
'Resolve-PodeValue',
'Invoke-PodeScriptBlock',
'Merge-PodeScriptblockArguments',
'Test-PodeIsUnix',
'Test-PodeIsWindows',
'Test-PodeIsMacOS',
'Test-PodeIsPSCore',
'Test-PodeIsEmpty',
'Out-PodeHost',
'Write-PodeHost',
'Test-PodeIsIIS',
'Test-PodeIsHeroku',
'Get-PodeIISApplicationPath',
'Out-PodeVariable',
'Test-PodeIsHosted',
'New-PodeCron',
'Test-PodeInRunspace',
'ConvertFrom-PodeXml',
'Set-PodeDefaultFolder',
'Get-PodeDefaultFolder',
# routes
'Add-PodeRoute',
'Add-PodeStaticRoute',
'Add-PodeSignalRoute',
'Remove-PodeRoute',
'Remove-PodeStaticRoute',
'Remove-PodeSignalRoute',
'Clear-PodeRoutes',
'Clear-PodeStaticRoutes',
'Clear-PodeSignalRoutes',
'ConvertTo-PodeRoute',
'Add-PodePage',
'Get-PodeRoute',
'Get-PodeStaticRoute',
'Get-PodeSignalRoute',
'Use-PodeRoutes',
'Add-PodeRouteGroup',
'Add-PodeStaticRouteGroup',
'Add-PodeSignalRouteGroup',
'Set-PodeRouteIfExistsPreference',
'Test-PodeRoute',
'Test-PodeStaticRoute',
'Test-PodeSignalRoute',
# handlers
'Add-PodeHandler',
'Remove-PodeHandler',
'Clear-PodeHandlers',
'Use-PodeHandlers',
# schedules
'Add-PodeSchedule',
'Remove-PodeSchedule',
'Clear-PodeSchedule',
'Invoke-PodeSchedule',
'Edit-PodeSchedule',
'Set-PodeScheduleConcurrency',
'Get-PodeSchedule',
'Get-PodeScheduleNextTrigger',
'Use-PodeSchedules',
'Test-PodeSchedule',
'Clear-PodeSchedules',
# timers
'Add-PodeTimer',
'Remove-PodeTimer',
'Clear-PodeTimers',
'Invoke-PodeTimer',
'Edit-PodeTimer',
'Get-PodeTimer',
'Use-PodeTimers',
'Test-PodeTimer',
# tasks
'Add-PodeTask',
'Set-PodeTaskConcurrency',
'Invoke-PodeTask',
'Remove-PodeTask',
'Clear-PodeTasks',
'Edit-PodeTask',
'Get-PodeTask',
'Use-PodeTasks',
'Close-PodeTask',
'Test-PodeTaskCompleted',
'Wait-PodeTask',
# middleware
'Add-PodeMiddleware',
'Remove-PodeMiddleware',
'Clear-PodeMiddleware',
'Add-PodeAccessRule',
'Add-PodeLimitRule',
'New-PodeCsrfToken',
'Get-PodeCsrfMiddleware',
'Initialize-PodeCsrf',
'Enable-PodeCsrfMiddleware',
'Use-PodeMiddleware',
'New-PodeMiddleware',
'Add-PodeBodyParser',
'Remove-PodeBodyParser',
# sessions
'Enable-PodeSessionMiddleware',
'Remove-PodeSession',
'Save-PodeSession',
'Get-PodeSessionId',
'Reset-PodeSessionExpiry',
'Get-PodeSessionDuration',
'Get-PodeSessionExpiry',
'Test-PodeSessionsEnabled',
'Get-PodeSessionTabId',
'Get-PodeSessionInfo',
'Test-PodeSessionScopeIsBrowser',
# auth
'New-PodeAuthScheme',
'New-PodeAuthAzureADScheme',
'New-PodeAuthTwitterScheme',
'Add-PodeAuth',
'Get-PodeAuth',
'Clear-PodeAuth',
'Add-PodeAuthWindowsAd',
'Add-PodeAuthWindowsLocal',
'Remove-PodeAuth',
'Add-PodeAuthMiddleware',
'Add-PodeAuthIIS',
'Add-PodeAuthUserFile',
'ConvertTo-PodeJwt',
'ConvertFrom-PodeJwt',
'Test-PodeJwt'
'Use-PodeAuth',
'ConvertFrom-PodeOIDCDiscovery',
'Test-PodeAuthUser',
'Merge-PodeAuth',
'Test-PodeAuth',
'Test-PodeAuthExists',
'Get-PodeAuthUser',
'Add-PodeAuthSession',
# access
'New-PodeAccessScheme',
'Add-PodeAccess',
'Add-PodeAccessCustom',
'Get-PodeAccess',
'Test-PodeAccessExists',
'Test-PodeAccess',
'Test-PodeAccessUser',
'Test-PodeAccessRoute',
'Merge-PodeAccess',
'Remove-PodeAccess',
'Clear-PodeAccess',
'Add-PodeAccessMiddleware',
'Use-PodeAccess',
# logging
'New-PodeLoggingMethod',
'Enable-PodeRequestLogging',
'Enable-PodeErrorLogging',
'Disable-PodeRequestLogging',
'Disable-PodeErrorLogging',
'Add-PodeLogger',
'Remove-PodeLogger',
'Clear-PodeLoggers',
'Write-PodeErrorLog',
'Write-PodeLog',
'Protect-PodeLogItem',
'Use-PodeLogging',
# core
'Start-PodeServer',
'Close-PodeServer',
'Restart-PodeServer',
'Start-PodeStaticServer',
'Show-PodeGui',
'Add-PodeEndpoint',
'Get-PodeEndpoint',
'Pode',
'Get-PodeServerDefaultSecret',
'Wait-PodeDebugger',
'Get-PodeVersion',
# openapi
'Enable-PodeOpenApi',
'Get-PodeOADefinition',
'Select-PodeOADefinition',
'Add-PodeOAResponse',
'Remove-PodeOAResponse',
'Set-PodeOARequest',
'New-PodeOARequestBody',
'Test-PodeOADefinitionTag',
'Test-PodeOADefinition',
# properties
'New-PodeOAIntProperty',
'New-PodeOANumberProperty',
'New-PodeOAStringProperty',
'New-PodeOABoolProperty',
'New-PodeOAObjectProperty',
'New-PodeOAMultiTypeProperty',
'Merge-PodeOAProperty',
'New-PodeOAComponentSchemaProperty',
'ConvertTo-PodeOAParameter',
'Set-PodeOARouteInfo',
'Enable-PodeOAViewer',
'Test-PodeOAJsonSchemaCompliance',
'Add-PodeOAInfo',
'Add-PodeOAExternalDoc',
'New-PodeOAExternalDoc',
'Add-PodeOATag',
'Add-PodeOAServerEndpoint',
'New-PodeOAExample',
'New-PodeOAEncodingObject',
'New-PodeOAResponse',
'Add-PodeOACallBack',
'New-PodeOAResponseLink',
'New-PodeOAContentMediaType',
'Add-PodeOAExternalRoute',
'New-PodeOAServerEndpoint',
'Test-PodeOAVersion',
# Components
'Add-PodeOAComponentResponse',
'Add-PodeOAComponentSchema',
'Add-PodeOAComponentRequestBody',
'Add-PodeOAComponentHeader',
'Add-PodeOAComponentExample',
'Add-PodeOAComponentParameter',
'Add-PodeOAComponentResponseLink',
'Add-PodeOAComponentCallBack',
'Add-PodeOAComponentPathItem',
'Add-PodeOAWebhook',
'Test-PodeOAComponent',
'Remove-PodeOAComponent',
# Metrics
'Get-PodeServerUptime',
'Get-PodeServerRestartCount',
'Get-PodeServerRequestMetric',
'Get-PodeServerSignalMetric',
'Get-PodeServerActiveRequestMetric',
'Get-PodeServerActiveSignalMetric',
# AutoImport
'Export-PodeModule',
'Export-PodeSnapin',
'Export-PodeFunction',
'Export-PodeSecretVault',
# Events
'Register-PodeEvent',
'Unregister-PodeEvent',
'Test-PodeEvent',
'Get-PodeEvent',
'Clear-PodeEvent',
'Use-PodeEvents',
# Security
'Add-PodeSecurityHeader',
'Add-PodeSecurityContentSecurityPolicy',
'Add-PodeSecurityPermissionsPolicy',
'Remove-PodeSecurity',
'Remove-PodeSecurityAccessControl',
'Remove-PodeSecurityContentSecurityPolicy',
'Remove-PodeSecurityContentTypeOptions',
'Remove-PodeSecurityCrossOrigin',
'Remove-PodeSecurityFrameOptions',
'Remove-PodeSecurityHeader',
'Remove-PodeSecurityPermissionsPolicy',
'Remove-PodeSecurityReferrerPolicy',
'Remove-PodeSecurityStrictTransportSecurity',
'Set-PodeSecurity',
'Set-PodeSecurityAccessControl',
'Set-PodeSecurityContentSecurityPolicy',
'Set-PodeSecurityContentTypeOptions',
'Set-PodeSecurityCrossOrigin',
'Set-PodeSecurityFrameOptions',
'Set-PodeSecurityPermissionsPolicy',
'Set-PodeSecurityReferrerPolicy',
'Set-PodeSecurityStrictTransportSecurity',
'Hide-PodeSecurityServer',
'Show-PodeSecurityServer',
# Verbs
'Add-PodeVerb',
'Remove-PodeVerb',
'Clear-PodeVerbs',
'Get-PodeVerb',
'Use-PodeVerbs',
# WebSockets
'Set-PodeWebSocketConcurrency',
'Connect-PodeWebSocket',
'Disconnect-PodeWebSocket',
'Remove-PodeWebSocket',
'Send-PodeWebSocket',
'Reset-PodeWebSocket',
'Test-PodeWebSocket'
# Secrets
'Register-PodeSecretVault',
'Unregister-PodeSecretVault',
'Unlock-PodeSecretVault',
'Get-PodeSecretVault',
'Test-PodeSecretVault',
'Mount-PodeSecret',
'Dismount-PodeSecret',
'Get-PodeSecret',
'Test-PodeSecret',
'Update-PodeSecret',
'Remove-PodeSecret',
'Read-PodeSecret',
'Set-PodeSecret',
# File Watchers
'Add-PodeFileWatcher',
'Test-PodeFileWatcher',
'Get-PodeFileWatcher',
'Remove-PodeFileWatcher',
'Clear-PodeFileWatchers',
'Use-PodeFileWatchers',
# Threading
'Lock-PodeObject',
'New-PodeLockable',
'Remove-PodeLockable',
'Get-PodeLockable',
'Test-PodeLockable',
'Enter-PodeLockable',
'Exit-PodeLockable',
'Clear-PodeLockables',
'New-PodeMutex',
'Test-PodeMutex',
'Get-PodeMutex',
'Remove-PodeMutex',
'Use-PodeMutex',
'Enter-PodeMutex',
'Exit-PodeMutex',
'Clear-PodeMutexes',
'New-PodeSemaphore',
'Test-PodeSemaphore',
'Get-PodeSemaphore',
'Remove-PodeSemaphore',
'Use-PodeSemaphore',
'Enter-PodeSemaphore',
'Exit-PodeSemaphore',
'Clear-PodeSemaphores',
# caching
'Get-PodeCache',
'Set-PodeCache',
'Test-PodeCache',
'Remove-PodeCache',
'Clear-PodeCache',
'Add-PodeCacheStorage',
'Remove-PodeCacheStorage',
'Get-PodeCacheStorage',
'Test-PodeCacheStorage',
'Set-PodeCacheDefaultStorage',
'Get-PodeCacheDefaultStorage',
'Set-PodeCacheDefaultTtl',
'Get-PodeCacheDefaultTtl',
# scoped variables
'Convert-PodeScopedVariables',
'Convert-PodeScopedVariable',
'Add-PodeScopedVariable',
'Remove-PodeScopedVariable',
'Test-PodeScopedVariable',
'Clear-PodeScopedVariables',
'Get-PodeScopedVariable',
'Use-PodeScopedVariables'
)
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @(
'Enable-PodeOpenApiViewer',
'Enable-PodeOA',
'Get-PodeOpenApiDefinition',
'New-PodeOASchemaProperty'
)
# 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.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
Tags = @(
'powershell', 'web', 'server', 'http', 'https', 'listener', 'rest', 'api', 'tcp',
'smtp', 'websites', 'powershell-core', 'windows', 'unix', 'linux', 'pode', 'PSEdition_Core',
'cross-platform', 'file-monitoring', 'multithreaded', 'schedule', 'middleware', 'session',
'authentication', 'authorisation', 'authorization', 'arm', 'raspberry-pi', 'aws-lambda',
'azure-functions', 'websockets', 'swagger', 'openapi', 'webserver', 'secrets', 'fim'
)
# A URL to the license for this module.
LicenseUri = 'https://raw.githubusercontent.com/Badgerati/Pode/master/LICENSE.txt'
# A URL to the main website for this project.
ProjectUri = 'https://github.com/Badgerati/Pode'
# A URL to an icon representing this module.
IconUri = 'https://raw.githubusercontent.com/Badgerati/Pode/master/images/icon.png'
# Release notes for this particular version of the module
ReleaseNotes = 'https://github.com/Badgerati/Pode/releases/tag/v2.10.1'
}
PwshVersions = @{
Untested = '7.3,7.1,7.0,6.2,6.1,6.0,5.0,4.0,3.0,2.0,1.0'
Supported = '7.4,7.2,5.1'
}
}
}
# root path
$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
# load assemblies
Add-Type -AssemblyName System.Web
Add-Type -AssemblyName System.Net.Http
# Construct the path to the module manifest (.psd1 file)
$moduleManifestPath = Join-Path -Path $root -ChildPath 'Pode.psd1'
# Import the module manifest to access its properties
$moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath
$podeDll = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' }
if ($podeDll) {
if ( $moduleManifest.ModuleVersion -ne '$version$') {
$moduleVersion = ([version]::new($moduleManifest.ModuleVersion + '.0'))
if ($podeDll.GetName().Version -ne $moduleVersion) {
throw "An existing incompatible Pode.DLL version $($podeDll.GetName().Version) is loaded. Version $moduleVersion is required. Open a new Powershell/pwsh session and retry."
}
}
}
else {
if ($PSVersionTable.PSVersion -ge [version]'7.4.0') {
Add-Type -LiteralPath "$($root)/Libs/net8.0/Pode.dll" -ErrorAction Stop
}
elseif ($PSVersionTable.PSVersion -ge [version]'7.2.0') {
Add-Type -LiteralPath "$($root)/Libs/net6.0/Pode.dll" -ErrorAction Stop
}
else {
Add-Type -LiteralPath "$($root)/Libs/netstandard2.0/Pode.dll" -ErrorAction Stop
}
}
# load private functions
Get-ChildItem "$($root)/Private/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }
# only import public functions
$sysfuncs = Get-ChildItem Function:
# only import public alias
$sysaliases = Get-ChildItem Alias:
# load public functions
Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) }
# get functions from memory and compare to existing to find new functions added
$funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }
$aliases = Get-ChildItem Alias: | Where-Object { $sysaliases -notcontains $_ }
# export the module's public functions
if ($funcs) {
if ($aliases) {
Export-ModuleMember -Function ($funcs.Name) -Alias $aliases.Name
}
else {
Export-ModuleMember -Function ($funcs.Name)
}
}
function Get-PodeAccessMiddlewareScript {
return {
param($opts)
if ($null -eq $WebEvent.Auth) {
Set-PodeResponseStatus -Code 403
return $false
}
# test access
$WebEvent.Auth.IsAuthorised = Invoke-PodeAccessValidation -Name $opts.Name
# 403 if unauthorised
if (!$WebEvent.Auth.IsAuthorised) {
Set-PodeResponseStatus -Code 403
}
# run next middleware or stop?
return $WebEvent.Auth.IsAuthorised
}
}
function Invoke-PodeAccessValidation {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# get the access method
$access = $PodeContext.Server.Authorisations.Methods[$Name]
# if it's a merged access, re-call this function and check against "succeed" value
if ($access.Merged) {
foreach ($accName in $access.Access) {
$result = Invoke-PodeAccessValidation -Name $accName
# if the access passed, and we only need one access to pass, return true
if ($result -and $access.PassOne) {
return $true
}
# if the access failed, but we need all to pass, return false
if (!$result -and !$access.PassOne) {
return $false
}
}
# if the last access failed, and we only need one access to pass, return false
if (!$result -and $access.PassOne) {
return $false
}
# if the last access succeeded, and we need all to pass, return true
if ($result -and !$access.PassOne) {
return $true
}
# default failure
return $false
}
# main access validation logic
return (Test-PodeAccessRoute -Name $Name)
}
function Get-PodeAuthBasicType {
return {
param($options)
# get the auth header
$header = (Get-PodeHeader -Name 'Authorization')
if ($null -eq $header) {
return @{
Message = 'No Authorization header found'
Code = 401
}
}
# ensure the first atom is basic (or opt override)
$atoms = $header -isplit '\s+'
if ($atoms.Length -lt 2) {
return @{
Message = 'Invalid Authorization header'
Code = 400
}
}
if ($atoms[0] -ine $options.HeaderTag) {
return @{
Message = "Header is not for $($options.HeaderTag) Authorization"
Code = 400
}
}
# decode the auth header
try {
$enc = [System.Text.Encoding]::GetEncoding($options.Encoding)
}
catch {
return @{
Message = 'Invalid encoding specified for Authorization'
Code = 400
}
}
try {
$decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1]))
}
catch {
return @{
Message = 'Invalid Base64 string found in Authorization header'
Code = 400
}
}
# validate and return user/result
$index = $decoded.IndexOf(':')
$username = $decoded.Substring(0, $index)
$password = $decoded.Substring($index + 1)
# build the result
$result = @($username, $password)
# convert to credential?
if ($options.AsCredential) {
$passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force
$creds = [pscredential]::new($username, $passSecure)
$result = @($creds)
}
# return data for calling validator
return $result
}
}
function Get-PodeAuthOAuth2Type {
return {
param($options, $schemes)
# set default scopes
if (($null -eq $options.Scopes) -or ($options.Scopes.Length -eq 0)) {
$options.Scopes = @('openid', 'profile', 'email')
}
$scopes = ($options.Scopes -join ' ')
# if there's an error, fail
if (![string]::IsNullOrWhiteSpace($WebEvent.Query['error'])) {
return @{
Message = $WebEvent.Query['error']
Code = 401
IsErrored = $true
}
}
# set grant type
$hasInnerScheme = (($null -ne $schemes) -and ($schemes.Length -gt 0))
$grantType = 'authorization_code'
if ($hasInnerScheme) {
$grantType = 'password'
}
# if there's a code query param, or inner scheme, get access token
if ($hasInnerScheme -or ![string]::IsNullOrWhiteSpace($WebEvent.Query['code'])) {
try {
# ensure the state is valid
if ((Test-PodeSessionsInUse) -and ($WebEvent.Query['state'] -ne $WebEvent.Session.Data['__pode_oauth_state__'])) {
return @{
Message = 'OAuth2 state returned is invalid'
Code = 401
IsErrored = $true
}
}
# build tokenUrl query with client info
$body = "client_id=$($options.Client.ID)"
$body += "&grant_type=$($grantType)"
if (![string]::IsNullOrEmpty($options.Client.Secret)) {
$body += "&client_secret=$([System.Web.HttpUtility]::UrlEncode($options.Client.Secret))"
}
# add PKCE code verifier
if ($options.PKCE.Enabled) {
$body += "&code_verifier=$($WebEvent.Session.Data['__pode_oauth_code_verifier__'])"
}
# if there's an inner scheme, get the username/password, and set query
if ($hasInnerScheme) {
$body += "&username=$($schemes[-1][0])"
$body += "&password=$($schemes[-1][1])"
$body += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))"
}
# otherwise, set query for auth_code
else {
$redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect
$body += "&code=$($WebEvent.Query['code'])"
$body += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))"
}
# POST the tokenUrl
try {
$result = Invoke-RestMethod -Method Post -Uri $options.Urls.Token -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop
}
catch [System.Net.WebException], [System.Net.Http.HttpRequestException] {
$response = Read-PodeWebExceptionDetails -ErrorRecord $_
$result = ($response.Body | ConvertFrom-Json)
}
# was there an error?
if (![string]::IsNullOrWhiteSpace($result.error)) {
return @{
Message = "$($result.error): $($result.error_description)"
Code = 401
IsErrored = $true
}
}
# get user details - if url supplied
if (![string]::IsNullOrWhiteSpace($options.Urls.User.Url)) {
try {
$user = Invoke-RestMethod -Method $options.Urls.User.Method -Uri $options.Urls.User.Url -Headers @{ Authorization = "Bearer $($result.access_token)" }
}
catch [System.Net.WebException], [System.Net.Http.HttpRequestException] {
$response = Read-PodeWebExceptionDetails -ErrorRecord $_
$user = ($response.Body | ConvertFrom-Json)
}
if (![string]::IsNullOrWhiteSpace($user.error)) {
return @{
Message = "$($user.error): $($user.error_description)"
Code = 401
IsErrored = $true
}
}
}
elseif (![string]::IsNullOrWhiteSpace($result.id_token)) {
try {
$user = ConvertFrom-PodeJwt -Token $result.id_token -IgnoreSignature
}
catch {
$user = @{ Provider = 'OAuth2' }
}
}
else {
$user = @{ Provider = 'OAuth2' }
}
# return the user for the validator
return @($user, $result.access_token, $result.refresh_token, $result)
}
finally {
if ($null -ne $WebEvent.Session.Data) {
# clear state
$WebEvent.Session.Data.Remove('__pode_oauth_state__')
# clear PKCE
if ($options.PKCE.Enabled) {
$WebEvent.Session.Data.Remove('__pode_oauth_code_verifier__')
}
}
}
}
# redirect to the authUrl - only if no inner scheme supplied
if (!$hasInnerScheme) {
# get the redirectUrl
$redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect
# add authUrl query params
$query = "client_id=$($options.Client.ID)"
$query += '&response_type=code'
$query += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))"
$query += '&response_mode=query'
$query += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))"
# add csrf state
if (Test-PodeSessionsInUse) {
$guid = New-PodeGuid
$WebEvent.Session.Data['__pode_oauth_state__'] = $guid
$query += "&state=$($guid)"
}
# build a code verifier for PKCE, and add to query
if ($options.PKCE.Enabled) {
$guid = New-PodeGuid
$codeVerifier = "$($guid)-$($guid)"
$WebEvent.Session.Data['__pode_oauth_code_verifier__'] = $codeVerifier
$codeChallenge = $codeVerifier
if ($options.PKCE.CodeChallenge.Method -ieq 'S256') {
$codeChallenge = ConvertTo-PodeBase64UrlValue -Value (Invoke-PodeSHA256Hash -Value $codeChallenge) -NoConvert
}
$query += "&code_challenge=$($codeChallenge)"
$query += "&code_challenge_method=$($options.PKCE.CodeChallenge.Method)"
}
# are custom parameters already on the URL?
$url = $options.Urls.Authorise
if (!$url.Contains('?')) {
$url += '?'
}
else {
$url += '&'
}
# redirect to OAuth2 endpoint
Move-PodeResponseUrl -Url "$($url)$($query)"
return @{ IsRedirected = $true }
}
# hmm, this is unexpected
return @{
Message = 'Well, this is awkward...'
Code = 500
IsErrored = $true
}
}
}
function Get-PodeOAuth2RedirectHost {
param(
[Parameter()]
[string]
$RedirectUrl
)
if ($RedirectUrl.StartsWith('/')) {
if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) {
$protocol = Get-PodeHeader -Name 'X-Forwarded-Proto'
if ([string]::IsNullOrWhiteSpace($protocol)) {
$protocol = 'https'
}
$domain = "$($protocol)://$($WebEvent.Request.Host)"
}
else {
$domain = Get-PodeEndpointUrl
}
$RedirectUrl = "$($domain.TrimEnd('/'))$($RedirectUrl)"
}
return $RedirectUrl
}
function Get-PodeAuthClientCertificateType {
return {
param($options)
$cert = $WebEvent.Request.ClientCertificate
# ensure we have a client cert
if ($null -eq $cert) {
return @{
Message = 'No client certificate supplied'
Code = 401
}
}
# ensure the cert has a thumbprint
if ([string]::IsNullOrWhiteSpace($cert.Thumbprint)) {
return @{
Message = 'Invalid client certificate supplied'
Code = 401
}
}
# ensure the cert hasn't expired, or has it even started
$now = [datetime]::Now
if (($cert.NotAfter -lt $now) -or ($cert.NotBefore -gt $now)) {
return @{
Message = 'Invalid client certificate supplied'
Code = 401
}
}
# return data for calling validator
return @($cert, $WebEvent.Request.ClientCertificateErrors)
}
}
function Get-PodeAuthApiKeyType {
return {
param($options)
# get api key from appropriate location
$apiKey = [string]::Empty
switch ($options.Location.ToLowerInvariant()) {
'header' {
$apiKey = Get-PodeHeader -Name $options.LocationName
}
'query' {
$apiKey = $WebEvent.Query[$options.LocationName]
}
'cookie' {
$apiKey = Get-PodeCookieValue -Name $options.LocationName
}
}
# 400 if no key
if ([string]::IsNullOrWhiteSpace($apiKey)) {
return @{
Message = "No $($options.LocationName) $($options.Location) found"
Code = 400
}
}
# build the result
$apiKey = $apiKey.Trim()
$result = @($apiKey)
# convert as jwt?
if ($options.AsJWT) {
try {
$payload = ConvertFrom-PodeJwt -Token $apiKey -Secret $options.Secret
Test-PodeJwt -Payload $payload
}
catch {
if ($_.Exception.Message -ilike '*jwt*') {
return @{
Message = $_.Exception.Message
Code = 400
}
}
throw
}
$result = @($payload)
}
# return the result
return $result
}
}
function Get-PodeAuthBearerType {
return {
param($options)
# get the auth header
$header = (Get-PodeHeader -Name 'Authorization')
if ($null -eq $header) {
return @{
Message = 'No Authorization header found'
Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request)
Code = 400
}
}
# ensure the first atom is bearer
$atoms = $header -isplit '\s+'
if ($atoms.Length -lt 2) {
return @{
Message = 'Invalid Authorization header'
Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request)
Code = 400
}
}
if ($atoms[0] -ine $options.HeaderTag) {
return @{
Message = "Authorization header is not $($options.HeaderTag)"
Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request)
Code = 400
}
}
# 400 if no token
$token = $atoms[1]
if ([string]::IsNullOrWhiteSpace($token)) {
return @{
Message = 'No Bearer token found'
Code = 400
}
}
# build the result
$token = $token.Trim()
$result = @($token)
# convert as jwt?
if ($options.AsJWT) {
try {
$payload = ConvertFrom-PodeJwt -Token $token -Secret $options.Secret
Test-PodeJwt -Payload $payload
}
catch {
if ($_.Exception.Message -ilike '*jwt*') {
return @{
Message = $_.Exception.Message
#https://www.rfc-editor.org/rfc/rfc6750 Bearer token should return 401
Code = 401
}
}
throw
}
$result = @($payload)
}
# return the result
return $result
}
}
function Get-PodeAuthBearerPostValidator {
return {
param($token, $result, $options)
# if there's no user, fail with challenge
if (($null -eq $result) -or ($null -eq $result.User)) {
return @{
Message = 'User not found'
Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_token)
Code = 401
}
}
# check for an error and description
if (![string]::IsNullOrWhiteSpace($result.Error)) {
return @{
Message = 'Authorization failed'
Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType $result.Error -ErrorDescription $result.ErrorDescription)
Code = 401
}
}
# check the scopes
$hasAuthScopes = (($null -ne $options.Scopes) -and ($options.Scopes.Length -gt 0))
$hasTokenScope = ![string]::IsNullOrWhiteSpace($result.Scope)
# 403 if we have auth scopes but no token scope
if ($hasAuthScopes -and !$hasTokenScope) {
return @{
Message = 'Invalid Scope'
Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope)
Code = 403
}
}
# 403 if we have both, but token not in auth scope
if ($hasAuthScopes -and $hasTokenScope -and ($options.Scopes -notcontains $result.Scope)) {
return @{
Message = 'Invalid Scope'
Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope)
Code = 403
}
}
# return result
return $result
}
}
function New-PodeAuthBearerChallenge {
param(
[Parameter()]
[string[]]
$Scopes,
[Parameter()]
[ValidateSet('', 'invalid_request', 'invalid_token', 'insufficient_scope')]
[string]
$ErrorType,
[Parameter()]
[string]
$ErrorDescription
)
$items = @()
if (($null -ne $Scopes) -and ($Scopes.Length -gt 0)) {
$items += "scope=`"$($Scopes -join ' ')`""
}
if (![string]::IsNullOrWhiteSpace($ErrorType)) {
$items += "error=`"$($ErrorType)`""
}
if (![string]::IsNullOrWhiteSpace($ErrorDescription)) {
$items += "error_description=`"$($ErrorDescription)`""
}
return ($items -join ', ')
}
function Get-PodeAuthDigestType {
return {
param($options)
# get the auth header - send challenge if missing
$header = (Get-PodeHeader -Name 'Authorization')
if ($null -eq $header) {
return @{
Message = 'No Authorization header found'
Challenge = (New-PodeAuthDigestChallenge)
Code = 401
}
}
# if auth header isn't digest send challenge
$atoms = $header -isplit '\s+'
if ($atoms.Length -lt 2) {
return @{
Message = 'Invalid Authorization header'
Code = 400
}
}
if ($atoms[0] -ine $options.HeaderTag) {
return @{
Message = "Authorization header is not $($options.HeaderTag)"
Challenge = (New-PodeAuthDigestChallenge)
Code = 401
}
}
# parse the other atoms of the header (after the scheme), return 400 if none
$params = ConvertFrom-PodeAuthDigestHeader -Parts ($atoms[1..$($atoms.Length - 1)])
if ($params.Count -eq 0) {
return @{
Message = 'Invalid Authorization header'
Code = 400
}
}
# if no username then 401 and challenge
if ([string]::IsNullOrWhiteSpace($params.username)) {
return @{
Message = 'Authorization header is missing username'
Challenge = (New-PodeAuthDigestChallenge)
Code = 401
}
}
# return 400 if domain doesnt match request domain
if ($WebEvent.Path -ine $params.uri) {
return @{
Message = 'Invalid Authorization header'
Code = 400
}
}
# return data for calling validator
return @($params.username, $params)
}
}
function Get-PodeAuthDigestPostValidator {
return {
param($username, $params, $result, $options)
# if there's no user or password, fail with challenge
if (($null -eq $result) -or ($null -eq $result.User) -or [string]::IsNullOrWhiteSpace($result.Password)) {
return @{
Message = 'User not found'
Challenge = (New-PodeAuthDigestChallenge)
Code = 401
}
}
# generate the first hash
$hash1 = Invoke-PodeMD5Hash -Value "$($params.username):$($params.realm):$($result.Password)"
# generate the second hash
$hash2 = Invoke-PodeMD5Hash -Value "$($WebEvent.Method.ToUpperInvariant()):$($params.uri)"
# generate final hash
$final = Invoke-PodeMD5Hash -Value "$($hash1):$($params.nonce):$($params.nc):$($params.cnonce):$($params.qop):$($hash2)"
# compare final hash to client response
if ($final -ne $params.response) {
return @{
Message = 'Hashes failed to match'
Challenge = (New-PodeAuthDigestChallenge)
Code = 401
}
}
# hashes are valid, remove password and return result
$null = $result.Remove('Password')
return $result
}
}
function ConvertFrom-PodeAuthDigestHeader {
param(
[Parameter()]
[string[]]
$Parts
)
if (($null -eq $Parts) -or ($Parts.Length -eq 0)) {
return @{}
}
$obj = @{}
$value = ($Parts -join ' ')
@($value -isplit ',(?=(?:[^"]|"[^"]*")*$)') | ForEach-Object {
if ($_ -imatch '(?<name>\w+)=["]?(?<value>[^"]+)["]?$') {
$obj[$Matches['name']] = $Matches['value']
}
}
return $obj
}
function New-PodeAuthDigestChallenge {
$items = @('qop="auth"', 'algorithm="MD5"', "nonce=`"$(New-PodeGuid -Secure -NoDashes)`"")
return ($items -join ', ')
}
function Get-PodeAuthFormType {
return {
param($options)
# get user/pass keys to get from payload
$userField = $options.Fields.Username
$passField = $options.Fields.Password
# get the user/pass
$username = $WebEvent.Data.$userField
$password = $WebEvent.Data.$passField
# if either are empty, fail auth
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
return @{
Message = 'Username or Password not supplied'
Code = 401
}
}
# build the result
$result = @($username, $password)
# convert to credential?
if ($options.AsCredential) {
$passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force
$creds = [pscredential]::new($username, $passSecure)
$result = @($creds)
}
# return data for calling validator
return $result
}
}
function Get-PodeAuthUserFileMethod {
return {
param($username, $password, $options)
# using pscreds?
if (($null -eq $options) -and ($username -is [pscredential])) {
$_username = ([pscredential]$username).UserName
$_password = ([pscredential]$username).GetNetworkCredential().Password
$_options = [hashtable]$password
}
else {
$_username = $username
$_password = $password
$_options = $options
}
# load the file
$users = (Get-Content -Path $_options.FilePath -Raw | ConvertFrom-Json)
# find the user by username - only use the first one
$user = @(foreach ($_user in $users) {
if ($_user.Username -ieq $_username) {
$_user
break
}
})[0]
# fail if no user
if ($null -eq $user) {
return @{ Message = 'You are not authorised to access this website' }
}
# check the user's password
if (![string]::IsNullOrWhiteSpace($_options.HmacSecret)) {
$hash = Invoke-PodeHMACSHA256Hash -Value $_password -Secret $_options.HmacSecret
}
else {
$hash = Invoke-PodeSHA256Hash -Value $_password
}
if ($user.Password -ne $hash) {
return @{ Message = 'You are not authorised to access this website' }
}
# convert the user to a hashtable
$user = @{
Name = $user.Name
Username = $user.Username
Email = $user.Email
Groups = $user.Groups
Metadata = $user.Metadata
}
# is the user valid for any users/groups?
if (!(Test-PodeAuthUserGroups -User $user -Users $_options.Users -Groups $_options.Groups)) {
return @{ Message = 'You are not authorised to access this website' }
}
$result = @{ User = $user }
# call additional scriptblock if supplied
if ($null -ne $_options.ScriptBlock.Script) {
$result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables
}
# return final result, this could contain a user obj, or an error message from custom scriptblock
return $result
}
}
function Get-PodeAuthWindowsADMethod {
return {
param($username, $password, $options)
# using pscreds?
if (($null -eq $options) -and ($username -is [pscredential])) {
$_username = ([pscredential]$username).UserName
$_password = ([pscredential]$username).GetNetworkCredential().Password
$_options = [hashtable]$password
}
else {
$_username = $username
$_password = $password
$_options = $options
}
# parse username to remove domains
$_username = (($_username -split '@')[0] -split '\\')[-1]
# validate and retrieve the AD user
$noGroups = $_options.NoGroups
$directGroups = $_options.DirectGroups
$keepCredential = $_options.KeepCredential
$result = Get-PodeAuthADResult `
-Server $_options.Server `
-Domain $_options.Domain `
-SearchBase $_options.SearchBase `
-Username $_username `
-Password $_password `
-Provider $_options.Provider `
-NoGroups:$noGroups `
-DirectGroups:$directGroups `
-KeepCredential:$keepCredential
# if there's a message, fail and return the message
if (![string]::IsNullOrWhiteSpace($result.Message)) {
return $result
}
# if there's no user, then, err, oops
if (Test-PodeIsEmpty $result.User) {
return @{ Message = 'An unexpected error occured' }
}
# is the user valid for any users/groups - if not, error!
if (!(Test-PodeAuthUserGroups -User $result.User -Users $_options.Users -Groups $_options.Groups)) {
return @{ Message = 'You are not authorised to access this website' }
}
# call additional scriptblock if supplied
if ($null -ne $_options.ScriptBlock.Script) {
$result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables
}
# return final result, this could contain a user obj, or an error message from custom scriptblock
return $result
}
}
function Invoke-PodeAuthInbuiltScriptBlock {
param(
[Parameter(Mandatory = $true)]
[hashtable]
$User,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
$UsingVariables,
[switch]
$NoSplat
)
return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $User -UsingVariables $UsingVariables -Return -Splat:(!$NoSplat))
}
function Get-PodeAuthWindowsLocalMethod {
return {
param($username, $password, $options)
# using pscreds?
if (($null -eq $options) -and ($username -is [pscredential])) {
$_username = ([pscredential]$username).UserName
$_password = ([pscredential]$username).GetNetworkCredential().Password
$_options = [hashtable]$password
}
else {
$_username = $username
$_password = $password
$_options = $options
}
$user = @{
UserType = 'Local'
AuthenticationType = 'WinNT'
Username = $_username
Name = [string]::Empty
Fqdn = $PodeContext.Server.ComputerName
Domain = 'localhost'
Groups = @()
}
Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop
$context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Machine', $PodeContext.Server.ComputerName)
$valid = $context.ValidateCredentials($_username, $_password)
if (!$valid) {
return @{ Message = 'Invalid credentials supplied' }
}
try {
$tmpUsername = $_username -replace '\\', '/'
if ($_username -inotlike "$($PodeContext.Server.ComputerName)*") {
$tmpUsername = "$($PodeContext.Server.ComputerName)/$($_username)"
}
$ad = [adsi]"WinNT://$($tmpUsername)"
$user.Name = @($ad.FullName)[0]
if (!$_options.NoGroups) {
$cmd = "`$ad = [adsi]'WinNT://$($tmpUsername)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })"
$user.Groups = [string[]](powershell -c $cmd)
}
}
finally {
Close-PodeDisposable -Disposable $ad -Close
}
# is the user valid for any users/groups - if not, error!
if (!(Test-PodeAuthUserGroups -User $user -Users $_options.Users -Groups $_options.Groups)) {
return @{ Message = 'You are not authorised to access this website' }
}
$result = @{ User = $user }
# call additional scriptblock if supplied
if ($null -ne $_options.ScriptBlock.Script) {
$result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables
}
# return final result, this could contain a user obj, or an error message from custom scriptblock
return $result
}
}
function Get-PodeAuthWindowsADIISMethod {
return {
param($token, $options)
# get the close handler
$win32Handler = Add-Type -Name Win32CloseHandle -PassThru -MemberDefinition @'
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
'@
try {
# parse the auth token and get the user
$winAuthToken = [System.IntPtr][Int]"0x$($token)"
$winIdentity = New-Object System.Security.Principal.WindowsIdentity($winAuthToken, 'Windows')
# get user and domain
$username = ($winIdentity.Name -split '\\')[-1]
$domain = ($winIdentity.Name -split '\\')[0]
# create base user object
$user = @{
UserType = 'Domain'
Identity = @{
AccessToken = $winIdentity.AccessToken
}
AuthenticationType = $winIdentity.AuthenticationType
DistinguishedName = [string]::Empty
Username = $username
Name = [string]::Empty
Email = [string]::Empty
Fqdn = [string]::Empty
Domain = $domain
Groups = @()
}
# if the domain isn't local, attempt AD user
if (![string]::IsNullOrWhiteSpace($domain) -and (@('.', $PodeContext.Server.ComputerName) -inotcontains $domain)) {
# get the server's fdqn (and name/email)
try {
# Open ADSISearcher and change context to given domain
$searcher = [adsisearcher]''
$searcher.SearchRoot = [adsi]"LDAP://$($domain)"
$searcher.Filter = "ObjectSid=$($winIdentity.User.Value.ToString())"
# Query the ADSISearcher for the above defined SID
$ad = $searcher.FindOne()
# Save it to our existing array for later usage
$user.DistinguishedName = @($ad.Properties.distinguishedname)[0]
$user.Name = @($ad.Properties.name)[0]
$user.Email = @($ad.Properties.mail)[0]
$user.Fqdn = (Get-PodeADServerFromDistinguishedName -DistinguishedName $user.DistinguishedName)
}
finally {
Close-PodeDisposable -Disposable $searcher
}
try {
if (!$options.NoGroups) {
# open a new connection
$result = (Open-PodeAuthADConnection -Server $user.Fqdn -Domain $domain -Provider $options.Provider)
if (!$result.Success) {
return @{ Message = "Failed to connect to Domain Server '$($user.Fqdn)' of $domain for $($user.DistinguishedName)." }
}
# get the connection
$connection = $result.Connection
# get the users groups
$directGroups = $options.DirectGroups
$user.Groups = (Get-PodeAuthADGroups -Connection $connection -DistinguishedName $user.DistinguishedName -Username $user.Username -Direct:$directGroups -Provider $options.Provider)
}
}
finally {
if ($null -ne $connection) {
Close-PodeDisposable -Disposable $connection.Searcher
Close-PodeDisposable -Disposable $connection.Entry -Close
$connection.Credential = $null
}
}
}
# otherwise, get details of local user
else {
# get the user's name and groups
try {
$user.UserType = 'Local'
if (!$options.NoLocalCheck) {
$localUser = $winIdentity.Name -replace '\\', '/'
$ad = [adsi]"WinNT://$($localUser)"
$user.Name = @($ad.FullName)[0]
# dirty, i know :/ - since IIS runs using pwsh, the InvokeMember part fails
# we can safely call windows powershell here, as IIS is only on windows.
if (!$options.NoGroups) {
$cmd = "`$ad = [adsi]'WinNT://$($localUser)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })"
$user.Groups = [string[]](powershell -c $cmd)
}
}
}
finally {
Close-PodeDisposable -Disposable $ad -Close
}
}
}
catch {
$_ | Write-PodeErrorLog
return @{ Message = 'Failed to retrieve user using Authentication Token' }
}
finally {
$win32Handler::CloseHandle($winAuthToken)
}
# is the user valid for any users/groups - if not, error!
if (!(Test-PodeAuthUserGroups -User $user -Users $options.Users -Groups $options.Groups)) {
return @{ Message = 'You are not authorised to access this website' }
}
$result = @{ User = $user }
# call additional scriptblock if supplied
if ($null -ne $options.ScriptBlock.Script) {
$result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables
}
# return final result, this could contain a user obj, or an error message from custom scriptblock
return $result
}
}
function Test-PodeAuthUserGroups {
param(
[Parameter(Mandatory = $true)]
[hashtable]
$User,
[Parameter()]
[string[]]
$Users,
[Parameter()]
[string[]]
$Groups
)
$haveUsers = (($null -ne $Users) -and ($Users.Length -gt 0))
$haveGroups = (($null -ne $Groups) -and ($Groups.Length -gt 0))
# if there are no groups/users supplied, return user is valid
if (!$haveUsers -and !$haveGroups) {
return $true
}
# before checking supplied groups, is the user in the supplied list of authorised users?
if ($haveUsers -and (@($Users) -icontains $User.Username)) {
return $true
}
# if there are groups supplied, check the user is a member of one
if ($haveGroups) {
foreach ($group in $Groups) {
if (@($User.Groups) -icontains $group) {
return $true
}
}
}
return $false
}
function Invoke-PodeAuthValidation {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# get auth method
$auth = $PodeContext.Server.Authentications.Methods[$Name]
# if it's a merged auth, re-call this function and check against "succeed" value
if ($auth.Merged) {
$results = @{}
foreach ($authName in $auth.Authentications) {
$result = Invoke-PodeAuthValidation -Name $authName
# if the auth is trying to redirect, we need to bubble the this back now
if ($result.Redirected) {
return $result
}
# if the auth passed, and we only need one auth to pass, return current result
if ($result.Success -and $auth.PassOne) {
return $result
}
# if the auth failed, but we need all to pass, return current result
if (!$result.Success -and !$auth.PassOne) {
return $result
}
# remember result if we need all to pass
if (!$auth.PassOne) {
$results[$authName] = $result
}
}
# if the last auth failed, and we only need one auth to pass, set failure and return
if (!$result.Success -and $auth.PassOne) {
return $result
}
# if the last auth succeeded, and we need all to pass, merge users/headers and return result
if ($result.Success -and !$auth.PassOne) {
# invoke scriptblock, or use result of merge default
if ($null -ne $auth.ScriptBlock.Script) {
$result = Invoke-PodeAuthInbuiltScriptBlock -User $results -ScriptBlock $auth.ScriptBlock.Script -UsingVariables $auth.ScriptBlock.UsingVariables -NoSplat
}
else {
$result = $results[$auth.MergeDefault]
}
# reset default properties and return
$result.Success = $true
$result.Auth = $results.Keys
return $result
}
# default failure
return @{
Success = $false
StatusCode = 500
}
}
# main auth validation logic
$result = (Test-PodeAuthValidation -Name $Name)
$result.Auth = $Name
return $result
}
function Test-PodeAuthValidation {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
try {
# get auth method
$auth = $PodeContext.Server.Authentications.Methods[$Name]
# auth result
$result = $null
# run pre-auth middleware
if ($null -ne $auth.Scheme.Middleware) {
if (!(Invoke-PodeMiddleware -Middleware $auth.Scheme.Middleware)) {
return @{
Success = $false
}
}
}
# run auth scheme script to parse request for data
$_args = @(Merge-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables)
# call inner schemes first
if ($null -ne $auth.Scheme.InnerScheme) {
$schemes = @()
$_scheme = $auth.Scheme
$_inner = @(while ($null -ne $_scheme.InnerScheme) {
$_scheme = $_scheme.InnerScheme
$_scheme
})
for ($i = $_inner.Length - 1; $i -ge 0; $i--) {
$_tmp_args = @(Merge-PodeScriptblockArguments -ArgumentList $_inner[$i].Arguments -UsingVariables $_inner[$i].ScriptBlock.UsingVariables)
$_tmp_args += , $schemes
$result = (Invoke-PodeScriptBlock -ScriptBlock $_inner[$i].ScriptBlock.Script -Arguments $_tmp_args -Return -Splat)
if ($result -is [hashtable]) {
break
}
$schemes += , $result
$result = $null
}
$_args += , $schemes
}
if ($null -eq $result) {
$result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat)
}
# if data is a hashtable, then don't call validator (parser either failed, or forced a success)
if ($result -isnot [hashtable]) {
$original = $result
$_args = @($result) + @($auth.Arguments)
$result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -UsingVariables $auth.UsingVariables -Return -Splat)
# if we have user, then run post validator if present
if ([string]::IsNullOrEmpty($result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) {
$_args = @($original) + @($result) + @($auth.Scheme.Arguments)
$result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables -Return -Splat)
}
}
# is the auth trying to redirect ie: oauth?
if ($result.IsRedirected) {
return @{
Success = $false
Redirected = $true
}
}
# if there's no result, or no user, then the auth failed - but allow auth if anon enabled
if (($null -eq $result) -or ($result.Count -eq 0) -or (Test-PodeIsEmpty $result.User)) {
$code = (Protect-PodeValue -Value $result.Code -Default 401)
# set the www-auth header
$validCode = (($code -eq 401) -or ![string]::IsNullOrEmpty($result.Challenge))
if ($validCode) {
if ($null -eq $result) {
$result = @{}
}
if ($null -eq $result.Headers) {
$result.Headers = @{}
}
if (![string]::IsNullOrWhiteSpace($auth.Scheme.Name) -and !$result.Headers.ContainsKey('WWW-Authenticate')) {
$authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge
$result.Headers['WWW-Authenticate'] = $authHeader
}
}
return @{
Success = $false
StatusCode = $code
Description = $result.Message
Headers = $result.Headers
FailureRedirect = [bool]$result.IsErrored
}
}
# authentication was successful
return @{
Success = $true
User = $result.User
Headers = $result.Headers
}
}
catch {
$_ | Write-PodeErrorLog
return @{
Success = $false
StatusCode = 500
Exception = $_
}
}
}
function Get-PodeAuthMiddlewareScript {
return {
param($opts)
return Test-PodeAuthInternal `
-Name $opts.Name `
-Login:($opts.Login) `
-Logout:($opts.Logout) `
-AllowAnon:($opts.Anon)
}
}
function Test-PodeAuthInternal {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[switch]
$Login,
[switch]
$Logout,
[switch]
$AllowAnon
)
# get the auth method
$auth = $PodeContext.Server.Authentications.Methods[$Name]
# check for logout command
if ($Logout) {
Remove-PodeAuthSession
if ($PodeContext.Server.Sessions.Info.UseHeaders) {
return Set-PodeAuthStatus `
-StatusCode 401 `
-Name $Name `
-NoSuccessRedirect
}
else {
$auth.Failure.Url = (Protect-PodeValue -Value $auth.Failure.Url -Default $WebEvent.Request.Url.AbsolutePath)
return Set-PodeAuthStatus `
-StatusCode 302 `
-Name $Name `
-NoSuccessRedirect
}
}
# if the session already has a user/isAuth'd, then skip auth - or allow anon
if (Test-PodeSessionsInUse) {
# existing session auth'd
if (Test-PodeAuthUser) {
$WebEvent.Auth = $WebEvent.Session.Data.Auth
return Set-PodeAuthStatus `
-Name $Name `
-LoginRoute:($Login) `
-NoSuccessRedirect
}
# if we're allowing anon access, and using sessions, then stop here - as a session will be created from a login route for auth'ing users
if ($AllowAnon) {
if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) {
Revoke-PodeSession
}
return $true
}
}
# check if the login flag is set, in which case just return and load a login get-page (allowing anon access)
if ($Login -and !$PodeContext.Server.Sessions.Info.UseHeaders -and ($WebEvent.Method -ieq 'get')) {
if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) {
Revoke-PodeSession
}
return $true
}
try {
$result = Invoke-PodeAuthValidation -Name $Name
}
catch {
$_ | Write-PodeErrorLog
return Set-PodeAuthStatus `
-StatusCode 500 `
-Description $_.Exception.Message `
-Name $Name
}
# did the auth force a redirect?
if ($result.Redirected) {
return $false
}
# if auth failed, are we allowing anon access?
if (!$result.Success -and $AllowAnon) {
return $true
}
# if auth failed, set appropriate response headers/redirects
if (!$result.Success) {
return Set-PodeAuthStatus `
-StatusCode $result.StatusCode `
-Description $result.Description `
-Headers $result.Headers `
-Name $Name `
-LoginRoute:$Login `
-NoFailureRedirect:($result.FailureRedirect)
}
# if auth passed, assign the user to the session
$WebEvent.Auth = [ordered]@{
User = $result.User
IsAuthenticated = $true
IsAuthorised = $true
Store = !$auth.Sessionless
Name = $result.Auth
}
# successful auth
$authName = $null
if ($auth.Merged -and !$auth.PassOne) {
$authName = $Name
}
else {
$authName = @($result.Auth)[0]
}
return Set-PodeAuthStatus `
-Headers $result.Headers `
-Name $authName `
-LoginRoute:$Login
}
function Get-PodeAuthWwwHeaderValue {
param(
[Parameter()]
[string]
$Name,
[Parameter()]
[string]
$Realm,
[Parameter()]
[string]
$Challenge
)
if ([string]::IsNullOrWhiteSpace($Name)) {
return [string]::Empty
}
$header = $Name
if (![string]::IsNullOrWhiteSpace($Realm)) {
$header += " realm=`"$($Realm)`""
}
if (![string]::IsNullOrWhiteSpace($Challenge)) {
$header += ", $($Challenge)"
}
return $header
}
function Remove-PodeAuthSession {
# blank out the auth
$WebEvent.Auth = @{}
# if a session auth is found, blank it
if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) {
$WebEvent.Session.Data.Remove('Auth')
}
# Delete the current session (remove from store, blank it, and remove from Response)
Revoke-PodeSession
}
function Get-PodeAuthFailureInfo {
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[hashtable]
$Info,
[Parameter()]
[string]
$BaseName
)
# base name
if ([string]::IsNullOrEmpty($BaseName)) {
$BaseName = $Name
}
# get auth method
$auth = $PodeContext.Server.Authentications.Methods[$Name]
# cached failure?
if ($null -ne $auth.Cache.Failure) {
return $auth.Cache.Failure
}
# find failure info
if ($null -eq $Info) {
$Info = @{
Url = $auth.Failure.Url
Message = $auth.Failure.Message
}
}
if ([string]::IsNullOrEmpty($Info.Url)) {
$Info.Url = $auth.Failure.Url
}
if ([string]::IsNullOrEmpty($Info.Message)) {
$Info.Message = $auth.Failure.Message
}
if ((![string]::IsNullOrEmpty($Info.Url) -and ![string]::IsNullOrEmpty($Info.Message)) -or [string]::IsNullOrEmpty($auth.Parent)) {
$PodeContext.Server.Authentications.Methods[$BaseName].Cache.Failure = $Info
return $Info
}
return (Get-PodeAuthFailureInfo -Name $auth.Parent -Info $Info -BaseName $BaseName)
}
function Get-PodeAuthSuccessInfo {
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[hashtable]
$Info,
[Parameter()]
[string]
$BaseName
)
# base name
if ([string]::IsNullOrEmpty($BaseName)) {
$BaseName = $Name
}
# get auth method
$auth = $PodeContext.Server.Authentications.Methods[$Name]
# cached success?
if ($null -ne $auth.Cache.Success) {
return $auth.Cache.Success
}
# find success info
if ($null -eq $Info) {
$Info = @{
Url = $auth.Success.Url
UseOrigin = $auth.Success.UseOrigin
}
}
if ([string]::IsNullOrEmpty($Info.Url)) {
$Info.Url = $auth.Success.Url
}
if (!$Info.UseOrigin) {
$Info.UseOrigin = $auth.Success.UseOrigin
}
if ((![string]::IsNullOrEmpty($Info.Url) -and $Info.UseOrigin) -or [string]::IsNullOrEmpty($auth.Parent)) {
$PodeContext.Server.Authentications.Methods[$BaseName].Cache.Success = $Info
return $Info
}
return (Get-PodeAuthSuccessInfo -Name $auth.Parent -Info $Info -BaseName $BaseName)
}
function Set-PodeAuthStatus {
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[int]
$StatusCode = 0,
[Parameter()]
[string]
$Description,
[Parameter()]
[hashtable]
$Headers,
[switch]
$LoginRoute,
[switch]
$NoSuccessRedirect,
[switch]
$NoFailureRedirect
)
# if we have any headers, set them
if (($null -ne $Headers) -and ($Headers.Count -gt 0)) {
foreach ($key in $Headers.Keys) {
Set-PodeHeader -Name $key -Value $Headers[$key]
}
}
# get auth method
$auth = $PodeContext.Server.Authentications.Methods[$Name]
# cookie redirect name
$redirectCookie = 'pode.redirecturl'
# get Success object from auth
$success = Get-PodeAuthSuccessInfo -Name $Name
# if a statuscode supplied, assume failure
if ($StatusCode -gt 0) {
# get Failure object from auth
$failure = Get-PodeAuthFailureInfo -Name $Name
# override description with the failureMessage if supplied
$Description = (Protect-PodeValue -Value $failure.Message -Default $Description)
# add error to flash
if ($LoginRoute -and !$auth.Sessionless -and ![string]::IsNullOrWhiteSpace($Description)) {
Add-PodeFlashMessage -Name 'auth-error' -Message $Description
}
# check if we have a failure url redirect
if (!$NoFailureRedirect -and ![string]::IsNullOrWhiteSpace($failure.Url)) {
if ($success.UseOrigin -and ($WebEvent.Method -ieq 'get')) {
$null = Set-PodeCookie -Name $redirectCookie -Value $WebEvent.Request.Url.PathAndQuery
}
Move-PodeResponseUrl -Url $failure.Url
}
else {
Set-PodeResponseStatus -Code $StatusCode -Description $Description
}
return $false
}
# 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) {
$tmpUrl = Get-PodeCookieValue -Name $redirectCookie
Remove-PodeCookie -Name $redirectCookie
if (![string]::IsNullOrWhiteSpace($tmpUrl)) {
$url = $tmpUrl
}
}
Move-PodeResponseUrl -Url $url
return $false
}
return $true
}
function Get-PodeADServerFromDistinguishedName {
param(
[Parameter()]
[string]
$DistinguishedName
)
if ([string]::IsNullOrWhiteSpace($DistinguishedName)) {
return [string]::Empty
}
$parts = @($DistinguishedName -split ',')
$name = @()
foreach ($part in $parts) {
if ($part -imatch '^DC=(?<name>.+)$') {
$name += $Matches['name']
}
}
return ($name -join '.')
}
function Get-PodeAuthADResult {
param(
[Parameter()]
[string]
$Server,
[Parameter()]
[string]
$Domain,
[Parameter()]
[string]
$SearchBase,
[Parameter()]
[string]
$Username,
[Parameter()]
[string]
$Password,
[Parameter()]
[ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
[string]
$Provider,
[switch]
$NoGroups,
[switch]
$DirectGroups,
[switch]
$KeepCredential
)
try {
# validate the user's AD creds
$result = (Open-PodeAuthADConnection -Server $Server -Domain $Domain -Username $Username -Password $Password -Provider $Provider)
if (!$result.Success) {
return @{ Message = 'Invalid credentials supplied' }
}
# get the connection
$connection = $result.Connection
# get the user
$user = (Get-PodeAuthADUser -Connection $connection -Username $Username -Provider $Provider)
if ($null -eq $user) {
return @{ Message = 'User not found in Active Directory' }
}
# get the users groups
$groups = @()
if (!$NoGroups) {
$groups = (Get-PodeAuthADGroups -Connection $connection -DistinguishedName $user.DistinguishedName -Username $Username -Direct:$DirectGroups -Provider $Provider)
}
# check if we want to keep the credentials in the User object
if ($KeepCredential) {
$credential = [pscredential]::new($($Domain + '\' + $Username), (ConvertTo-SecureString -String $Password -AsPlainText -Force))
}
else {
$credential = $null
}
# return the user
return @{
User = @{
UserType = 'Domain'
AuthenticationType = 'LDAP'
DistinguishedName = $user.DistinguishedName
Username = ($Username -split '\\')[-1]
Name = $user.Name
Email = $user.Email
Fqdn = $Server
Domain = $Domain
Groups = $groups
Credential = $credential
}
}
}
finally {
if ($null -ne $connection) {
switch ($Provider.ToLowerInvariant()) {
'openldap' {
$connection.Username = $null
$connection.Password = $null
}
'activedirectory' {
$connection.Credential = $null
}
'directoryservices' {
Close-PodeDisposable -Disposable $connection.Searcher
Close-PodeDisposable -Disposable $connection.Entry -Close
}
}
}
}
}
function Open-PodeAuthADConnection {
param(
[Parameter(Mandatory = $true)]
[string]
$Server,
[Parameter()]
[string]
$Domain,
[Parameter()]
[string]
$SearchBase,
[Parameter()]
[string]
$Username,
[Parameter()]
[string]
$Password,
[Parameter()]
[ValidateSet('LDAP', 'WinNT')]
[string]
$Protocol = 'LDAP',
[Parameter()]
[ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
[string]
$Provider
)
$result = $true
$connection = $null
# validate the user's AD creds
switch ($Provider.ToLowerInvariant()) {
'openldap' {
if (![string]::IsNullOrWhiteSpace($SearchBase)) {
$baseDn = $SearchBase
}
else {
$baseDn = "DC=$(($Server -split '\.') -join ',DC=')"
}
$query = (Get-PodeAuthADQuery -Username $Username)
$hostname = "$($Protocol)://$($Server)"
$user = $Username
if (!$Username.StartsWith($Domain)) {
$user = "$($Domain)\$($Username)"
}
$null = (ldapsearch -x -LLL -H "$($hostname)" -D "$($user)" -w "$($Password)" -b "$($baseDn)" -o ldif-wrap=no "$($query)" dn)
if (!$? -or ($LASTEXITCODE -ne 0)) {
$result = $false
}
else {
$connection = @{
Hostname = $hostname
Username = $user
BaseDN = $baseDn
Password = $Password
}
}
}
'activedirectory' {
try {
$creds = [pscredential]::new($Username, (ConvertTo-SecureString -String $Password -AsPlainText -Force))
$null = Get-ADUser -Identity $Username -Credential $creds -ErrorAction Stop
$connection = @{
Credential = $creds
}
}
catch {
$result = $false
}
}
'directoryservices' {
if ([string]::IsNullOrWhiteSpace($Password)) {
$ad = (New-Object System.DirectoryServices.DirectoryEntry "$($Protocol)://$($Server)")
}
else {
$ad = (New-Object System.DirectoryServices.DirectoryEntry "$($Protocol)://$($Server)", "$($Username)", "$($Password)")
}
if (Test-PodeIsEmpty $ad.distinguishedName) {
$result = $false
}
else {
$connection = @{
Entry = $ad
}
}
}
}
return @{
Success = $result
Connection = $connection
}
}
function Get-PodeAuthADQuery {
param(
[Parameter(Mandatory = $true)]
[string]
$Username
)
return "(&(objectCategory=person)(samaccountname=$($Username)))"
}
function Get-PodeAuthADUser {
param(
[Parameter(Mandatory = $true)]
$Connection,
[Parameter(Mandatory = $true)]
[string]
$Username,
[Parameter()]
[ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
[string]
$Provider
)
$query = (Get-PodeAuthADQuery -Username $Username)
$user = $null
# generate query to find user
switch ($Provider.ToLowerInvariant()) {
'openldap' {
$result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" name mail)
if (!$? -or ($LASTEXITCODE -ne 0)) {
return $null
}
$user = @{
DistinguishedName = (Get-PodeOpenLdapValue -Lines $result -Property 'dn')
Name = (Get-PodeOpenLdapValue -Lines $result -Property 'name')
Email = (Get-PodeOpenLdapValue -Lines $result -Property 'mail')
}
}
'activedirectory' {
$result = Get-ADUser -LDAPFilter $query -Credential $Connection.Credential -Properties mail
$user = @{
DistinguishedName = $result.DistinguishedName
Name = $result.Name
Email = $result.mail
}
}
'directoryservices' {
$Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry
$Connection.Searcher.filter = $query
$result = $Connection.Searcher.FindOne().Properties
if (Test-PodeIsEmpty $result) {
return $null
}
$user = @{
DistinguishedName = @($result.distinguishedname)[0]
Name = @($result.name)[0]
Email = @($result.mail)[0]
}
}
}
return $user
}
function Get-PodeOpenLdapValue {
param(
[Parameter()]
[string[]]
$Lines,
[Parameter()]
[string]
$Property,
[switch]
$All
)
foreach ($line in $Lines) {
if ($line -imatch "^$($Property)\:\s+(?<$($Property)>.+)$") {
# return the first found
if (!$All) {
return $Matches[$Property]
}
# return array of all
$Matches[$Property]
}
}
}
function Get-PodeAuthADGroups {
param(
[Parameter(Mandatory = $true)]
$Connection,
[Parameter()]
[string]
$DistinguishedName,
[Parameter()]
[string]
$Username,
[Parameter()]
[ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
[string]
$Provider,
[switch]
$Direct
)
if ($Direct) {
return (Get-PodeAuthADGroupsDirect -Connection $Connection -Username $Username -Provider $Provider)
}
return (Get-PodeAuthADGroupsAll -Connection $Connection -DistinguishedName $DistinguishedName -Provider $Provider)
}
function Get-PodeAuthADGroupsDirect {
param(
[Parameter(Mandatory = $true)]
$Connection,
[Parameter()]
[string]
$Username,
[Parameter()]
[ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
[string]
$Provider
)
# create the query
$query = "(&(objectCategory=person)(samaccountname=$($Username)))"
$groups = @()
# get the groups
switch ($Provider.ToLowerInvariant()) {
'openldap' {
$result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" memberof)
$groups = (Get-PodeOpenLdapValue -Lines $result -Property 'memberof' -All)
}
'activedirectory' {
$groups = (Get-ADPrincipalGroupMembership -Identity $Username -Credential $Connection.Credential).distinguishedName
}
'directoryservices' {
if ($null -eq $Connection.Searcher) {
$Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry
}
$Connection.Searcher.filter = $query
$groups = @($Connection.Searcher.FindOne().Properties.memberof)
}
}
$groups = @(foreach ($group in $groups) {
if ($group -imatch '^CN=(?<group>.+?),') {
$Matches['group']
}
})
return $groups
}
function Get-PodeAuthADGroupsAll {
param(
[Parameter(Mandatory = $true)]
$Connection,
[Parameter()]
[string]
$DistinguishedName,
[Parameter()]
[ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')]
[string]
$Provider
)
# create the query
$query = "(member:1.2.840.113556.1.4.1941:=$($DistinguishedName))"
$groups = @()
# get the groups
switch ($Provider.ToLowerInvariant()) {
'openldap' {
$result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" samaccountname)
$groups = (Get-PodeOpenLdapValue -Lines $result -Property 'sAMAccountName' -All)
}
'activedirectory' {
$groups = (Get-ADObject -LDAPFilter $query -Credential $Connection.Credential).Name
}
'directoryservices' {
if ($null -eq $Connection.Searcher) {
$Connection.Searcher = New-Object System.DirectoryServices.DirectorySearcher $Connection.Entry
}
$null = $Connection.Searcher.PropertiesToLoad.Add('samaccountname')
$Connection.Searcher.filter = $query
$groups = @($Connection.Searcher.FindAll().Properties.samaccountname)
}
}
return $groups
}
function Get-PodeAuthDomainName {
if (Test-PodeIsUnix) {
$dn = (dnsdomainname)
if ([string]::IsNullOrWhiteSpace($dn)) {
$dn = (/usr/sbin/realm list --name-only)
}
return $dn
}
else {
$domain = $env:USERDNSDOMAIN
if ([string]::IsNullOrWhiteSpace($domain)) {
$domain = (Get-CimInstance -Class Win32_ComputerSystem -Verbose:$false).Domain
}
return $domain
}
}
function Find-PodeAuth {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name
)
return $PodeContext.Server.Authentications.Methods[$Name]
}
<#
.SYNOPSIS
Expands a list of authentication names, including merged authentication methods.
.DESCRIPTION
The Expand-PodeAuthMerge function takes an array of authentication names and expands it by resolving any merged authentication methods
into their individual components. It is particularly useful in scenarios where authentication methods are combined or merged, and there
is a need to process each individual method separately.
.PARAMETER Names
An array of authentication method names. These names can include both discrete authentication methods and merged ones.
.EXAMPLE
$expandedAuthNames = Expand-PodeAuthMerge -Names @('BasicAuth', 'CustomMergedAuth')
Expands the provided authentication names, resolving 'CustomMergedAuth' into its constituent authentication methods if it's a merged one.
#>
function Expand-PodeAuthMerge {
param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]
$Names
)
# Initialize a hashtable to store expanded authentication names
$authNames = @{}
# Iterate over each authentication name
foreach ($authName in $Names) {
# Handle the special case of anonymous access
if ($authName -eq '%_allowanon_%') {
$authNames[$authName] = $true
}
else {
# Retrieve the authentication method from the Pode context
$_auth = $PodeContext.Server.Authentications.Methods[$authName]
# Check if the authentication is a merged one and expand it
if ($_auth.merged) {
foreach ($key in (Expand-PodeAuthMerge -Names $_auth.Authentications)) {
$authNames[$key] = $true
}
}
else {
# If not merged, add the authentication name to the list
$authNames[$_auth.Name] = $true
}
}
}
# Return the keys of the hashtable, which are the expanded authentication names
return $authNames.Keys
}
function Import-PodeAuthADModule {
if (!(Test-PodeIsWindows)) {
throw 'Active Directory module only available on Windows'
}
if (!(Test-PodeModuleInstalled -Name ActiveDirectory)) {
throw 'Active Directory module is not installed'
}
Import-Module -Name ActiveDirectory -Force -ErrorAction Stop
Export-PodeModule -Name ActiveDirectory
}
function Get-PodeAuthADProvider {
param(
[switch]
$OpenLDAP,
[switch]
$ADModule
)
# openldap (literal, or not windows)
if ($OpenLDAP -or !(Test-PodeIsWindows)) {
return 'OpenLDAP'
}
# ad module
if ($ADModule) {
return 'ActiveDirectory'
}
# ds
return 'DirectoryServices'
}
function Import-PodeFunctionsIntoRunspaceState {
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath
)
# do nothing if disabled
if (!$PodeContext.Server.AutoImport.Functions.Enabled) {
return
}
# if export only, and there are none, do nothing
if ($PodeContext.Server.AutoImport.Functions.ExportOnly -and ($PodeContext.Server.AutoImport.Functions.ExportList.Length -eq 0)) {
return
}
# script or file functions?
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'script' {
$funcs = (Get-PodeFunctionsFromScriptBlock -ScriptBlock $ScriptBlock)
}
'file' {
$funcs = (Get-PodeFunctionsFromFile -FilePath $FilePath)
}
}
# looks like we have nothing!
if (($null -eq $funcs) -or ($funcs.Length -eq 0)) {
return
}
# groups funcs in case there or multiple definitions
$funcs = ($funcs | Group-Object -Property { $_.Name })
# import them, but also check if they're exported
foreach ($func in $funcs) {
# only exported funcs? is the func exported?
if ($PodeContext.Server.AutoImport.Functions.ExportOnly -and ($PodeContext.Server.AutoImport.Functions.ExportList -inotcontains $func.Name)) {
continue
}
# load the function
$funcDef = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new($func.Name, $func.Group[-1].Definition)
$PodeContext.RunspaceState.Commands.Add($funcDef)
}
}
function Import-PodeModulesIntoRunspaceState {
# do nothing if disabled
if (!$PodeContext.Server.AutoImport.Modules.Enabled) {
return
}
# if export only, and there are none, do nothing
if ($PodeContext.Server.AutoImport.Modules.ExportOnly -and ($PodeContext.Server.AutoImport.Modules.ExportList.Length -eq 0)) {
return
}
# get modules currently loaded in session
$modules = Get-Module |
Where-Object {
($_.Name -inotin @('pode', 'pode.internal')) -and ($_.Name -inotlike 'microsoft.powershell.*')
} | Select-Object -Unique
# work out which order the modules need to be loaded
$modulesOrder = @(foreach ($module in $modules) {
Get-PodeModuleDependencies -Module $module
}) |
Where-Object {
($_.Name -inotin @('pode', 'pode.internal')) -and ($_.Name -inotlike 'microsoft.powershell.*')
} | Select-Object -Unique
# load modules into runspaces, if allowed
foreach ($module in $modulesOrder) {
# only exported modules? is the module exported?
if ($PodeContext.Server.AutoImport.Modules.ExportOnly -and ($PodeContext.Server.AutoImport.Modules.ExportList -inotcontains $module.Name)) {
continue
}
# import the module
$path = Find-PodeModuleFile -Module $module
if ([string]::IsNullOrEmpty($path) -or !(Test-Path $path)) {
continue
}
if (($module.ModuleType -ieq 'Manifest') -or ($path.EndsWith('.ps1'))) {
$PodeContext.RunspaceState.ImportPSModule($path)
}
else {
$PodeContext.Server.Modules[$module.Name] = $path
}
}
}
function Import-PodeSnapinsIntoRunspaceState {
# if non-windows or core, do nothing
if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) {
return
}
# do nothing if disabled
if (!$PodeContext.Server.AutoImport.Snapins.Enabled) {
return
}
# if export only, and there are none, do nothing
if ($PodeContext.Server.AutoImport.Snapins.ExportOnly -and ($PodeContext.Server.AutoImport.Snapins.ExportList.Length -eq 0)) {
return
}
# load snapins into runspaces, if allowed
$snapins = (Get-PSSnapin | Where-Object { !$_.IsDefault }).Name | Sort-Object -Unique
foreach ($snapin in $snapins) {
# only exported snapins? is the snapin exported?
if ($PodeContext.Server.AutoImport.Snapins.ExportOnly -and ($PodeContext.Server.AutoImport.Snapins.ExportList -inotcontains $snapin)) {
continue
}
$PodeContext.RunspaceState.ImportPSSnapIn($snapin, [ref]$null)
}
}
function Initialize-PodeAutoImportConfiguration {
return @{
Modules = @{
Enabled = $true
ExportList = @()
ExportOnly = $false
}
Snapins = @{
Enabled = $true
ExportList = @()
ExportOnly = $false
}
Functions = @{
Enabled = $true
ExportList = @()
ExportOnly = $false
}
SecretVaults = @{
Enabled = $true
SecretManagement = @{
Enabled = $false
ExportList = @()
ExportOnly = $false
}
}
}
}
function Import-PodeSecretVaultsIntoRegistry {
# do nothing if disabled
if (!$PodeContext.Server.AutoImport.SecretVaults.Enabled) {
return
}
Import-PodeSecretManagementVaultsIntoRegistry
}
function Import-PodeSecretManagementVaultsIntoRegistry {
# do nothing if disabled
if (!$PodeContext.Server.AutoImport.SecretVaults.SecretManagement.Enabled) {
return
}
# if export only, and there are none, do nothing
if ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportOnly -and ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportList.Length -eq 0)) {
return
}
# error if SecretManagement module not installed
if (!(Test-PodeModuleInstalled -Name Microsoft.PowerShell.SecretManagement)) {
throw 'Microsoft.PowerShell.SecretManagement module not installed'
}
# import the module
$null = Import-Module -Name Microsoft.PowerShell.SecretManagement -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false
# get the current secret vaults
$vaults = @(Get-SecretVault -ErrorAction Stop)
# register the vaults
foreach ($vault in $vaults) {
# only exported vaults? is the vault exported?
if ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportOnly -and ($PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportList -inotcontains $vault.Name)) {
continue
}
# is a vault with this name already registered?
if (Test-PodeSecretVault -Name $vault.Name) {
throw "A Secret Vault with the name '$($vault.Name)' has already been registered while auto-importing Secret Vaults"
}
# register the vault
$PodeContext.Server.Secrets.Vaults[$vault.Name] = @{
Name = $vault.Name
Type = 'secretmanagement'
Parameters = $vault.VaultParameters
AutoImported = $true
Unlock = $null
Cache = $null
SecretManagement = @{
VaultName = $vault.Name
ModuleName = $vault.ModulePath
}
}
}
}
function Read-PodeAutoImportConfiguration {
param(
[Parameter()]
[hashtable]
$Configuration
)
$impModules = $Configuration.AutoImport.Modules
$impSnapins = $Configuration.AutoImport.Snapins
$impFuncs = $Configuration.AutoImport.Functions
$impSecretVaults = $Configuration.AutoImport.SecretVaults
return @{
Modules = @{
Enabled = (($null -eq $impModules.Enable) -or [bool]$impModules.Enable)
ExportList = @()
ExportOnly = ([bool]$impModules.ExportOnly)
}
Snapins = @{
Enabled = (($null -eq $impSnapins.Enable) -or [bool]$impSnapins.Enable)
ExportList = @()
ExportOnly = ([bool]$impSnapins.ExportOnly)
}
Functions = @{
Enabled = (($null -eq $impFuncs.Enable) -or [bool]$impFuncs.Enable)
ExportList = @()
ExportOnly = ([bool]$impFuncs.ExportOnly)
}
SecretVaults = @{
Enabled = (($null -eq $impSecretVaults.Enable) -or [bool]$impSecretVaults.Enable)
SecretManagement = @{
Enabled = ((($null -eq $impSecretVaults.Enable) -and (Test-PodeModuleInstalled -Name Microsoft.PowerShell.SecretManagement)) -or [bool]$impSecretVaults.Enable)
ExportList = @()
ExportOnly = ([bool]$impSecretVaults.SecretManagement.ExportOnly)
}
}
}
}
function Reset-PodeAutoImportConfiguration {
$PodeContext.Server.AutoImport.Modules.ExportList = @()
$PodeContext.Server.AutoImport.Snapins.ExportList = @()
$PodeContext.Server.AutoImport.Functions.ExportList = @()
$PodeContext.Server.AutoImport.SecretVaults.SecretManagement.ExportList = @()
}
function Get-PodeCacheInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[switch]
$Metadata
)
$meta = $PodeContext.Server.Cache.Items[$Key]
if ($null -eq $meta) {
return $null
}
# check ttl/expiry
if ($meta.Expiry -lt [datetime]::UtcNow) {
Remove-PodeCacheInternal -Key $Key
return $null
}
# return value an metadata if required
if ($Metadata) {
return $meta
}
# return just the value as default
return $meta.Value
}
function Set-PodeCacheInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter(Mandatory = $true)]
[object]
$InputObject,
[Parameter()]
[int]
$Ttl = 0
)
# crete (or update) value value
$PodeContext.Server.Cache.Items[$Key] = @{
Value = $InputObject
Ttl = $Ttl
Expiry = [datetime]::UtcNow.AddSeconds($Ttl)
}
}
function Test-PodeCacheInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Key
)
# if it's not in the cache at all, return false
if (!$PodeContext.Server.Cache.Items.ContainsKey($Key)) {
return $false
}
# fetch the items metadata, and check expiry. If it's expired return false.
$meta = $PodeContext.Server.Cache.Items[$Key]
# check ttl/expiry
if ($meta.Expiry -lt [datetime]::UtcNow) {
Remove-PodeCacheInternal -Key $Key
return $false
}
# it exists, and isn't expired
return $true
}
function Remove-PodeCacheInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Key
)
Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock {
$null = $PodeContext.Server.Cache.Items.Remove($Key)
}
}
function Clear-PodeCacheInternal {
Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock {
$null = $PodeContext.Server.Cache.Items.Clear()
}
}
function Start-PodeCacheHousekeeper {
if (![string]::IsNullOrEmpty((Get-PodeCacheDefaultStorage))) {
return
}
Add-PodeTimer -Name '__pode_cache_housekeeper__' -Interval 10 -ScriptBlock {
$keys = Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -Return -ScriptBlock {
if ($PodeContext.Server.Cache.Items.Count -eq 0) {
return
}
return $PodeContext.Server.Cache.Items.Keys.Clone()
}
if (Test-PodeIsEmpty $keys) {
return
}
$now = [datetime]::UtcNow
foreach ($key in $keys) {
if ($PodeContext.Server.Cache.Items[$key].Expiry -lt $now) {
Remove-PodeCacheInternal -Key $key
}
}
}
}
using namespace Pode
function New-PodeContext {
[CmdletBinding()]
param(
[Parameter()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[string]
$FilePath,
[Parameter()]
[int]
$Threads = 1,
[Parameter()]
[int]
$Interval = 0,
[Parameter()]
[string]
$ServerRoot,
[Parameter()]
[string]
$Name = $null,
[Parameter()]
[string]
$ServerlessType,
[Parameter()]
[string]
$StatusPageExceptions,
[Parameter()]
[string]
$ListenerType,
[Parameter()]
[string[]]
$EnablePool,
[switch]
$DisableTermination,
[switch]
$Quiet,
[switch]
$EnableBreakpoints
)
# set a random server name if one not supplied
if (Test-PodeIsEmpty $Name) {
$Name = Get-PodeRandomName
}
# are we running in a serverless context
$isServerless = ![string]::IsNullOrWhiteSpace($ServerlessType)
# ensure threads are always >0, for to 1 if we're serverless
if (($Threads -le 0) -or $isServerless) {
$Threads = 1
}
# basic context object
$ctx = New-Object -TypeName psobject |
Add-Member -MemberType NoteProperty -Name Threads -Value @{} -PassThru |
Add-Member -MemberType NoteProperty -Name Timers -Value @{} -PassThru |
Add-Member -MemberType NoteProperty -Name Schedules -Value @{} -PassThru |
Add-Member -MemberType NoteProperty -Name Tasks -Value @{} -PassThru |
Add-Member -MemberType NoteProperty -Name RunspacePools -Value $null -PassThru |
Add-Member -MemberType NoteProperty -Name Runspaces -Value $null -PassThru |
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 Threading -Value @{} -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 |
Add-Member -MemberType NoteProperty -Name Receivers -Value @() -PassThru |
Add-Member -MemberType NoteProperty -Name Watchers -Value @() -PassThru |
Add-Member -MemberType NoteProperty -Name Fim -Value @{} -PassThru
# set the server name, logic and root, and other basic properties
$ctx.Server.Name = $Name
$ctx.Server.Logic = $ScriptBlock
$ctx.Server.LogicPath = $FilePath
$ctx.Server.Interval = $Interval
$ctx.Server.PodeModule = (Get-PodeModuleDetails)
$ctx.Server.DisableTermination = $DisableTermination.IsPresent
$ctx.Server.Quiet = $Quiet.IsPresent
$ctx.Server.ComputerName = [System.Net.DNS]::GetHostName()
# list of created listeners/receivers
$ctx.Listeners = @()
$ctx.Receivers = @()
$ctx.Watchers = @()
# default secret that can used when needed, and a secret isn't supplied
$ctx.Server.DefaultSecret = New-PodeGuid -Secure
# list of timers/schedules/tasks/fim
$ctx.Timers = @{
Enabled = ($EnablePool -icontains 'timers')
Items = @{}
}
$ctx.Schedules = @{
Enabled = ($EnablePool -icontains 'schedules')
Items = @{}
Processes = @{}
}
$ctx.Tasks = @{
Enabled = ($EnablePool -icontains 'tasks')
Items = @{}
Results = @{}
}
$ctx.Fim = @{
Enabled = ($EnablePool -icontains 'files')
Items = @{}
}
# auto importing (modules, funcs, snap-ins)
$ctx.Server.AutoImport = Initialize-PodeAutoImportConfiguration
# basic logging setup
$ctx.Server.Logging = @{
Enabled = $true
Types = @{}
}
# set thread counts
$ctx.Threads = @{
General = $Threads
Schedules = 10
Files = 1
Tasks = 2
WebSockets = 2
}
# set socket details for pode server
$ctx.Server.Sockets = @{
Ssl = @{
Protocols = Get-PodeDefaultSslProtocols
}
ReceiveTimeout = 100
}
$ctx.Server.Signals = @{
Enabled = $false
Listener = $null
}
$ctx.Server.Http = @{
Listener = $null
}
$ctx.Server.Sse = @{
Signed = $false
Secret = $null
Strict = $false
DefaultScope = 'Global'
BroadcastLevel = @{}
}
$ctx.Server.WebSockets = @{
Enabled = ($EnablePool -icontains 'websockets')
Receiver = $null
Connections = @{}
}
# set default request config
$ctx.Server.Request = @{
Timeout = 30
BodySize = 100MB
}
# default Folders
$ctx.Server.DefaultFolders = @{
'Views' = 'views'
'Public' = 'public'
'Errors' = 'errors'
}
# check if there is any global configuration
$ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx
# over status page exceptions
if (!(Test-PodeIsEmpty $StatusPageExceptions)) {
if ($null -eq $ctx.Server.Web) {
$ctx.Server.Web = @{ ErrorPages = @{} }
}
$ctx.Server.Web.ErrorPages.ShowExceptions = ($StatusPageExceptions -eq 'show')
}
# configure the server's root path
$ctx.Server.Root = $ServerRoot
if (!(Test-PodeIsEmpty $ctx.Server.Configuration.Server.Root)) {
$ctx.Server.Root = Get-PodeRelativePath -Path $ctx.Server.Configuration.Server.Root -RootPath $ctx.Server.Root -JoinRoot -Resolve -TestPath
}
if (Test-PodeIsEmpty $ctx.Server.Root) {
$ctx.Server.Root = $PWD.Path
}
# debugging
if ($EnableBreakpoints) {
if ($null -eq $ctx.Server.Debug) {
$ctx.Server.Debug = @{ Breakpoints = @{} }
}
$ctx.Server.Debug.Breakpoints.Enabled = $EnableBreakpoints.IsPresent
}
# set the server's listener type
$ctx.Server.ListenerType = $ListenerType
# set serverless info
$ctx.Server.ServerlessType = $ServerlessType
$ctx.Server.IsServerless = $isServerless
if ($isServerless) {
$ctx.Server.DisableTermination = $true
}
# set the server types
$ctx.Server.IsService = ($Interval -gt 0)
$ctx.Server.Types = @()
# is the server running under IIS? (also, disable termination)
$ctx.Server.IsIIS = (!$isServerless -and (!(Test-PodeIsEmpty $env:ASPNETCORE_PORT)) -and (!(Test-PodeIsEmpty $env:ASPNETCORE_TOKEN)))
if ($ctx.Server.IsIIS) {
$ctx.Server.DisableTermination = $true
# if under IIS and Azure Web App, force quiet
if (!(Test-PodeIsEmpty $env:WEBSITE_IIS_SITE_NAME)) {
$ctx.Server.Quiet = $true
}
# set iis token/settings
$ctx.Server.IIS = @{
Token = $env:ASPNETCORE_TOKEN
Port = $env:ASPNETCORE_PORT
Path = @{
Raw = '/'
Pattern = '^/'
IsNonRoot = $false
}
Shutdown = $false
}
if (![string]::IsNullOrWhiteSpace($env:ASPNETCORE_APPL_PATH) -and ($env:ASPNETCORE_APPL_PATH -ne '/')) {
$ctx.Server.IIS.Path.Raw = $env:ASPNETCORE_APPL_PATH
$ctx.Server.IIS.Path.Pattern = "^$($env:ASPNETCORE_APPL_PATH)"
$ctx.Server.IIS.Path.IsNonRoot = $true
}
}
# is the server running under Heroku?
$ctx.Server.IsHeroku = (!$isServerless -and (!(Test-PodeIsEmpty $env:PORT)) -and (!(Test-PodeIsEmpty $env:DYNO)))
# if we're inside a remote host, stop termination
if ($Host.Name -ieq 'ServerRemoteHost') {
$ctx.Server.DisableTermination = $true
}
# set the IP address details
$ctx.Server.Endpoints = @{}
$ctx.Server.EndpointsMap = @{}
# general encoding for the server
$ctx.Server.Encoding = New-Object System.Text.UTF8Encoding
# setup gui details
$ctx.Server.Gui = @{}
# shared temp drives
$ctx.Server.Drives = @{}
$ctx.Server.InbuiltDrives = @{}
# shared state between runspaces
$ctx.Server.State = @{}
# setup caching
$ctx.Server.Cache = @{
Items = @{}
Storage = @{}
DefaultStorage = $null
DefaultTtl = 3600 # 1hr
}
# output details, like variables, to be set once the server stops
$ctx.Server.Output = @{
Variables = @{}
}
# view engine for rendering pages
$ctx.Server.ViewEngine = @{
Type = 'html'
Extension = 'html'
ScriptBlock = $null
UsingVariables = $null
IsDynamic = $false
}
# pode default preferences
$ctx.Server.Preferences = @{
Routes = @{
IfExists = $null
}
}
# routes for pages and api
$ctx.Server.Routes = @{
'connect' = [ordered]@{}
'delete' = [ordered]@{}
'get' = [ordered]@{}
'head' = [ordered]@{}
'merge' = [ordered]@{}
'options' = [ordered]@{}
'patch' = [ordered]@{}
'post' = [ordered]@{}
'put' = [ordered]@{}
'trace' = [ordered]@{}
'static' = [ordered]@{}
'signal' = [ordered]@{}
'*' = [ordered]@{}
}
# verbs for tcp
$ctx.Server.Verbs = @{}
# secrets
$ctx.Server.Secrets = @{
Vaults = @{}
Keys = @{}
}
# custom view paths
$ctx.Server.Views = @{}
# handlers for tcp
$ctx.Server.Handlers = @{
smtp = @{}
service = @{}
}
# setup basic access placeholders
$ctx.Server.Access = @{
Allow = @{}
Deny = @{}
}
# setup basic limit rules
$ctx.Server.Limits = @{
Rules = @{}
Active = @{}
}
# cookies and session logic
$ctx.Server.Cookies = @{
Csrf = @{}
Secrets = @{}
}
# sessions
$ctx.Server.Sessions = @{}
#OpenApi Definition Tag
$ctx.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $ctx.Server.Configuration.Web.OpenApi.DefaultDefinitionTag
# server metrics
$ctx.Metrics = @{
Server = @{
InitialLoadTime = [datetime]::UtcNow
StartTime = [datetime]::UtcNow
RestartCount = 0
}
Requests = @{
Total = 0
StatusCodes = @{}
}
Signals = @{
Total = 0
}
}
# authentication and authorisation methods
$ctx.Server.Authentications = @{
Methods = @{}
}
$ctx.Server.Authorisations = @{
Methods = @{}
}
# create new cancellation tokens
$ctx.Tokens = @{
Cancellation = New-Object System.Threading.CancellationTokenSource
Restart = New-Object System.Threading.CancellationTokenSource
}
# requests that should be logged
$ctx.LogsToProcess = New-Object System.Collections.ArrayList
# middleware that needs to run
$ctx.Server.Middleware = @()
$ctx.Server.BodyParsers = @{}
# common support values
$ctx.Server.Compression = @{
Encodings = @('gzip', 'deflate', 'x-gzip')
}
# endware that needs to run
$ctx.Server.Endware = @()
# runspace pools
$ctx.RunspacePools = @{
Main = $null
Web = $null
Smtp = $null
Tcp = $null
Signals = $null
Schedules = $null
Gui = $null
Tasks = $null
Files = $null
}
# threading locks, etc.
$ctx.Threading.Lockables = @{
Global = [hashtable]::Synchronized(@{})
Cache = [hashtable]::Synchronized(@{})
Custom = @{}
}
$ctx.Threading.Mutexes = @{}
$ctx.Threading.Semaphores = @{}
# setup runspaces
$ctx.Runspaces = @()
# setup events
$ctx.Server.Events = @{
Start = [ordered]@{}
Terminate = [ordered]@{}
Restart = [ordered]@{}
Browser = [ordered]@{}
Crash = [ordered]@{}
Stop = [ordered]@{}
Running = [ordered]@{}
}
# modules
$ctx.Server.Modules = [ordered]@{}
# setup security
$ctx.Server.Security = @{
ServerDetails = $true
Headers = @{}
Cache = @{
ContentSecurity = @{}
PermissionsPolicy = @{}
}
}
# scoped variables
$ctx.Server.ScopedVariables = [ordered]@{}
# an internal cache for adhoc values, such as module importing checks
$ctx.Server.InternalCache = @{
YamlModuleImported = $null
}
# return the new context
return $ctx
}
function New-PodeRunspaceState {
# create the state, and add the pode modules
$state = [initialsessionstate]::CreateDefault()
$state.ImportPSModule($PodeContext.Server.PodeModule.DataPath)
$state.ImportPSModule($PodeContext.Server.PodeModule.InternalPath)
# load the vars into the share state
$session = New-PodeStateContext -Context $PodeContext
$variables = @(
(New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PodeContext', $session, $null),
(New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'Console', $Host, $null),
(New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PODE_SCOPE_RUNSPACE', $true, $null)
)
foreach ($var in $variables) {
$state.Variables.Add($var)
}
$PodeContext.RunspaceState = $state
}
function New-PodeRunspacePools {
if ($PodeContext.Server.IsServerless) {
return
}
# setup main runspace pool
$threadsCounts = @{
Default = 3
Timer = 1
Log = 1
Schedule = 1
Misc = 1
}
if (!(Test-PodeTimersExist)) {
$threadsCounts.Timer = 0
}
if (!(Test-PodeSchedulesExist)) {
$threadsCounts.Schedule = 0
}
if (!(Test-PodeLoggersExist)) {
$threadsCounts.Log = 0
}
# main runspace - for timers, schedules, etc
$totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum
$PodeContext.RunspacePools.Main = @{
Pool = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
# web runspace - if we have any http/s endpoints
if (Test-PodeEndpoints -Type Http) {
$PodeContext.RunspacePools.Web = @{
Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
}
# smtp runspace - if we have any smtp endpoints
if (Test-PodeEndpoints -Type Smtp) {
$PodeContext.RunspacePools.Smtp = @{
Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
}
# tcp runspace - if we have any tcp endpoints
if (Test-PodeEndpoints -Type Tcp) {
$PodeContext.RunspacePools.Tcp = @{
Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
}
# signals runspace - if we have any ws/s endpoints
if (Test-PodeEndpoints -Type Ws) {
$PodeContext.RunspacePools.Signals = @{
Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 2), $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
}
# web socket connections runspace - for receiving data for external sockets
if (Test-PodeWebSocketsExist) {
$PodeContext.RunspacePools.WebSockets = @{
Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.WebSockets + 1, $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
New-PodeWebSocketReceiver
}
# setup schedule runspace pool -if we have any schedules
if (Test-PodeSchedulesExist) {
$PodeContext.RunspacePools.Schedules = @{
Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Schedules, $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
}
# setup tasks runspace pool -if we have any tasks
if (Test-PodeTasksExist) {
$PodeContext.RunspacePools.Tasks = @{
Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Tasks, $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
}
# setup files runspace pool -if we have any file watchers
if (Test-PodeFileWatchersExist) {
$PodeContext.RunspacePools.Files = @{
Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Files + 1, $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
}
# setup gui runspace pool (only for non-ps-core) - if gui enabled
if (Test-PodeGuiEnabled) {
$PodeContext.RunspacePools.Gui = @{
Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host)
State = 'Waiting'
}
$PodeContext.RunspacePools.Gui.Pool.ApartmentState = 'STA'
}
}
function Open-PodeRunspacePools {
if ($PodeContext.Server.IsServerless) {
return
}
$start = [datetime]::Now
Write-Verbose 'Opening RunspacePools'
# open pools async
foreach ($key in $PodeContext.RunspacePools.Keys) {
$item = $PodeContext.RunspacePools[$key]
if ($null -eq $item) {
continue
}
$item.Pool.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::ReuseThread
$item.Pool.CleanupInterval = [timespan]::FromMinutes(5)
$item.Result = $item.Pool.BeginOpen($null, $null)
}
# wait for them all to open
$queue = @($PodeContext.RunspacePools.Keys)
while ($queue.Length -gt 0) {
foreach ($key in $queue) {
$item = $PodeContext.RunspacePools[$key]
if ($null -eq $item) {
$queue = ($queue | Where-Object { $_ -ine $key })
continue
}
if ($item.Pool.RunspacePoolStateInfo.State -iin @('Opened', 'Broken')) {
$queue = ($queue | Where-Object { $_ -ine $key })
Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
}
}
if ($queue.Length -gt 0) {
Start-Sleep -Milliseconds 100
}
}
# report errors for failed pools
foreach ($key in $PodeContext.RunspacePools.Keys) {
$item = $PodeContext.RunspacePools[$key]
if ($null -eq $item) {
continue
}
if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') {
$item.Pool.EndOpen($item.Result) | Out-Default
throw "Failed to open RunspacePool: $($key)"
}
}
Write-Verbose "RunspacePools opened [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
}
function Close-PodeRunspacePools {
if ($PodeContext.Server.IsServerless -or ($null -eq $PodeContext.RunspacePools)) {
return
}
$start = [datetime]::Now
Write-Verbose 'Closing RunspacePools'
# close pools async
foreach ($key in $PodeContext.RunspacePools.Keys) {
$item = $PodeContext.RunspacePools[$key]
if (($null -eq $item) -or ($item.Pool.IsDisposed)) {
continue
}
$item.Result = $item.Pool.BeginClose($null, $null)
}
# wait for them all to close
$queue = @($PodeContext.RunspacePools.Keys)
while ($queue.Length -gt 0) {
foreach ($key in $queue) {
$item = $PodeContext.RunspacePools[$key]
if ($null -eq $item) {
$queue = ($queue | Where-Object { $_ -ine $key })
continue
}
if ($item.Pool.RunspacePoolStateInfo.State -iin @('Closed', 'Broken')) {
$queue = ($queue | Where-Object { $_ -ine $key })
Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
}
}
if ($queue.Length -gt 0) {
Start-Sleep -Milliseconds 100
}
}
# report errors for failed pools
foreach ($key in $PodeContext.RunspacePools.Keys) {
$item = $PodeContext.RunspacePools[$key]
if ($null -eq $item) {
continue
}
if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') {
$item.Pool.EndClose($item.Result) | Out-Default
throw "Failed to close RunspacePool: $($key)"
}
}
# dispose pools
foreach ($key in $PodeContext.RunspacePools.Keys) {
$item = $PodeContext.RunspacePools[$key]
if (($null -eq $item) -or ($item.Pool.IsDisposed)) {
continue
}
Close-PodeDisposable -Disposable $item.Pool
}
Write-Verbose "RunspacePools closed [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
}
function New-PodeStateContext {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Context
)
return (New-Object -TypeName psobject |
Add-Member -MemberType NoteProperty -Name Threads -Value $Context.Threads -PassThru |
Add-Member -MemberType NoteProperty -Name Timers -Value $Context.Timers -PassThru |
Add-Member -MemberType NoteProperty -Name Schedules -Value $Context.Schedules -PassThru |
Add-Member -MemberType NoteProperty -Name Tasks -Value $Context.Tasks -PassThru |
Add-Member -MemberType NoteProperty -Name Fim -Value $Context.Fim -PassThru |
Add-Member -MemberType NoteProperty -Name RunspacePools -Value $Context.RunspacePools -PassThru |
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 Threading -Value $Context.Threading -PassThru |
Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru)
}
function Open-PodeConfiguration {
param(
[Parameter()]
[string]
$ServerRoot = $null,
[Parameter()]
$Context
)
$config = @{}
# set the path to the root config file
$configPath = (Join-PodeServerRoot -Folder '.' -FilePath 'server.psd1' -Root $ServerRoot)
# check to see if an environmental config exists (if the env var is set)
if (!(Test-PodeIsEmpty $env:PODE_ENVIRONMENT)) {
$_path = (Join-PodeServerRoot -Folder '.' -FilePath "server.$($env:PODE_ENVIRONMENT).psd1" -Root $ServerRoot)
if (Test-PodePath -Path $_path -NoStatus) {
$configPath = $_path
}
}
# check the path exists, and load the config
if (Test-PodePath -Path $configPath -NoStatus) {
$config = Import-PowerShellDataFile -Path $configPath -ErrorAction Stop
Set-PodeServerConfiguration -Configuration $config.Server -Context $Context
Set-PodeWebConfiguration -Configuration $config.Web -Context $Context
}
return $config
}
function Set-PodeServerConfiguration {
param(
[Parameter()]
[hashtable]
$Configuration,
[Parameter()]
$Context
)
# file monitoring
$Context.Server.FileMonitor = @{
Enabled = [bool]$Configuration.FileMonitor.Enable
Exclude = (Convert-PodePathPatternsToRegex -Paths @($Configuration.FileMonitor.Exclude))
Include = (Convert-PodePathPatternsToRegex -Paths @($Configuration.FileMonitor.Include))
ShowFiles = [bool]$Configuration.FileMonitor.ShowFiles
Files = @()
}
# logging
$Context.Server.Logging = @{
Enabled = (($null -eq $Configuration.Logging.Enable) -or [bool]$Configuration.Logging.Enable)
Masking = @{
Patterns = (Remove-PodeEmptyItemsFromArray -Array @($Configuration.Logging.Masking.Patterns))
Mask = (Protect-PodeValue -Value $Configuration.Logging.Masking.Mask -Default '********')
}
Types = @{}
}
# sockets
if (!(Test-PodeIsEmpty $Configuration.Ssl.Protocols)) {
$Context.Server.Sockets.Ssl.Protocols = (ConvertTo-PodeSslProtocols -Protocols $Configuration.Ssl.Protocols)
}
if ([int]$Configuration.ReceiveTimeout -gt 0) {
$Context.Server.Sockets.ReceiveTimeout = (Protect-PodeValue -Value $Configuration.ReceiveTimeout $Context.Server.Sockets.ReceiveTimeout)
}
# auto-import
$Context.Server.AutoImport = Read-PodeAutoImportConfiguration -Configuration $Configuration
# request
if ([int]$Configuration.Request.Timeout -gt 0) {
$Context.Server.Request.Timeout = [int]$Configuration.Request.Timeout
}
if ([long]$Configuration.Request.BodySize -gt 0) {
$Context.Server.Request.BodySize = [long]$Configuration.Request.BodySize
}
# default folders
if ($Configuration.DefaultFolders) {
if ($Configuration.DefaultFolders.Public) {
$Context.Server.DefaultFolders.Public = $Configuration.DefaultFolders.Public
}
if ($Configuration.DefaultFolders.Views) {
$Context.Server.DefaultFolders.Views = $Configuration.DefaultFolders.Views
}
if ($Configuration.DefaultFolders.Errors) {
$Context.Server.DefaultFolders.Errors = $Configuration.DefaultFolders.Errors
}
}
# debug
$Context.Server.Debug = @{
Breakpoints = @{
Enabled = [bool]$Configuration.Debug.Breakpoints.Enable
}
}
}
function Set-PodeWebConfiguration {
param(
[Parameter()]
[hashtable]
$Configuration,
[Parameter()]
$Context
)
# setup the main web config
$Context.Server.Web = @{
Static = @{
Defaults = $Configuration.Static.Defaults
RedirectToDefault = [bool]$Configuration.Static.RedirectToDefault
Cache = @{
Enabled = [bool]$Configuration.Static.Cache.Enable
MaxAge = [int](Protect-PodeValue -Value $Configuration.Static.Cache.MaxAge -Default 3600)
Include = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Include) -NotSlashes)
Exclude = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Exclude) -NotSlashes)
}
ValidateLast = [bool]$Configuration.Static.ValidateLast
}
ErrorPages = @{
ShowExceptions = [bool]$Configuration.ErrorPages.ShowExceptions
StrictContentTyping = [bool]$Configuration.ErrorPages.StrictContentTyping
Default = $Configuration.ErrorPages.Default
Routes = @{}
}
ContentType = @{
Default = $Configuration.ContentType.Default
Routes = @{}
}
TransferEncoding = @{
Default = $Configuration.TransferEncoding.Default
Routes = @{}
}
Compression = @{
Enabled = [bool]$Configuration.Compression.Enable
}
}
# setup content type route patterns for forced content types
$Configuration.ContentType.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
$_type = $Configuration.ContentType.Routes[$_]
$_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
$Context.Server.Web.ContentType.Routes[$_pattern] = $_type
}
# setup transfer encoding route patterns for forced transfer encodings
$Configuration.TransferEncoding.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
$_type = $Configuration.TransferEncoding.Routes[$_]
$_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
$Context.Server.Web.TransferEncoding.Routes[$_pattern] = $_type
}
# setup content type route patterns for error pages
$Configuration.ErrorPages.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
$_type = $Configuration.ErrorPages.Routes[$_]
$_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
$Context.Server.Web.ErrorPages.Routes[$_pattern] = $_type
}
}
function New-PodeAutoRestartServer {
# don't configure if not supplied, or running as serverless
$config = (Get-PodeConfig)
if (($null -eq $config) -or ($null -eq $config.Server.Restart) -or $PodeContext.Server.IsServerless) {
return
}
$restart = $config.Server.Restart
# period - setup a timer
$period = [int]$restart.period
if ($period -gt 0) {
Add-PodeTimer -Name '__pode_restart_period__' -Interval ($period * 60) -ScriptBlock {
$PodeContext.Tokens.Restart.Cancel()
}
}
# times - convert into cron expressions
$times = @(@($restart.times) -ne $null)
if (($times | Measure-Object).Count -gt 0) {
$crons = @()
@($times) | ForEach-Object {
$atoms = $_ -split '\:'
$crons += "$([int]$atoms[1]) $([int]$atoms[0]) * * *"
}
Add-PodeSchedule -Name '__pode_restart_times__' -Cron @($crons) -ScriptBlock {
$PodeContext.Tokens.Restart.Cancel()
}
}
# crons - setup schedules
$crons = @(@($restart.crons) -ne $null)
if (($crons | Measure-Object).Count -gt 0) {
Add-PodeSchedule -Name '__pode_restart_crons__' -Cron @($crons) -ScriptBlock {
$PodeContext.Tokens.Restart.Cancel()
}
}
}
function Set-PodeOutputVariables {
if (Test-PodeIsEmpty $PodeContext.Server.Output.Variables) {
return
}
foreach ($key in $PodeContext.Server.Output.Variables.Keys) {
try {
Set-Variable -Name $key -Value $PodeContext.Server.Output.Variables[$key] -Force -Scope Global
}
catch {
$_ | Write-PodeErrorLog
}
}
}
function Get-PodeCronFields {
return @(
'Minute',
'Hour',
'DayOfMonth',
'Month',
'DayOfWeek'
)
}
function Get-PodeCronFieldConstraints {
return @{
MinMax = @(
@(0, 59),
@(0, 23),
@(1, 31),
@(1, 12),
@(0, 6)
)
DaysInMonths = @(
31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
)
Months = @(
'January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'
)
}
}
function Get-PodeCronPredefined {
return @{
# normal
'@minutely' = '* * * * *'
'@hourly' = '0 * * * *'
'@daily' = '0 0 * * *'
'@weekly' = '0 0 * * 0'
'@monthly' = '0 0 1 * *'
'@quarterly' = '0 0 1 1,4,7,10 *'
'@yearly' = '0 0 1 1 *'
'@annually' = '0 0 1 1 *'
# twice
'@twice-hourly' = '0,30 * * * *'
'@twice-daily' = '0 0,12 * * *'
'@twice-weekly' = '0 0 * * 0,4'
'@twice-monthly' = '0 0 1,15 * *'
'@twice-yearly' = '0 0 1 1,6 *'
'@twice-annually' = '0 0 1 1,6 *'
}
}
function Get-PodeCronFieldAliases {
return @{
Month = @{
Jan = 1
Feb = 2
Mar = 3
Apr = 4
May = 5
Jun = 6
Jul = 7
Aug = 8
Sep = 9
Oct = 10
Nov = 11
Dec = 12
}
DayOfWeek = @{
Sun = 0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
}
}
}
function ConvertFrom-PodeCronExpressions {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]
$Expressions
)
return @(@($Expressions) | ForEach-Object {
ConvertFrom-PodeCronExpression -Expression $_
})
}
function ConvertFrom-PodeCronExpression {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Expression
)
$Expression = $Expression.Trim()
# check predefineds
$predef = Get-PodeCronPredefined
if (!(Test-PodeIsEmpty $predef[$Expression])) {
$Expression = $predef[$Expression]
}
# split and check atoms length
$atoms = @($Expression -isplit '\s+')
if ($atoms.Length -ne 5) {
throw "Cron expression should only consist of 5 parts: $($Expression)"
}
# basic variables
$aliasRgx = '(?<tag>[a-z]{3})'
# get cron obj and validate atoms
$fields = Get-PodeCronFields
$constraints = Get-PodeCronFieldConstraints
$aliases = Get-PodeCronFieldAliases
$cron = @{}
for ($i = 0; $i -lt $atoms.Length; $i++) {
$_cronExp = @{
Range = $null
Values = $null
Constraints = $null
Random = $false
WildCard = $false
}
$_atom = $atoms[$i]
$_field = $fields[$i]
$_constraint = $constraints.MinMax[$i]
$_aliases = $aliases[$_field]
# replace day of week and months with numbers
if (@('month', 'dayofweek') -icontains $_field) {
while ($_atom -imatch $aliasRgx) {
$_alias = $_aliases[$Matches['tag']]
if ($null -eq $_alias) {
throw "Invalid $($_field) alias found: $($Matches['tag'])"
}
$_atom = $_atom -ireplace $Matches['tag'], $_alias
$null = $_atom -imatch $aliasRgx
}
}
# ensure atom is a valid value
if (!($_atom -imatch '^[\d|/|*|\-|,r]+$')) {
throw "Invalid atom character: $($_atom)"
}
# replace * with min/max constraint
if ($_atom -ieq '*') {
$_cronExp.WildCard = $true
$_atom = ($_constraint -join '-')
}
# parse the atom for either a literal, range, array, or interval
# literal
if ($_atom -imatch '^(\d+|r)$') {
# check if it's random
if ($_atom -ieq 'r') {
$_cronExp.Values = @(Get-Random -Minimum $_constraint[0] -Maximum ($_constraint[1] + 1))
$_cronExp.Random = $true
}
else {
$_cronExp.Values = @([int]$_atom)
}
}
# range
elseif ($_atom -imatch '^(?<min>\d+)\-(?<max>\d+)$') {
$_cronExp.Range = @{ 'Min' = [int]($Matches['min'].Trim()); 'Max' = [int]($Matches['max'].Trim()); }
}
# array
elseif ($_atom -imatch '^[\d,]+$') {
$_cronExp.Values = [int[]](@($_atom -split ',').Trim())
}
# interval
elseif ($_atom -imatch '(?<start>(\d+|\*))\/(?<interval>(\d+|r))$') {
$start = $Matches['start']
$interval = $Matches['interval']
if ($interval -ieq '0') {
$interval = '1'
}
if ([string]::IsNullOrWhiteSpace($start) -or ($start -ieq '*')) {
$start = '0'
}
# set the initial trigger value
$_cronExp.Values = @([int]$start)
# check if it's random
if ($interval -ieq 'r') {
$_cronExp.Random = $true
}
else {
# loop to get all next values
$next = [int]$start + [int]$interval
while ($next -le $_constraint[1]) {
$_cronExp.Values += $next
$next += [int]$interval
}
}
}
# error
else {
throw "Invalid cron atom format found: $($_atom)"
}
# ensure cron expression values are valid
if ($null -ne $_cronExp.Range) {
if ($_cronExp.Range.Min -gt $_cronExp.Range.Max) {
throw "Min value for $($_field) should not be greater than the max value"
}
if ($_cronExp.Range.Min -lt $_constraint[0]) {
throw "Min value '$($_cronExp.Range.Min)' for $($_field) is invalid, should be greater than/equal to $($_constraint[0])"
}
if ($_cronExp.Range.Max -gt $_constraint[1]) {
throw "Max value '$($_cronExp.Range.Max)' for $($_field) is invalid, should be less than/equal to $($_constraint[1])"
}
}
if ($null -ne $_cronExp.Values) {
$_cronExp.Values | ForEach-Object {
if ($_ -lt $_constraint[0] -or $_ -gt $_constraint[1]) {
throw "Value '$($_)' for $($_field) is invalid, should be between $($_constraint[0]) and $($_constraint[1])"
}
}
}
# assign value
$_cronExp.Constraints = $_constraint
$cron[$_field] = $_cronExp
}
# post validation for month/days in month
if (($null -ne $cron['Month'].Values) -and ($null -ne $cron['DayOfMonth'].Values)) {
foreach ($mon in $cron['Month'].Values) {
foreach ($day in $cron['DayOfMonth'].Values) {
if ($day -gt $constraints.DaysInMonths[$mon - 1]) {
throw "$($constraints.Months[$mon - 1]) only has $($constraints.DaysInMonths[$mon - 1]) days, but $($day) was supplied"
}
}
}
}
# flag if this cron contains a random atom
$cron['Random'] = (($cron.Values | Where-Object { $_.Random } | Measure-Object).Count -gt 0)
# return the parsed cron expression
return $cron
}
function Reset-PodeRandomCronExpressions {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Expressions
)
return @(@($Expressions) | ForEach-Object {
Reset-PodeRandomCronExpression -Expression $_
})
}
function Reset-PodeRandomCronExpression {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Expression
)
function Reset-Atom($Atom) {
if (!$Atom.Random) {
return $Atom
}
if ($Atom.Random) {
$Atom.Values = @(Get-Random -Minimum $Atom.Constraints[0] -Maximum ($Atom.Constraints[1] + 1))
}
return $Atom
}
if (!$Expression.Random) {
return $Expression
}
$Expression.Minute = (Reset-Atom -Atom $Expression.Minute)
$Expression.Hour = (Reset-Atom -Atom $Expression.Hour)
$Expression.DayOfMonth = (Reset-Atom -Atom $Expression.DayOfMonth)
$Expression.Month = (Reset-Atom -Atom $Expression.Month)
$Expression.DayOfWeek = (Reset-Atom -Atom $Expression.DayOfWeek)
return $Expression
}
function Test-PodeCronExpressions {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Expressions,
[Parameter()]
$DateTime = $null
)
return ((@($Expressions) | Where-Object {
Test-PodeCronExpression -Expression $_ -DateTime $DateTime
} | Measure-Object).Count -gt 0)
}
function Test-PodeCronExpression {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Expression,
[Parameter()]
$DateTime = $null
)
function Test-RangeAndValue($AtomContraint, $NowValue) {
if ($null -ne $AtomContraint.Range) {
return (!(($NowValue -lt $AtomContraint.Range.Min) -or ($NowValue -gt $AtomContraint.Range.Max)))
}
return ($AtomContraint.Values -icontains $NowValue)
}
# current time
if ($null -eq $DateTime) {
$DateTime = [datetime]::Now
}
# check day of month
if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfMonth -NowValue $DateTime.Day)) {
return $false
}
# check day of week
if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfWeek -NowValue ([int]$DateTime.DayOfWeek))) {
return $false
}
# check month
if (!(Test-RangeAndValue -AtomContraint $Expression.Month -NowValue $DateTime.Month)) {
return $false
}
# check hour
if (!(Test-RangeAndValue -AtomContraint $Expression.Hour -NowValue $DateTime.Hour)) {
return $false
}
# check minute
if (!(Test-RangeAndValue -AtomContraint $Expression.Minute -NowValue $DateTime.Minute)) {
return $false
}
# date is valid
return $true
}
function Get-PodeCronNextEarliestTrigger {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Expressions,
[Parameter()]
$StartTime = $null,
[Parameter()]
$EndTime = $null
)
return (@($Expressions) | Foreach-Object {
Get-PodeCronNextTrigger -Expression $_ -StartTime $StartTime -EndTime $EndTime
} | Where-Object { $null -ne $_ } | Sort-Object | Select-Object -First 1)
}
function Get-PodeCronNextTrigger {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Expression,
[Parameter()]
$StartTime = $null,
[Parameter()]
$EndTime = $null
)
# start from the current time, if a start time not defined
if ($null -eq $StartTime) {
$StartTime = [datetime]::Now
}
$StartTime = $StartTime.AddMinutes(1)
# the next time to trigger
$NextTime = [datetime]::new($StartTime.Year, $StartTime.Month, $StartTime.Day, $StartTime.Hour, $StartTime.Minute, 0)
# first, is the current time valid?
if (Test-PodeCronExpression -Expression $Expression -DateTime $NextTime) {
return $NextTime
}
# functions for getting the closest value
function Get-ClosestValue($AtomContraint, $NowValue) {
$_values = $AtomContraint.Values
if ($null -eq $_values) {
$_values = ($AtomContraint.Range.Min..$AtomContraint.Range.Max)
}
if (($_values.Length -eq 1) -or ($_values[-1] -lt $NowValue) -or ($_values[0] -gt $NowValue)) {
return $_values[0]
}
return ($_values -ge $NowValue)[0]
}
# loop until we get a date
while ($true) {
# check the minute
if (!$Expression.Minute.WildCard) {
$minute = Get-ClosestValue -AtomContraint $Expression.Minute -NowValue $NextTime.Minute
if ($minute -lt $NextTime.Minute) {
$NextTime = $NextTime.AddHours(1)
}
$NextTime = $NextTime.AddMinutes($minute - $NextTime.Minute)
}
# check hour
if (!$Expression.Hour.WildCard) {
$hour = Get-ClosestValue -AtomContraint $Expression.Hour -NowValue $NextTime.Hour
if ($hour -lt $NextTime.Hour) {
$NextTime = $NextTime.AddDays(1)
}
$_hour = $NextTime.Hour
$NextTime = $NextTime.AddHours($hour - $NextTime.Hour)
if ($_hour -ne $hour) {
$NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, $NextTime.Day, $NextTime.Hour, 0, 0)
continue
}
}
# check day
if (!$Expression.DayOfMonth.WildCard) {
$day = Get-ClosestValue -AtomContraint $Expression.DayOfMonth -NowValue $NextTime.Day
if (($day -lt $NextTime.Day) -or ($day -gt [datetime]::DaysInMonth($NextTime.Year, $NextTime.Month))) {
$NextTime = $NextTime.AddMonths(1)
}
if ($day -gt [datetime]::DaysInMonth($NextTime.Year, $NextTime.Month)) {
$NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, 1, 0, 0, 0)
continue
}
$_day = $NextTime.Day
$NextTime = $NextTime.AddDays($day - $NextTime.Day)
if ($_day -ne $day) {
$NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, $NextTime.Day, 0, 0, 0)
continue
}
}
# check month
if (!$Expression.Month.WildCard) {
$month = Get-ClosestValue -AtomContraint $Expression.Month -NowValue $NextTime.Month
if ($month -lt $NextTime.Month) {
$NextTime = $NextTime.AddYears(1)
}
$_month = $NextTime.Month
$NextTime = $NextTime.AddMonths($month - $NextTime.Month)
if ($_month -ne $month) {
$NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, 1, 0, 0, 0)
continue
}
}
# check day of week
if (!$Expression.DayOfWeek.WildCard) {
$doweek = Get-ClosestValue -AtomContraint $Expression.DayOfWeek -NowValue $NextTime.DayOfWeek
$_doweek = $NextTime.DayOfWeek
if ($doweek -lt $NextTime.DayOfWeek) {
$NextTime = $NextTime.AddDays(7 - ($NextTime.DayOfWeek - $doweek))
}
elseif ($doweek -gt $NextTime.DayOfWeek) {
$NextTime = $NextTime.AddDays($doweek - $NextTime.DayOfWeek)
}
if ($_doweek -ne $doweek) {
$NextTime = [datetime]::new($NextTime.Year, $NextTime.Month, $NextTime.Day, 0, 0, 0)
continue
}
}
break
}
# before we return, make sure the time is valid
if (!(Test-PodeCronExpression -Expression $Expression -DateTime $NextTime)) {
throw "Looks like something went wrong trying to calculate the next trigger datetime: $($NextTime)"
}
# if before the start or after end then return null
if (($NextTime -lt $StartTime) -or (($null -ne $EndTime) -and ($NextTime -gt $EndTime))) {
return $null
}
return $NextTime
}
function Invoke-PodeHMACSHA256Hash {
[CmdletBinding(DefaultParameterSetName = 'String')]
param(
[Parameter(Mandatory = $true)]
[string]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'String')]
[string]
$Secret,
[Parameter(Mandatory = $true, ParameterSetName = 'Bytes')]
[byte[]]
$SecretBytes
)
if (![string]::IsNullOrWhiteSpace($Secret)) {
$SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
}
if ($SecretBytes.Length -eq 0) {
throw 'No secret supplied for HMAC256 hash'
}
$crypto = [System.Security.Cryptography.HMACSHA256]::new($SecretBytes)
return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}
function Invoke-PodeHMACSHA384Hash {
[CmdletBinding(DefaultParameterSetName = 'String')]
param(
[Parameter(Mandatory = $true)]
[string]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'String')]
[string]
$Secret,
[Parameter(Mandatory = $true, ParameterSetName = 'Bytes')]
[byte[]]
$SecretBytes
)
if (![string]::IsNullOrWhiteSpace($Secret)) {
$SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
}
if ($SecretBytes.Length -eq 0) {
throw 'No secret supplied for HMAC384 hash'
}
$crypto = [System.Security.Cryptography.HMACSHA384]::new($SecretBytes)
return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}
function Invoke-PodeHMACSHA512Hash {
[CmdletBinding(DefaultParameterSetName = 'String')]
param(
[Parameter(Mandatory = $true)]
[string]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'String')]
[string]
$Secret,
[Parameter(Mandatory = $true, ParameterSetName = 'Bytes')]
[byte[]]
$SecretBytes
)
if (![string]::IsNullOrWhiteSpace($Secret)) {
$SecretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
}
if ($SecretBytes.Length -eq 0) {
throw 'No secret supplied for HMAC512 hash'
}
$crypto = [System.Security.Cryptography.HMACSHA512]::new($SecretBytes)
return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}
function Invoke-PodeSHA256Hash {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Value
)
$crypto = [System.Security.Cryptography.SHA256]::Create()
return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}
function Invoke-PodeSHA1Hash {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Value
)
$crypto = [System.Security.Cryptography.SHA1]::Create()
return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}
function ConvertTo-PodeBase64Auth {
param(
[Parameter(Mandatory = $true)]
[string]
$Username,
[Parameter(Mandatory = $true)]
[string]
$Password
)
return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($Username):$($Password)"))
}
function Invoke-PodeMD5Hash {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Value
)
$crypto = [System.Security.Cryptography.MD5]::Create()
return [System.BitConverter]::ToString($crypto.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($Value))).Replace('-', '').ToLowerInvariant()
}
function Get-PodeRandomBytes {
param(
[Parameter()]
[int]
$Length = 16
)
return (Use-PodeStream -Stream ([System.Security.Cryptography.RandomNumberGenerator]::Create()) {
param($p)
$bytes = [byte[]]::new($Length)
$p.GetBytes($bytes)
return $bytes
})
}
function New-PodeSalt {
param(
[Parameter()]
[int]
$Length = 8
)
$bytes = [byte[]](Get-PodeRandomBytes -Length $Length)
return [System.Convert]::ToBase64String($bytes)
}
function New-PodeGuid {
param(
[Parameter()]
[int]
$Length = 16,
[switch]
$Secure,
[switch]
$NoDashes
)
# generate a cryptographically secure guid
if ($Secure) {
$bytes = [byte[]](Get-PodeRandomBytes -Length $Length)
$guid = ([guid]::new($bytes)).ToString()
}
# return a normal guid
else {
$guid = ([guid]::NewGuid()).ToString()
}
if ($NoDashes) {
$guid = ($guid -ireplace '-', '')
}
return $guid
}
function Invoke-PodeValueSign {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Value,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Secret,
[switch]
$Strict
)
if ($Strict) {
$Secret = ConvertTo-PodeStrictSecret -Secret $Secret
}
return "s:$($Value).$(Invoke-PodeHMACSHA256Hash -Value $Value -Secret $Secret)"
}
function Invoke-PodeValueUnsign {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Value,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Secret,
[switch]
$Strict
)
# the signed value must start with "s:"
if (!$Value.StartsWith('s:')) {
return $null
}
# the signed value must contain a dot - splitting value and signature
$Value = $Value.Substring(2)
$periodIndex = $Value.LastIndexOf('.')
if ($periodIndex -eq -1) {
return $null
}
if ($Strict) {
$Secret = ConvertTo-PodeStrictSecret -Secret $Secret
}
# get the raw value and signature
$raw = $Value.Substring(0, $periodIndex)
$sig = $Value.Substring($periodIndex + 1)
if ((Invoke-PodeHMACSHA256Hash -Value $raw -Secret $Secret) -ne $sig) {
return $null
}
return $raw
}
function Test-PodeValueSigned {
param(
[Parameter(ValueFromPipeline = $true)]
[string]
$Value,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Secret,
[switch]
$Strict
)
if ([string]::IsNullOrEmpty($Value)) {
return $false
}
$result = Invoke-PodeValueUnsign -Value $Value -Secret $Secret -Strict:$Strict
return ![string]::IsNullOrEmpty($result)
}
function ConvertTo-PodeStrictSecret {
param(
[Parameter(Mandatory = $true)]
[string]
$Secret
)
return "$($Secret);$($WebEvent.Request.UserAgent);$($WebEvent.Request.RemoteEndPoint.Address.IPAddressToString)"
}
function New-PodeJwtSignature {
param(
[Parameter(Mandatory = $true)]
[string]
$Algorithm,
[Parameter(Mandatory = $true)]
[string]
$Token,
[Parameter()]
[byte[]]
$SecretBytes
)
if (($Algorithm -ine 'none') -and (($null -eq $SecretBytes) -or ($SecretBytes.Length -eq 0))) {
throw 'No Secret supplied for JWT signature'
}
if (($Algorithm -ieq 'none') -and (($null -ne $secretBytes) -and ($SecretBytes.Length -gt 0))) {
throw 'Expected no secret to be supplied for no signature'
}
$sig = $null
switch ($Algorithm.ToUpperInvariant()) {
'HS256' {
$sig = Invoke-PodeHMACSHA256Hash -Value $Token -SecretBytes $SecretBytes
$sig = ConvertTo-PodeBase64UrlValue -Value $sig -NoConvert
}
'HS384' {
$sig = Invoke-PodeHMACSHA384Hash -Value $Token -SecretBytes $SecretBytes
$sig = ConvertTo-PodeBase64UrlValue -Value $sig -NoConvert
}
'HS512' {
$sig = Invoke-PodeHMACSHA512Hash -Value $Token -SecretBytes $SecretBytes
$sig = ConvertTo-PodeBase64UrlValue -Value $sig -NoConvert
}
'NONE' {
$sig = [string]::Empty
}
default {
throw "The JWT algorithm is not currently supported: $($Algorithm)"
}
}
return $sig
}
function ConvertTo-PodeBase64UrlValue {
param(
[Parameter(Mandatory = $true)]
[string]
$Value,
[switch]
$NoConvert
)
if (!$NoConvert) {
$Value = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value))
}
$Value = ($Value -ireplace '\+', '-')
$Value = ($Value -ireplace '/', '_')
$Value = ($Value -ireplace '=', '')
return $Value
}
function ConvertFrom-PodeJwtBase64Value {
param(
[Parameter(Mandatory = $true)]
[string]
$Value
)
# map chars
$Value = ($Value -ireplace '-', '+')
$Value = ($Value -ireplace '_', '/')
# add padding
switch ($Value.Length % 4) {
1 {
$Value = $Value.Substring(0, $Value.Length - 1)
}
2 {
$Value += '=='
}
3 {
$Value += '='
}
}
# convert base64 to string
try {
$Value = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Value))
}
catch {
throw 'Invalid Base64 encoded value found in JWT'
}
# return json
try {
return ($Value | ConvertFrom-Json)
}
catch {
throw 'Invalid JSON value found in JWT'
}
}
function Find-PodeEndpoints {
param(
[Parameter()]
[ValidateSet('', 'Http', 'Https')]
[string]
$Protocol,
[Parameter()]
[string]
$Address,
[Parameter()]
[string[]]
$EndpointName
)
$endpoints = @()
# just use a single endpoint/protocol
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
$endpoints += @{
Protocol = $Protocol
Address = $Address
Name = [string]::Empty
}
}
# get all defined endpoints by name
else {
foreach ($name in @($EndpointName)) {
$_endpoint = Get-PodeEndpointByName -Name $name -ThrowError
if ($null -ne $_endpoint) {
$endpoints += @{
Protocol = $_endpoint.Protocol
Address = $_endpoint.RawAddress
Name = $name
}
}
}
}
# convert the endpoint's address into host:port format
foreach ($_endpoint in $endpoints) {
if (![string]::IsNullOrWhiteSpace($_endpoint.Address)) {
$_addr = Get-PodeEndpointInfo -Address $_endpoint.Address -AnyPortOnZero
$_endpoint.Address = "$($_addr.Host):$($_addr.Port)"
}
}
return $endpoints
}
function Get-PodeEndpoints {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Http', 'Ws', 'Smtp', 'Tcp')]
[string[]]
$Type
)
$endpoints = @()
foreach ($t in $Type) {
switch ($t.ToLowerInvariant()) {
'http' {
$endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('http', 'https') -icontains $_.Protocol })
}
'ws' {
$endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('ws', 'wss') -icontains $_.Protocol })
}
'smtp' {
$endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('smtp', 'smtps') -icontains $_.Protocol })
}
'tcp' {
$endpoints += @($PodeContext.Server.Endpoints.Values | Where-Object { @('tcp', 'tcps') -icontains $_.Protocol })
}
}
}
return $endpoints
}
function Test-PodeEndpointProtocol {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Http', 'Https', 'Ws', 'Wss', 'Smtp', 'Smtps', 'Tcp', 'Tcps')]
[string]
$Protocol
)
$endpoint = $PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -ieq $Protocol }
return ($null -ne $endpoint)
}
function Get-PodeEndpointType {
param(
[Parameter()]
[ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
[string]
$Protocol
)
switch ($Protocol) {
{ $_ -iin @('http', 'https') } {
'Http'
}
{ $_ -iin @('ws', 'wss') } {
'Ws'
}
{ $_ -iin @('smtp', 'smtps') } {
'Smtp'
}
{ $_ -iin @('tcp', 'tcps') } {
'Tcp'
}
default {
$Protocol
}
}
}
function Get-PodeEndpointRunspacePoolName {
param(
[Parameter()]
[ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
[string]
$Protocol
)
switch ($Protocol) {
{ $_ -iin @('http', 'https') } {
'Web'
}
{ $_ -iin @('ws', 'wss') } {
'Signals'
}
{ $_ -iin @('smtp', 'smtps') } {
'Smtp'
}
{ $_ -iin @('tcp', 'tcps') } {
'Tcp'
}
default {
$Protocol
}
}
}
function Test-PodeEndpoints {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Http', 'Ws', 'Smtp', 'Tcp')]
[string]
$Type
)
$endpoints = (Get-PodeEndpoints -Type $Type)
return (($null -ne $endpoints) -and ($endpoints.Length -gt 0))
}
function Find-PodeEndpointName {
param(
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Address,
[Parameter()]
[System.Net.EndPoint]
$LocalAddress,
[switch]
$Force,
[switch]
$ThrowError,
[switch]
$Enabled
)
if (!$Enabled -and !$Force) {
return $null
}
if ([string]::IsNullOrWhiteSpace($Protocol) -or
[string]::IsNullOrWhiteSpace($Address) -or
[string]::IsNullOrWhiteSpace($LocalAddress)) {
return $null
}
<#
using Host header
#>
# add a default port to the address if missing
if (!$Address.Contains(':')) {
$port = Get-PodeDefaultPort -Protocol $Protocol -Real -TlsMode Implicit
$Address = "$($Address):$($port)"
}
# change localhost/computer name to ip address
if (($Address -ilike 'localhost:*') -or ($Address -ilike "$($PodeContext.Server.ComputerName):*")) {
$Address = ($Address -ireplace "(localhost|$([regex]::Escape($PodeContext.Server.ComputerName)))\:", "(127\.0\.0\.1|0\.0\.0\.0|\:\:ffff\:127\.0\.0\.1|\:\:ffff\:0\:0|\[\:\:\]|\[\:\:1\]|\:\:1|\:\:|localhost|$([regex]::Escape($PodeContext.Server.ComputerName))):")
}
else {
$Address = [regex]::Escape($Address)
}
# create the endpoint key for address
$key = "$($Protocol)\|$($Address)"
# try and find endpoint for address
$key = @(foreach ($k in $PodeContext.Server.EndpointsMap.Keys) {
if ($k -imatch $key) {
$k
break
}
})[0]
if (![string]::IsNullOrWhiteSpace($key) -and $PodeContext.Server.EndpointsMap.ContainsKey($key)) {
return $PodeContext.Server.EndpointsMap[$key]
}
<#
using local endpoint from socket
#>
# setup the local address as a string
$_localAddress = "$($LocalAddress.Address.IPAddressToString):$($LocalAddress.Port)"
$_localAddress = [regex]::Escape($_localAddress)
# create the endpoint key for local address
$key = "$($Protocol)\|$($_localAddress)"
# try and find endpoint for local address
$key = @(foreach ($k in $PodeContext.Server.EndpointsMap.Keys) {
if ($k -imatch $key) {
$k
break
}
})[0]
if (![string]::IsNullOrWhiteSpace($key) -and $PodeContext.Server.EndpointsMap.ContainsKey($key)) {
return $PodeContext.Server.EndpointsMap[$key]
}
<#
check for * address
#>
# set * address as string
$_anyAddress = "(0\.0\.0\.0|\[\:\:\]|\:\:|\:\:ffff\:0\:0):$($LocalAddress.Port)"
$key = "$($Protocol)\|$($_anyAddress)"
# try and find endpoint for any address
$key = @(foreach ($k in $PodeContext.Server.EndpointsMap.Keys) {
if ($k -imatch $key) {
$k
break
}
})[0]
if (![string]::IsNullOrWhiteSpace($key) -and $PodeContext.Server.EndpointsMap.ContainsKey($key)) {
return $PodeContext.Server.EndpointsMap[$key]
}
# error?
if ($ThrowError) {
throw "Endpoint with protocol '$($Protocol)' and address '$($Address)' or local address '$($_localAddress)' does not exist"
}
return $null
}
function Get-PodeEndpointByName {
param(
[Parameter()]
[string]
$Name,
[switch]
$ThrowError
)
# if an EndpointName was supplied, find it and use it
if ([string]::IsNullOrWhiteSpace($Name)) {
return $null
}
# ensure it exists
if ($PodeContext.Server.Endpoints.ContainsKey($Name)) {
return $PodeContext.Server.Endpoints[$Name]
}
# error?
if ($ThrowError) {
throw "Endpoint with name '$($Name)' does not exist"
}
return $null
}
function Invoke-PodeEndware {
param(
[Parameter()]
$Endware
)
# if there's no endware, do nothing
if (($null -eq $Endware) -or ($Endware.Length -eq 0)) {
return
}
# loop through each of the endware, invoking the next if it returns true
foreach ($eware in @($Endware)) {
if (($null -eq $eware) -or ($null -eq $eware.Logic)) {
continue
}
try {
$null = Invoke-PodeScriptBlock -ScriptBlock $eware.Logic -Arguments $eware.Arguments -UsingVariables $eware.UsingVariables -Scoped -Splat
}
catch {
$_ | Write-PodeErrorLog
}
}
}
function Invoke-PodeEvent {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')]
[string]
$Type
)
# do nothing if no events
if (($null -eq $PodeContext.Server.Events) -or ($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 {
$null = Invoke-PodeScriptBlock -ScriptBlock $evt.ScriptBlock -Arguments $evt.Arguments -UsingVariables $evt.UsingVariables -Scoped -Splat
}
catch {
$_ | Write-PodeErrorLog
}
}
}
function Start-PodeFileMonitor {
# don't configure if not supplied, or we're running as serverless
if (!$PodeContext.Server.FileMonitor.Enabled -or $PodeContext.Server.IsServerless) {
return
}
# what folder and filter are we moitoring?
$folder = $PodeContext.Server.Root
$filter = '*.*'
# setup the file monitor
$watcher = New-Object System.IO.FileSystemWatcher $folder, $filter -Property @{
IncludeSubdirectories = $true
NotifyFilter = [System.IO.NotifyFilters]'FileName,LastWrite,CreationTime'
}
$watcher.EnableRaisingEvents = $true
# setup the monitor timer - only restart server after changes + 2s of no changes
$timer = New-Object System.Timers.Timer
$timer.AutoReset = $false
$timer.Interval = 2000
# setup the message data for the events
$msgData = @{
Timer = $timer
Settings = $PodeContext.Server.FileMonitor
}
# setup the events script logic
$action = {
# if there are exclusions, and one matches, return
if (($null -ne $Event.MessageData.Settings.Exclude) -and ($Event.SourceEventArgs.Name -imatch $Event.MessageData.Settings.Exclude)) {
return
}
# if there are inclusions, and none match, return
if (($null -ne $Event.MessageData.Settings.Include) -and ($Event.SourceEventArgs.Name -inotmatch $Event.MessageData.Settings.Include)) {
return
}
# if enabled, add the file to the list of files that trigggered the restart
if ($Event.MessageData.Settings.ShowFiles) {
$name = "[$($Event.SourceEventArgs.ChangeType)] $($Event.SourceEventArgs.Name)"
if ($Event.MessageData.Settings.Files -inotcontains $name) {
$Event.MessageData.Settings.Files += $name
}
}
# restart the timer
$Event.MessageData.Timer.Stop()
$Event.MessageData.Timer.Start()
}
# listen out of file created, changed, deleted events
Register-ObjectEvent -InputObject $watcher -EventName 'Created' `
-SourceIdentifier (Get-PodeFileMonitorName Create) -Action $action -MessageData $msgData -SupportEvent
Register-ObjectEvent -InputObject $watcher -EventName 'Changed' `
-SourceIdentifier (Get-PodeFileMonitorName Update) -Action $action -MessageData $msgData -SupportEvent
Register-ObjectEvent -InputObject $watcher -EventName 'Deleted' `
-SourceIdentifier (Get-PodeFileMonitorName Delete) -Action $action -MessageData $msgData -SupportEvent
# listen out for timer ticks to reset server
Register-ObjectEvent -InputObject $timer -EventName 'Elapsed' -SourceIdentifier (Get-PodeFileMonitorTimerName) -Action {
# if enabled, show the files that triggered the restart
if ($Event.MessageData.FileSettings.ShowFiles) {
if (!$Event.MessageData.Quiet) {
Write-Host 'The following files have changed:' -ForegroundColor Magenta
foreach ($file in $Event.MessageData.FileSettings.Files) {
Write-Host "> $($file)" -ForegroundColor Magenta
}
}
$Event.MessageData.FileSettings.Files = @()
}
# trigger the restart
$Event.MessageData.Tokens.Restart.Cancel()
$Event.Sender.Stop()
} -MessageData @{
Tokens = $PodeContext.Tokens
FileSettings = $PodeContext.Server.FileMonitor
Quiet = $PodeContext.Server.Quiet
} -SupportEvent
}
function Stop-PodeFileMonitor {
if ($PodeContext.Server.IsServerless) {
return
}
if ($PodeContext.Server.FileMonitor.Enabled) {
Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Create) -Force
Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Delete) -Force
Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Update) -Force
Unregister-Event -SourceIdentifier (Get-PodeFileMonitorTimerName) -Force
}
}
function Get-PodeFileMonitorName {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Create', 'Delete', 'Update')]
[string]
$Type
)
return "PodeFileMonitor$($Type)"
}
function Get-PodeFileMonitorTimerName {
return 'PodeFileMonitorTimer'
}
using namespace Pode
function Test-PodeFileWatchersExist {
return (($null -ne $PodeContext.Fim) -and (($PodeContext.Fim.Enabled) -or ($PodeContext.Fim.Items.Count -gt 0)))
}
function New-PodeFileWatcher {
$watcher = [PodeWatcher]::new($PodeContext.Tokens.Cancellation.Token)
$watcher.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
$watcher.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
return $watcher
}
function Start-PodeFileWatcherRunspace {
if (!(Test-PodeFileWatchersExist)) {
return
}
try {
# create the watcher
$watcher = New-PodeFileWatcher
# register file watchers and events
foreach ($item in $PodeContext.Fim.Items.Values) {
foreach ($path in $item.Paths) {
Write-Verbose "Creating FileWatcher for '$($path)'"
$fileWatcher = [PodeFileWatcher]::new($item.Name, $path, $item.IncludeSubdirectories, $item.InternalBufferSize, $item.NotifyFilters)
foreach ($evt in $item.Events) {
Write-Verbose "-> Registering event: $($evt)"
$fileWatcher.RegisterEvent($evt)
}
$watcher.AddFileWatcher($fileWatcher)
}
}
$watcher.Start()
$PodeContext.Watchers += $watcher
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Close-PodeDisposable -Disposable $watcher
throw $_.Exception
}
$watchScript = {
param(
[Parameter(Mandatory = $true)]
$Watcher,
[Parameter(Mandatory = $true)]
[int]
$ThreadId
)
try {
while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
$evt = (Wait-PodeTask -Task $Watcher.GetFileEventAsync($PodeContext.Tokens.Cancellation.Token))
try {
try {
# get file watcher
$fileWatcher = $PodeContext.Fim.Items[$evt.FileWatcher.Name]
if ($null -eq $fileWatcher) {
continue
}
# if there are exclusions, and one matches, return
$exc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Exclude)
if (($null -ne $exc) -and ($evt.Name -imatch $exc)) {
continue
}
# if there are inclusions, and none match, return
$inc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Include)
if (($null -ne $inc) -and ($evt.Name -inotmatch $inc)) {
continue
}
# set file event object
$FileEvent = @{
Type = $evt.ChangeType
FullPath = $evt.FullPath
Name = $evt.Name
Old = @{
FullPath = $evt.OldFullPath
Name = $evt.OldName
}
Parameters = @{}
Lockable = $PodeContext.Threading.Lockables.Global
Timestamp = [datetime]::UtcNow
Metadata = @{}
}
# do we have any parameters?
if ($fileWatcher.Placeholders.Exist -and ($FileEvent.FullPath -imatch $fileWatcher.Placeholders.Path)) {
$FileEvent.Parameters = $Matches
}
# invoke main script
$null = Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat
}
catch [System.OperationCanceledException] {
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
}
finally {
$FileEvent = $null
Close-PodeDisposable -Disposable $evt
}
}
}
catch [System.OperationCanceledException] {
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
}
1..$PodeContext.Threads.Files | ForEach-Object {
Add-PodeRunspace -Type Files -ScriptBlock $watchScript -Parameters @{ 'Watcher' = $watcher; 'ThreadId' = $_ }
}
# script to keep file watcher server alive until cancelled
$waitScript = {
param(
[Parameter(Mandatory = $true)]
$Watcher
)
try {
while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
finally {
Close-PodeDisposable -Disposable $Watcher
}
}
Add-PodeRunspace -Type Files -ScriptBlock $waitScript -Parameters @{ 'Watcher' = $watcher } -NoProfile
}
function Test-PodeGuiEnabled {
return ($PodeContext.Server.Gui.Enabled -and
!$PodeContext.Server.IsServerless -and
!$PodeContext.Server.IsIIS -and
!$PodeContext.Server.IsHeroku)
}
function Start-PodeGuiRunspace {
# do nothing if gui not enabled, or running as serverless
if (!(Test-PodeGuiEnabled)) {
return
}
$script = {
try {
# if there are multiple endpoints, flag warning we're only using the first - unless explicitly set
if ($null -eq $PodeContext.Server.Gui.Endpoint) {
if ($PodeContext.Server.Endpoints.Values.Count -gt 1) {
Write-PodeHost 'Multiple endpoints defined, only the first will be used for the GUI' -ForegroundColor Yellow
}
}
# get the endpoint on which we're currently listening, or use explicitly passed one
$uri = (Get-PodeEndpointUrl -Endpoint $PodeContext.Server.Gui.Endpoint)
# poll the server for a response
$count = 0
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
try {
$null = Invoke-WebRequest -Method Get -Uri $uri -UseBasicParsing -ErrorAction Stop
if (!$?) {
throw
}
break
}
catch {
$count++
if ($count -le 50) {
Start-Sleep -Milliseconds 200
}
else {
throw "Failed to connect to URL: $($uri)"
}
}
}
# import the WPF assembly
$null = [System.Reflection.Assembly]::LoadWithPartialName('PresentationFramework')
$null = [System.Reflection.Assembly]::LoadWithPartialName('PresentationCore')
# Check for CefSharp
$loadCef = [bool]([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.FullName.StartsWith('CefSharp.Wpf,') })
# setup the WPF XAML for the server
# Check for CefSharp and used Chromium based WPF if Modules exists
if ($loadCef) {
$gui_browser = "
<Window
xmlns=`"http://schemas.microsoft.com/winfx/2006/xaml/presentation`"
xmlns:wpf=`"clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf`"
xmlns:x=`"http://schemas.microsoft.com/winfx/2006/xaml`"
Title=`"$($PodeContext.Server.Gui.Title)`"
Height=`"$($PodeContext.Server.Gui.Height)`"
Width=`"$($PodeContext.Server.Gui.Width)`"
ResizeMode=`"$($PodeContext.Server.Gui.ResizeMode)`"
WindowStartupLocation=`"CenterScreen`"
ShowInTaskbar = `"$($PodeContext.Server.Gui.ShowInTaskbar)`"
WindowStyle = `"$($PodeContext.Server.Gui.WindowStyle)`">
<Window.TaskbarItemInfo>
<TaskbarItemInfo />
</Window.TaskbarItemInfo>
<Border Grid.Row=`"1`" BorderBrush=`"Gray`" BorderThickness=`"0,1`">
<wpf:ChromiumWebBrowser x:Name=`"Browser`" Address=`"$uri`"/>
</Border>
</Window>"
}
else {
# Fall back to the IE based WPF Browser
$gui_browser = "
<Window
xmlns=`"http://schemas.microsoft.com/winfx/2006/xaml/presentation`"
xmlns:x=`"http://schemas.microsoft.com/winfx/2006/xaml`"
Title=`"$($PodeContext.Server.Gui.Title)`"
Height=`"$($PodeContext.Server.Gui.Height)`"
Width=`"$($PodeContext.Server.Gui.Width)`"
ResizeMode=`"$($PodeContext.Server.Gui.ResizeMode)`"
WindowStartupLocation=`"CenterScreen`"
ShowInTaskbar = `"$($PodeContext.Server.Gui.ShowInTaskbar)`"
WindowStyle = `"$($PodeContext.Server.Gui.WindowStyle)`">
<Window.TaskbarItemInfo>
<TaskbarItemInfo />
</Window.TaskbarItemInfo>
<WebBrowser Name=`"WebBrowser`"></WebBrowser>
</Window>"
}
# read in the XAML
$reader = [System.Xml.XmlNodeReader]::new([xml]$gui_browser)
$form = [Windows.Markup.XamlReader]::Load($reader)
# set other options
$form.TaskbarItemInfo.Description = $form.Title
# add the icon to the form
if (!(Test-PodeIsEmpty $PodeContext.Server.Gui.Icon)) {
$icon = [Uri]::new($PodeContext.Server.Gui.Icon)
$form.Icon = [Windows.Media.Imaging.BitmapFrame]::Create($icon)
}
# set the state of the window onload
if (!(Test-PodeIsEmpty $PodeContext.Server.Gui.WindowState)) {
$form.WindowState = $PodeContext.Server.Gui.WindowState
}
# get the browser object from XAML and navigate to base page if Cef is not loaded
if (!$loadCef) {
$form.FindName('WebBrowser').Navigate($uri)
}
# display the form
Write-PodeHost 'Opening GUI' -ForegroundColor Yellow
$null = $form.ShowDialog()
Start-Sleep -Seconds 1
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
finally {
# invoke the cancellation token to close the server
$PodeContext.Tokens.Cancellation.Cancel()
}
}
Add-PodeRunspace -Type Gui -ScriptBlock $script
}
using namespace Pode
# read in the content from a dynamic pode file and invoke its content
function ConvertFrom-PodeFile {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Content,
[Parameter()]
$Data = @{}
)
# if we have data, then setup the data param
if ($null -ne $Data -and $Data.Count -gt 0) {
$Content = "param(`$data)`nreturn `"$($Content -replace '"', '``"')`""
}
else {
$Content = "return `"$($Content -replace '"', '``"')`""
}
# invoke the content as a script to generate the dynamic content
return (Invoke-PodeScriptBlock -ScriptBlock ([scriptblock]::Create($Content)) -Arguments $Data -Return)
}
function Get-PodeViewEngineType {
param(
[Parameter(Mandatory = $true)]
[string]
$Path
)
# work out the engine to use when parsing the file
$type = $PodeContext.Server.ViewEngine.Type
$ext = Get-PodeFileExtension -Path $Path -TrimPeriod
if (![string]::IsNullOrWhiteSpace($ext) -and ($ext -ine $PodeContext.Server.ViewEngine.Extension)) {
$type = $ext
}
return $type
}
function Get-PodeFileContentUsingViewEngine {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[hashtable]
$Data
)
# work out the engine to use when parsing the file
$engine = Get-PodeViewEngineType -Path $Path
# setup the content
$content = [string]::Empty
# run the relevant engine logic
switch ($engine.ToLowerInvariant()) {
'html' {
$content = Get-Content -Path $Path -Raw -Encoding utf8
}
'md' {
$content = Get-Content -Path $Path -Raw -Encoding utf8
}
'pode' {
$content = Get-Content -Path $Path -Raw -Encoding utf8
$content = ConvertFrom-PodeFile -Content $content -Data $Data
}
default {
if ($null -ne $PodeContext.Server.ViewEngine.ScriptBlock) {
$_args = @($Path)
if (($null -ne $Data) -and ($Data.Count -gt 0)) {
$_args = @($Path, $Data)
}
$content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.ScriptBlock -Arguments $_args -UsingVariables $PodeContext.Server.ViewEngine.UsingVariables -Return -Splat)
}
}
}
return $content
}
function Get-PodeFileContent {
param(
[Parameter(Mandatory = $true)]
[string]
$Path
)
return (Get-Content -Path $Path -Raw -Encoding utf8)
}
function Get-PodeType {
param(
[Parameter()]
$Value
)
if ($null -eq $Value) {
return $null
}
$type = $Value.GetType()
return @{
Name = $type.Name.ToLowerInvariant()
BaseName = $type.BaseType.Name.ToLowerInvariant()
}
}
function Get-PodePSVersionTable {
return $PSVersionTable
}
function Test-PodeIsAdminUser {
# check the current platform, if it's unix then return true
if (Test-PodeIsUnix) {
return $true
}
try {
$principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
if ($null -eq $principal) {
return $false
}
return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
catch [exception] {
Write-PodeHost 'Error checking user administrator priviledges' -ForegroundColor Red
Write-PodeHost $_.Exception.Message -ForegroundColor Red
return $false
}
}
function Get-PodeHostIPRegex {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Both', 'Hostname', 'IP')]
[string]
$Type
)
$ip_rgx = '\[?([a-f0-9]*\:){1,}[a-f0-9]*((\d+\.){3}\d+)?\]?|((\d+\.){3}\d+)|\*|all'
$host_rgx = '([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+'
switch ($Type.ToLowerInvariant()) {
'both' {
return "(?<host>($($ip_rgx)|$($host_rgx)))"
}
'hostname' {
return "(?<host>($($host_rgx)))"
}
'ip' {
return "(?<host>($($ip_rgx)))"
}
}
}
function Get-PodePortRegex {
return '(?<port>\d+)'
}
function Get-PodeEndpointInfo {
param(
[Parameter()]
[string]
$Address,
[switch]
$AnyPortOnZero
)
if ([string]::IsNullOrWhiteSpace($Address)) {
return $null
}
$hostRgx = Get-PodeHostIPRegex -Type Both
$portRgx = Get-PodePortRegex
$cmbdRgx = "$($hostRgx)\:$($portRgx)"
# validate that we have a valid ip/host:port address
if (!(($Address -imatch "^$($cmbdRgx)$") -or ($Address -imatch "^$($hostRgx)[\:]{0,1}") -or ($Address -imatch "[\:]{0,1}$($portRgx)$"))) {
throw "Failed to parse '$($Address)' as a valid IP/Host:Port address"
}
# grab the ip address/hostname
$_host = $Matches['host']
if ([string]::IsNullOrWhiteSpace($_host)) {
$_host = '*'
}
# ensure we have a valid ip address/hostname
if (!(Test-PodeIPAddress -IP $_host)) {
throw "The IP address supplied is invalid: $($_host)"
}
# grab the port
$_port = $Matches['port']
if ([string]::IsNullOrWhiteSpace($_port)) {
$_port = 0
}
# ensure the port is valid
if ($_port -lt 0) {
throw "The port cannot be negative: $($_port)"
}
# return the info
return @{
Host = $_host
Port = (Resolve-PodeValue -Check ($AnyPortOnZero -and ($_port -eq 0)) -TrueValue '*' -FalseValue $_port)
}
}
function Test-PodeIPAddress {
param(
[Parameter()]
[string]
$IP,
[switch]
$IPOnly
)
if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) {
return $true
}
if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") {
return (!$IPOnly)
}
try {
$null = [System.Net.IPAddress]::Parse($IP)
return $true
}
catch [exception] {
return $false
}
}
function Test-PodeHostname {
param(
[Parameter()]
[string]
$Hostname
)
return ($Hostname -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$")
}
function ConvertTo-PodeIPAddress {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Address
)
return [System.Net.IPAddress]::Parse(([System.Net.IPEndPoint]$Address).Address.ToString())
}
function Get-PodeIPAddressesForHostname {
param(
[Parameter(Mandatory = $true)]
[string]
$Hostname,
[Parameter(Mandatory = $true)]
[ValidateSet('All', 'IPv4', 'IPv6')]
[string]
$Type
)
if (!(Test-PodeHostname -Hostname $Hostname)) {
return $Hostname
}
# get the ip addresses for the hostname
try {
$ips = @([System.Net.Dns]::GetHostAddresses($Hostname))
}
catch {
return '127.0.0.1'
}
# return ips based on type
switch ($Type.ToLowerInvariant()) {
'ipv4' {
$ips = @(foreach ($ip in $ips) {
if ($ip.AddressFamily -ieq 'InterNetwork') {
$ip
}
})
}
'ipv6' {
$ips = @(foreach ($ip in $ips) {
if ($ip.AddressFamily -ieq 'InterNetworkV6') {
$ip
}
})
}
}
return (@($ips)).IPAddressToString
}
function Test-PodeIPAddressLocal {
param(
[Parameter(Mandatory = $true)]
[string]
$IP
)
return (@('127.0.0.1', '::1', '[::1]', '::ffff:127.0.0.1', 'localhost') -icontains $IP)
}
function Test-PodeIPAddressAny {
param(
[Parameter(Mandatory = $true)]
[string]
$IP
)
return (@('0.0.0.0', '*', 'all', '::', '[::]') -icontains $IP)
}
function Test-PodeIPAddressLocalOrAny {
param(
[Parameter(Mandatory = $true)]
[string]
$IP
)
return ((Test-PodeIPAddressLocal -IP $IP) -or (Test-PodeIPAddressAny -IP $IP))
}
function Resolve-PodeIPDualMode {
param(
[Parameter()]
[ipaddress]
$IP
)
# do nothing if IPv6Any
if ($IP -eq [ipaddress]::IPv6Any) {
return $IP
}
# check loopbacks
if (($IP -eq [ipaddress]::Loopback) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) {
return @($IP, [ipaddress]::IPv6Loopback)
}
if ($IP -eq [ipaddress]::IPv6Loopback) {
return @($IP, [ipaddress]::Loopback)
}
# if iIPv4, convert and return both
if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) {
return @($IP, $IP.MapToIPv6())
}
# if IPv6, only convert if valid IPv4
if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) -and $IP.IsIPv4MappedToIPv6) {
return @($IP, $IP.MapToIPv4())
}
# just return the IP
return $IP
}
function Get-PodeIPAddress {
param(
[Parameter()]
[string]
$IP,
[switch]
$DualMode
)
# any address for IPv4 (or IPv6 for DualMode)
if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) {
if ($DualMode) {
return [System.Net.IPAddress]::IPv6Any
}
return [System.Net.IPAddress]::Any
}
# any address for IPv6 explicitly
if ($IP -iin @('::', '[::]')) {
return [System.Net.IPAddress]::IPv6Any
}
# localhost
if ($IP -ieq 'localhost') {
return [System.Net.IPAddress]::Loopback
}
# localhost IPv6 explicitly
if ($IP -iin @('[::1]', '::1')) {
return [System.Net.IPAddress]::IPv6Loopback
}
# hostname
if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") {
return $IP
}
# raw ip
return [System.Net.IPAddress]::Parse($IP)
}
function Test-PodeIPAddressInRange {
param(
[Parameter(Mandatory = $true)]
$IP,
[Parameter(Mandatory = $true)]
$LowerIP,
[Parameter(Mandatory = $true)]
$UpperIP
)
if ($IP.Family -ine $LowerIP.Family) {
return $false
}
$valid = $true
foreach ($i in 0..3) {
if (($IP.Bytes[$i] -lt $LowerIP.Bytes[$i]) -or ($IP.Bytes[$i] -gt $UpperIP.Bytes[$i])) {
$valid = $false
break
}
}
return $valid
}
function Test-PodeIPAddressIsSubnetMask {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$IP
)
return (($IP -split '/').Length -gt 1)
}
function Get-PodeSubnetRange {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$SubnetMask
)
# split for ip and number of 1 bits
$split = $SubnetMask -split '/'
if ($split.Length -le 1) {
return $null
}
$ip_parts = $split[0] -isplit '\.'
$bits = [int]$split[1]
# generate the netmask
$network = @('', '', '', '')
$count = 0
foreach ($i in 0..3) {
foreach ($b in 1..8) {
$count++
if ($count -le $bits) {
$network[$i] += '1'
}
else {
$network[$i] += '0'
}
}
}
# covert netmask to bytes
foreach ($i in 0..3) {
$network[$i] = [Convert]::ToByte($network[$i], 2)
}
# calculate the bottom range
$bottom = @(foreach ($i in 0..3) {
[byte]([byte]$network[$i] -band [byte]$ip_parts[$i])
})
# calculate the range
$range = @(foreach ($i in 0..3) {
256 + (-bnot [byte]$network[$i])
})
# calculate the top range
$top = @(foreach ($i in 0..3) {
[byte]([byte]$ip_parts[$i] + [byte]$range[$i])
})
return @{
'Lower' = ($bottom -join '.')
'Upper' = ($top -join '.')
'Range' = ($range -join '.')
'Netmask' = ($network -join '.')
'IP' = ($ip_parts -join '.')
}
}
function Add-PodeRunspace {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files')]
[string]
$Type,
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock,
[Parameter()]
$Parameters,
[Parameter()]
[System.Management.Automation.PSDataCollection[psobject]]
$OutputStream = $null,
[switch]
$Forget,
[switch]
$NoProfile,
[switch]
$PassThru
)
try {
# create powershell pipelines
$ps = [powershell]::Create()
$ps.RunspacePool = $PodeContext.RunspacePools[$Type].Pool
# load modules/drives
if (!$NoProfile) {
$null = $ps.AddScript("Open-PodeRunspace -Type '$($Type)'")
}
# load main script
$null = $ps.AddScript($ScriptBlock)
# load parameters
if (!(Test-PodeIsEmpty $Parameters)) {
$Parameters.Keys | ForEach-Object {
$null = $ps.AddParameter($_, $Parameters[$_])
}
}
# start the pipeline
if ($null -eq $OutputStream) {
$pipeline = $ps.BeginInvoke()
}
else {
$pipeline = $ps.BeginInvoke($OutputStream, $OutputStream)
}
# do we need to remember this pipeline? sorry, what did you say?
if ($Forget) {
$null = $pipeline
}
# or do we need to return it for custom processing? ie: tasks
elseif ($PassThru) {
return @{
Pipeline = $ps
Handler = $pipeline
}
}
# or store it here for later clean-up
else {
$PodeContext.Runspaces += @{
Pool = $Type
Pipeline = $ps
Handler = $pipeline
Stopped = $false
}
}
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
}
function Open-PodeRunspace {
param(
[Parameter(Mandatory = $true)]
[string]
$Type
)
try {
Import-PodeModules
Add-PodePSDrives
$PodeContext.RunspacePools[$Type].State = 'Ready'
}
catch {
if ($PodeContext.RunspacePools[$Type].State -ieq 'waiting') {
$PodeContext.RunspacePools[$Type].State = 'Error'
}
$_ | Out-Default
$_.ScriptStackTrace | Out-Default
throw
}
}
function Close-PodeRunspaces {
param(
[switch]
$ClosePool
)
if ($PodeContext.Server.IsServerless) {
return
}
try {
if (!(Test-PodeIsEmpty $PodeContext.Runspaces)) {
Write-Verbose 'Waiting until all Listeners are disposed'
$count = 0
$continue = $false
while ($count -le 10) {
Start-Sleep -Seconds 1
$count++
$continue = $false
foreach ($listener in $PodeContext.Listeners) {
if (!$listener.IsDisposed) {
$continue = $true
break
}
}
foreach ($receiver in $PodeContext.Receivers) {
if (!$receiver.IsDisposed) {
$continue = $true
break
}
}
foreach ($watcher in $PodeContext.Watchers) {
if (!$watcher.IsDisposed) {
$continue = $true
break
}
}
if ($continue) {
continue
}
break
}
Write-Verbose 'All Listeners disposed'
# now dispose runspaces
Write-Verbose 'Disposing Runspaces'
$runspaceErrors = @(foreach ($item in $PodeContext.Runspaces) {
if ($item.Stopped) {
continue
}
try {
# only do this, if the pool is in error
if ($PodeContext.RunspacePools[$item.Pool].State -ieq 'error') {
$item.Pipeline.EndInvoke($item.Handler)
}
}
catch {
"$($item.Pool) runspace failed to load: $($_.Exception.InnerException.Message)"
}
Close-PodeDisposable -Disposable $item.Pipeline
$item.Stopped = $true
})
# dispose of schedule runspaces
if ($PodeContext.Schedules.Processes.Count -gt 0) {
foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) {
Close-PodeScheduleInternal -Process $PodeContext.Schedules.Processes[$key]
}
}
# dispose of task runspaces
if ($PodeContext.Tasks.Results.Count -gt 0) {
foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) {
Close-PodeTaskInternal -Result $PodeContext.Tasks.Results[$key]
}
}
$PodeContext.Runspaces = @()
Write-Verbose 'Runspaces disposed'
}
# close/dispose the runspace pools
if ($ClosePool) {
Close-PodeRunspacePools
}
# check for runspace errors
if (($null -ne $runspaceErrors) -and ($runspaceErrors.Length -gt 0)) {
foreach ($err in $runspaceErrors) {
if ($null -eq $err) {
continue
}
throw $err
}
}
# garbage collect
[GC]::Collect()
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
}
function Get-PodeConsoleKey {
if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) {
return $null
}
return [Console]::ReadKey($true)
}
function Test-PodeTerminationPressed {
param(
[Parameter()]
$Key = $null
)
if ($PodeContext.Server.DisableTermination) {
return $false
}
return (Test-PodeKeyPressed -Key $Key -Character 'c')
}
function Test-PodeRestartPressed {
param(
[Parameter()]
$Key = $null
)
return (Test-PodeKeyPressed -Key $Key -Character 'r')
}
function Test-PodeOpenBrowserPressed {
param(
[Parameter()]
$Key = $null
)
return (Test-PodeKeyPressed -Key $Key -Character 'b')
}
function Test-PodeKeyPressed {
param(
[Parameter()]
$Key = $null,
[Parameter(Mandatory = $true)]
[string]
$Character
)
if ($null -eq $Key) {
$Key = Get-PodeConsoleKey
}
return (($null -ne $Key) -and ($Key.Key -ieq $Character) -and
(($Key.Modifiers -band [ConsoleModifiers]::Control) -or ((Test-PodeIsUnix) -and ($Key.Modifiers -band [ConsoleModifiers]::Shift))))
}
function Close-PodeServerInternal {
param(
[switch]
$ShowDoneMessage
)
# ensure the token is cancelled
if ($null -ne $PodeContext.Tokens.Cancellation) {
Write-Verbose 'Cancelling main cancellation token'
$PodeContext.Tokens.Cancellation.Cancel()
}
# stop all current runspaces
Write-Verbose 'Closing runspaces'
Close-PodeRunspaces -ClosePool
# stop the file monitor if it's running
Write-Verbose 'Stopping file monitor'
Stop-PodeFileMonitor
try {
# remove all the cancellation tokens
Write-Verbose 'Disposing cancellation tokens'
Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation
Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart
# dispose mutex/semaphores
Write-Verbose 'Diposing mutex and semaphores'
Clear-PodeMutexes
Clear-PodeSemaphores
}
catch {
$_ | Out-Default
}
# remove all of the pode temp drives
Write-Verbose 'Removing internal PSDrives'
Remove-PodePSDrives
if ($ShowDoneMessage -and ($PodeContext.Server.Types.Length -gt 0) -and !$PodeContext.Server.IsServerless) {
Write-PodeHost ' Done' -ForegroundColor Green
}
}
function New-PodePSDrive {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$Name
)
# if the path is a share, do nothing
if ($Path.StartsWith('\\')) {
return $Path
}
# if no name is passed, used a randomly generated one
if ([string]::IsNullOrWhiteSpace($Name)) {
$Name = "PodeDir$(New-PodeGuid)"
}
# if the path supplied doesn't exist, error
if (!(Test-Path $Path)) {
throw "Path does not exist: $($Path)"
}
# resolve the path
$Path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve
# create the temp drive
if (!(Test-PodePSDrive -Name $Name -Path $Path)) {
$drive = (New-PSDrive -Name $Name -PSProvider FileSystem -Root $Path -Scope Global -ErrorAction Stop)
}
else {
$drive = Get-PodePSDrive -Name $Name
}
# store internally, and return the drive's name
if (!$PodeContext.Server.Drives.ContainsKey($drive.Name)) {
$PodeContext.Server.Drives[$drive.Name] = $Path
}
return "$($drive.Name):$([System.IO.Path]::DirectorySeparatorChar)"
}
function Get-PodePSDrive {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return (Get-PSDrive -Name $Name -PSProvider FileSystem -Scope Global -ErrorAction Ignore)
}
function Test-PodePSDrive {
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string]
$Path
)
$drive = Get-PodePSDrive -Name $Name
if ($null -eq $drive) {
return $false
}
if (![string]::IsNullOrWhiteSpace($Path)) {
return ($drive.Root -ieq $Path)
}
return $true
}
function Add-PodePSDrives {
foreach ($key in $PodeContext.Server.Drives.Keys) {
$null = New-PodePSDrive -Path $PodeContext.Server.Drives[$key] -Name $key
}
}
function Import-PodeModules {
# import other modules in the session
foreach ($path in $PodeContext.Server.Modules.Values) {
if (Test-Path $path) {
$null = Import-Module $path -DisableNameChecking -Scope Global -ErrorAction Stop
}
}
}
<#
.SYNOPSIS
Creates and registers inbuilt PowerShell drives for the Pode server's default folders.
.DESCRIPTION
This function sets up inbuilt PowerShell drives for the Pode web server's default directories: views, public content, and error pages. For each of these directories, if the physical path exists on the server, a new PowerShell drive is created and mapped to this path. These drives provide an easy and consistent way to access server resources like views, static files, and custom error pages within the Pode application.
The function leverages `$PodeContext` to access the server's configuration and to determine the paths for these default folders. If a folder's path exists, the function uses `New-PodePSDrive` to create a PowerShell drive for it and stores this drive in the server's `InbuiltDrives` dictionary, keyed by the folder type.
.PARAMETER None
.EXAMPLE
Add-PodePSInbuiltDrive
This example is typically called within the Pode server setup script or internally by the Pode framework to initialize the PowerShell drives for the server's default folders.
.NOTES
- The function is designed to be used within the Pode framework and relies on the global `$PodeContext` variable for configuration.
- It specifically checks for the existence of paths for views, public content, and errors before attempting to create drives for them.
- This is an internal function and may change in future releases of Pode.
#>
function Add-PodePSInbuiltDrive {
# create drive for views, if path exists
$path = (Join-PodeServerRoot -Folder $PodeContext.Server.DefaultFolders.Views)
if (Test-Path $path) {
$PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Views] = (New-PodePSDrive -Path $path)
}
# create drive for public content, if path exists
$path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Public)
if (Test-Path $path) {
$PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Public] = (New-PodePSDrive -Path $path)
}
# create drive for errors, if path exists
$path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Errors)
if (Test-Path $path) {
$PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Errors] = (New-PodePSDrive -Path $path)
}
}
function Remove-PodePSDrives {
$null = Get-PSDrive PodeDir* | Remove-PSDrive
}
function Join-PodeServerRoot {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Folder,
[Parameter()]
[string]
$FilePath,
[Parameter()]
[string]
$Root
)
# use the root path of the server
if ([string]::IsNullOrWhiteSpace($Root)) {
$Root = $PodeContext.Server.Root
}
# join the folder/file to the root path
return [System.IO.Path]::Combine($Root, $Folder, $FilePath)
}
function Remove-PodeEmptyItemsFromArray {
param(
[Parameter(ValueFromPipeline = $true)]
$Array
)
if ($null -eq $Array) {
return @()
}
return @( @($Array -ne ([string]::Empty)) -ne $null )
}
function Remove-PodeNullKeysFromHashtable {
param(
[Parameter(ValueFromPipeline = $true)]
[hashtable]
$Hashtable
)
foreach ($key in ($Hashtable.Clone()).Keys) {
if ($null -eq $Hashtable[$key]) {
$null = $Hashtable.Remove($key)
continue
}
if (($Hashtable[$key] -is [string]) -and [string]::IsNullOrEmpty($Hashtable[$key])) {
$null = $Hashtable.Remove($key)
continue
}
if ($Hashtable[$key] -is [array]) {
if (($Hashtable[$key].Length -eq 1) -and ($null -eq $Hashtable[$key][0])) {
$null = $Hashtable.Remove($key)
continue
}
foreach ($item in $Hashtable[$key]) {
if (($item -is [hashtable]) -or ($item -is [System.Collections.Specialized.OrderedDictionary])) {
$item | Remove-PodeNullKeysFromHashtable
}
}
continue
}
if (($Hashtable[$key] -is [hashtable]) -or ($Hashtable[$key] -is [System.Collections.Specialized.OrderedDictionary])) {
$Hashtable[$key] | Remove-PodeNullKeysFromHashtable
continue
}
}
}
function Get-PodeFileExtension {
param(
[Parameter()]
[string]
$Path,
[switch]
$TrimPeriod
)
$ext = [System.IO.Path]::GetExtension($Path)
if ($TrimPeriod) {
$ext = $ext.Trim('.')
}
return $ext
}
function Get-PodeFileName {
param(
[Parameter()]
[string]
$Path,
[switch]
$WithoutExtension
)
if ($WithoutExtension) {
return [System.IO.Path]::GetFileNameWithoutExtension($Path)
}
return [System.IO.Path]::GetFileName($Path)
}
function Test-PodeValidNetworkFailure {
param(
[Parameter()]
$Exception
)
$msgs = @(
'*network name is no longer available*',
'*nonexistent network connection*',
'*the response has completed*',
'*broken pipe*'
)
$match = @(foreach ($msg in $msgs) {
if ($Exception.Message -ilike $msg) {
$msg
}
})[0]
return ($null -ne $match)
}
function ConvertFrom-PodeHeaderQValue {
param(
[Parameter(ValueFromPipeline = $true)]
[string]
$Value
)
$qs = [ordered]@{}
# return if no value
if ([string]::IsNullOrWhiteSpace($Value)) {
return $qs
}
# split the values up
$parts = @($Value -isplit ',').Trim()
# go through each part and check its q-value
foreach ($part in $parts) {
# default of 1 if no q-value
if ($part.IndexOf(';q=') -eq -1) {
$qs[$part] = 1.0
continue
}
# parse for q-value
$atoms = @($part -isplit ';q=')
$qs[$atoms[0]] = [double]$atoms[1]
}
return $qs
}
function Get-PodeAcceptEncoding {
param(
[Parameter()]
[string]
$AcceptEncoding,
[switch]
$ThrowError
)
# return if no encoding
if ([string]::IsNullOrWhiteSpace($AcceptEncoding)) {
return [string]::Empty
}
# return empty if not compressing
if (!$PodeContext.Server.Web.Compression.Enabled) {
return [string]::Empty
}
# convert encoding form q-form
$encodings = ConvertFrom-PodeHeaderQValue -Value $AcceptEncoding
if ($encodings.Count -eq 0) {
return [string]::Empty
}
# check the encodings for one that matches
$normal = @('identity', '*')
$valid = @()
# build up supported and invalid
foreach ($encoding in $encodings.Keys) {
if (($encoding -iin $PodeContext.Server.Compression.Encodings) -or ($encoding -iin $normal)) {
$valid += @{
Name = $encoding
Value = $encodings[$encoding]
}
}
}
# if it's empty, just return empty
if ($valid.Length -eq 0) {
return [string]::Empty
}
# find the highest ranked match
$found = @{}
$failOnIdentity = $false
foreach ($encoding in $valid) {
if ($encoding.Value -gt $found.Value) {
$found = $encoding
}
if (!$failOnIdentity -and ($encoding.Value -eq 0) -and ($encoding.Name -iin $normal)) {
$failOnIdentity = $true
}
}
# force found to identity/* if the 0 is not identity - meaning it's still allowed
if (($found.Value -eq 0) -and !$failOnIdentity) {
$found = @{
Name = 'identity'
Value = 1.0
}
}
# return invalid, error, or return empty for idenity?
if ($found.Value -eq 0) {
if ($ThrowError) {
throw (New-PodeRequestException -StatusCode 406)
}
}
# else, we're safe
if ($found.Name -iin $normal) {
return [string]::Empty
}
if ($found.Name -ieq 'x-gzip') {
return 'gzip'
}
return $found.Name
}
function Get-PodeRanges {
param(
[Parameter()]
[string]
$Range,
[switch]
$ThrowError
)
# return if no ranges
if ([string]::IsNullOrWhiteSpace($Range)) {
return $null
}
# split on '='
$parts = @($Range -isplit '=').Trim()
if (($parts.Length -le 1) -or ([string]::IsNullOrWhiteSpace($parts[1]))) {
return $null
}
$unit = $parts[0]
if ($unit -ine 'bytes') {
if ($ThrowError) {
throw (New-PodeRequestException -StatusCode 416)
}
return $null
}
# split on ','
$parts = @($parts[1] -isplit ',').Trim()
# parse into From-To hashtable array
$ranges = @()
foreach ($atom in $parts) {
if ($atom -inotmatch '(?<start>[\d]+){0,1}\s?\-\s?(?<end>[\d]+){0,1}') {
if ($ThrowError) {
throw (New-PodeRequestException -StatusCode 416)
}
return $null
}
$ranges += @{
Start = $Matches['start']
End = $Matches['end']
}
}
return $ranges
}
function Get-PodeTransferEncoding {
param(
[Parameter()]
[string]
$TransferEncoding,
[switch]
$ThrowError
)
# return if no encoding
if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
return [string]::Empty
}
# convert encoding form q-form
$encodings = ConvertFrom-PodeHeaderQValue -Value $TransferEncoding
if ($encodings.Count -eq 0) {
return [string]::Empty
}
# check the encodings for one that matches
$normal = @('chunked', 'identity')
$invalid = @()
# if we see a supported one, return immediately. else build up invalid one
foreach ($encoding in $encodings.Keys) {
if ($encoding -iin $PodeContext.Server.Compression.Encodings) {
if ($encoding -ieq 'x-gzip') {
return 'gzip'
}
return $encoding
}
if ($encoding -iin $normal) {
continue
}
$invalid += $encoding
}
# if we have any invalid, throw a 415 error
if ($invalid.Length -gt 0) {
if ($ThrowError) {
throw (New-PodeRequestException -StatusCode 415)
}
return $invalid[0]
}
# else, we're safe
return [string]::Empty
}
function Get-PodeEncodingFromContentType {
param(
[Parameter()]
[string]
$ContentType
)
if ([string]::IsNullOrWhiteSpace($ContentType)) {
return [System.Text.Encoding]::UTF8
}
$parts = @($ContentType -isplit ';').Trim()
foreach ($part in $parts) {
if ($part.StartsWith('charset')) {
return [System.Text.Encoding]::GetEncoding(($part -isplit '=')[1].Trim())
}
}
return [System.Text.Encoding]::UTF8
}
function New-PodeRequestException {
param(
[Parameter(Mandatory = $true)]
[int]
$StatusCode
)
$err = [System.Net.Http.HttpRequestException]::new()
$err.Data.Add('PodeStatusCode', $StatusCode)
return $err
}
function ConvertTo-PodeResponseContent {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[int]
$Depth = 10,
[Parameter()]
[string]
$Delimiter = ',',
[switch]
$AsHtml
)
# split for the main content type
$ContentType = Split-PodeContentType -ContentType $ContentType
# if there is no content-type then convert straight to string
if ([string]::IsNullOrWhiteSpace($ContentType)) {
return ([string]$InputObject)
}
# run action for the content type
switch ($ContentType) {
{ $_ -ilike '*/json' } {
if ($InputObject -isnot [string]) {
if ($Depth -le 0) {
return (ConvertTo-Json -InputObject $InputObject -Compress)
}
else {
return (ConvertTo-Json -InputObject $InputObject -Depth $Depth -Compress)
}
}
if ([string]::IsNullOrWhiteSpace($InputObject)) {
return '{}'
}
}
{ $_ -ilike '*/yaml' -or $_ -ilike '*/x-yaml' } {
if ($InputObject -isnot [string]) {
if ($Depth -le 0) {
return (ConvertTo-PodeYamlInternal -InputObject $InputObject )
}
else {
return (ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth )
}
}
if ([string]::IsNullOrWhiteSpace($InputObject)) {
return '[]'
}
}
{ $_ -ilike '*/xml' } {
if ($InputObject -isnot [string]) {
$temp = @(foreach ($item in $InputObject) {
New-Object psobject -Property $item
})
return ($temp | ConvertTo-Xml -Depth $Depth -As String -NoTypeInformation)
}
if ([string]::IsNullOrWhiteSpace($InputObject)) {
return [string]::Empty
}
}
{ $_ -ilike '*/csv' } {
if ($InputObject -isnot [string]) {
$temp = @(foreach ($item in $InputObject) {
New-Object psobject -Property $item
})
if (Test-PodeIsPSCore) {
$temp = ($temp | ConvertTo-Csv -Delimiter $Delimiter -IncludeTypeInformation:$false)
}
else {
$temp = ($temp | ConvertTo-Csv -Delimiter $Delimiter -NoTypeInformation)
}
return ($temp -join ([environment]::NewLine))
}
if ([string]::IsNullOrWhiteSpace($InputObject)) {
return [string]::Empty
}
}
{ $_ -ilike '*/html' } {
if ($InputObject -isnot [string]) {
return (($InputObject | ConvertTo-Html) -join ([environment]::NewLine))
}
if ([string]::IsNullOrWhiteSpace($InputObject)) {
return [string]::Empty
}
}
{ $_ -ilike '*/markdown' } {
if ($AsHtml -and ($PSVersionTable.PSVersion.Major -ge 7)) {
return ($InputObject | ConvertFrom-Markdown).Html
}
}
}
return ([string]$InputObject)
}
function ConvertFrom-PodeRequestContent {
param(
[Parameter()]
$Request,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[string]
$TransferEncoding
)
# get the requests content type
$ContentType = Split-PodeContentType -ContentType $ContentType
# result object for data/files
$Result = @{
Data = @{}
Files = @{}
}
# if there is no content-type then do nothing
if ([string]::IsNullOrWhiteSpace($ContentType)) {
return $Result
}
# if the content-type is not multipart/form-data, get the string data
if ($ContentType -ine 'multipart/form-data') {
# get the content based on server type
if ($PodeContext.Server.IsServerless) {
switch ($PodeContext.Server.ServerlessType.ToLowerInvariant()) {
'awslambda' {
$Content = $Request.body
}
'azurefunctions' {
$Content = $Request.RawBody
}
}
}
else {
# if the request is compressed, attempt to uncompress it
if (![string]::IsNullOrWhiteSpace($TransferEncoding)) {
# create a compressed stream to decompress the req bytes
$ms = New-Object -TypeName System.IO.MemoryStream
$ms.Write($Request.RawBody, 0, $Request.RawBody.Length)
$null = $ms.Seek(0, 0)
$stream = New-Object "System.IO.Compression.$($TransferEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Decompress)
# read the decompressed bytes
$Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Request.ContentEncoding
}
else {
$Content = $Request.Body
}
}
# if there is no content then do nothing
if ([string]::IsNullOrWhiteSpace($Content)) {
return $Result
}
# check if there is a defined custom body parser
if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
$parser = $PodeContext.Server.BodyParsers[$ContentType]
$Result.Data = (Invoke-PodeScriptBlock -ScriptBlock $parser.ScriptBlock -Arguments $Content -UsingVariables $parser.UsingVariables -Return)
$Content = $null
return $Result
}
}
# run action for the content type
switch ($ContentType) {
{ $_ -ilike '*/json' } {
if (Test-PodeIsPSCore) {
$Result.Data = ($Content | ConvertFrom-Json -AsHashtable)
}
else {
$Result.Data = ($Content | ConvertFrom-Json)
}
}
{ $_ -ilike '*/xml' } {
$Result.Data = [xml]($Content)
}
{ $_ -ilike '*/csv' } {
$Result.Data = ($Content | ConvertFrom-Csv)
}
{ $_ -ilike '*/x-www-form-urlencoded' } {
$Result.Data = (ConvertFrom-PodeNameValueToHashTable -Collection ([System.Web.HttpUtility]::ParseQueryString($Content)))
}
{ $_ -ieq 'multipart/form-data' } {
# parse multipart form data
$form = $null
if ($PodeContext.Server.IsServerless) {
switch ($PodeContext.Server.ServerlessType.ToLowerInvariant()) {
'awslambda' {
$Content = $Request.body
}
'azurefunctions' {
$Content = $Request.Body
}
}
$form = [PodeForm]::Parse($Content, $WebEvent.ContentType, [System.Text.Encoding]::UTF8)
}
else {
$Request.ParseFormData()
$form = $Request.Form
}
# set the files/data
foreach ($file in $form.Files) {
$Result.Files.Add($file.FileName, $file)
}
foreach ($item in $form.Data) {
if ($item.IsSingular) {
$Result.Data.Add($item.Key, $item.Values[0])
}
else {
$Result.Data.Add($item.Key, $item.Values)
}
}
$form = $null
}
default {
$Result.Data = $Content
}
}
$Content = $null
return $Result
}
function Split-PodeContentType {
param(
[Parameter()]
[string]
$ContentType
)
if ([string]::IsNullOrWhiteSpace($ContentType)) {
return [string]::Empty
}
return @($ContentType -isplit ';')[0].Trim()
}
function ConvertFrom-PodeNameValueToHashTable {
param(
[Parameter()]
[System.Collections.Specialized.NameValueCollection]
$Collection
)
if ((Get-PodeCount -Object $Collection) -eq 0) {
return @{}
}
$ht = @{}
foreach ($key in $Collection.Keys) {
$htKey = $key
if (!$key) {
$htKey = ''
}
$ht[$htKey] = $Collection.Get($key)
}
return $ht
}
function Get-PodeCount {
param(
[Parameter()]
$Object
)
if ($null -eq $Object) {
return 0
}
if ($Object -is [string]) {
return $Object.Length
}
if ($Object -is [System.Collections.Specialized.NameValueCollection] -and $Object.Count -eq 0) {
return 0
}
return $Object.Count
}
<#
.SYNOPSIS
Tests if a given file system path is valid and optionally if it is not a directory.
.DESCRIPTION
This function tests if the provided file system path is valid. It checks if the path is not null or whitespace, and if the item at the path exists. If the item exists and is not a directory (unless the $FailOnDirectory switch is not used), it returns true. If the path is not valid, it can optionally set a 404 response status code.
.PARAMETER Path
The file system path to test for validity.
.PARAMETER NoStatus
A switch to suppress setting the 404 response status code if the path is not valid.
.PARAMETER FailOnDirectory
A switch to indicate that the function should return false if the path is a directory.
.PARAMETER Force
A switch to indicate that the file with the hidden attribute has to be includede
.PARAMETER ReturnItem
Return the item file item itself instead of true or false
.EXAMPLE
$isValid = Test-PodePath -Path "C:\temp\file.txt"
if ($isValid) {
# The file exists and is not a directory
}
.EXAMPLE
$isValid = Test-PodePath -Path "C:\temp\folder" -FailOnDirectory
if (!$isValid) {
# The path is a directory or does not exist
}
.NOTES
This function is used within the Pode framework to validate file system paths for serving static content.
#>
function Test-PodePath {
param(
[Parameter()]
$Path,
[switch]
$NoStatus,
[switch]
$FailOnDirectory,
[switch]
$Force,
[switch]
$ReturnItem
)
$statusCode = 404
if (![string]::IsNullOrWhiteSpace($Path)) {
try {
$item = Get-Item $Path -Force:$Force -ErrorAction Stop
if (($null -ne $item) -and (!$FailOnDirectory -or !$item.PSIsContainer)) {
$statusCode = 200
}
}
catch [System.Management.Automation.ItemNotFoundException] {
$statusCode = 404
}
catch [System.UnauthorizedAccessException] {
$statusCode = 401
}
catch {
$statusCode = 400
}
}
if ($statusCode -eq 200) {
if ($ReturnItem.IsPresent) {
return $item
}
return $true
}
# if we failed to get the file, report back the status code and/or return true/false
if (!$NoStatus.IsPresent) {
Set-PodeResponseStatus -Code $statusCode
}
if ($ReturnItem.IsPresent) {
return $null
}
return $false
}
function Test-PodePathIsFile {
param(
[Parameter()]
[string]
$Path,
[switch]
$FailOnWildcard
)
if ([string]::IsNullOrWhiteSpace($Path)) {
return $false
}
if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) {
return $false
}
return (![string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}
function Test-PodePathIsWildcard {
param(
[Parameter()]
[string]
$Path
)
if ([string]::IsNullOrWhiteSpace($Path)) {
return $false
}
return $Path.Contains('*')
}
function Test-PodePathIsDirectory {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Path,
[switch]
$FailOnWildcard
)
if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) {
return $false
}
return ([string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}
function Convert-PodePathSeparators {
param(
[Parameter()]
$Paths
)
return @($Paths | ForEach-Object {
if (![string]::IsNullOrWhiteSpace($_)) {
$_ -ireplace '[\\/]', [System.IO.Path]::DirectorySeparatorChar
}
})
}
function Convert-PodePathPatternToRegex {
param(
[Parameter()]
[string]
$Path,
[switch]
$NotSlashes,
[switch]
$NotStrict
)
if (!$NotSlashes) {
if ($Path -match '[\\/]\*$') {
$Path = $Path -replace '[\\/]\*$', '/{0,1}*'
}
$Path = $Path -ireplace '[\\/]', '[\\/]'
}
$Path = $Path -ireplace '\.', '\.'
$Path = $Path -ireplace '\*', '.*?'
if ($NotStrict) {
return $Path
}
return "^$($Path)$"
}
function Convert-PodePathPatternsToRegex {
param(
[Parameter()]
[string[]]
$Paths,
[switch]
$NotSlashes,
[switch]
$NotStrict
)
# replace certain chars
$Paths = @(foreach ($path in $Paths) {
if (![string]::IsNullOrEmpty($path)) {
Convert-PodePathPatternToRegex -Path $path -NotStrict -NotSlashes:$NotSlashes
}
})
# if no paths, return null
if (($null -eq $Paths) -or ($Paths.Length -eq 0)) {
return $null
}
# join them all together
$joined = "($($Paths -join '|'))"
if ($NotStrict) {
return $joined
}
return "^$($joined)$"
}
function Get-PodeDefaultSslProtocols {
if (Test-PodeIsMacOS) {
return (ConvertTo-PodeSslProtocols -Protocols Tls12)
}
return (ConvertTo-PodeSslProtocols -Protocols Ssl3, Tls12)
}
function ConvertTo-PodeSslProtocols {
param(
[Parameter()]
[ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')]
[string[]]
$Protocols
)
$protos = 0
foreach ($protocol in $Protocols) {
$protos = [int]($protos -bor [System.Security.Authentication.SslProtocols]::$protocol)
}
return [System.Security.Authentication.SslProtocols]($protos)
}
function Get-PodeModuleDetails {
# if there's 1 module imported already, use that
$importedModule = @(Get-Module -Name Pode)
if (($importedModule | Measure-Object).Count -eq 1) {
return (Convert-PodeModuleDetails -Module @($importedModule)[0])
}
# if there's none or more, attempt to get the module used for 'engine'
try {
$usedModule = (Get-Command -Name 'Set-PodeViewEngine').Module
if (($usedModule | Measure-Object).Count -eq 1) {
return (Convert-PodeModuleDetails -Module $usedModule)
}
}
catch {
}
# if there were multiple to begin with, use the newest version
if (($importedModule | Measure-Object).Count -gt 1) {
return (Convert-PodeModuleDetails -Module @($importedModule | Sort-Object -Property Version)[-1])
}
# otherwise there were none, use the latest installed
return (Convert-PodeModuleDetails -Module @(Get-Module -ListAvailable -Name Pode | Sort-Object -Property Version)[-1])
}
function Convert-PodeModuleDetails {
param(
[Parameter(Mandatory = $true)]
[psmoduleinfo]
$Module
)
$details = @{
Name = $Module.Name
Path = $Module.Path
BasePath = $Module.ModuleBase
DataPath = (Find-PodeModuleFile -Module $Module -CheckVersion)
InternalPath = $null
InPath = (Test-PodeModuleInPath -Module $Module)
}
$details.InternalPath = $details.DataPath -ireplace 'Pode\.(ps[md]1)', 'Pode.Internal.$1'
return $details
}
function Test-PodeModuleInPath {
param(
[Parameter(Mandatory = $true)]
[psmoduleinfo]
$Module
)
$separator = ';'
if (Test-PodeIsUnix) {
$separator = ':'
}
$paths = @($env:PSModulePath -split $separator)
foreach ($path in $paths) {
if ($Module.Path.StartsWith($path)) {
return $true
}
}
return $false
}
function Get-PodeModuleDependencies {
param(
[Parameter(Mandatory = $true)]
[psmoduleinfo]
$Module
)
if (!$Module.RequiredModules) {
return $Module
}
$mods = @()
foreach ($mod in $Module.RequiredModules) {
$mods += (Get-PodeModuleDependencies -Module $mod)
}
return ($mods + $module)
}
function Get-PodeModuleRootPath {
return (Split-Path -Parent -Path $PodeContext.Server.PodeModule.Path)
}
function Get-PodeModuleMiscPath {
return [System.IO.Path]::Combine((Get-PodeModuleRootPath), 'Misc')
}
function Get-PodeUrl {
return "$($WebEvent.Endpoint.Protocol)://$($WebEvent.Endpoint.Address)$($WebEvent.Path)"
}
function Find-PodeErrorPage {
param(
[Parameter()]
[int]
$Code,
[Parameter()]
[string]
$ContentType
)
# if a defined content type is supplied, attempt to find an error page for that first
if (![string]::IsNullOrWhiteSpace($ContentType)) {
$path = Get-PodeErrorPage -Code $Code -ContentType $ContentType
if (![string]::IsNullOrWhiteSpace($path)) {
return @{ 'Path' = $path; 'ContentType' = $ContentType }
}
}
# if a defined route error page content type is supplied, attempt to find an error page for that
if (![string]::IsNullOrWhiteSpace($WebEvent.ErrorType)) {
$path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ErrorType
if (![string]::IsNullOrWhiteSpace($path)) {
return @{ 'Path' = $path; 'ContentType' = $WebEvent.ErrorType }
}
}
# if route patterns have been defined, see if an error content type matches and attempt that
if (!(Test-PodeIsEmpty $PodeContext.Server.Web.ErrorPages.Routes)) {
# find type by pattern
$matched = @(foreach ($key in $PodeContext.Server.Web.ErrorPages.Routes.Keys) {
if ($WebEvent.Path -imatch $key) {
$key
}
})[0]
# if we have a match, see if a page exists
if (!(Test-PodeIsEmpty $matched)) {
$type = $PodeContext.Server.Web.ErrorPages.Routes[$matched]
$path = Get-PodeErrorPage -Code $Code -ContentType $type
if (![string]::IsNullOrWhiteSpace($path)) {
return @{ 'Path' = $path; 'ContentType' = $type }
}
}
}
# if we're using strict typing, attempt that, if we have a content type
if ($PodeContext.Server.Web.ErrorPages.StrictContentTyping -and ![string]::IsNullOrWhiteSpace($WebEvent.ContentType)) {
$path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ContentType
if (![string]::IsNullOrWhiteSpace($path)) {
return @{ 'Path' = $path; 'ContentType' = $WebEvent.ContentType }
}
}
# if we have a default defined, attempt that
if (!(Test-PodeIsEmpty $PodeContext.Server.Web.ErrorPages.Default)) {
$path = Get-PodeErrorPage -Code $Code -ContentType $PodeContext.Server.Web.ErrorPages.Default
if (![string]::IsNullOrWhiteSpace($path)) {
return @{ 'Path' = $path; 'ContentType' = $PodeContext.Server.Web.ErrorPages.Default }
}
}
# if there's still no error page, use default HTML logic
$type = Get-PodeContentType -Extension 'html'
$path = (Get-PodeErrorPage -Code $Code -ContentType $type)
if (![string]::IsNullOrWhiteSpace($path)) {
return @{ 'Path' = $path; 'ContentType' = $type }
}
return $null
}
function Get-PodeErrorPage {
param(
[Parameter()]
[int]
$Code,
[Parameter()]
[string]
$ContentType
)
# parse the passed content type
$ContentType = Split-PodeContentType -ContentType $ContentType
# object for the page path
$path = $null
# attempt to find a custom error page
$path = Find-PodeCustomErrorPage -Code $Code -ContentType $ContentType
# if there's no custom page found, attempt to find an inbuilt page
if ([string]::IsNullOrWhiteSpace($path)) {
$podeRoot = Get-PodeModuleMiscPath
$path = Find-PodeFileForContentType -Path $podeRoot -Name 'default-error-page' -ContentType $ContentType -Engine 'pode'
}
# if there's no path found, or it's inaccessible, return null
if (!(Test-PodePath $path -NoStatus)) {
return $null
}
return $path
}
function Find-PodeCustomErrorPage {
param(
[Parameter()]
[int]
$Code,
[Parameter()]
[string]
$ContentType
)
# get the custom errors path
$customErrPath = $PodeContext.Server.InbuiltDrives['errors']
# if there's no custom error path, return
if ([string]::IsNullOrWhiteSpace($customErrPath)) {
return $null
}
# retrieve a status code page
$path = (Find-PodeFileForContentType -Path $customErrPath -Name "$($Code)" -ContentType $ContentType)
if (![string]::IsNullOrWhiteSpace($path)) {
return $path
}
# retrieve default page
$path = (Find-PodeFileForContentType -Path $customErrPath -Name 'default' -ContentType $ContentType)
if (![string]::IsNullOrWhiteSpace($path)) {
return $path
}
# no file was found
return $null
}
function Find-PodeFileForContentType {
param(
[Parameter()]
[string]
$Path,
[Parameter()]
[string]
$Name,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[string]
$Engine = $null
)
# get all files at the path that start with the 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) {
return $null
}
# filter the files by the view engine extension (but only if the current engine is dynamic - non-html)
if ([string]::IsNullOrWhiteSpace($Engine) -and $PodeContext.Server.ViewEngine.IsDynamic) {
$Engine = $PodeContext.Server.ViewEngine.Extension
}
$Engine = (Protect-PodeValue -Value $Engine -Default 'pode')
if ($Engine -ine 'pode') {
$Engine = "($($Engine)|pode)"
}
$engineFiles = @(foreach ($file in $files) {
if ($file.Name -imatch "\.$($Engine)$") {
$file
}
})
$files = @(foreach ($file in $files) {
if ($file.Name -inotmatch "\.$($Engine)$") {
$file
}
})
# only attempt static files if we still have files after any engine filtering
if ($null -ne $files -and $files.Length -gt 0) {
# get files of the format '<name>.<type>'
$file = @(foreach ($f in $files) {
if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)$") {
if (($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext']))) {
$f.FullName
}
}
})[0]
if (![string]::IsNullOrWhiteSpace($file)) {
return $file
}
}
# only attempt these formats if we have a files for the view engine
if ($null -ne $engineFiles -and $engineFiles.Length -gt 0) {
# get files of the format '<name>.<type>.<engine>'
$file = @(foreach ($f in $engineFiles) {
if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)\.$($engine)$") {
if ($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext'])) {
$f.FullName
}
}
})[0]
if (![string]::IsNullOrWhiteSpace($file)) {
return $file
}
# get files of the format '<name>.<engine>'
$file = @(foreach ($f in $engineFiles) {
if ($f.Name -imatch "^$($Name)\.$($engine)$") {
$f.FullName
}
})[0]
if (![string]::IsNullOrWhiteSpace($file)) {
return $file
}
}
# no file was found
return $null
}
function Get-PodeRelativePath {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$RootPath,
[switch]
$JoinRoot,
[switch]
$Resolve,
[switch]
$TestPath
)
# if the path is relative, join to root if flagged
if ($JoinRoot -and ($Path -match '^\.{1,2}([\\\/]|$)')) {
if ([string]::IsNullOrWhiteSpace($RootPath)) {
$RootPath = $PodeContext.Server.Root
}
$Path = [System.IO.Path]::Combine($RootPath, $Path)
}
# if flagged, resolve the path
if ($Resolve) {
$_rawPath = $Path
$Path = [System.IO.Path]::GetFullPath($Path.Replace('\', '/'))
}
# if flagged, test the path and throw error if it doesn't exist
if ($TestPath -and !(Test-PodePath $Path -NoStatus)) {
throw "The path does not exist: $(Protect-PodeValue -Value $Path -Default $_rawPath)"
}
return $Path
}
function Get-PodeWildcardFiles {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$Wildcard = '*.*',
[Parameter()]
[string]
$RootPath
)
# if the OriginalPath is a directory, add wildcard
if (Test-PodePathIsDirectory -Path $Path) {
$Path = [System.IO.Path]::Combine($Path, $Wildcard)
}
# if path has a *, assume wildcard
if (Test-PodePathIsWildcard -Path $Path) {
$Path = Get-PodeRelativePath -Path $Path -RootPath $RootPath -JoinRoot
return @((Get-ChildItem $Path -Recurse -Force).FullName)
}
return $null
}
function Test-PodeIsServerless {
param(
[Parameter()]
[string]
$FunctionName,
[switch]
$ThrowError
)
if ($PodeContext.Server.IsServerless -and $ThrowError) {
throw "The $($FunctionName) function is not supported in a serverless context"
}
if (!$ThrowError) {
return $PodeContext.Server.IsServerless
}
}
function Get-PodeEndpointUrl {
param(
[Parameter()]
$Endpoint
)
# get the endpoint on which we're currently listening - use first http/https if there are many
if ($null -eq $Endpoint) {
$Endpoint = @($PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -iin @('http', 'https') -and $_.Default })[0]
if ($null -eq $Endpoint) {
$Endpoint = @($PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -iin @('http', 'https') })[0]
}
}
$url = $Endpoint.Url
if ([string]::IsNullOrWhiteSpace($url)) {
$url = "$($Endpoint.Protocol)://$($Endpoint.FriendlyName):$($Endpoint.Port)"
}
return $url
}
function Get-PodeDefaultPort {
param(
[Parameter()]
[ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
[string]
$Protocol,
[Parameter()]
[ValidateSet('Implicit', 'Explicit')]
[string]
$TlsMode = 'Implicit',
[switch]
$Real
)
# are we after the real default ports?
if ($Real) {
return (@{
Http = @{ Implicit = 80 }
Https = @{ Implicit = 443 }
Smtp = @{ Implicit = 25 }
Smtps = @{ Implicit = 465; Explicit = 587 }
Tcp = @{ Implicit = 9001 }
Tcps = @{ Implicit = 9002; Explicit = 9003 }
Ws = @{ Implicit = 80 }
Wss = @{ Implicit = 443 }
})[$Protocol.ToLowerInvariant()][$TlsMode.ToLowerInvariant()]
}
# if we running as iis, return the ASPNET port
if ($PodeContext.Server.IsIIS) {
return [int]$env:ASPNETCORE_PORT
}
# if we running as heroku, return the port
if ($PodeContext.Server.IsHeroku) {
return [int]$env:PORT
}
# otherwise, get the port for the protocol
return (@{
Http = @{ Implicit = 8080 }
Https = @{ Implicit = 8443 }
Smtp = @{ Implicit = 25 }
Smtps = @{ Implicit = 465; Explicit = 587 }
Tcp = @{ Implicit = 9001 }
Tcps = @{ Implicit = 9002; Explicit = 9003 }
Ws = @{ Implicit = 9080 }
Wss = @{ Implicit = 9443 }
})[$Protocol.ToLowerInvariant()][$TlsMode.ToLowerInvariant()]
}
function Set-PodeServerHeader {
param(
[Parameter()]
[string]
$Type,
[switch]
$AllowEmptyType
)
$name = 'Pode'
if (![string]::IsNullOrWhiteSpace($Type) -or $AllowEmptyType) {
$name += " - $($Type)"
}
Set-PodeHeader -Name 'Server' -Value $name
}
function Get-PodeHandler {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Service', 'Smtp')]
[string]
$Type,
[Parameter()]
[string]
$Name
)
if ([string]::IsNullOrWhiteSpace($Name)) {
return $PodeContext.Server.Handlers[$Type]
}
return $PodeContext.Server.Handlers[$Type][$Name]
}
function Convert-PodeFileToScriptBlock {
param(
[Parameter(Mandatory = $true)]
[string]
$FilePath
)
# resolve for relative path
$FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot
# if file doesn't exist, error
if (!(Test-PodePath -Path $FilePath -NoStatus)) {
throw "The FilePath supplied does not exist: $($FilePath)"
}
# if the path is a wildcard or directory, error
if (!(Test-PodePathIsFile -Path $FilePath -FailOnWildcard)) {
throw "The FilePath supplied cannot be a wildcard or a directory: $($FilePath)"
}
return ([scriptblock](Use-PodeScript -Path $FilePath))
}
function Convert-PodeQueryStringToHashTable {
param(
[Parameter()]
[string]
$Uri
)
if ([string]::IsNullOrWhiteSpace($Uri)) {
return @{}
}
$qmIndex = $Uri.IndexOf('?')
if ($qmIndex -eq -1) {
return @{}
}
if ($qmIndex -gt 0) {
$Uri = $Uri.Substring($qmIndex)
}
$tmpQuery = [System.Web.HttpUtility]::ParseQueryString($Uri)
return (ConvertFrom-PodeNameValueToHashTable -Collection $tmpQuery)
}
function Get-PodeDotSourcedFiles {
param(
[Parameter(Mandatory = $true)]
[System.Management.Automation.Language.Ast]
$Ast,
[Parameter()]
[string]
$RootPath
)
# set default root path
if ([string]::IsNullOrWhiteSpace($RootPath)) {
$RootPath = $PodeContext.Server.Root
}
# get all dot-sourced files
$cmdTypes = @('dot', 'ampersand')
$files = ($Ast.FindAll({
($args[0] -is [System.Management.Automation.Language.CommandAst]) -and
($args[0].InvocationOperator -iin $cmdTypes) -and
($args[0].CommandElements.StaticType.Name -ieq 'string')
}, $false)).CommandElements.Value
$fileOrder = @()
# no files found
if (($null -eq $files) -or ($files.Length -eq 0)) {
return $fileOrder
}
# get any sub sourced files
foreach ($file in $files) {
$file = Get-PodeRelativePath -Path $file -RootPath $RootPath -JoinRoot
$fileOrder += $file
$ast = Get-PodeAstFromFile -FilePath $file
$result = Get-PodeDotSourcedFiles -Ast $ast -RootPath (Split-Path -Parent -Path $file)
if (($null -ne $result) -and ($result.Length -gt 0)) {
$fileOrder += $result
}
}
# return all found files
return $fileOrder
}
function Get-PodeAstFromFile {
param(
[Parameter(Mandatory = $true)]
[string]
$FilePath
)
if (!(Test-Path $FilePath)) {
throw "Path to script file does not exist: $($FilePath)"
}
return [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$null, [ref]$null)
}
function Get-PodeFunctionsFromFile {
param(
[Parameter(Mandatory = $true)]
[string]
$FilePath
)
$ast = Get-PodeAstFromFile -FilePath $FilePath
return @(Get-PodeFunctionsFromAst -Ast $ast)
}
function Get-PodeFunctionsFromAst {
param(
[Parameter(Mandatory = $true)]
[System.Management.Automation.Language.Ast]
$Ast
)
$funcs = @(($Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)))
return @(foreach ($func in $funcs) {
# skip null
if ($null -eq $func) {
continue
}
# skip pode funcs
if ($func.Name -ilike '*-Pode*') {
continue
}
# definition
$def = "$($func.Body)".Trim('{}').Trim()
if (($null -ne $func.Parameters) -and ($func.Parameters.Count -gt 0)) {
$def = "param($($func.Parameters.Name -join ','))`n$($def)"
}
# the found func
@{
Name = $func.Name
Definition = $def
}
})
}
function Get-PodeFunctionsFromScriptBlock {
param(
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock
)
# functions that have been found
$foundFuncs = @()
# get each function in the callstack
$callstack = Get-PSCallStack
if ($callstack.Count -gt 3) {
$callstack = ($callstack | Select-Object -Skip 4)
$bindingFlags = [System.Reflection.BindingFlags]'NonPublic, Instance, Static'
foreach ($call in $callstack) {
$_funcContext = $call.GetType().GetProperty('FunctionContext', $bindingFlags).GetValue($call, $null)
$_scriptBlock = $_funcContext.GetType().GetField('_scriptBlock', $bindingFlags).GetValue($_funcContext)
$foundFuncs += @(Get-PodeFunctionsFromAst -Ast $_scriptBlock.Ast)
}
}
# get each function from the main script
$foundFuncs += @(Get-PodeFunctionsFromAst -Ast $ScriptBlock.Ast)
# return the found functions
return $foundFuncs
}
function Read-PodeWebExceptionDetails {
param(
[Parameter(Mandatory = $true)]
[System.Management.Automation.ErrorRecord]
$ErrorRecord
)
switch ($ErrorRecord) {
{ $_.Exception -is [System.Net.WebException] } {
$stream = $_.Exception.Response.GetResponseStream()
$stream.Position = 0
$body = [System.IO.StreamReader]::new($stream).ReadToEnd()
$code = [int]$_.Exception.Response.StatusCode
$desc = $_.Exception.Response.StatusDescription
}
{ $_.Exception -is [System.Net.Http.HttpRequestException] } {
$body = $_.ErrorDetails.Message
$code = [int]$_.Exception.Response.StatusCode
$desc = $_.Exception.Response.ReasonPhrase
}
default {
throw "Exception is of an invalid type, should be either WebException or HttpRequestException, but got: $($_.Exception.GetType().Name)"
}
}
return @{
Status = @{
Code = $code
Description = $desc
}
Body = $body
}
}
function Use-PodeFolder {
param(
[Parameter()]
[string]
$Path,
[Parameter(Mandatory = $true)]
[string]
$DefaultPath
)
# use default, or custom path
if ([string]::IsNullOrWhiteSpace($Path)) {
$Path = Join-PodeServerRoot -Folder $DefaultPath
}
else {
$Path = Get-PodeRelativePath -Path $Path -JoinRoot
}
# fail if path not found
if (!(Test-PodePath -Path $Path -NoStatus)) {
throw "Path to load $($DefaultPath) not found: $($Path)"
}
# get .ps1 files and load them
Get-ChildItem -Path $Path -Filter *.ps1 -Force -Recurse | ForEach-Object {
Use-PodeScript -Path $_.FullName
}
}
function Find-PodeModuleFile {
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Name')]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Module')]
[psmoduleinfo]
$Module,
[switch]
$ListAvailable,
[switch]
$DataOnly,
[switch]
$CheckVersion
)
# get module and check psd1, then psm1
if ($null -eq $Module) {
$Module = (Get-Module -Name $Name -ListAvailable:$ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1)
}
# if the path isn't already a psd1 do this
$path = Join-Path $Module.ModuleBase "$($Module.Name).psd1"
if (!(Test-Path $path)) {
# if we only want a psd1, return null
if ($DataOnly) {
$path = $null
}
else {
$path = $Module.Path
}
}
# check the Version of the psd1
elseif ($CheckVersion) {
$data = Import-PowerShellDataFile -Path $path -ErrorAction Stop
$version = $null
if (![version]::TryParse($data.ModuleVersion, [ref]$version)) {
if ($DataOnly) {
$path = $null
}
else {
$path = $Module.Path
}
}
}
return $path
}
function Clear-PodeHashtableInnerKeys {
param(
[Parameter(ValueFromPipeline = $true)]
[hashtable]
$InputObject
)
if (Test-PodeIsEmpty $InputObject) {
return
}
$InputObject.Keys.Clone() | ForEach-Object {
$InputObject[$_].Clear()
}
}
function Set-PodeCronInterval {
param(
[Parameter()]
[hashtable]
$Cron,
[Parameter()]
[string]
$Type,
[Parameter()]
[int[]]
$Value,
[Parameter()]
[int]
$Interval
)
if ($Interval -le 0) {
return $false
}
if ($Value.Length -gt 1) {
throw "You can only supply a single $($Type) value when using intervals"
}
if ($Value.Length -eq 1) {
$Cron[$Type] = "$(@($Value)[0])"
}
$Cron[$Type] += "/$($Interval)"
return ($Value.Length -eq 1)
}
function Test-PodeModuleInstalled {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return ($null -ne (Get-Module -Name $Name -ListAvailable -ErrorAction Ignore -Verbose:$false))
}
function Get-PodePlaceholderRegex {
return '\:(?<tag>[\w]+)'
}
function Resolve-PodePlaceholders {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$Pattern,
[Parameter()]
[string]
$Prepend = '(?<',
[Parameter()]
[string]
$Append = '>[^\/]+?)',
[switch]
$Slashes
)
if ([string]::IsNullOrWhiteSpace($Pattern)) {
$Pattern = Get-PodePlaceholderRegex
}
if ($Path -imatch $Pattern) {
$Path = [regex]::Escape($Path)
}
if ($Slashes) {
$Path = ($Path.TrimEnd('\/') -replace '(\\\\|\/)', '[\\\/]')
$Path = "$($Path)[\\\/]"
}
return (Convert-PodePlaceholders -Path $Path -Pattern $Pattern -Prepend $Prepend -Append $Append)
}
function Convert-PodePlaceholders {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$Pattern,
[Parameter()]
[string]
$Prepend = '(?<',
[Parameter()]
[string]
$Append = '>[^\/]+?)'
)
if ([string]::IsNullOrWhiteSpace($Pattern)) {
$Pattern = Get-PodePlaceholderRegex
}
while ($Path -imatch $Pattern) {
$Path = ($Path -ireplace $Matches[0], "$($Prepend)$($Matches['tag'])$($Append)")
}
return $Path
}
function Test-PodePlaceholders {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$Placeholder
)
if ([string]::IsNullOrWhiteSpace($Placeholder)) {
$Placeholder = Get-PodePlaceholderRegex
}
return ($Path -imatch $Placeholder)
}
<#
.SYNOPSIS
Retrieves the PowerShell module manifest object for the specified module.
.DESCRIPTION
This function constructs the path to a PowerShell module manifest file (.psd1) located in the parent directory of the script root. It then imports the module manifest file to access its properties and returns the manifest object. This can be useful for scripts that need to dynamically discover and utilize module metadata, such as version, dependencies, and exported functions.
.PARAMETERS
This function does not accept any parameters.
.EXAMPLE
$manifest = Get-PodeModuleManifest
This example calls the `Get-PodeModuleManifest` function to retrieve the module manifest object and stores it in the variable `$manifest`.
#>
function Get-PodeModuleManifest {
# Construct the path to the module manifest (.psd1 file)
$moduleManifestPath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Pode.psd1'
# Import the module manifest to access its properties
$moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath
return $moduleManifest
}
<#
.SYNOPSIS
Tests if the Pode module is from the development branch.
.DESCRIPTION
The Test-PodeVersionDev function checks if the Pode module's version matches the placeholder value ('$version$'), which is used to indicate the development branch of the module. It returns $true if the version matches, indicating the module is from the development branch, and $false otherwise.
.PARAMETER None
This function does not accept any parameters.
.OUTPUTS
System.Boolean
Returns $true if the Pode module version is '$version$', indicating the development branch. Returns $false for any other version.
.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '$version$' }
PS> Test-PodeVersionDev
Returns $true, indicating the development branch.
.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '1.2.3' }
PS> Test-PodeVersionDev
Returns $false, indicating a specific release version.
.NOTES
This function assumes that $moduleManifest is a hashtable representing the loaded module manifest, with a key of ModuleVersion.
#>
function Test-PodeVersionDev {
return (Get-PodeModuleManifest).ModuleVersion -eq '$version$'
}
<#
.SYNOPSIS
Tests the running PowerShell version for compatibility with Pode, identifying end-of-life (EOL) and untested versions.
.DESCRIPTION
The `Test-PodeVersionPwshEOL` function checks the current PowerShell version against a list of versions that were either supported or EOL at the time of the Pode release. It uses the module manifest to determine which PowerShell versions are considered EOL and which are officially supported. If the current version is EOL or was not tested with the current release of Pode, the function generates a warning. This function aids in maintaining best practices for using supported PowerShell versions with Pode.
.PARAMETER ReportUntested
If specified, the function will report if the current PowerShell version was not available and thus untested at the time of the Pode release. This is useful for identifying potential compatibility issues with newer versions of PowerShell.
.OUTPUTS
A hashtable containing two keys:
- `eol`: A boolean indicating if the current PowerShell version was EOL at the time of the Pode release.
- `supported`: A boolean indicating if the current PowerShell version was officially supported by Pode at the time of the release.
.EXAMPLE
Test-PodeVersionPwshEOL
Checks the current PowerShell version against Pode's supported and EOL versions list. Outputs a warning if the version is EOL or untested, and returns a hashtable indicating the compatibility status.
.EXAMPLE
Test-PodeVersionPwshEOL -ReportUntested
Similar to the basic usage, but also reports if the current PowerShell version was untested because it was not available at the time of the Pode release.
.NOTES
This function is part of the Pode module's utilities to ensure compatibility and encourage the use of supported PowerShell versions.
#>
function Test-PodeVersionPwshEOL {
param(
[switch] $ReportUntested
)
$moduleManifest = Get-PodeModuleManifest
if ($moduleManifest.ModuleVersion -eq '$version$') {
return @{
eol = $false
supported = $true
}
}
$psVersion = $PSVersionTable.PSVersion
$eolVersions = $moduleManifest.PrivateData.PwshVersions.Untested -split ','
$isEol = "$($psVersion.Major).$($psVersion.Minor)" -in $eolVersions
if ($isEol) {
Write-PodeHost "[WARNING] Pode $(Get-PodeVersion) has not been tested on PowerShell $($PSVersionTable.PSVersion), as it is EOL." -ForegroundColor Yellow
}
$SupportedVersions = $moduleManifest.PrivateData.PwshVersions.Supported -split ','
$isSupported = "$($psVersion.Major).$($psVersion.Minor)" -in $SupportedVersions
if ((! $isSupported) -and (! $isEol) -and $ReportUntested) {
Write-PodeHost "[WARNING] Pode $(Get-PodeVersion) has not been tested on PowerShell $($PSVersionTable.PSVersion), as it was not available when Pode was released." -ForegroundColor Yellow
}
return @{
eol = $isEol
supported = $isSupported
}
}
<#
.SYNOPSIS
creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml
.DESCRIPTION
This produces YAML from any object you pass to it. It isn't suitable for the huge objects produced by some of the cmdlets such as Get-Process, but fine for simple objects
.PARAMETER Object
the object that you want scripted out
.PARAMETER Depth
The depth that you want your object scripted to
.EXAMPLE
Get-PodeOpenApiDefinition|ConvertTo-PodeYaml
#>
function ConvertTo-PodeYaml {
[CmdletBinding()]
[OutputType([string])]
param (
[parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
[AllowNull()]
$InputObject,
[parameter()]
[int]
$Depth = 16
)
if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) {
$PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml'))
}
if ($PodeContext.Server.InternalCache.YamlModuleImported) {
return ($InputObject | ConvertTo-Yaml)
}
else {
return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine
}
}
<#
.SYNOPSIS
Converts PowerShell objects into a YAML-formatted string.
.DESCRIPTION
This function takes PowerShell objects and converts them to a YAML string representation.
It supports various data types including arrays, hashtables, strings, and more.
The depth of conversion can be controlled, allowing for nested objects to be accurately represented.
.PARAMETER InputObject
The PowerShell object to convert to YAML. This parameter accepts input via the pipeline.
.PARAMETER Depth
Specifies the maximum depth of object nesting to convert. Default is 10 levels deep.
.PARAMETER NestingLevel
Used internally to track the current depth of recursion. Generally not specified by the user.
.PARAMETER NoNewLine
If specified, suppresses the newline characters in the output to create a single-line string.
.OUTPUTS
System.String. Returns a string in YAML format.
.EXAMPLE
$object | ConvertTo-PodeYamlInternal
Converts the object piped to it into a YAML string.
.NOTES
This is an internal function and may change in future releases of Pode.
It converts only basic PowerShell types, such as strings, integers, booleans, arrays, hashtables, and ordered dictionaries into a YAML format.
#>
function ConvertTo-PodeYamlInternal {
[CmdletBinding()]
[OutputType([string])]
param (
[parameter(Mandatory = $true, ValueFromPipeline = $true)]
[AllowNull()]
$InputObject,
[parameter()]
[int]
$Depth = 10,
[parameter()]
[int]
$NestingLevel = 0,
[parameter()]
[switch]
$NoNewLine
)
process {
# if it is null return null
If ( !($InputObject) ) {
if ($InputObject -is [Object[]]) {
return '[]'
}
else {
return ''
}
}
$padding = [string]::new(' ', $NestingLevel * 2) # lets just create our left-padding for the block
try {
$Type = $InputObject.GetType().Name # we start by getting the object's type
if ($InputObject -is [object[]]) {
#what it really is
$Type = "$($InputObject.GetType().BaseType.Name)"
}
#report the leaves in terms of object type
if ($Depth -ilt $NestingLevel) {
$Type = 'OutOfDepth'
}
# prevent these values being identified as an object
if ($InputObject -is [System.Collections.Specialized.OrderedDictionary]) {
$Type = 'HashTable'
}
elseif ($Type -ieq 'List`1') {
$Type = 'Array'
}
elseif ($InputObject -is [array]) {
$Type = 'Array'
} # whatever it thinks it is called
elseif ($InputObject -is [hashtable]) {
$Type = 'HashTable'
} # for our purposes it is a hashtable
$output += switch ($Type.ToLower()) {
'string' {
$String = "$InputObject"
if (($string -match '[\r\n]' -or $string.Length -gt 80) -and ($string -notlike 'http*')) {
$multiline = [System.Text.StringBuilder]::new("|`n")
$items = $string.Split("`n")
for ($i = 0; $i -lt $items.Length; $i++) {
$workingString = $items[$i] -replace '\r$'
$length = $workingString.Length
$index = 0
$wrap = 80
while ($index -lt $length) {
$breakpoint = $wrap
$linebreak = $false
if (($length - $index) -gt $wrap) {
$lastSpaceIndex = $workingString.LastIndexOf(' ', $index + $wrap, $wrap)
if ($lastSpaceIndex -ne -1) {
$breakpoint = $lastSpaceIndex - $index
}
else {
$linebreak = $true
$breakpoint--
}
}
else {
$breakpoint = $length - $index
}
$null = $multiline.Append($padding).Append($workingString.Substring($index, $breakpoint).Trim())
if ($linebreak) {
$null = $multiline.Append('\')
}
$index += $breakpoint
if ($index -lt $length) {
$null = $multiline.Append([System.Environment]::NewLine)
}
}
if ($i -lt ($items.Length - 1)) {
$null = $multiline.Append([System.Environment]::NewLine)
}
}
$multiline.ToString().TrimEnd()
break
}
else {
if ($string -match '^[#\[\]@\{\}\!\*]') {
"'$($string -replace '''', '''''')'"
}
else {
$string
}
break
}
break
}
'hashtable' {
if ($InputObject.Count -gt 0 ) {
$index = 0
$string = [System.Text.StringBuilder]::new()
foreach ($item in $InputObject.Keys) {
if ($InputObject[$item] -is [string]) { $increment = 2 } else { $increment = 1 }
if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" }
$null = $string.Append( $NewPadding).Append( $item).Append(': ').Append((ConvertTo-PodeYamlInternal -InputObject $InputObject[$item] -Depth $Depth -NestingLevel ($NestingLevel + $increment)))
}
$string.ToString()
}
else { '{}' }
break
}
'boolean' {
if ($InputObject -eq $true) { 'true' } else { 'false' }
break
}
'array' {
$string = [System.Text.StringBuilder]::new()
$index = 0
foreach ($item in $InputObject ) {
if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" }
$null = $string.Append($NewPadding).Append('- ').Append((ConvertTo-PodeYamlInternal -InputObject $item -depth $Depth -NestingLevel ($NestingLevel + 1) -NoNewLine))
}
$string.ToString()
break
}
'int32' {
$InputObject
}
'double' {
$InputObject
}
default {
"'$InputObject'"
}
}
return $Output
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw "Error'$($_)' in script $($_.InvocationInfo.ScriptName) $($_.InvocationInfo.Line.Trim()) (line $($_.InvocationInfo.ScriptLineNumber)) char $($_.InvocationInfo.OffsetInLine) executing $($_.InvocationInfo.MyCommand) on $type object '$($InputObject)' Class: $($InputObject.GetType().Name) BaseClass: $($InputObject.GetType().BaseType.Name) "
}
}
}
function Get-PodeLoggingTerminalMethod {
return {
param($item, $options)
if ($PodeContext.Server.Quiet) {
return
}
# check if it's an array from batching
if ($item -is [array]) {
$item = ($item -join [System.Environment]::NewLine)
}
# protect then write
$item = ($item | Protect-PodeLogItem)
$item.ToString() | Out-PodeHost
}
}
function Get-PodeLoggingFileMethod {
return {
param($item, $options)
# check if it's an array from batching
if ($item -is [array]) {
$item = ($item -join [System.Environment]::NewLine)
}
# mask values
$item = ($item | Protect-PodeLogItem)
# variables
$date = [DateTime]::Now.ToString('yyyy-MM-dd')
# do we need to reset the fileId?
if ($options.Date -ine $date) {
$options.Date = $date
$options.FileId = 0
}
# get the fileId
if ($options.FileId -eq 0) {
$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
}
}
$id = "$($options.FileId)".PadLeft(3, '0')
if ($options.MaxSize -gt 0) {
$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')
}
}
# get the file to write to
$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
# if set, remove log files beyond days set (ensure this is only run once a day)
if (($options.MaxDays -gt 0) -and ($options.NextClearDown -lt [DateTime]::Now.Date)) {
$date = [DateTime]::Now.Date.AddDays(-$options.MaxDays)
$null = Get-ChildItem -Path $options.Path -Filter '*.log' -Force |
Where-Object { $_.CreationTime -lt $date } |
Remove-Item $_ -Force
$options.NextClearDown = [DateTime]::Now.Date.AddDays(1)
}
}
}
function Get-PodeLoggingEventViewerMethod {
return {
param($item, $options, $rawItem)
if ($item -isnot [array]) {
$item = @($item)
}
if ($rawItem -isnot [array]) {
$rawItem = @($rawItem)
}
for ($i = 0; $i -lt $item.Length; $i++) {
# convert log level - info if no level present
$entryType = ConvertTo-PodeEventViewerLevel -Level $rawItem[$i].Level
# create log instance
$entryInstance = [System.Diagnostics.EventInstance]::new($options.ID, 0, $entryType)
# create event log
$entryLog = [System.Diagnostics.EventLog]::new()
$entryLog.Log = $options.LogName
$entryLog.Source = $options.Source
try {
$message = ($item[$i] | Protect-PodeLogItem)
$entryLog.WriteEvent($entryInstance, $message)
}
catch {
}
}
}
}
function ConvertTo-PodeEventViewerLevel {
param(
[Parameter()]
[string]
$Level
)
if ([string]::IsNullOrWhiteSpace($Level)) {
return [System.Diagnostics.EventLogEntryType]::Information
}
if ($Level -ieq 'error') {
return [System.Diagnostics.EventLogEntryType]::Error
}
if ($Level -ieq 'warning') {
return [System.Diagnostics.EventLogEntryType]::Warning
}
return [System.Diagnostics.EventLogEntryType]::Information
}
function Get-PodeLoggingInbuiltType {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Errors', 'Requests')]
[string]
$Type
)
switch ($Type.ToLowerInvariant()) {
'requests' {
$script = {
param($item, $options)
# just return the item if Raw is set
if ($options.Raw) {
return $item
}
function sg($value) {
if ([string]::IsNullOrWhiteSpace($value)) {
return '-'
}
return $value
}
# build the url with http method
$url = "$(sg $item.Request.Method) $(sg $item.Request.Resource) $(sg $item.Request.Protocol)"
# build and return the request row
return "$(sg $item.Host) $(sg $item.RfcUserIdentity) $(sg $item.User) [$(sg $item.Date)] `"$($url)`" $(sg $item.Response.StatusCode) $(sg $item.Response.Size) `"$(sg $item.Request.Referrer)`" `"$(sg $item.Request.Agent)`""
}
}
'errors' {
$script = {
param($item, $options)
# do nothing if the error level isn't present
if (@($options.Levels) -inotcontains $item.Level) {
return
}
# just return the item if Raw is set
if ($options.Raw) {
return $item
}
# build the exception details
$row = @(
"Date: $($item.Date.ToString('yyyy-MM-dd HH:mm:ss'))",
"Level: $($item.Level)",
"ThreadId: $($item.ThreadId)",
"Server: $($item.Server)",
"Category: $($item.Category)",
"Message: $($item.Message)",
"StackTrace: $($item.StackTrace)"
)
# join the details and return
return "$($row -join "`n")`n"
}
}
}
return $script
}
function Get-PodeRequestLoggingName {
return '__pode_log_requests__'
}
function Get-PodeErrorLoggingName {
return '__pode_log_errors__'
}
function Get-PodeLogger {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.Logging.Types[$Name]
}
function Test-PodeLoggerEnabled {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return ($PodeContext.Server.Logging.Enabled -and $PodeContext.Server.Logging.Types.ContainsKey($Name))
}
function Get-PodeErrorLoggingLevels {
return (Get-PodeLogger -Name (Get-PodeErrorLoggingName)).Arguments.Levels
}
function Test-PodeErrorLoggingEnabled {
return (Test-PodeLoggerEnabled -Name (Get-PodeErrorLoggingName))
}
function Test-PodeRequestLoggingEnabled {
return (Test-PodeLoggerEnabled -Name (Get-PodeRequestLoggingName))
}
function Write-PodeRequestLog {
param(
[Parameter(Mandatory = $true)]
$Request,
[Parameter(Mandatory = $true)]
$Response,
[Parameter()]
[string]
$Path
)
# do nothing if logging is disabled, or request logging isn't setup
$name = Get-PodeRequestLoggingName
if (!(Test-PodeLoggerEnabled -Name $name)) {
return
}
# build a request object
$item = @{
Host = $Request.RemoteEndPoint.Address.IPAddressToString
RfcUserIdentity = '-'
User = '-'
Date = [DateTime]::Now.ToString('dd/MMM/yyyy:HH:mm:ss zzz')
Request = @{
Method = $Request.HttpMethod.ToUpperInvariant()
Resource = $Path
Protocol = "HTTP/$($Request.ProtocolVersion)"
Referrer = $Request.UrlReferrer
Agent = $Request.UserAgent
}
Response = @{
StatusCode = $Response.StatusCode
StatusDescription = $Response.StatusDescription
Size = '-'
}
}
# set size if >0
if ($Response.ContentLength64 -gt 0) {
$item.Response.Size = $Response.ContentLength64
}
# set username - dot spaces
if (Test-PodeAuthUser -IgnoreSession) {
$userProps = (Get-PodeLogger -Name $name).Properties.Username.Split('.')
$user = $WebEvent.Auth.User
foreach ($atom in $userProps) {
$user = $user.($atom)
}
if (![string]::IsNullOrWhiteSpace($user)) {
$item.User = $user -ireplace '\s+', '.'
}
}
# add the item to be processed
$null = $PodeContext.LogsToProcess.Add(@{
Name = $name
Item = $item
})
}
function Add-PodeRequestLogEndware {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$WebEvent
)
# do nothing if logging is disabled, or request logging isn't setup
$name = Get-PodeRequestLoggingName
if (!(Test-PodeLoggerEnabled -Name $name)) {
return
}
# add the request logging endware
$WebEvent.OnEnd += @{
Logic = {
Write-PodeRequestLog -Request $WebEvent.Request -Response $WebEvent.Response -Path $WebEvent.Path
}
}
}
function Test-PodeLoggersExist {
if (($null -eq $PodeContext.Server.Logging) -or ($null -eq $PodeContext.Server.Logging.Types)) {
return $false
}
return (($PodeContext.Server.Logging.Types.Count -gt 0) -or ($PodeContext.Server.Logging.Enabled))
}
function Start-PodeLoggingRunspace {
# skip if there are no loggers configured, or logging is disabled
if (!(Test-PodeLoggersExist)) {
return
}
$script = {
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
# if there are no logs to process, just sleep for a few seconds - but after checking the batch
if ($PodeContext.LogsToProcess.Count -eq 0) {
Test-PodeLoggerBatches
Start-Sleep -Seconds 5
continue
}
# safely pop off the first log from the array
$log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock {
$log = $PodeContext.LogsToProcess[0]
$null = $PodeContext.LogsToProcess.RemoveAt(0)
return $log
})
# run the log item through the appropriate method
$logger = Get-PodeLogger -Name $log.Name
$now = [datetime]::Now
# if the log is null, check batch then sleep and skip
if ($null -eq $log) {
Start-Sleep -Milliseconds 100
continue
}
# convert to log item into a writable format
$rawItems = $log.Item
$_args = @($log.Item) + @($logger.Arguments)
$result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat)
# check batching
$batch = $logger.Method.Batch
if ($batch.Size -gt 1) {
# add current item to batch
$batch.Items += $result
$batch.RawItems += $log.Item
$batch.LastUpdate = $now
# if the current amount of items matches the batch, write
$result = $null
if ($batch.Items.Length -ge $batch.Size) {
$result = $batch.Items
$rawItems = $batch.RawItems
}
# if we're writing, reset the items
if ($null -ne $result) {
$batch.Items = @()
$batch.RawItems = @()
}
}
# send the writable log item off to the log writer
if ($null -ne $result) {
$_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems)
$null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat
}
# small sleep to lower cpu usage
Start-Sleep -Milliseconds 100
}
}
Add-PodeRunspace -Type Main -ScriptBlock $script
}
function Test-PodeLoggerBatches {
$now = [datetime]::Now
# check each logger, and see if its batch needs to be written
foreach ($logger in $PodeContext.Server.Logging.Types.Values) {
$batch = $logger.Method.Batch
if (($batch.Size -gt 1) -and ($batch.Items.Length -gt 0) -and ($batch.Timeout -gt 0) -and ($null -ne $batch.LastUpdate) -and ($batch.LastUpdate.AddSeconds($batch.Timeout) -le $now)) {
$result = $batch.Items
$rawItems = $batch.RawItems
$batch.Items = @()
$batch.RawItems = @()
$_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems)
$null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat
}
}
}
function Get-PodeContentType {
param(
[Parameter()]
[string]
$Extension,
[switch]
$DefaultIsNull
)
if ([string]::IsNullOrWhiteSpace($Extension)) {
$Extension = [string]::Empty
}
if (!$Extension.StartsWith('.')) {
$Extension = ".$($Extension)"
}
# Sourced from https://github.com/samuelneff/MimeTypeMap
switch ($Extension.ToLowerInvariant()) {
'.323' { return 'text/h323' }
'.3g2' { return 'video/3gpp2' }
'.3gp' { return 'video/3gpp' }
'.3gp2' { return 'video/3gpp2' }
'.3gpp' { return 'video/3gpp' }
'.7z' { return 'application/x-7z-compressed' }
'.aa' { return 'audio/audible' }
'.aac' { return 'audio/aac' }
'.aaf' { return 'application/octet-stream' }
'.aax' { return 'audio/vnd.audible.aax' }
'.ac3' { return 'audio/ac3' }
'.aca' { return 'application/octet-stream' }
'.accda' { return 'application/msaccess.addin' }
'.accdb' { return 'application/msaccess' }
'.accdc' { return 'application/msaccess.cab' }
'.accde' { return 'application/msaccess' }
'.accdr' { return 'application/msaccess.runtime' }
'.accdt' { return 'application/msaccess' }
'.accdw' { return 'application/msaccess.webapplication' }
'.accft' { return 'application/msaccess.ftemplate' }
'.acx' { return 'application/internet-property-stream' }
'.addin' { return 'text/xml' }
'.ade' { return 'application/msaccess' }
'.adobebridge' { return 'application/x-bridge-url' }
'.adp' { return 'application/msaccess' }
'.adt' { return 'audio/vnd.dlna.adts' }
'.adts' { return 'audio/aac' }
'.afm' { return 'application/octet-stream' }
'.ai' { return 'application/postscript' }
'.aif' { return 'audio/aiff' }
'.aifc' { return 'audio/aiff' }
'.aiff' { return 'audio/aiff' }
'.air' { return 'application/vnd.adobe.air-application-installer-package+zip' }
'.amc' { return 'application/mpeg' }
'.anx' { return 'application/annodex' }
'.apk' { return 'application/vnd.android.package-archive' }
'.application' { return 'application/x-ms-application' }
'.art' { return 'image/x-jg' }
'.asa' { return 'application/xml' }
'.asax' { return 'application/xml' }
'.ascx' { return 'application/xml' }
'.asd' { return 'application/octet-stream' }
'.asf' { return 'video/x-ms-asf' }
'.ashx' { return 'application/xml' }
'.asi' { return 'application/octet-stream' }
'.asm' { return 'text/plain' }
'.asmx' { return 'application/xml' }
'.aspx' { return 'application/xml' }
'.asr' { return 'video/x-ms-asf' }
'.asx' { return 'video/x-ms-asf' }
'.atom' { return 'application/atom+xml' }
'.au' { return 'audio/basic' }
'.avi' { return 'video/x-msvideo' }
'.axa' { return 'audio/annodex' }
'.axs' { return 'application/olescript' }
'.axv' { return 'video/annodex' }
'.bas' { return 'text/plain' }
'.bcpio' { return 'application/x-bcpio' }
'.bin' { return 'application/octet-stream' }
'.bmp' { return 'image/bmp' }
'.c' { return 'text/plain' }
'.cab' { return 'application/octet-stream' }
'.caf' { return 'audio/x-caf' }
'.calx' { return 'application/vnd.ms-office.calx' }
'.cat' { return 'application/vnd.ms-pki.seccat' }
'.cc' { return 'text/plain' }
'.cd' { return 'text/plain' }
'.cdda' { return 'audio/aiff' }
'.cdf' { return 'application/x-cdf' }
'.cer' { return 'application/x-x509-ca-cert' }
'.cfg' { return 'text/plain' }
'.chm' { return 'application/octet-stream' }
'.class' { return 'application/x-java-applet' }
'.clp' { return 'application/x-msclip' }
'.cmd' { return 'text/plain' }
'.cmx' { return 'image/x-cmx' }
'.cnf' { return 'text/plain' }
'.cod' { return 'image/cis-cod' }
'.config' { return 'application/xml' }
'.contact' { return 'text/x-ms-contact' }
'.coverage' { return 'application/xml' }
'.cpio' { return 'application/x-cpio' }
'.cpp' { return 'text/plain' }
'.crd' { return 'application/x-mscardfile' }
'.crl' { return 'application/pkix-crl' }
'.crt' { return 'application/x-x509-ca-cert' }
'.cs' { return 'text/plain' }
'.csdproj' { return 'text/plain' }
'.csh' { return 'application/x-csh' }
'.csproj' { return 'text/plain' }
'.css' { return 'text/css' }
'.csv' { return 'text/csv' }
'.cur' { return 'application/octet-stream' }
'.cxx' { return 'text/plain' }
'.dat' { return 'application/octet-stream' }
'.datasource' { return 'application/xml' }
'.dbproj' { return 'text/plain' }
'.dcr' { return 'application/x-director' }
'.def' { return 'text/plain' }
'.deploy' { return 'application/octet-stream' }
'.der' { return 'application/x-x509-ca-cert' }
'.dgml' { return 'application/xml' }
'.dib' { return 'image/bmp' }
'.dif' { return 'video/x-dv' }
'.dir' { return 'application/x-director' }
'.disco' { return 'text/xml' }
'.divx' { return 'video/divx' }
'.dll' { return 'application/x-msdownload' }
'.dll.config' { return 'text/xml' }
'.dlm' { return 'text/dlm' }
'.doc' { return 'application/msword' }
'.docm' { return 'application/vnd.ms-word.document.macroEnabled.12' }
'.docx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }
'.dot' { return 'application/msword' }
'.dotm' { return 'application/vnd.ms-word.template.macroEnabled.12' }
'.dotx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' }
'.dsp' { return 'application/octet-stream' }
'.dsw' { return 'text/plain' }
'.dtd' { return 'text/xml' }
'.dtsconfig' { return 'text/xml' }
'.dv' { return 'video/x-dv' }
'.dvi' { return 'application/x-dvi' }
'.dwf' { return 'drawing/x-dwf' }
'.dwg' { return 'application/acad' }
'.dwp' { return 'application/octet-stream' }
'.dxf' { return 'application/x-dxf' }
'.dxr' { return 'application/x-director' }
'.eml' { return 'message/rfc822' }
'.emz' { return 'application/octet-stream' }
'.eot' { return 'application/vnd.ms-fontobject' }
'.eps' { return 'application/postscript' }
'.etl' { return 'application/etl' }
'.etx' { return 'text/x-setext' }
'.evy' { return 'application/envoy' }
'.exe' { return 'application/octet-stream' }
'.exe.config' { return 'text/xml' }
'.fdf' { return 'application/vnd.fdf' }
'.fif' { return 'application/fractals' }
'.filters' { return 'application/xml' }
'.fla' { return 'application/octet-stream' }
'.flac' { return 'audio/flac' }
'.flr' { return 'x-world/x-vrml' }
'.flv' { return 'video/x-flv' }
'.fsscript' { return 'application/fsharp-script' }
'.fsx' { return 'application/fsharp-script' }
'.generictest' { return 'application/xml' }
'.gif' { return 'image/gif' }
'.gpx' { return 'application/gpx+xml' }
'.group' { return 'text/x-ms-group' }
'.gsm' { return 'audio/x-gsm' }
'.gtar' { return 'application/x-gtar' }
'.gz' { return 'application/x-gzip' }
'.gzip' { return 'application/x-gzip' }
'.h' { return 'text/plain' }
'.hdf' { return 'application/x-hdf' }
'.hdml' { return 'text/x-hdml' }
'.hhc' { return 'application/x-oleobject' }
'.hhk' { return 'application/octet-stream' }
'.hhp' { return 'application/octet-stream' }
'.hlp' { return 'application/winhlp' }
'.hpp' { return 'text/plain' }
'.hqx' { return 'application/mac-binhex40' }
'.hta' { return 'application/hta' }
'.htc' { return 'text/x-component' }
'.htm' { return 'text/html' }
'.html' { return 'text/html' }
'.htt' { return 'text/webviewhtml' }
'.hxa' { return 'application/xml' }
'.hxc' { return 'application/xml' }
'.hxd' { return 'application/octet-stream' }
'.hxe' { return 'application/xml' }
'.hxf' { return 'application/xml' }
'.hxh' { return 'application/octet-stream' }
'.hxi' { return 'application/octet-stream' }
'.hxk' { return 'application/xml' }
'.hxq' { return 'application/octet-stream' }
'.hxr' { return 'application/octet-stream' }
'.hxs' { return 'application/octet-stream' }
'.hxt' { return 'text/html' }
'.hxv' { return 'application/xml' }
'.hxw' { return 'application/octet-stream' }
'.hxx' { return 'text/plain' }
'.i' { return 'text/plain' }
'.ico' { return 'image/x-icon' }
'.ics' { return 'application/octet-stream' }
'.idl' { return 'text/plain' }
'.ief' { return 'image/ief' }
'.iii' { return 'application/x-iphone' }
'.inc' { return 'text/plain' }
'.inf' { return 'application/octet-stream' }
'.ini' { return 'text/plain' }
'.inl' { return 'text/plain' }
'.ins' { return 'application/x-internet-signup' }
'.ipa' { return 'application/x-itunes-ipa' }
'.ipg' { return 'application/x-itunes-ipg' }
'.ipproj' { return 'text/plain' }
'.ipsw' { return 'application/x-itunes-ipsw' }
'.iqy' { return 'text/x-ms-iqy' }
'.isp' { return 'application/x-internet-signup' }
'.ite' { return 'application/x-itunes-ite' }
'.itlp' { return 'application/x-itunes-itlp' }
'.itms' { return 'application/x-itunes-itms' }
'.itpc' { return 'application/x-itunes-itpc' }
'.ivf' { return 'video/x-ivf' }
'.jar' { return 'application/java-archive' }
'.java' { return 'application/octet-stream' }
'.jck' { return 'application/liquidmotion' }
'.jcz' { return 'application/liquidmotion' }
'.jfif' { return 'image/pjpeg' }
'.jnlp' { return 'application/x-java-jnlp-file' }
'.jpb' { return 'application/octet-stream' }
'.jpe' { return 'image/jpeg' }
'.jpeg' { return 'image/jpeg' }
'.jpg' { return 'image/jpeg' }
'.js' { return 'application/javascript' }
'.json' { return 'application/json' }
'.jsx' { return 'text/jscript' }
'.jsxbin' { return 'text/plain' }
'.jwt' { return 'application/jwt' }
'.latex' { return 'application/x-latex' }
'.library-ms' { return 'application/windows-library+xml' }
'.lit' { return 'application/x-ms-reader' }
'.loadtest' { return 'application/xml' }
'.lpk' { return 'application/octet-stream' }
'.lsf' { return 'video/x-la-asf' }
'.lst' { return 'text/plain' }
'.lsx' { return 'video/x-la-asf' }
'.lzh' { return 'application/octet-stream' }
'.m13' { return 'application/x-msmediaview' }
'.m14' { return 'application/x-msmediaview' }
'.m1v' { return 'video/mpeg' }
'.m2t' { return 'video/vnd.dlna.mpeg-tts' }
'.m2ts' { return 'video/vnd.dlna.mpeg-tts' }
'.m2v' { return 'video/mpeg' }
'.m3u' { return 'audio/x-mpegurl' }
'.m3u8' { return 'audio/x-mpegurl' }
'.m4a' { return 'audio/m4a' }
'.m4b' { return 'audio/m4b' }
'.m4p' { return 'audio/m4p' }
'.m4r' { return 'audio/x-m4r' }
'.m4v' { return 'video/x-m4v' }
'.mac' { return 'image/x-macpaint' }
'.mak' { return 'text/plain' }
'.man' { return 'application/x-troff-man' }
'.manifest' { return 'application/x-ms-manifest' }
'.map' { return 'text/plain' }
'.markdown' { return 'text/markdown' }
'.master' { return 'application/xml' }
'.mbox' { return 'application/mbox' }
'.md' { return 'text/markdown' }
'.mda' { return 'application/msaccess' }
'.mdb' { return 'application/x-msaccess' }
'.mde' { return 'application/msaccess' }
'.mdp' { return 'application/octet-stream' }
'.me' { return 'application/x-troff-me' }
'.mfp' { return 'application/x-shockwave-flash' }
'.mht' { return 'message/rfc822' }
'.mhtml' { return 'message/rfc822' }
'.mid' { return 'audio/mid' }
'.midi' { return 'audio/mid' }
'.mix' { return 'application/octet-stream' }
'.mk' { return 'text/plain' }
'.mk3d' { return 'video/x-matroska-3d' }
'.mka' { return 'audio/x-matroska' }
'.mkv' { return 'video/x-matroska' }
'.mmf' { return 'application/x-smaf' }
'.mno' { return 'text/xml' }
'.mny' { return 'application/x-msmoney' }
'.mod' { return 'video/mpeg' }
'.mov' { return 'video/quicktime' }
'.movie' { return 'video/x-sgi-movie' }
'.mp2' { return 'video/mpeg' }
'.mp2v' { return 'video/mpeg' }
'.mp3' { return 'audio/mpeg' }
'.mp4' { return 'video/mp4' }
'.mp4v' { return 'video/mp4' }
'.mpa' { return 'video/mpeg' }
'.mpe' { return 'video/mpeg' }
'.mpeg' { return 'video/mpeg' }
'.mpf' { return 'application/vnd.ms-mediapackage' }
'.mpg' { return 'video/mpeg' }
'.mpp' { return 'application/vnd.ms-project' }
'.mpv2' { return 'video/mpeg' }
'.mqv' { return 'video/quicktime' }
'.ms' { return 'application/x-troff-ms' }
'.msg' { return 'application/vnd.ms-outlook' }
'.msi' { return 'application/octet-stream' }
'.mso' { return 'application/octet-stream' }
'.mts' { return 'video/vnd.dlna.mpeg-tts' }
'.mtx' { return 'application/xml' }
'.mvb' { return 'application/x-msmediaview' }
'.mvc' { return 'application/x-miva-compiled' }
'.mxp' { return 'application/x-mmxp' }
'.nc' { return 'application/x-netcdf' }
'.nsc' { return 'video/x-ms-asf' }
'.nws' { return 'message/rfc822' }
'.ocx' { return 'application/octet-stream' }
'.oda' { return 'application/oda' }
'.odb' { return 'application/vnd.oasis.opendocument.database' }
'.odc' { return 'application/vnd.oasis.opendocument.chart' }
'.odf' { return 'application/vnd.oasis.opendocument.formula' }
'.odg' { return 'application/vnd.oasis.opendocument.graphics' }
'.odh' { return 'text/plain' }
'.odi' { return 'application/vnd.oasis.opendocument.image' }
'.odl' { return 'text/plain' }
'.odm' { return 'application/vnd.oasis.opendocument.text-master' }
'.odp' { return 'application/vnd.oasis.opendocument.presentation' }
'.ods' { return 'application/vnd.oasis.opendocument.spreadsheet' }
'.odt' { return 'application/vnd.oasis.opendocument.text' }
'.oga' { return 'audio/ogg' }
'.ogg' { return 'audio/ogg' }
'.ogv' { return 'video/ogg' }
'.ogx' { return 'application/ogg' }
'.one' { return 'application/onenote' }
'.onea' { return 'application/onenote' }
'.onepkg' { return 'application/onenote' }
'.onetmp' { return 'application/onenote' }
'.onetoc' { return 'application/onenote' }
'.onetoc2' { return 'application/onenote' }
'.opus' { return 'audio/ogg' }
'.orderedtest' { return 'application/xml' }
'.osdx' { return 'application/opensearchdescription+xml' }
'.otf' { return 'application/font-sfnt' }
'.otg' { return 'application/vnd.oasis.opendocument.graphics-template' }
'.oth' { return 'application/vnd.oasis.opendocument.text-web' }
'.otp' { return 'application/vnd.oasis.opendocument.presentation-template' }
'.ots' { return 'application/vnd.oasis.opendocument.spreadsheet-template' }
'.ott' { return 'application/vnd.oasis.opendocument.text-template' }
'.oxt' { return 'application/vnd.openofficeorg.extension' }
'.p10' { return 'application/pkcs10' }
'.p12' { return 'application/x-pkcs12' }
'.p7b' { return 'application/x-pkcs7-certificates' }
'.p7c' { return 'application/pkcs7-mime' }
'.p7m' { return 'application/pkcs7-mime' }
'.p7r' { return 'application/x-pkcs7-certreqresp' }
'.p7s' { return 'application/pkcs7-signature' }
'.pbm' { return 'image/x-portable-bitmap' }
'.pcast' { return 'application/x-podcast' }
'.pct' { return 'image/pict' }
'.pcx' { return 'application/octet-stream' }
'.pcz' { return 'application/octet-stream' }
'.pdf' { return 'application/pdf' }
'.pfb' { return 'application/octet-stream' }
'.pfm' { return 'application/octet-stream' }
'.pfx' { return 'application/x-pkcs12' }
'.pgm' { return 'image/x-portable-graymap' }
'.pic' { return 'image/pict' }
'.pict' { return 'image/pict' }
'.pkgdef' { return 'text/plain' }
'.pkgundef' { return 'text/plain' }
'.pko' { return 'application/vnd.ms-pki.pko' }
'.pls' { return 'audio/scpls' }
'.pma' { return 'application/x-perfmon' }
'.pmc' { return 'application/x-perfmon' }
'.pml' { return 'application/x-perfmon' }
'.pmr' { return 'application/x-perfmon' }
'.pmw' { return 'application/x-perfmon' }
'.png' { return 'image/png' }
'.pnm' { return 'image/x-portable-anymap' }
'.pnt' { return 'image/x-macpaint' }
'.pntg' { return 'image/x-macpaint' }
'.pnz' { return 'image/png' }
'.pode' { return 'application/PowerShell' }
'.pot' { return 'application/vnd.ms-powerpoint' }
'.potm' { return 'application/vnd.ms-powerpoint.template.macroEnabled.12' }
'.potx' { return 'application/vnd.openxmlformats-officedocument.presentationml.template' }
'.ppa' { return 'application/vnd.ms-powerpoint' }
'.ppam' { return 'application/vnd.ms-powerpoint.addin.macroEnabled.12' }
'.ppm' { return 'image/x-portable-pixmap' }
'.pps' { return 'application/vnd.ms-powerpoint' }
'.ppsm' { return 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' }
'.ppsx' { return 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' }
'.ppt' { return 'application/vnd.ms-powerpoint' }
'.pptm' { return 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' }
'.pptx' { return 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }
'.prf' { return 'application/pics-rules' }
'.prm' { return 'application/octet-stream' }
'.prx' { return 'application/octet-stream' }
'.ps' { return 'application/postscript' }
'.ps1' { return 'application/PowerShell' }
'.psc1' { return 'application/PowerShell' }
'.psd1' { return 'application/PowerShell' }
'.psm1' { return 'application/PowerShell' }
'.psd' { return 'application/octet-stream' }
'.psess' { return 'application/xml' }
'.psm' { return 'application/octet-stream' }
'.psp' { return 'application/octet-stream' }
'.pst' { return 'application/vnd.ms-outlook' }
'.pub' { return 'application/x-mspublisher' }
'.pwz' { return 'application/vnd.ms-powerpoint' }
'.qht' { return 'text/x-html-insertion' }
'.qhtm' { return 'text/x-html-insertion' }
'.qt' { return 'video/quicktime' }
'.qti' { return 'image/x-quicktime' }
'.qtif' { return 'image/x-quicktime' }
'.qtl' { return 'application/x-quicktimeplayer' }
'.qxd' { return 'application/octet-stream' }
'.ra' { return 'audio/x-pn-realaudio' }
'.ram' { return 'audio/x-pn-realaudio' }
'.rar' { return 'application/x-rar-compressed' }
'.ras' { return 'image/x-cmu-raster' }
'.rat' { return 'application/rat-file' }
'.rc' { return 'text/plain' }
'.rc2' { return 'text/plain' }
'.rct' { return 'text/plain' }
'.rdlc' { return 'application/xml' }
'.reg' { return 'text/plain' }
'.resx' { return 'application/xml' }
'.rf' { return 'image/vnd.rn-realflash' }
'.rgb' { return 'image/x-rgb' }
'.rgs' { return 'text/plain' }
'.rm' { return 'application/vnd.rn-realmedia' }
'.rmi' { return 'audio/mid' }
'.rmp' { return 'application/vnd.rn-rn_music_package' }
'.roff' { return 'application/x-troff' }
'.rpm' { return 'audio/x-pn-realaudio-plugin' }
'.rqy' { return 'text/x-ms-rqy' }
'.rtf' { return 'application/rtf' }
'.rtx' { return 'text/richtext' }
'.rvt' { return 'application/octet-stream' }
'.ruleset' { return 'application/xml' }
'.s' { return 'text/plain' }
'.safariextz' { return 'application/x-safari-safariextz' }
'.scd' { return 'application/x-msschedule' }
'.scr' { return 'text/plain' }
'.sct' { return 'text/scriptlet' }
'.sd2' { return 'audio/x-sd2' }
'.sdp' { return 'application/sdp' }
'.sea' { return 'application/octet-stream' }
'.searchconnector-ms' { return 'application/windows-search-connector+xml' }
'.setpay' { return 'application/set-payment-initiation' }
'.setreg' { return 'application/set-registration-initiation' }
'.settings' { return 'application/xml' }
'.sgimb' { return 'application/x-sgimb' }
'.sgml' { return 'text/sgml' }
'.sh' { return 'application/x-sh' }
'.shar' { return 'application/x-shar' }
'.shtml' { return 'text/html' }
'.sit' { return 'application/x-stuffit' }
'.sitemap' { return 'application/xml' }
'.skin' { return 'application/xml' }
'.skp' { return 'application/x-koan' }
'.sldm' { return 'application/vnd.ms-powerpoint.slide.macroEnabled.12' }
'.sldx' { return 'application/vnd.openxmlformats-officedocument.presentationml.slide' }
'.slk' { return 'application/vnd.ms-excel' }
'.sln' { return 'text/plain' }
'.slupkg-ms' { return 'application/x-ms-license' }
'.smd' { return 'audio/x-smd' }
'.smi' { return 'application/octet-stream' }
'.smx' { return 'audio/x-smd' }
'.smz' { return 'audio/x-smd' }
'.snd' { return 'audio/basic' }
'.snippet' { return 'application/xml' }
'.snp' { return 'application/octet-stream' }
'.sol' { return 'text/plain' }
'.sor' { return 'text/plain' }
'.spc' { return 'application/x-pkcs7-certificates' }
'.spl' { return 'application/futuresplash' }
'.spx' { return 'audio/ogg' }
'.src' { return 'application/x-wais-source' }
'.srf' { return 'text/plain' }
'.ssisdeploymentmanifest' { return 'text/xml' }
'.ssm' { return 'application/streamingmedia' }
'.sst' { return 'application/vnd.ms-pki.certstore' }
'.stl' { return 'application/vnd.ms-pki.stl' }
'.sv4cpio' { return 'application/x-sv4cpio' }
'.sv4crc' { return 'application/x-sv4crc' }
'.svc' { return 'application/xml' }
'.svg' { return 'image/svg+xml' }
'.swf' { return 'application/x-shockwave-flash' }
'.step' { return 'application/step' }
'.stp' { return 'application/step' }
'.t' { return 'application/x-troff' }
'.tar' { return 'application/x-tar' }
'.tcl' { return 'application/x-tcl' }
'.testrunconfig' { return 'application/xml' }
'.testsettings' { return 'application/xml' }
'.tex' { return 'application/x-tex' }
'.texi' { return 'application/x-texinfo' }
'.texinfo' { return 'application/x-texinfo' }
'.tgz' { return 'application/x-compressed' }
'.thmx' { return 'application/vnd.ms-officetheme' }
'.thn' { return 'application/octet-stream' }
'.tif' { return 'image/tiff' }
'.tiff' { return 'image/tiff' }
'.tlh' { return 'text/plain' }
'.tli' { return 'text/plain' }
'.toc' { return 'application/octet-stream' }
'.tr' { return 'application/x-troff' }
'.trm' { return 'application/x-msterminal' }
'.trx' { return 'application/xml' }
'.ts' { return 'video/vnd.dlna.mpeg-tts' }
'.tsv' { return 'text/tab-separated-values' }
'.ttf' { return 'application/font-sfnt' }
'.tts' { return 'video/vnd.dlna.mpeg-tts' }
'.txt' { return 'text/plain' }
'.u32' { return 'application/octet-stream' }
'.uls' { return 'text/iuls' }
'.user' { return 'text/plain' }
'.ustar' { return 'application/x-ustar' }
'.vb' { return 'text/plain' }
'.vbdproj' { return 'text/plain' }
'.vbk' { return 'video/mpeg' }
'.vbproj' { return 'text/plain' }
'.vbs' { return 'text/vbscript' }
'.vcf' { return 'text/x-vcard' }
'.vcproj' { return 'application/xml' }
'.vcs' { return 'text/plain' }
'.vcxproj' { return 'application/xml' }
'.vddproj' { return 'text/plain' }
'.vdp' { return 'text/plain' }
'.vdproj' { return 'text/plain' }
'.vdx' { return 'application/vnd.ms-visio.viewer' }
'.vml' { return 'text/xml' }
'.vscontent' { return 'application/xml' }
'.vsct' { return 'text/xml' }
'.vsd' { return 'application/vnd.visio' }
'.vsi' { return 'application/ms-vsi' }
'.vsix' { return 'application/vsix' }
'.vsixlangpack' { return 'text/xml' }
'.vsixmanifest' { return 'text/xml' }
'.vsmdi' { return 'application/xml' }
'.vspscc' { return 'text/plain' }
'.vss' { return 'application/vnd.visio' }
'.vsscc' { return 'text/plain' }
'.vssettings' { return 'text/xml' }
'.vssscc' { return 'text/plain' }
'.vst' { return 'application/vnd.visio' }
'.vstemplate' { return 'text/xml' }
'.vsto' { return 'application/x-ms-vsto' }
'.vsw' { return 'application/vnd.visio' }
'.vsx' { return 'application/vnd.visio' }
'.vtx' { return 'application/vnd.visio' }
'.wasm' { return 'application/wasm' }
'.wav' { return 'audio/wav' }
'.wave' { return 'audio/wav' }
'.wax' { return 'audio/x-ms-wax' }
'.wbk' { return 'application/msword' }
'.wbmp' { return 'image/vnd.wap.wbmp' }
'.wcm' { return 'application/vnd.ms-works' }
'.wdb' { return 'application/vnd.ms-works' }
'.wdp' { return 'image/vnd.ms-photo' }
'.webarchive' { return 'application/x-safari-webarchive' }
'.webm' { return 'video/webm' }
'.webp' { return 'image/webp' }
'.webtest' { return 'application/xml' }
'.wiq' { return 'application/xml' }
'.wiz' { return 'application/msword' }
'.wks' { return 'application/vnd.ms-works' }
'.wlmp' { return 'application/wlmoviemaker' }
'.wlpginstall' { return 'application/x-wlpg-detect' }
'.wlpginstall3' { return 'application/x-wlpg3-detect' }
'.wm' { return 'video/x-ms-wm' }
'.wma' { return 'audio/x-ms-wma' }
'.wmd' { return 'application/x-ms-wmd' }
'.wmf' { return 'application/x-msmetafile' }
'.wml' { return 'text/vnd.wap.wml' }
'.wmlc' { return 'application/vnd.wap.wmlc' }
'.wmls' { return 'text/vnd.wap.wmlscript' }
'.wmlsc' { return 'application/vnd.wap.wmlscriptc' }
'.wmp' { return 'video/x-ms-wmp' }
'.wmv' { return 'video/x-ms-wmv' }
'.wmx' { return 'video/x-ms-wmx' }
'.wmz' { return 'application/x-ms-wmz' }
'.woff' { return 'application/font-woff' }
'.woff2' { return 'application/font-woff2' }
'.wpl' { return 'application/vnd.ms-wpl' }
'.wps' { return 'application/vnd.ms-works' }
'.wri' { return 'application/x-mswrite' }
'.wrl' { return 'x-world/x-vrml' }
'.wrz' { return 'x-world/x-vrml' }
'.wsc' { return 'text/scriptlet' }
'.wsdl' { return 'text/xml' }
'.wvx' { return 'video/x-ms-wvx' }
'.x' { return 'application/directx' }
'.xaf' { return 'x-world/x-vrml' }
'.xaml' { return 'application/xaml+xml' }
'.xap' { return 'application/x-silverlight-app' }
'.xbap' { return 'application/x-ms-xbap' }
'.xbm' { return 'image/x-xbitmap' }
'.xdr' { return 'text/plain' }
'.xht' { return 'application/xhtml+xml' }
'.xhtml' { return 'application/xhtml+xml' }
'.xla' { return 'application/vnd.ms-excel' }
'.xlam' { return 'application/vnd.ms-excel.addin.macroEnabled.12' }
'.xlc' { return 'application/vnd.ms-excel' }
'.xld' { return 'application/vnd.ms-excel' }
'.xlk' { return 'application/vnd.ms-excel' }
'.xll' { return 'application/vnd.ms-excel' }
'.xlm' { return 'application/vnd.ms-excel' }
'.xls' { return 'application/vnd.ms-excel' }
'.xlsb' { return 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' }
'.xlsm' { return 'application/vnd.ms-excel.sheet.macroEnabled.12' }
'.xlsx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
'.xlt' { return 'application/vnd.ms-excel' }
'.xltm' { return 'application/vnd.ms-excel.template.macroEnabled.12' }
'.xltx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' }
'.xlw' { return 'application/vnd.ms-excel' }
'.xml' { return 'text/xml' }
'.xmp' { return 'application/octet-stream' }
'.xmta' { return 'application/xml' }
'.xof' { return 'x-world/x-vrml' }
'.xoml' { return 'text/plain' }
'.xpm' { return 'image/x-xpixmap' }
'.xps' { return 'application/vnd.ms-xpsdocument' }
'.xrm-ms' { return 'text/xml' }
'.xsc' { return 'application/xml' }
'.xsd' { return 'text/xml' }
'.xsf' { return 'text/xml' }
'.xsl' { return 'text/xml' }
'.xslt' { return 'text/xml' }
'.xsn' { return 'application/octet-stream' }
'.xss' { return 'application/xml' }
'.xspf' { return 'application/xspf+xml' }
'.xtp' { return 'application/octet-stream' }
'.xwd' { return 'image/x-xwindowdump' }
'.yaml' { return 'application/x-yaml' }
'.yml' { return 'application/x-yaml' }
'.z' { return 'application/x-compress' }
'.zip' { return 'application/zip' }
default { return (Resolve-PodeValue -Check $DefaultIsNull -TrueValue $null -FalseValue 'text/plain') }
}
}
function Get-PodeStatusDescription {
param(
[Parameter()]
[int]
$StatusCode
)
switch ($StatusCode) {
100 { return 'Continue' }
101 { return 'Switching Protocols' }
102 { return 'Processing' }
103 { return 'Early Hints' }
200 { return 'OK' }
201 { return 'Created' }
202 { return 'Accepted' }
203 { return 'Non-Authoritative Information' }
204 { return 'No Content' }
205 { return 'Reset Content' }
206 { return 'Partial Content' }
207 { return 'Multi-Status' }
208 { return 'Already Reported' }
226 { return 'IM Used' }
300 { return 'Multiple Choices' }
301 { return 'Moved Permanently' }
302 { return 'Found' }
303 { return 'See Other' }
304 { return 'Not Modified' }
305 { return 'Use Proxy' }
306 { return 'Switch Proxy' }
307 { return 'Temporary Redirect' }
308 { return 'Permanent Redirect' }
400 { return 'Bad Request' }
401 { return 'Unauthorized' }
402 { return 'Payment Required' }
403 { return 'Forbidden' }
404 { return 'Not Found' }
405 { return 'Method Not Allowed' }
406 { return 'Not Acceptable' }
407 { return 'Proxy Authentication Required' }
408 { return 'Request Timeout' }
409 { return 'Conflict' }
410 { return 'Gone' }
411 { return 'Length Required' }
412 { return 'Precondition Failed' }
413 { return 'Payload Too Large' }
414 { return 'URI Too Long' }
415 { return 'Unsupported Media Type' }
416 { return 'Range Not Satisfiable' }
417 { return 'Expectation Failed' }
418 { return "I'm a Teapot" }
419 { return 'Page Expired' }
420 { return 'Enhance Your Calm' }
421 { return 'Misdirected Request' }
422 { return 'Unprocessable Entity' }
423 { return 'Locked' }
424 { return 'Failed Dependency' }
425 { return 'Too Early' }
426 { return 'Upgrade Required' }
428 { return 'Precondition Required' }
429 { return 'Too Many Requests' }
431 { return 'Request Header Fields Too Large' }
440 { return 'Login Time-out' }
450 { return 'Blocked by Windows Parental Controls' }
451 { return 'Unavailable For Legal Reasons' }
500 { return 'Internal Server Error' }
501 { return 'Not Implemented' }
502 { return 'Bad Gateway' }
503 { return 'Service Unavailable' }
504 { return 'Gateway Timeout' }
505 { return 'HTTP Version Not Supported' }
506 { return 'Variant Also Negotiates' }
507 { return 'Insufficient Storage' }
508 { return 'Loop Detected' }
510 { return 'Not Extended' }
511 { return 'Network Authentication Required' }
526 { return 'Invalid SSL Certificate' }
default { return ([string]::Empty) }
}
}
function Update-PodeServerRequestMetrics {
param(
[Parameter()]
[hashtable]
$WebEvent
)
if ($null -eq $WebEvent) {
return
}
# status code
$status = "$($WebEvent.Response.StatusCode)"
# metrics to update
$metrics = @($PodeContext.Metrics.Requests)
if ($null -ne $WebEvent.Route) {
$metrics += $WebEvent.Route.Metrics.Requests
}
# increment the request metrics
foreach ($metric in $metrics) {
Lock-PodeObject -Object $metric -ScriptBlock {
$metric.Total++
if (!$metric.StatusCodes.ContainsKey($status)) {
$metric.StatusCodes[$status] = 0
}
$metric.StatusCodes[$status]++
}
}
}
function Update-PodeServerSignalMetrics {
param(
[Parameter()]
[hashtable]
$SignalEvent
)
if ($null -eq $SignalEvent) {
return
}
# metrics to update
$metrics = @($PodeContext.Metrics.Signals)
if ($null -ne $SignalEvent.Route) {
$metrics += $SignalEvent.Route.Metrics.Requests
}
# increment the request metrics
foreach ($metric in $metrics) {
Lock-PodeObject -Object $metric -ScriptBlock {
$metric.Total++
}
}
}
using namespace System.Security.Cryptography
function Invoke-PodeMiddleware {
param(
[Parameter()]
$Middleware,
[Parameter()]
[string]
$Route
)
# if there's no middleware, do nothing
if (($null -eq $Middleware) -or ($Middleware.Length -eq 0)) {
return $true
}
# filter the middleware down by route (retaining order)
if (![string]::IsNullOrWhiteSpace($Route)) {
$Middleware = @(foreach ($mware in $Middleware) {
if ($null -eq $mware) {
continue
}
if ([string]::IsNullOrWhiteSpace($mware.Route) -or ($mware.Route -ieq '/') -or ($mware.Route -ieq $Route) -or ($Route -imatch "^$($mware.Route)$")) {
$mware
}
})
}
# continue or halt?
$continue = $true
# loop through each of the middleware, invoking the next if it returns true
foreach ($midware in @($Middleware)) {
if (($null -eq $midware) -or ($null -eq $midware.Logic)) {
continue
}
try {
$continue = Invoke-PodeScriptBlock -ScriptBlock $midware.Logic -Arguments $midware.Arguments -UsingVariables $midware.UsingVariables -Return -Scoped -Splat
if ($null -eq $continue) {
$continue = $true
}
}
catch {
Set-PodeResponseStatus -Code 500 -Exception $_
$continue = $false
$_ | Write-PodeErrorLog
}
if (!$continue) {
break
}
}
return $continue
}
function New-PodeMiddlewareInternal {
[OutputType([hashtable])]
param(
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[string]
$Route,
[Parameter()]
[object[]]
$ArgumentList,
[Parameter(Mandatory = $true)]
[System.Management.Automation.SessionState]
$PSSession
)
if (Test-PodeIsEmpty $ScriptBlock) {
throw '[Middleware]: No ScriptBlock supplied'
}
# if route is empty, set it to root
$Route = ConvertTo-PodeRouteRegex -Path $Route
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSSession
# create the middleware hashtable from a scriptblock
$HashTable = @{
Route = $Route
Logic = $ScriptBlock
Arguments = $ArgumentList
UsingVariables = $usingVars
}
# return the middleware, so it can be cached/added at a later date
return $HashTable
}
function Get-PodeInbuiltMiddleware {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
# check if middleware contains an override
$override = ($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name })
# if override there, remove it from middleware
if ($override) {
$PodeContext.Server.Middleware = @($PodeContext.Server.Middleware | Where-Object { $_.Name -ine $Name })
$ScriptBlock = $override.Logic
}
# return the script
return @{
Name = $Name
Logic = $ScriptBlock
}
}
function Get-PodeAccessMiddleware {
return (Get-PodeInbuiltMiddleware -Name '__pode_mw_access__' -ScriptBlock {
# are there any rules?
if (($PodeContext.Server.Access.Allow.Count -eq 0) -and ($PodeContext.Server.Access.Deny.Count -eq 0)) {
return $true
}
# ensure the request IP address is allowed
if (!(Test-PodeIPAccess -IP $WebEvent.Request.RemoteEndPoint.Address)) {
Set-PodeResponseStatus -Code 403
return $false
}
# request is allowed
return $true
})
}
function Get-PodeLimitMiddleware {
return (Get-PodeInbuiltMiddleware -Name '__pode_mw_rate_limit__' -ScriptBlock {
# are there any rules?
if ($PodeContext.Server.Limits.Rules.Count -eq 0) {
return $true
}
# check the request IP address has not hit a rate limit
if (!(Test-PodeIPLimit -IP $WebEvent.Request.RemoteEndPoint.Address)) {
Set-PodeResponseStatus -Code 429
return $false
}
# check the route
if (!(Test-PodeRouteLimit -Path $WebEvent.Path)) {
Set-PodeResponseStatus -Code 429
return $false
}
# check the endpoint
if (!(Test-PodeEndpointLimit -EndpointName $WebEvent.Endpoint.Name)) {
Set-PodeResponseStatus -Code 429
return $false
}
# request is allowed
return $true
})
}
<#
.SYNOPSIS
Retrieves middleware for serving public static content in a Pode web server.
.DESCRIPTION
This function retrieves middleware for serving public static content in a Pode web server.
It searches for static content based on the requested path and serves it if found.
.PARAMETER WebEvent
The PodeWebEvent object representing the incoming web request.
.PARAMETER PodeContext
The PodeContext object representing the current Pode server context.
.EXAMPLE
Get-PodePublicMiddleware
Retrieves middleware for serving public static content.
#>
function Get-PodePublicMiddleware {
return (Get-PodeInbuiltMiddleware -Name '__pode_mw_static_content__' -ScriptBlock {
# only find public static content here
$path = Find-PodePublicRoute -Path $WebEvent.Path
if ([string]::IsNullOrWhiteSpace($path)) {
return $true
}
# check current state of caching
$cachable = Test-PodeRouteValidForCaching -Path $WebEvent.Path
# write the file to the response
Write-PodeFileResponse -Path $path -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable
# public static content found, stop
return $false
})
}
<#
.SYNOPSIS
Middleware function to validate the route for an incoming web request.
.DESCRIPTION
This function is used as middleware to validate the route for an incoming web request. It checks if the route exists for the requested method and path. If the route does not exist, it sets the appropriate response status code (404 for not found, 405 for method not allowed) and returns false to halt further processing. If the route exists, it sets various properties on the WebEvent object, such as parameters, content type, and transfer encoding, and returns true to continue processing.
.PARAMETER None
.EXAMPLE
$middleware = Get-PodeRouteValidateMiddleware
Add-PodeMiddleware -Middleware $middleware
.NOTES
This function is part of the internal Pode server logic and is typically not called directly by users.
#>
function Get-PodeRouteValidateMiddleware {
return @{
Name = '__pode_mw_route_validation__'
Logic = {
if ($PodeContext.Server.Web.Static.ValidateLast) {
# check the main routes and check the static routes
$route = Find-PodeRoute -Method $WebEvent.Method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name -CheckWildMethod
if ($null -eq $route) {
$route = Find-PodeStaticRoute -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name
}
}
else {
# check if the path is static route first, then check the main routes
$route = Find-PodeStaticRoute -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name
if ($null -eq $route) {
$route = Find-PodeRoute -Method $WebEvent.Method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name -CheckWildMethod
}
}
# if there's no route defined, it's a 404 - or a 405 if a route exists for any other method
if ($null -eq $route) {
# check if a route exists for another method
$methods = @('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE')
$diff_route = @(foreach ($method in $methods) {
$r = Find-PodeRoute -Method $method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name
if ($null -ne $r) {
$r
break
}
})[0]
if ($null -ne $diff_route) {
Set-PodeResponseStatus -Code 405
return $false
}
# otheriwse, it's a 404
Set-PodeResponseStatus -Code 404
return $false
}
# check if static and split
if ($null -ne $route.Content) {
$WebEvent.StaticContent = $route.Content
$route = $route.Route
}
# set the route parameters
$WebEvent.Parameters = @{}
if ($WebEvent.Path -imatch "$($route.Path)$") {
$WebEvent.Parameters = $Matches
}
# set the route on the WebEvent
$WebEvent.Route = $route
# override the content type from the route if it's not empty
if (![string]::IsNullOrWhiteSpace($route.ContentType)) {
$WebEvent.ContentType = $route.ContentType
}
# override the transfer encoding from the route if it's not empty
if (![string]::IsNullOrWhiteSpace($route.TransferEncoding)) {
$WebEvent.TransferEncoding = $route.TransferEncoding
}
# set the content type for any pages for the route if it's not empty
$WebEvent.ErrorType = $route.ErrorType
# route exists
return $true
}
}
}
function Get-PodeBodyMiddleware {
return (Get-PodeInbuiltMiddleware -Name '__pode_mw_body_parsing__' -ScriptBlock {
try {
# attempt to parse that data
$result = ConvertFrom-PodeRequestContent -Request $WebEvent.Request -ContentType $WebEvent.ContentType -TransferEncoding $WebEvent.TransferEncoding
# set session data
$WebEvent.Data = $result.Data
$WebEvent.Files = $result.Files
# payload parsed
return $true
}
catch {
Set-PodeResponseStatus -Code 400 -Exception $_
return $false
}
})
}
function Get-PodeQueryMiddleware {
return (Get-PodeInbuiltMiddleware -Name '__pode_mw_query_parsing__' -ScriptBlock {
try {
# set the query string from the request
$WebEvent.Query = (ConvertFrom-PodeNameValueToHashTable -Collection $WebEvent.Request.QueryString)
return $true
}
catch {
Set-PodeResponseStatus -Code 400 -Exception $_
return $false
}
})
}
function Get-PodeCookieMiddleware {
return (Get-PodeInbuiltMiddleware -Name '__pode_mw_cookie_parsing__' -ScriptBlock {
# if cookies already set, return
if ($WebEvent.Cookies.Count -gt 0) {
return $true
}
# if the request's header has no cookies, return
$h_cookie = (Get-PodeHeader -Name 'Cookie')
if ([string]::IsNullOrWhiteSpace($h_cookie)) {
return $true
}
# parse the cookies from the header
$cookies = @($h_cookie -split '; ')
$WebEvent.Cookies = @{}
foreach ($cookie in $cookies) {
$atoms = $cookie.Split('=', 2)
$value = [string]::Empty
if ($atoms.Length -gt 1) {
foreach ($atom in $atoms[1..($atoms.Length - 1)]) {
$value += $atom
}
}
$WebEvent.Cookies[$atoms[0]] = [System.Net.Cookie]::new($atoms[0], $value)
}
return $true
})
}
function Get-PodeSecurityMiddleware {
return (Get-PodeInbuiltMiddleware -Name '__pode_mw_security__' -ScriptBlock {
# are there any security headers setup?
if ($PodeContext.Server.Security.Headers.Count -eq 0) {
return $true
}
# add security headers
Set-PodeHeaderBulk -Value $PodeContext.Server.Security.Headers
# continue to next middleware/route
return $true
})
}
function Initialize-PodeIISMiddleware {
# do nothing if not iis
if (!$PodeContext.Server.IsIIS) {
return
}
# fail if no iis token - because there should be!
if ([string]::IsNullOrWhiteSpace($PodeContext.Server.IIS.Token)) {
throw 'IIS ASPNETCORE_TOKEN is missing'
}
# add middleware to check every request has the token
Add-PodeMiddleware -Name '__pode_iis_token_check__' -ScriptBlock {
$token = Get-PodeHeader -Name 'MS-ASPNETCORE-TOKEN'
if ($token -ne $PodeContext.Server.IIS.Token) {
Set-PodeResponseStatus -Code 400 -Description 'MS-ASPNETCORE-TOKEN header missing'
return $false
}
return $true
}
# add middleware to check if there's a client cert
Add-PodeMiddleware -Name '__pode_iis_clientcert_check__' -ScriptBlock {
if (!$WebEvent.Request.AllowClientCertificate -or ($null -ne $WebEvent.Request.ClientCertificate)) {
return $true
}
$headers = @('MS-ASPNETCORE-CLIENTCERT', 'X-ARR-ClientCert')
foreach ($header in $headers) {
if (!(Test-PodeHeader -Name $header)) {
continue
}
try {
$value = Get-PodeHeader -Name $header
$WebEvent.Request.ClientCertificate = [X509Certificates.X509Certificate2]::new([Convert]::FromBase64String($value))
}
catch {
$WebEvent.Request.ClientCertificateErrors = [System.Net.Security.SslPolicyErrors]::RemoteCertificateNotAvailable
}
}
return $true
}
# add route to gracefully shutdown server for iis
Add-PodeRoute -Method Post -Path '/iisintegration' -ScriptBlock {
$eventType = Get-PodeHeader -Name 'MS-ASPNETCORE-EVENT'
# no x-forward
if (Test-PodeHeader -Name 'X-Forwarded-For') {
Set-PodeResponseStatus -Code 400
return
}
# no user-agent
if (Test-PodeHeader -Name 'User-Agent') {
Set-PodeResponseStatus -Code 400
return
}
# valid local Host
$hostValue = Get-PodeHeader -Name 'Host'
if ($hostValue -ine "127.0.0.1:$($PodeContext.Server.IIS.Port)") {
Set-PodeResponseStatus -Code 400
return
}
# no content-length
if ($WebEvent.Request.ContentLength -gt 0) {
Set-PodeResponseStatus -Code 400
return
}
# valid event type
if ($eventType -ine 'shutdown') {
Set-PodeResponseStatus -Code 400
return
}
# shutdown
$PodeContext.Server.IIS.Shutdown = $true
Close-PodeServer
Set-PodeResponseStatus -Code 202
}
}
function Get-PodeRandomName {
$adjs = @(
'admiring',
'agitated',
'blissful',
'dazzling',
'ecstatic',
'eloquent',
'friendly',
'gracious',
'hardcore',
'laughing',
'peaceful',
'pedantic',
'reverent',
'romantic',
'trusting',
'vigilant',
'vigorous',
'wizardly',
'youthful'
)
$names = @(
'almeida',
'babbage',
'bardeen',
'shannon',
'davinci',
'feynman',
'galileo',
'goodall',
'hawking',
'hermann',
'hodgkin',
'hypatia',
'jackson',
'johnson',
'kapitsa',
'keldysh',
'khorana',
'lalande',
'lamport',
'leavitt',
'lumiere',
'mcnulty',
'meitner',
'mestorf',
'murdock',
'neumann',
'noether',
'pasteur',
'perlman',
'poitras',
'ptolemy',
'ritchie',
'shirley',
'swanson',
'swirles',
'vaughan',
'volhard',
'villani',
'wescoff',
'wozniak'
)
$adjsRand = (Get-Random -Minimum 0 -Maximum $adjs.Length)
$namesRand = (Get-Random -Minimum 0 -Maximum $names.Length)
return "$($adjs[$adjsRand])_$($names[$namesRand])"
}
<#
.SYNOPSIS
Converts content into an OpenAPI schema object format.
.DESCRIPTION
The ConvertTo-PodeOAObjectSchema function takes a hashtable representing content and converts it into a format suitable for OpenAPI schema objects.
It validates the content types, processes array structures, and converts each property or reference into the appropriate OpenAPI schema format.
The function is designed to handle complex content structures for OpenAPI documentation within the Pode framework.
.PARAMETER Content
A hashtable representing the content to be converted into an OpenAPI schema object. The content can include various types and structures.
.PARAMETER Properties
A switch to indicate if the content represents properties of an object schema.
.PARAMETER DefinitionTag
A string representing the definition tag to be used in the conversion process. This tag is essential for correctly formatting the content according to OpenAPI specifications.
.EXAMPLE
$schemaObject = ConvertTo-PodeOAObjectSchema -Content $myContent -DefinitionTag 'myTag'
Converts a hashtable of content into an OpenAPI schema object using the definition tag 'myTag'.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOAObjectSchema {
param(
[Parameter(ValueFromPipeline = $true)]
[hashtable]
$Content,
[Parameter(ValueFromPipeline = $false)]
[switch]
$Properties,
[Parameter(Mandatory = $true)]
[string ]
$DefinitionTag
)
# Ensure all content types are valid MIME types
foreach ($type in $Content.Keys) {
if ($type -inotmatch '^(application|audio|image|message|model|multipart|text|video|\*)\/[\w\.\-\*]+(;[\s]*(charset|boundary)=[\w\.\-\*]+)*$|^"\*\/\*"$') {
throw "Invalid content-type found for schema: $($type)"
}
}
# manage generic schema json conversion issue
if ( $Content.ContainsKey('*/*')) {
$Content['"*/*"'] = $Content['*/*']
$Content.Remove('*/*')
}
# convert each schema to OpenAPI format
# Initialize an empty hashtable for the schema
$obj = @{}
# Process each content type
$types = [string[]]$Content.Keys
foreach ($type in $types) {
# Initialize schema structure for the type
$obj[$type] = @{ }
# Handle upload content, array structures, and shared component schema references
if ($Content[$type].__upload) {
if ($Content[$type].__array) {
$upload = $Content[$type].__content.__upload
}
else {
$upload = $Content[$type].__upload
}
if ($type -ieq 'multipart/form-data' -and $upload.content ) {
if ((Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag ) -and $upload.partContentMediaType) {
foreach ($key in $upload.content.Properties ) {
if ($key.type -eq 'string' -and $key.format -and $key.format -ieq 'binary' -or $key.format -ieq 'base64') {
$key.ContentMediaType = $PartContentMediaType
$key.remove('format')
break
}
}
}
$newContent = $upload.content
}
else {
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) {
$newContent = [ordered]@{
'type' = 'string'
'format' = $upload.contentEncoding
}
}
else {
if ($ContentEncoding -ieq 'Base64') {
$newContent = [ordered]@{
'type' = 'string'
'contentEncoding' = $upload.contentEncoding
}
}
}
}
if ($Content[$type].__array) {
$Content[$type].__content = $newContent
}
else {
$Content[$type] = $newContent
}
}
if ($Content[$type].__array) {
$isArray = $true
$item = $Content[$type].__content
$obj[$type].schema = [ordered]@{
'type' = 'array'
'items' = $null
}
if ( $Content[$type].__title) {
$obj[$type].schema.title = $Content[$type].__title
}
if ( $Content[$type].__uniqueItems) {
$obj[$type].schema.uniqueItems = $Content[$type].__uniqueItems
}
if ( $Content[$type].__maxItems) {
$obj[$type].schema.__maxItems = $Content[$type].__maxItems
}
if ( $Content[$type].minItems) {
$obj[$type].schema.minItems = $Content[$type].__minItems
}
}
else {
$item = $Content[$type]
$isArray = $false
}
# Add set schema objects or empty content
if ($item -is [string]) {
if (![string]::IsNullOrEmpty($item )) {
#Check for empty reference
if (@('string', 'integer' , 'number', 'boolean' ) -icontains $item) {
if ($isArray) {
$obj[$type].schema.items = @{
'type' = $item.ToLower()
}
}
else {
$obj[$type].schema = @{
'type' = $item.ToLower()
}
}
}
else {
Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $item -PostValidation
if ($isArray) {
$obj[$type].schema.items = @{
'$ref' = "#/components/schemas/$($item)"
}
}
else {
$obj[$type].schema = @{
'$ref' = "#/components/schemas/$($item)"
}
}
}
}
else {
# Create an empty content
$obj[$type] = @{}
}
}
else {
if ($item.Count -eq 0) {
$result = @{}
}
else {
$result = ($item | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag)
}
if ($Properties) {
if ($item.Name) {
$obj[$type].schema = @{
'properties' = @{
$item.Name = $result
}
}
}
else {
Throw 'The Properties parameters cannot be used if the Property has no name'
}
}
else {
if ($isArray) {
$obj[$type].schema.items = $result
}
else {
$obj[$type].schema = $result
}
}
}
}
return $obj
}
<#
.SYNOPSIS
Check if an ComponentSchemaJson reference exist.
.DESCRIPTION
Check if an ComponentSchemaJson reference with a given name exist.
.PARAMETER Name
The Name of the ComponentSchemaJson reference.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Test-PodeOAComponentSchemaJson {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string[]]
$DefinitionTag
)
foreach ($tag in $DefinitionTag) {
if (!($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson.keys -ccontains $Name)) {
# If $Name is not found in the current $tag, return $false
return $false
}
}
return $true
}
<#
.SYNOPSIS
Tests if a given name exists in the external path keys of OpenAPI definitions for specified definition tags.
.DESCRIPTION
The Test-PodeOAComponentExternalPath function iterates over a list of definition tags and checks if a given name
is present in the external path keys of OpenAPI definitions within the Pode server context. This function is typically
used to validate if a specific component name is already defined in the external paths of the OpenAPI documentation.
.PARAMETER Name
The name of the external path component to be checked within the OpenAPI definitions.
.PARAMETER DefinitionTag
An array of definition tags against which the existence of the name will be checked in the OpenAPI definitions.
.EXAMPLE
$exists = Test-PodeOAComponentExternalPath -Name 'MyComponentName' -DefinitionTag @('tag1', 'tag2')
Checks if 'MyComponentName' exists in the external path keys of OpenAPI definitions for 'tag1' and 'tag2'.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Test-PodeOAComponentExternalPath {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string[]]
$DefinitionTag
)
# Iterate over each definition tag
foreach ($tag in $DefinitionTag) {
# Check if the name exists in the external path keys of the current definition tag
if (!($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath.keys -ccontains $Name)) {
# If the name is not found in the current tag, return false
return $false
}
}
# If the name exists in all specified tags, return true
return $true
}
<#
.SYNOPSIS
Converts a property into an OpenAPI 'Of' property structure based on a given definition tag.
.DESCRIPTION
The ConvertTo-PodeOAOfProperty function is used to convert a given property into one of the OpenAPI 'Of' properties:
allOf, oneOf, or anyOf. These structures are used in OpenAPI documentation to define complex types. The function
constructs the appropriate structure based on the type of the property and the definition tag provided.
.PARAMETER Property
A hashtable representing the property to be converted. It should contain the type (allOf, oneOf, or anyOf) and
potentially a list of schemas.
.PARAMETER DefinitionTag
A mandatory string parameter specifying the definition tag in OpenAPI documentation, used for validating components.
.EXAMPLE
$ofProperty = ConvertTo-PodeOAOfProperty -Property $myProperty -DefinitionTag 'myTag'
Converts a given property into an OpenAPI 'Of' structure using the specified definition tag.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOAOfProperty {
param (
[hashtable]
$Property,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
# Check if the property type is one of the supported 'Of' types
if (@('allOf', 'oneOf', 'anyOf') -inotcontains $Property.type) {
return @{}
}
# Initialize the schema with the 'Of' type
$schema = [ordered]@{
$Property.type = @()
}
# Process each schema defined in the property
if ($Property.schemas) {
foreach ($prop in $Property.schemas) {
if ($prop -is [string]) {
# Validate the schema component and add a reference to it
Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $prop -PostValidation
$schema[$Property.type] += @{ '$ref' = "#/components/schemas/$prop" }
}
else {
# Convert the property to an OpenAPI schema property
$schema[$Property.type] += $prop | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag
}
}
}
# Add discriminator if present
if ($Property.discriminator) {
$schema['discriminator'] = $Property.discriminator
}
# Return the constructed 'Of' property schema
return $schema
}
function ConvertTo-PodeOASchemaProperty {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Property,
[switch]
$NoDescription,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
if ( @('allof', 'oneof', 'anyof') -icontains $Property.type) {
$schema = ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $Property
}
else {
# base schema type
$schema = [ordered]@{ }
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) {
if ($Property.type -is [string[]]) {
throw 'Multi type properties requeired OpenApi Version 3.1 or above'
}
$schema['type'] = $Property.type.ToLower()
}
else {
$schema.type = @($Property.type.ToLower())
if ($Property.nullable) {
$schema.type += 'null'
}
}
}
if ($Property.externalDocs) {
$schema['externalDocs'] = $Property.externalDocs
}
if (!$NoDescription -and $Property.description) {
$schema['description'] = $Property.description
}
if ($Property.default) {
$schema['default'] = $Property.default
}
if ($Property.deprecated) {
$schema['deprecated'] = $Property.deprecated
}
if ($Property.nullable -and (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag )) {
$schema['nullable'] = $Property.nullable
}
if ($Property.writeOnly) {
$schema['writeOnly'] = $Property.writeOnly
}
if ($Property.readOnly) {
$schema['readOnly'] = $Property.readOnly
}
if ($Property.example) {
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) {
$schema['example'] = $Property.example
}
else {
if ($Property.example -is [Array]) {
$schema['examples'] = $Property.example
}
else {
$schema['examples'] = @( $Property.example)
}
}
}
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag ) {
if ($Property.minimum) {
$schema['minimum'] = $Property.minimum
}
if ($Property.maximum) {
$schema['maximum'] = $Property.maximum
}
if ($Property.exclusiveMaximum) {
$schema['exclusiveMaximum'] = $Property.exclusiveMaximum
}
if ($Property.exclusiveMinimum) {
$schema['exclusiveMinimum'] = $Property.exclusiveMinimum
}
}
else {
if ($Property.maximum) {
if ($Property.exclusiveMaximum) {
$schema['exclusiveMaximum'] = $Property.maximum
}
else {
$schema['maximum'] = $Property.maximum
}
}
if ($Property.minimum) {
if ($Property.exclusiveMinimum) {
$schema['exclusiveMinimum'] = $Property.minimum
}
else {
$schema['minimum'] = $Property.minimum
}
}
}
if ($Property.multipleOf) {
$schema['multipleOf'] = $Property.multipleOf
}
if ($Property.pattern) {
$schema['pattern'] = $Property.pattern
}
if ($Property.minLength) {
$schema['minLength'] = $Property.minLength
}
if ($Property.maxLength) {
$schema['maxLength'] = $Property.maxLength
}
if ($Property.xml ) {
$schema['xml'] = $Property.xml
}
if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag ) {
if ($Property.ContentMediaType) {
$schema['contentMediaType'] = $Property.ContentMediaType
}
if ($Property.ContentEncoding) {
$schema['contentEncoding'] = $Property.ContentEncoding
}
}
# are we using an array?
if ($Property.array) {
if ($Property.maxItems ) {
$schema['maxItems'] = $Property.maxItems
}
if ($Property.minItems ) {
$schema['minItems'] = $Property.minItems
}
if ($Property.uniqueItems ) {
$schema['uniqueItems'] = $Property.uniqueItems
}
$schema['type'] = 'array'
if ($Property.type -ieq 'schema') {
Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation
$schema['items'] = @{ '$ref' = "#/components/schemas/$($Property['schema'])" }
}
else {
$Property.array = $false
if ($Property.xml) {
$xmlFromProperties = $Property.xml
$Property.Remove('xml')
}
$schema['items'] = ($Property | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag)
$Property.array = $true
if ($xmlFromProperties) {
$Property.xml = $xmlFromProperties
}
if ($Property.xmlItemName) {
$schema.items.xml = @{'name' = $Property.xmlItemName }
}
}
return $schema
}
else {
#format is not applicable to array
if ($Property.format) {
$schema['format'] = $Property.format
}
# schema refs
if ($Property.type -ieq 'schema') {
Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Property['schema'] -PostValidation
$schema = @{
'$ref' = "#/components/schemas/$($Property['schema'])"
}
}
#only if it's not an array
if ($Property.enum ) {
$schema['enum'] = $Property.enum
}
}
if ($Property.object) {
# are we using an object?
$Property.object = $false
$schema = @{
type = 'object'
properties = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property)
}
$Property.object = $true
if ($Property.required) {
$schema['required'] = @($Property.name)
}
}
if ($Property.type -ieq 'object') {
foreach ($prop in $Property.properties) {
if ( @('allOf', 'oneOf', 'anyOf') -icontains $prop.type) {
switch ($prop.type.ToLower()) {
'allof' { $prop.type = 'allOf' }
'oneof' { $prop.type = 'oneOf' }
'anyof' { $prop.type = 'anyOf' }
}
$schema += ConvertTo-PodeOAofProperty -DefinitionTag $DefinitionTag -Property $prop
}
}
if ($Property.properties) {
$schema['properties'] = (ConvertTo-PodeOASchemaObjectProperty -DefinitionTag $DefinitionTag -Properties $Property.properties)
$RequiredList = @(($Property.properties | Where-Object { $_.required }) )
if ( $RequiredList.Count -gt 0) {
$schema['required'] = @($RequiredList.name)
}
}
else {
#if noproperties parameter create an empty properties
if ( $Property.properties.Count -eq 1 -and $null -eq $Property.properties[0]) {
$schema['properties'] = @{}
}
}
if ($Property.minProperties) {
$schema['minProperties'] = $Property.minProperties
}
if ($Property.maxProperties) {
$schema['maxProperties'] = $Property.maxProperties
}
if ($Property.additionalProperties) {
$schema['additionalProperties'] = $Property.additionalProperties
}
if ($Property.discriminator) {
$schema['discriminator'] = $Property.discriminator
}
}
return $schema
}
<#
.SYNOPSIS
Converts a collection of properties into an OpenAPI schema object format.
.DESCRIPTION
The ConvertTo-PodeOASchemaObjectProperty function takes an array of property hashtables and converts them into
a format suitable for OpenAPI schema objects. It specifically processes properties that are not 'allOf', 'oneOf',
or 'anyOf' types, using the ConvertTo-PodeOASchemaProperty function for conversion based on a given definition tag.
.PARAMETER Properties
An array of hashtables representing properties to be converted. Each hashtable should contain the property's details.
.PARAMETER DefinitionTag
A string representing the definition tag to be used in the conversion process. This tag is crucial for correctly
formatting the properties according to OpenAPI specifications.
.EXAMPLE
$schemaObject = ConvertTo-PodeOASchemaObjectProperty -Properties $myProperties -DefinitionTag 'myTag'
Converts an array of property hashtables into an OpenAPI schema object using the definition tag 'myTag'.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOASchemaObjectProperty {
param(
[Parameter(Mandatory = $true)]
[hashtable[]]
$Properties,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
# Initialize an empty hashtable for the schema
$schema = @{}
# Iterate over each property and convert to OpenAPI schema property if applicable
foreach ($prop in $Properties) {
# Exclude properties of type 'allOf', 'oneOf', or 'anyOf'
if (@('allOf', 'oneOf', 'anyOf') -inotcontains $prop.type) {
# Convert the property to an OpenAPI schema property and add to the schema hashtable
$schema[$prop.name] = ($prop | ConvertTo-PodeOASchemaProperty -DefinitionTag $DefinitionTag)
}
}
# Return the constructed schema object
return $schema
}
<#
.SYNOPSIS
Sets OpenAPI specifications for a given route.
.DESCRIPTION
The Set-PodeOpenApiRouteValue function processes and sets various OpenAPI specifications for a given route based on the provided definition tag.
It handles route attributes such as deprecated status, tags, summary, description, operation ID, parameters, request body, callbacks, authentication,
and responses to build a complete OpenAPI specification for the route.
.PARAMETER Route
A hashtable representing the route for which OpenAPI specifications are being set.
.PARAMETER DefinitionTag
A string representing the definition tag used for specifying OpenAPI documentation details for the route.
.EXAMPLE
$routeValues = Set-PodeOpenApiRouteValue -Route $route -DefinitionTag 'myTag'
Sets OpenAPI specifications for the given route using the definition tag 'myTag'.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Set-PodeOpenApiRouteValue {
param(
[Parameter(Mandatory = $true)]
[hashtable]
$Route,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
# Initialize an ordered hashtable to store route properties
$pm = [ordered]@{}
# Process various OpenAPI attributes for the route
if ($Route.OpenApi.Deprecated) {
$pm.deprecated = $Route.OpenApi.Deprecated
}
if ($Route.OpenApi.Tags) {
$pm.tags = $Route.OpenApi.Tags
}
if ($Route.OpenApi.Summary) {
$pm.summary = $Route.OpenApi.Summary
}
if ($Route.OpenApi.Description) {
$pm.description = $Route.OpenApi.Description
}
if ($Route.OpenApi.OperationId) {
$pm.operationId = $Route.OpenApi.OperationId
}
if ($Route.OpenApi.Parameters) {
$pm.parameters = $Route.OpenApi.Parameters
}
if ($Route.OpenApi.RequestBody.$DefinitionTag) {
$pm.requestBody = $Route.OpenApi.RequestBody.$DefinitionTag
}
if ($Route.OpenApi.CallBacks.$DefinitionTag) {
$pm.callbacks = $Route.OpenApi.CallBacks.$DefinitionTag
}
if ($Route.OpenApi.Servers) {
$pm.servers = $Route.OpenApi.Servers
}
if ($Route.OpenApi.Authentication.Count -gt 0) {
$pm.security = @()
foreach ($sct in (Expand-PodeAuthMerge -Names $Route.OpenApi.Authentication.Keys)) {
if ($PodeContext.Server.Authentications.Methods.$sct.Scheme.Scheme -ieq 'oauth2') {
if ($Route.AccessMeta.Scope ) {
$sctValue = $Route.AccessMeta.Scope
}
else {
#if scope is empty means 'any role' => assign an empty array
$sctValue = @()
}
$pm.security += @{ $sct = $sctValue }
}
elseif ($sct -eq '%_allowanon_%') {
#allow anonymous access
$pm.security += @{}
}
else {
$pm.security += @{$sct = @() }
}
}
}
if ($Route.OpenApi.Responses.$DefinitionTag ) {
$pm.responses = $Route.OpenApi.Responses.$DefinitionTag
}
else {
# Set responses or default to '204 No Content' if not specified
$pm.responses = @{'204' = @{'description' = (Get-PodeStatusDescription -StatusCode 204) } }
}
# Return the processed route properties
return $pm
}
<#
.SYNOPSIS
Generates an internal OpenAPI definition based on the current Pode server context and specific parameters.
.DESCRIPTION
This function constructs an OpenAPI definition by gathering metadata, route information, and API structure based on the provided parameters.
It supports customization of the API documentation through MetaInfo and directly influences the output by including specific server, authentication, and endpoint details.
.PARAMETER EndpointName
The name of the endpoint for which the OpenAPI definition is generated.
.PARAMETER MetaInfo
A hashtable containing metadata for the OpenAPI definition such as the API title, version, and description.
.PARAMETER DefinitionTag
Mandatory. A tag that identifies the specific OpenAPI definition to be generated or manipulated.
.OUTPUTS
Ordered dictionary representing the OpenAPI definition, which can be further processed into JSON or YAML format.
.EXAMPLE
$metaInfo = @{
Title = "My API";
Version = "v1";
Description = "This is my API description."
}
Get-PodeOpenApiDefinitionInternal -Protocol 'HTTPS' -Address 'myapi.example.com' -EndpointName 'MyAPI' -MetaInfo $metaInfo -DefinitionTag 'MyTag'
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Get-PodeOpenApiDefinitionInternal {
param(
[string]
$EndpointName,
[hashtable]
$MetaInfo,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
$Definition = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag]
if (!$Definition.Version) {
throw 'OpenApi openapi field is required'
}
$localEndpoint = $null
# set the openapi version
$def = [ordered]@{
openapi = $Definition.Version
}
if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag) {
$def['jsonSchemaDialect'] = 'https://spec.openapis.org/oas/3.1/dialect/base'
}
if ($Definition.info) {
$def['info'] = $Definition.info
}
#overwite default values
if ($MetaInfo.Title) {
$def.info.title = $MetaInfo.Title
}
if ($MetaInfo.Version) {
$def.info.version = $MetaInfo.Version
}
if ($MetaInfo.Description) {
$def.info.description = $MetaInfo.Description
}
if ($Definition.externalDocs) {
$def['externalDocs'] = $Definition.externalDocs
}
if ($Definition.servers) {
$def['servers'] = $Definition.servers
if ($Definition.servers.Count -eq 1 -and $Definition.servers[0].url.StartsWith('/')) {
$localEndpoint = $Definition.servers[0].url
}
}
elseif (!$MetaInfo.RestrictRoutes -and ($PodeContext.Server.Endpoints.Count -gt 1)) {
#$def['servers'] = $null
$def.servers = @(foreach ($endpoint in $PodeContext.Server.Endpoints.Values) {
@{
url = $endpoint.Url
description = (Protect-PodeValue -Value $endpoint.Description -Default $endpoint.Name)
}
})
}
if ($Definition.tags.Count -gt 0) {
$def['tags'] = @($Definition.tags.Values)
}
# paths
$def['paths'] = [ordered]@{}
if ($Definition.webhooks.count -gt 0) {
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) {
throw 'Feature webhooks is unsupported in OpenAPI v3.0.x'
}
else {
$keys = [string[]]$Definition.webhooks.Keys
foreach ($key in $keys) {
if ($Definition.webhooks[$key].NotPrepared) {
$Definition.webhooks[$key] = @{
$Definition.webhooks[$key].Method = Set-PodeOpenApiRouteValue -Route $Definition.webhooks[$key] -DefinitionTag $DefinitionTag
}
}
}
$def['webhooks'] = $Definition.webhooks
}
}
# components
$def['components'] = [ordered]@{}#$Definition.components
$components = $Definition.components
if ($components.schemas.count -gt 0) {
$def['components'].schemas = $components.schemas
}
if ($components.responses.count -gt 0) {
$def['components'].responses = $components.responses
}
if ($components.parameters.count -gt 0) {
$def['components'].parameters = $components.parameters
}
if ($components.examples.count -gt 0) {
$def['components'].examples = $components.examples
}
if ($components.requestBodies.count -gt 0) {
$def['components'].requestBodies = $components.requestBodies
}
if ($components.headers.count -gt 0) {
$def['components'].headers = $components.headers
}
if ($components.securitySchemes.count -gt 0) {
$def['components'].securitySchemes = $components.securitySchemes
}
if ($components.links.count -gt 0) {
$def['components'].links = $components.links
}
if ($components.callbacks.count -gt 0) {
$def['components'].callbacks = $components.callbacks
}
if ($components.pathItems.count -gt 0) {
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $DefinitionTag) {
throw 'Feature pathItems is unsupported in OpenAPI v3.0.x'
}
else {
$keys = [string[]]$components.pathItems.Keys
foreach ($key in $keys) {
if ($components.pathItems[$key].NotPrepared) {
$components.pathItems[$key] = @{
$components.pathItems[$key].Method = Set-PodeOpenApiRouteValue -Route $components.pathItems[$key] -DefinitionTag $DefinitionTag
}
}
}
$def['components'].pathItems = $components.pathItems
}
}
# auth/security components
if ($PodeContext.Server.Authentications.Methods.Count -gt 0) {
#if ($null -eq $def.components.securitySchemes) {
# $def.components.securitySchemes = @{}
# }
$authNames = (Expand-PodeAuthMerge -Names $PodeContext.Server.Authentications.Methods.Keys)
foreach ($authName in $authNames) {
$authType = (Find-PodeAuth -Name $authName).Scheme
$_authName = ($authName -replace '\s+', '')
$_authObj = @{}
if ($authType.Scheme -ieq 'apikey') {
$_authObj = [ordered]@{
type = $authType.Scheme
in = $authType.Arguments.Location.ToLowerInvariant()
name = $authType.Arguments.LocationName
}
if ($authType.Arguments.Description) {
$_authObj.description = $authType.Arguments.Description
}
}
elseif ($authType.Scheme -ieq 'oauth2') {
if ($authType.Arguments.Urls.Token -and $authType.Arguments.Urls.Authorise) {
$oAuthFlow = 'authorizationCode'
}
elseif ($authType.Arguments.Urls.Token ) {
if ($null -ne $authType.InnerScheme) {
if ($authType.InnerScheme.Name -ieq 'basic' -or $authType.InnerScheme.Name -ieq 'form') {
$oAuthFlow = 'password'
}
else {
$oAuthFlow = 'implicit'
}
}
}
$_authObj = [ordered]@{
type = $authType.Scheme
}
if ($authType.Arguments.Description) {
$_authObj.description = $authType.Arguments.Description
}
$_authObj.flows = @{
$oAuthFlow = [ordered]@{
}
}
if ($authType.Arguments.Urls.Token) {
$_authObj.flows.$oAuthFlow.tokenUrl = $authType.Arguments.Urls.Token
}
if ($authType.Arguments.Urls.Authorise) {
$_authObj.flows.$oAuthFlow.authorizationUrl = $authType.Arguments.Urls.Authorise
}
if ($authType.Arguments.Urls.Refresh) {
$_authObj.flows.$oAuthFlow.refreshUrl = $authType.Arguments.Urls.Refresh
}
$_authObj.flows.$oAuthFlow.scopes = @{}
if ($authType.Arguments.Scopes ) {
foreach ($scope in $authType.Arguments.Scopes) {
if ($PodeContext.Server.Authorisations.Methods.ContainsKey($scope) -and $PodeContext.Server.Authorisations.Methods[$scope].Scheme.Type -ieq 'Scope' -and $PodeContext.Server.Authorisations.Methods[$scope].Description) {
$_authObj.flows.$oAuthFlow.scopes[$scope] = $PodeContext.Server.Authorisations.Methods[$scope].Description
}
else {
$_authObj.flows.$oAuthFlow.scopes[$scope] = 'No description.'
}
}
}
}
else {
$_authObj = @{
type = $authType.Scheme.ToLowerInvariant()
scheme = $authType.Name.ToLowerInvariant()
}
if ($authType.Arguments.Description) {
$_authObj.description = $authType.Arguments.Description
}
}
if (!$def.components.securitySchemes) {
$def.components.securitySchemes = [ordered]@{}
}
$def.components.securitySchemes[$_authName] = $_authObj
}
if ($Definition.Security.Definition -and $Definition.Security.Definition.Length -gt 0) {
$def['security'] = @($Definition.Security.Definition)
}
}
if ($MetaInfo.RouteFilter) {
$filter = "^$($MetaInfo.RouteFilter)"
}
else {
$filter = ''
}
foreach ($method in $PodeContext.Server.Routes.Keys) {
foreach ($path in ($PodeContext.Server.Routes[$method].Keys | Sort-Object)) {
# does it match the route?
if ($path -inotmatch $filter) {
continue
}
# the current route
$_routes = @($PodeContext.Server.Routes[$method][$path])
if ( $MetaInfo -and $MetaInfo.RestrictRoutes) {
$_routes = @(Get-PodeRouteByUrl -Routes $_routes -EndpointName $EndpointName)
}
# continue if no routes
if (($_routes.Length -eq 0) -or ($null -eq $_routes[0])) {
continue
}
# get the first route for base definition
$_route = $_routes[0]
# check if the route has to be published
if (($_route.OpenApi.Swagger -and $_route.OpenApi.DefinitionTag -contains $DefinitionTag ) -or $Definition.hiddenComponents.enableMinimalDefinitions) {
#remove the ServerUrl part
if ( $localEndpoint) {
$_route.OpenApi.Path = $_route.OpenApi.Path.replace($localEndpoint, '')
}
# do nothing if it has no responses set
if ($_route.OpenApi.Responses.Count -eq 0) {
continue
}
# add path to defintion
if ($null -eq $def.paths[$_route.OpenApi.Path]) {
$def.paths[$_route.OpenApi.Path] = @{}
}
# add path's http method to defintition
$pm = Set-PodeOpenApiRouteValue -Route $_route -DefinitionTag $DefinitionTag
$def.paths[$_route.OpenApi.Path][$method] = $pm
# add any custom server endpoints for route
foreach ($_route in $_routes) {
if ($_route.OpenApi.Servers.count -gt 0) {
if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
$def.paths[$_route.OpenApi.Path][$method].servers = @()
}
if ($localEndpoint) {
$def.paths[$_route.OpenApi.Path][$method].servers += $Definition.servers[0]
}
}
if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Address) -and ($_route.Endpoint.Address -ine '*:*')) {
if ($null -eq $def.paths[$_route.OpenApi.Path][$method].servers) {
$def.paths[$_route.OpenApi.Path][$method].servers = @()
}
$serverDef = $null
if (![string]::IsNullOrWhiteSpace($_route.Endpoint.Name)) {
$serverDef = @{
url = (Get-PodeEndpointByName -Name $_route.Endpoint.Name).Url
}
}
else {
$serverDef = @{
url = "$($_route.Endpoint.Protocol)://$($_route.Endpoint.Address)"
}
}
if ($null -ne $serverDef) {
$def.paths[$_route.OpenApi.Path][$method].servers += $serverDef
}
}
}
}
}
}
#deal with the external OpenAPI paths
if ( $Definition.hiddenComponents.externalPath) {
foreach ($extPath in $Definition.hiddenComponents.externalPath.values) {
foreach ($method in $extPath.keys) {
$_route = $extPath[$method]
if (! ( $def.paths.keys -ccontains $_route.Path)) {
$def.paths[$_route.OpenAPI.Path] = @{}
}
$pm = Set-PodeOpenApiRouteValue -Route $_route -DefinitionTag $DefinitionTag
# add path's http method to defintition
$def.paths[$_route.OpenAPI.Path][$method.ToLower()] = $pmF
}
}
}
return $def
}
function ConvertTo-PodeOAPropertyFromCmdletParameter {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[System.Management.Automation.ParameterMetadata]
$Parameter
)
if ($Parameter.SwitchParameter -or ($Parameter.ParameterType.Name -ieq 'boolean')) {
New-PodeOABoolProperty -Name $Parameter.Name
}
else {
switch ($Parameter.ParameterType.Name) {
{ @('int32', 'int64') -icontains $_ } {
New-PodeOAIntProperty -Name $Parameter.Name -Format $_
}
{ @('double', 'float') -icontains $_ } {
New-PodeOANumberProperty -Name $Parameter.Name -Format $_
}
}
}
New-PodeOAStringProperty -Name $Parameter.Name
}
<#
.SYNOPSIS
Creates a base OpenAPI object structure.
.DESCRIPTION
The Get-PodeOABaseObject function generates a foundational structure for an OpenAPI object.
This structure includes empty ordered dictionaries for info, paths, webhooks, components, and other OpenAPI elements.
It is used as a base template for building OpenAPI documentation in the Pode framework.
.OUTPUTS
Hashtable
Returns a hashtable representing the base structure of an OpenAPI object.
.EXAMPLE
$baseObject = Get-PodeOABaseObject
This example creates a base OpenAPI object structure.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Get-PodeOABaseObject {
# Returns a base template for an OpenAPI object
return @{
info = [ordered]@{}
Path = $null
webhooks = [ordered]@{}
components = [ordered]@{
schemas = [ordered]@{}
responses = [ordered]@{}
parameters = [ordered]@{}
examples = [ordered]@{}
requestBodies = [ordered]@{}
headers = [ordered]@{}
securitySchemes = [ordered]@{}
links = [ordered]@{}
callbacks = [ordered]@{}
pathItems = [ordered]@{}
}
Security = @()
tags = [ordered]@{}
hiddenComponents = @{
enabled = $false
schemaValidation = $false
version = 3.0
depth = 20
schemaJson = @{}
viewer = @{}
postValidation = @{
schemas = @{}
responses = @{}
parameters = @{}
examples = @{}
requestBodies = @{}
headers = @{}
securitySchemes = @{}
links = @{}
callbacks = @{}
pathItems = @{}
}
externalPath = [ordered]@{}
defaultResponses = @{
'200' = @{ description = 'OK' }
'default' = @{ description = 'Internal server error' }
}
operationId = @()
}
}
}
<#
.SYNOPSIS
Initializes a table to manage OpenAPI definitions.
.DESCRIPTION
The Initialize-PodeOpenApiTable function creates a table to manage OpenAPI definitions within the Pode framework.
It sets up a default definition tag and initializes a dictionary to hold OpenAPI definitions for each tag.
The function is essential for managing OpenAPI documentation across different parts of the application.
.PARAMETER DefaultDefinitionTag
An optional parameter to set the default OpenAPI definition tag. If not provided, 'default' is used.
.OUTPUTS
Hashtable
Returns a hashtable for managing OpenAPI definitions.
.EXAMPLE
$openApiTable = Initialize-PodeOpenApiTable -DefaultDefinitionTag 'api-v1'
Initializes the OpenAPI table with 'api-v1' as the default definition tag.
.EXAMPLE
$openApiTable = Initialize-PodeOpenApiTable
Initializes the OpenAPI table with 'default' as the default definition tag.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Initialize-PodeOpenApiTable {
param(
[string]
$DefaultDefinitionTag = $null
)
# Initialization of the OpenAPI table with default settings
$OpenAPI = @{
DefinitionTagSelectionStack = New-Object 'System.Collections.Generic.Stack[System.Object]'
}
# Set the default definition tag
if ([string]::IsNullOrEmpty($DefaultDefinitionTag)) {
$OpenAPI['DefaultDefinitionTag'] = 'default'
}
else {
$OpenAPI['DefaultDefinitionTag'] = $DefaultDefinitionTag
}
# Set the currently selected definition tag
$OpenAPI['SelectedDefinitionTag'] = $OpenAPI['DefaultDefinitionTag']
# Initialize the Definitions dictionary with a base OpenAPI object for the selected definition tag
$OpenAPI['Definitions'] = @{ $OpenAPI['SelectedDefinitionTag'] = Get-PodeOABaseObject }
# Return the initialized OpenAPI table
return $OpenAPI
}
<#
.SYNOPSIS
Sets authentication methods for specific routes in OpenAPI documentation.
.DESCRIPTION
The Set-PodeOAAuth function assigns specified authentication methods to given routes for OpenAPI documentation.
It supports setting multiple authentication methods and optionally allows anonymous access.
The function validates the existence of the authentication methods before applying them to the routes.
.PARAMETER Route
An array of hashtables representing the routes to which the authentication methods will be applied.
Each route should contain an OpenApi key for updating OpenAPI documentation.
.PARAMETER Name
An array of names of the authentication methods to be applied to the routes.
These methods should already be defined in the Pode framework.
.PARAMETER AllowAnon
A switch parameter that, if set, allows anonymous access in addition to the specified authentication methods.
.EXAMPLE
Set-PodeOAAuth -Route $myRoute -Name @('BasicAuth', 'ApiKeyAuth') -AllowAnon
Applies 'BasicAuth' and 'ApiKeyAuth' authentication methods to the specified route and allows anonymous access.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Set-PodeOAAuth {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[hashtable[]]
$Route,
[string[]]
$Name,
[switch]
$AllowAnon
)
begin {
# Validate the existence of specified authentication methods
foreach ($n in @($Name)) {
if (!(Test-PodeAuthExists -Name $n)) {
throw "Authentication method does not exist: $($n)"
}
}
}
process {
# Iterate over each route to set authentication
foreach ($r in @($Route)) {
#exclude static route
if ($r.Method -ne 'Static') {
# Set the authentication methods for the route
$r.OpenApi.Authentication = @(foreach ($n in @($Name)) {
@{
"$($n -replace '\s+', '')" = @() # Clean up auth name and initialize empty scopes
}
})
# Add anonymous access if allowed
if ($AllowAnon) {
$r.OpenApi.Authentication += @{'%_allowanon_%' = '' }
}
}
}
}
}
<#
.SYNOPSIS
Sets global authentication methods for specified OpenAPI definitions in the Pode framework.
.DESCRIPTION
The Set-PodeOAGlobalAuth function is used to apply authentication methods globally to specified OpenAPI definitions.
It verifies the existence of the authentication methods and then updates the OpenAPI definitions with these methods,
associating them with specific routes.
.PARAMETER Name
The name of the authentication method to apply. This method should already be defined in the Pode framework.
.PARAMETER Route
The route to which the authentication method is to be applied.
.PARAMETER DefinitionTag
An array of definition tags specifying the OpenAPI definitions to which the authentication method should be applied.
.EXAMPLE
Set-PodeOAGlobalAuth -Name 'BasicAuth' -Route '/api/*' -DefinitionTag @('tag1', 'tag2')
Applies 'BasicAuth' authentication method to all routes under '/api/*' in the OpenAPI definitions tagged with 'tag1' and 'tag2'.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Set-PodeOAGlobalAuth {
param(
[string]
$Name,
[string]
$Route,
[Parameter(Mandatory = $true)]
[string[]]
$DefinitionTag
)
# Check if the specified authentication method exists
if (!(Test-PodeAuthExists -Name $Name)) {
throw "Authentication method does not exist: $($Name)"
}
# Iterate over each definition tag to apply the authentication method
foreach ($tag in $DefinitionTag) {
# Initialize security array if it's empty
if (Test-PodeIsEmpty $PodeContext.Server.OpenAPI.Definitions[$tag].Security) {
$PodeContext.Server.OpenAPI.Definitions[$tag].Security = @()
}
# Apply authentication to each expanded auth name
foreach ($authName in (Expand-PodeAuthMerge -Names $Name)) {
$authType = Get-PodeAuth $authName
# Determine the scopes of the authentication
if ($authType.Scheme.Arguments.Scopes) {
$Scopes = @($authType.Scheme.Arguments.Scopes)
}
else {
$Scopes = @()
}
# Update the OpenAPI definition with the authentication information
$PodeContext.Server.OpenAPI.Definitions[$tag].Security += @{
Definition = @{ "$($authName -replace '\s+', '')" = $Scopes }
Route = (ConvertTo-PodeRouteRegex -Path $Route)
}
}
}
}
function Resolve-PodeOAReference {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$ComponentSchema,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
begin {
$Schemas = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson
$Keys = @()
}
process {
if ($ComponentSchema.properties) {
foreach ($item in $ComponentSchema.properties.Keys) {
$Keys += $item
}
}
foreach ($item in $ComponentSchema.Keys) {
if ( @('allof', 'oneof', 'anyof') -icontains $item ) {
$Keys += $item
}
}
foreach ($key in $Keys) {
if ( @('allof', 'oneof', 'anyof') -icontains $key ) {
if ($key -ieq 'allof') {
$tmpProp = @()
foreach ( $comp in $ComponentSchema[$key] ) {
if ($comp.'$ref') {
if (($comp.'$ref').StartsWith('#/components/schemas/')) {
$refName = ($comp.'$ref') -replace '#/components/schemas/', ''
if ($Schemas.ContainsKey($refName)) {
$tmpProp += $Schemas[$refName].schema
}
}
}
elseif ( $comp.properties) {
if ($comp.type -eq 'object') {
$tmpProp += Resolve-PodeOAReference -DefinitionTag $DefinitionTag -ComponentSchema$comp
}
else {
throw 'Unsupported object'
}
}
}
$ComponentSchema.type = 'object'
$ComponentSchema.remove('allOf')
if ($tmpProp.count -gt 0) {
foreach ($t in $tmpProp) {
$ComponentSchema.properties += $t.properties
}
}
}
elseif ($key -ieq 'oneof') {
throw 'Validation of schema with oneof is not supported'
}
elseif ($key -ieq 'anyof') {
throw 'Validation of schema with anyof is not supported'
}
}
elseif ($ComponentSchema.properties[$key].type -eq 'object') {
$ComponentSchema.properties[$key].properties = Resolve-PodeOAReference -DefinitionTag $DefinitionTag -ComponentSchema $ComponentSchema.properties[$key].properties
}
elseif ($ComponentSchema.properties[$key].'$ref') {
if (($ComponentSchema.properties[$key].'$ref').StartsWith('#/components/schemas/')) {
$refName = ($ComponentSchema.properties[$key].'$ref') -replace '#/components/schemas/', ''
if ($Schemas.ContainsKey($refName)) {
$ComponentSchema.properties[$key] = $Schemas[$refName].schema
}
}
}
elseif ($ComponentSchema.properties[$key].items -and $ComponentSchema.properties[$key].items.'$ref' ) {
if (($ComponentSchema.properties[$key].items.'$ref').StartsWith('#/components/schemas/')) {
$refName = ($ComponentSchema.properties[$key].items.'$ref') -replace '#/components/schemas/', ''
if ($Schemas.ContainsKey($refName)) {
$ComponentSchema.properties[$key].items = $schemas[$refName].schema
}
}
}
}
}
end {
return $ComponentSchema
}
}
<#
.SYNOPSIS
Creates a new OpenAPI property object based on provided parameters.
.DESCRIPTION
The New-PodeOAPropertyInternal function constructs an OpenAPI property object using parameters like type, name,
description, and various other attributes. It is used internally for building OpenAPI documentation elements in the Pode framework.
.PARAMETER Type
The type of the property. This parameter is optional if the type is specified in the Params hashtable.
.PARAMETER Params
A hashtable containing various attributes of the property such as name, description, format, and constraints like
required, readOnly, writeOnly, etc.
.OUTPUTS
System.Collections.Specialized.OrderedDictionary
An ordered dictionary representing the constructed OpenAPI property object.
.EXAMPLE
$property = New-PodeOAPropertyInternal -Type 'string' -Params $myParams
Demonstrates how to create an OpenAPI property object of type 'string' using the specified parameters.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOAPropertyInternal {
[OutputType([System.Collections.Specialized.OrderedDictionary])]
param (
[String]
$Type,
[Parameter(Mandatory = $true)]
[hashtable]
$Params
)
# Initialize an ordered dictionary for the property
$param = [ordered]@{}
# Set the type of the property
if ($type) {
$param.type = $type
}
else {
if ( $Params.type) {
$param.type = $Params.type
}
else {
throw 'Cannot create the property no type is defined'
}
}
# Set name if provided
if ($Params.Name) {
$param.name = $Params.Name
}
# Set description if provided
if ($Params.Description) {
$param.description = $Params.Description
}
# Additional property settings based on provided parameters
if ($Params.Array.IsPresent) { $param.array = $Params.Array.IsPresent }
if ($Params.Object.IsPresent) { $param.object = $Params.Object.IsPresent }
if ($Params.Required.IsPresent) { $param.required = $Params.Required.IsPresent }
if ($Params.Default) { $param.default = $Params.Default }
if ($Params.Format) { $param.format = $Params.Format.ToLowerInvariant() }
if ($Params.Deprecated.IsPresent) { $param.deprecated = $Params.Deprecated.IsPresent }
if ($Params.Nullable.IsPresent) { $param.nullable = $Params.Nullable.IsPresent }
if ($Params.WriteOnly.IsPresent) { $param.writeOnly = $Params.WriteOnly.IsPresent }
if ($Params.ReadOnly.IsPresent) { $param.readOnly = $Params.ReadOnly.IsPresent }
if ($Params.Example) { $param.example = $Params.Example }
if ($Params.UniqueItems.IsPresent) { $param.uniqueItems = $Params.UniqueItems.IsPresent }
if ($Params.MaxItems) { $param.maxItems = $Params.MaxItems }
if ($Params.MinItems) { $param.minItems = $Params.MinItems }
if ($Params.Enum) { $param.enum = $Params.Enum }
if ($Params.Minimum) { $param.minimum = $Params.Minimum }
if ($Params.Maximum) { $param.maximum = $Params.Maximum }
if ($Params.ExclusiveMaximum.IsPresent) { $param.exclusiveMaximum = $Params.ExclusiveMaximum.IsPresent }
if ($Params.ExclusiveMinimum.IsPresent) { $param.exclusiveMinimum = $Params.ExclusiveMinimum.IsPresent }
if ($Params.MultiplesOf) { $param.multipleOf = $Params.MultiplesOf }
if ($Params.Pattern) { $param.pattern = $Params.Pattern }
if ($Params.MinLength) { $param.minLength = $Params.MinLength }
if ($Params.MaxLength) { $param.maxLength = $Params.MaxLength }
if ($Params.MinProperties) { $param.minProperties = $Params.MinProperties }
if ($Params.MaxProperties) { $param.maxProperties = $Params.MaxProperties }
if ($Params.XmlName -or $Params.XmlNamespace -or $Params.XmlPrefix -or $Params.XmlAttribute.IsPresent -or $Params.XmlWrapped.IsPresent) {
$param.xml = @{}
if ($Params.XmlName) { $param.xml.name = $Params.XmlName }
if ($Params.XmlNamespace) { $param.xml.namespace = $Params.XmlNamespace }
if ($Params.XmlPrefix) { $param.xml.prefix = $Params.XmlPrefix }
if ($Params.XmlAttribute.IsPresent) { $param.xml.attribute = $Params.XmlAttribute.IsPresent }
if ($Params.XmlWrapped.IsPresent) { $param.xml.wrapped = $Params.XmlWrapped.IsPresent }
}
if ($Params.XmlItemName) { $param.xmlItemName = $Params.XmlItemName }
if ($Params.ExternalDocs) { $param.externalDocs = $Params.ExternalDocs }
if ($Params.NoAdditionalProperties.IsPresent -and $Params.AdditionalProperties) {
throw 'Params -NoAdditionalProperties and -AdditionalProperties are mutually exclusive'
}
else {
if ($Params.NoAdditionalProperties.IsPresent) { $param.additionalProperties = $false }
if ($Params.AdditionalProperties) { $param.additionalProperties = $Params.AdditionalProperties }
}
return $param
}
<#
.SYNOPSIS
Converts header properties to a format compliant with OpenAPI specifications.
.DESCRIPTION
The ConvertTo-PodeOAHeaderProperty function is designed to take an array of hashtables representing header properties and
convert them into a structure suitable for OpenAPI documentation. It ensures that each header property includes a name and
schema definition and can handle additional attributes like description.
.PARAMETER Headers
An array of hashtables, where each hashtable represents a header property with attributes like name, type, description, etc.
.EXAMPLE
$headerProperties = ConvertTo-PodeOAHeaderProperty -Headers $myHeaders
This example demonstrates how to convert an array of header properties into a format suitable for OpenAPI documentation.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function ConvertTo-PodeOAHeaderProperty {
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable[]]
$Headers
)
$elems = @{}
foreach ($e in $Headers) {
# Ensure each header has a name
if ($e.name) {
$elems.$($e.name) = @{}
# Add description if present
if ($e.description) {
$elems.$($e.name).description = $e.description
}
# Define the schema, including the type and any additional properties
$elems.$($e.name).schema = @{
type = $($e.type)
}
foreach ($k in $e.keys) {
if (@('name', 'description') -notcontains $k) {
$elems.$($e.name).schema.$k = $e.$k
}
}
}
else {
throw 'Header requires a name when used in an encoding context'
}
}
return $elems
}
<#
.SYNOPSIS
Creates a new OpenAPI callback component for a given definition tag.
.DESCRIPTION
The New-PodeOAComponentCallBackInternal function constructs an OpenAPI callback component based on provided parameters.
This function is designed for internal use within the Pode framework to define callbacks in OpenAPI documentation.
It handles the creation of callback structures including the path, HTTP method, request bodies, and responses
based on the given definition tag.
.PARAMETER Params
A hashtable containing parameters for the callback component, such as Method, Path, RequestBody, and Responses.
.PARAMETER DefinitionTag
A mandatory string parameter that specifies the definition tag in OpenAPI documentation.
.EXAMPLE
$callback = New-PodeOAComponentCallBackInternal -Params $myParams -DefinitionTag 'myTag'
This example demonstrates how to create an OpenAPI callback component for 'myTag' using the provided parameters.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOAComponentCallBackInternal {
param(
[Parameter(Mandatory = $true)]
[hashtable]
$Params,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
# Convert HTTP method to lower case
$_method = $Params.Method.ToLower()
# Construct the base structure for the callback with the given path and method
$callBack = [ordered]@{
"'$($Params.Path)'" = [ordered]@{
$_method = [ordered]@{}
}
}
# Add request body to the callback if it is specified for the given definition tag
if ($Params.RequestBody.ContainsKey($DefinitionTag)) {
$callBack."'$($Params.Path)'".$_method.requestBody = $Params.RequestBody[$DefinitionTag]
}
# Add responses to the callback if they are specified for the given definition tag
if ($Params.Responses.ContainsKey($DefinitionTag)) {
$callBack."'$($Params.Path)'".$_method.responses = $Params.Responses[$DefinitionTag]
}
# Return the constructed callback object
return $callBack
}
<#
.SYNOPSIS
Creates a new OpenAPI response object based on provided parameters and a definition tag.
.DESCRIPTION
The New-PodeOResponseInternal function constructs an OpenAPI response object using provided parameters.
It sets a description for the status code, references existing components if specified,
and builds content-type and header schemas. This function is intended for internal use within the
Pode framework for API documentation purposes.
.PARAMETER Params
A hashtable containing parameters for building the OpenAPI response object, including description,
status code, content, headers, links, and reference to existing components.
.PARAMETER DefinitionTag
A mandatory string parameter that specifies the definition tag in OpenAPI documentation.
.EXAMPLE
$response = New-PodeOResponseInternal -Params $myParams -DefinitionTag 'myTag'
This example demonstrates how to create an OpenAPI response object for 'myTag' using the provided parameters.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOResponseInternal {
param(
[hashtable]
$Params,
[Parameter(Mandatory = $true)]
[string]
$DefinitionTag
)
# Set a general description for the status code
if ([string]::IsNullOrWhiteSpace($Params.Description)) {
if ($Params.Default) {
$Description = 'Default Response.'
}
elseif ($Params.StatusCode) {
$Description = Get-PodeStatusDescription -StatusCode $Params.StatusCode
}
else {
throw 'A Description is required'
}
}
else {
$Description = $Params.Description
}
# Handle response referencing an existing component
if ($Params.Reference) {
Test-PodeOAComponentInternal -Field responses -DefinitionTag $DefinitionTag -Name $Params.Reference -PostValidation
$response = @{
'$ref' = "#/components/responses/$($Params.Reference)"
}
}
else {
# Build content-type schemas if provided
$_content = $null
if ($null -ne $Params.Content) {
$_content = ConvertTo-PodeOAObjectSchema -DefinitionTag $DefinitionTag -Content $Params.Content
}
# Build header schemas based on the type of the Headers parameter
$_headers = $null
if ($null -ne $Params.Headers) {
if ($Params.Headers -is [System.Object[]] -or $Params.Headers -is [string] -or $Params.Headers -is [string[]]) {
if ($Params.Headers -is [System.Object[]] -and $Params.Headers.Count -gt 0 -and ($Params.Headers[0] -is [hashtable] -or $Params.Headers[0] -is [ordered])) {
$_headers = ConvertTo-PodeOAHeaderProperty -Headers $Params.Headers
}
else {
$_headers = @{}
foreach ($h in $Params.Headers) {
Test-PodeOAComponentInternal -Field headers -DefinitionTag $DefinitionTag -Name $h -PostValidation
$_headers[$h] = @{
'$ref' = "#/components/headers/$h"
}
}
}
}
elseif ($Params.Headers -is [hashtable]) {
$_headers = ConvertTo-PodeOAObjectSchema -DefinitionTag $DefinitionTag -Content $Params.Headers
}
}
# Construct the response object
$response = [ordered]@{
description = $Description
}
if ($_headers) { $response.headers = $_headers }
if ($_content) { $response.content = $_content }
if ($Params.Links) { $response.links = $Params.Links }
}
return $response
}
<#
.SYNOPSIS
Creates a new OpenAPI response link object.
.DESCRIPTION
The New-PodeOAResponseLinkInternal function generates an OpenAPI response link object from provided parameters.
This includes setting up descriptions, operation IDs, references, parameters, and request bodies for the link.
This function is designed for internal use within the Pode framework to facilitate the creation of response
link objects in OpenAPI documentation.
.PARAMETER Params
A hashtable of parameters for the OpenAPI response link.
.EXAMPLE
$link = New-PodeOAResponseLinkInternal -Params $myParams
Generates a new OpenAPI response link object using the provided parameters in $myParams.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function New-PodeOAResponseLinkInternal {
param(
[hashtable]
$Params
)
# Initialize an ordered dictionary for the link
$link = [ordered]@{}
# Add properties to the link based on the provided parameters
if ($Params.Description) { $link.description = $Params.Description }
if ($Params.OperationId) { $link.operationId = $Params.OperationId }
if ($Params.OperationRef) { $link.operationRef = $Params.OperationRef }
if ($Params.Parameters) { $link.parameters = $Params.Parameters }
if ($Params.RequestBody) { $link.requestBody = $Params.RequestBody }
return $link
}
<#
.SYNOPSIS
Tests the internal OpenAPI definitions for compliance and validity.
.DESCRIPTION
The Test-PodeOADefinitionInternal function validates OpenAPI definitions within the Pode framework.
It checks for various issues like undefined references, mandatory fields (like title and version),
and missing components. If any issues are found, they are displayed with detailed messages, and
the function throws an error indicating non-compliance with OpenAPI document standards.
.EXAMPLE
Test-PodeOADefinitionInternal
This example demonstrates how to call the function to validate OpenAPI definitions.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Test-PodeOADefinitionInternal {
# Validate OpenAPI definitions and store any issues found
$definitionIssues = Test-PodeOADefinition
# Check if the validation result indicates issues
if (! $definitionIssues.valid) {
# Print a header for undefined OpenAPI references
Write-PodeHost 'Undefined OpenAPI References :' -ForegroundColor Red
# Iterate over each issue found in the definitions
foreach ($tag in $definitionIssues.issues.keys) {
Write-PodeHost "Definition $tag :" -ForegroundColor Red
# Check and display issues related to OpenAPI document generation error
if ($definitionIssues.issues[$tag].definition ) {
Write-PodeHost ' OpenAPI generation document error: ' -ForegroundColor Red
Write-PodeHost " $($definitionIssues.issues[$tag].definition)" -ForegroundColor Red
}
# Check for missing mandatory 'title' field
if ($definitionIssues.issues[$tag].title ) {
Write-PodeHost ' info.title is mandatory' -ForegroundColor Red
}
# Check for missing mandatory 'version' field
if ($definitionIssues.issues[$tag].version ) {
Write-PodeHost ' info.version is mandatory' -ForegroundColor Red
}
# Check for missing components and list them
if ($definitionIssues.issues[$tag].components ) {
Write-PodeHost ' Missing component(s)' -ForegroundColor Red
foreach ($key in $definitionIssues.issues[$tag].components.keys) {
$occurences = $definitionIssues.issues[$tag].components[$key]
# Adjust occurrence count based on schema validation setting
if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaValidation) {
$occurences = $occurences / 2
}
Write-PodeHost "`$refs : $key ($occurences)" -ForegroundColor Red
}
}
# Add a blank line for readability
Write-PodeHost
}
# Throw an error indicating non-compliance with OpenAPI standards
throw 'OpenAPI document compliance issues'
}
}
<#
.SYNOPSIS
Check the OpenAPI component exist (Internal Function)
.DESCRIPTION
Check the OpenAPI component exist (Internal Function)
.PARAMETER Field
The component type
.PARAMETER Name
The component Name
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.PARAMETER ThrowException
Generate an exception if the component doesn't exist
.PARAMETER PostValidation
Postpone the check before the server start
.EXAMPLE
Test-PodeOAComponentInternal -Field 'responses' -Name 'myresponse' -DefinitionTag 'default'
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Test-PodeOAComponentInternal {
param(
[Parameter(Mandatory = $true)]
[ValidateSet( 'schemas' , 'responses' , 'parameters' , 'examples' , 'requestBodies' , 'headers' , 'securitySchemes' , 'links' , 'callbacks' , 'pathItems')]
[string]
$Field,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[string[]]
$DefinitionTag,
[switch]
$ThrowException,
[switch]
$PostValidation
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
if ($PostValidation.IsPresent) {
foreach ($tag in $DefinitionTag) {
if (! ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field].keys -ccontains $Name)) {
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field][$name] = 1
}
else {
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field][$name] += 1
}
}
}
else {
foreach ($tag in $DefinitionTag) {
if (!($PodeContext.Server.OpenAPI.Definitions[$tag].components[$field].keys -ccontains $Name)) {
# If $Name is not found in the current $tag, return $false or throw an exception
if ($ThrowException.IsPresent ) {
throw "No components of type $field named $Name are available in the $tag definition."
}
else {
return $false
}
}
}
if (!$ThrowException.IsPresent) {
return $true
}
}
}
using namespace Pode
function Start-PodeWebServer {
param(
[switch]
$Browse
)
# setup any inbuilt middleware
$inbuilt_middleware = @(
(Get-PodeSecurityMiddleware),
(Get-PodeAccessMiddleware),
(Get-PodeLimitMiddleware),
(Get-PodePublicMiddleware),
(Get-PodeRouteValidateMiddleware),
(Get-PodeBodyMiddleware),
(Get-PodeQueryMiddleware),
(Get-PodeCookieMiddleware)
)
$PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)
# work out which endpoints to listen on
$endpoints = @()
$endpointsMap = @{}
@(Get-PodeEndpoints -Type Http, Ws) | ForEach-Object {
# get the ip address
$_ip = [string]($_.Address)
$_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1
$_ip = Get-PodeIPAddress -IP $_ip -DualMode:($_.DualMode)
# dual mode?
$addrs = $_ip
if ($_.DualMode) {
$addrs = Resolve-PodeIPDualMode -IP $_ip
}
# the endpoint
$_endpoint = @{
Name = $_.Name
Key = "$($_ip):$($_.Port)"
Address = $addrs
Hostname = $_.HostName
IsIPAddress = $_.IsIPAddress
Port = $_.Port
Certificate = $_.Certificate.Raw
AllowClientCertificate = $_.Certificate.AllowClientCertificate
Url = $_.Url
Protocol = $_.Protocol
Type = $_.Type
Pool = $_.Runspace.PoolName
SslProtocols = $_.Ssl.Protocols
DualMode = $_.DualMode
}
# add endpoint to list
$endpoints += $_endpoint
# add to map
if (!$endpointsMap.ContainsKey($_endpoint.Key)) {
$endpointsMap[$_endpoint.Key] = @{ Type = $_.Type }
}
else {
if ($endpointsMap[$_endpoint.Key].Type -ine $_.Type) {
$endpointsMap[$_endpoint.Key].Type = 'HttpAndWs'
}
}
}
# create the listener
$listener = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)Listener -CancellationToken `$PodeContext.Tokens.Cancellation.Token")))
$listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
$listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
$listener.RequestTimeout = $PodeContext.Server.Request.Timeout
$listener.RequestBodySize = $PodeContext.Server.Request.BodySize
$listener.ShowServerDetails = [bool]$PodeContext.Server.Security.ServerDetails
try {
# register endpoints on the listener
$endpoints | ForEach-Object {
$socket = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)ListenerSocket -Name `$_.Name -Address `$_.Address -Port `$_.Port -SslProtocols `$_.SslProtocols -Type `$endpointsMap[`$_.Key].Type -Certificate `$_.Certificate -AllowClientCertificate `$_.AllowClientCertificate -DualMode:`$_.DualMode")))
$socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout
if (!$_.IsIPAddress) {
$socket.Hostnames.Add($_.HostName)
}
$listener.Add($socket)
}
$listener.Start()
$PodeContext.Listeners += $listener
$PodeContext.Server.Signals.Enabled = $true
$PodeContext.Server.Signals.Listener = $listener
$PodeContext.Server.Http.Listener = $listener
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Close-PodeDisposable -Disposable $listener
throw $_.Exception
}
# only if HTTP endpoint
if (Test-PodeEndpoints -Type Http) {
# script for listening out for incoming requests
$listenScript = {
param(
[Parameter(Mandatory = $true)]
$Listener,
[Parameter(Mandatory = $true)]
[int]
$ThreadId
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
# get request and response
$context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token))
try {
try {
$Request = $context.Request
$Response = $context.Response
# reset with basic event data
$WebEvent = @{
OnEnd = @()
Auth = @{}
Response = $Response
Request = $Request
Lockable = $PodeContext.Threading.Lockables.Global
Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath)
Method = $Request.HttpMethod.ToLowerInvariant()
Query = $null
Endpoint = @{
Protocol = $Request.Url.Scheme
Address = $Request.Host
Name = $context.EndpointName
}
ContentType = $Request.ContentType
ErrorType = $null
Cookies = @{}
PendingCookies = @{}
Parameters = $null
Data = $null
Files = $null
Streamed = $true
Route = $null
StaticContent = $null
Timestamp = [datetime]::UtcNow
TransferEncoding = $null
AcceptEncoding = $null
Ranges = $null
Sse = $null
Metadata = @{}
}
# if iis, and we have an app path, alter it
if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Path.IsNonRoot) {
$WebEvent.Path = ($WebEvent.Path -ireplace $PodeContext.Server.IIS.Path.Pattern, '')
if ([string]::IsNullOrEmpty($WebEvent.Path)) {
$WebEvent.Path = '/'
}
}
# accept/transfer encoding
$WebEvent.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding (Get-PodeHeader -Name 'Transfer-Encoding') -ThrowError)
$WebEvent.AcceptEncoding = (Get-PodeAcceptEncoding -AcceptEncoding (Get-PodeHeader -Name 'Accept-Encoding') -ThrowError)
$WebEvent.Ranges = (Get-PodeRanges -Range (Get-PodeHeader -Name 'Range') -ThrowError)
# add logging endware for post-request
Add-PodeRequestLogEndware -WebEvent $WebEvent
# stop now if the request has an error
if ($Request.IsAborted) {
throw $Request.Error
}
# if we have an sse clientId, verify it and then set details in WebEvent
if ($WebEvent.Request.HasSseClientId) {
if (!(Test-PodeSseClientIdValid)) {
throw [System.Net.Http.HttpRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)")
}
if (![string]::IsNullOrEmpty($WebEvent.Request.SseClientName) -and !(Test-PodeSseClientId -Name $WebEvent.Request.SseClientName -ClientId $WebEvent.Request.SseClientId)) {
throw [System.Net.Http.HttpRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)")
}
$WebEvent.Sse = @{
Name = $WebEvent.Request.SseClientName
Group = $WebEvent.Request.SseClientGroup
ClientId = $WebEvent.Request.SseClientId
LastEventId = $null
IsLocal = $false
}
}
# invoke global and route middleware
if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
# has the request been aborted
if ($Request.IsAborted) {
throw $Request.Error
}
if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
# has the request been aborted
if ($Request.IsAborted) {
throw $Request.Error
}
# invoke the route
if ($null -ne $WebEvent.StaticContent) {
$fileBrowser = $WebEvent.Route.FileBrowser
if ($WebEvent.StaticContent.IsDownload) {
Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser
}
elseif ($WebEvent.StaticContent.RedirectToDefault) {
$file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source)
Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)"
}
else {
$cachable = $WebEvent.StaticContent.IsCachable
Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge `
-Cache:$cachable -FileBrowser:$fileBrowser
}
}
elseif ($null -ne $WebEvent.Route.Logic) {
$null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments `
-UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
}
}
}
}
catch [System.OperationCanceledException] {}
catch [System.Net.Http.HttpRequestException] {
if ($Response.StatusCode -ge 500) {
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
$code = [int]($_.Exception.Data['PodeStatusCode'])
if ($code -le 0) {
$code = 400
}
Set-PodeResponseStatus -Code $code -Exception $_
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Set-PodeResponseStatus -Code 500 -Exception $_
}
finally {
Update-PodeServerRequestMetrics -WebEvent $WebEvent
}
# invoke endware specifc to the current web event
$_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
Invoke-PodeEndware -Endware $_endware
}
finally {
$WebEvent = $null
Close-PodeDisposable -Disposable $context
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads.General | ForEach-Object {
Add-PodeRunspace -Type Web -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
}
}
# only if WS endpoint
if (Test-PodeEndpoints -Type Ws) {
# script to write messages back to the client(s)
$signalScript = {
param(
[Parameter(Mandatory = $true)]
$Listener
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
$message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token))
try {
# get the sockets for the message
$sockets = @()
# by clientId
if (![string]::IsNullOrWhiteSpace($message.ClientId)) {
$sockets = @($Listener.Signals[$message.ClientId])
}
else {
$sockets = @($Listener.Signals.Values)
# by path
if (![string]::IsNullOrWhiteSpace($message.Path)) {
$sockets = @(foreach ($socket in $sockets) {
if ($socket.Path -ieq $message.Path) {
$socket
}
})
}
}
# do nothing if no socket found
if (($null -eq $sockets) -or ($sockets.Length -eq 0)) {
continue
}
# send the message to all found sockets
foreach ($socket in $sockets) {
try {
$socket.Context.Response.SendSignal($message)
}
catch {
$null = $Listener.Signals.Remove($socket.ClientId)
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
finally {
Close-PodeDisposable -Disposable $message
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
}
Add-PodeRunspace -Type Signals -ScriptBlock $signalScript -Parameters @{ 'Listener' = $listener }
}
# only if WS endpoint
if (Test-PodeEndpoints -Type Ws) {
# script to queue messages from clients to send back to other clients from the server
$clientScript = {
param(
[Parameter(Mandatory = $true)]
$Listener,
[Parameter(Mandatory = $true)]
[int]
$ThreadId
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
$context = (Wait-PodeTask -Task $Listener.GetClientSignalAsync($PodeContext.Tokens.Cancellation.Token))
try {
$payload = ($context.Message | ConvertFrom-Json)
$Request = $context.Signal.Context.Request
$Response = $context.Signal.Context.Response
$SignalEvent = @{
Response = $Response
Request = $Request
Lockable = $PodeContext.Threading.Lockables.Global
Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath)
Data = @{
Path = [System.Web.HttpUtility]::UrlDecode($payload.path)
Message = $payload.message
ClientId = $payload.clientId
Direct = [bool]$payload.direct
}
Endpoint = @{
Protocol = $Request.Url.Scheme
Address = $Request.Host
Name = $context.Signal.Context.EndpointName
}
Route = $null
ClientId = $context.Signal.ClientId
Timestamp = $context.Timestamp
Streamed = $true
Metadata = @{}
}
# see if we have a route and invoke it, otherwise auto-send
$SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name
if ($null -ne $SignalEvent.Route) {
$null = Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables -Scoped -Splat
}
else {
Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
finally {
Update-PodeServerSignalMetrics -SignalEvent $SignalEvent
Close-PodeDisposable -Disposable $context
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads.General | ForEach-Object {
Add-PodeRunspace -Type Signals -ScriptBlock $clientScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
}
}
# script to keep web server listening until cancelled
$waitScript = {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Listener
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
finally {
Close-PodeDisposable -Disposable $Listener
}
}
$waitType = 'Web'
if (!(Test-PodeEndpoints -Type Http)) {
$waitType = 'Signals'
}
Add-PodeRunspace -Type $waitType -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile
# browse to the first endpoint, if flagged
if ($Browse) {
Start-Process $endpoints[0].Url
}
return @(foreach ($endpoint in $endpoints) {
@{
Url = $endpoint.Url
Pool = $endpoint.Pool
DualMode = $endpoint.DualMode
}
})
}
function New-PodeListener {
[CmdletBinding()]
[OutputType([Pode.PodeListener])]
param(
[Parameter(Mandatory = $true)]
[System.Threading.CancellationToken]
$CancellationToken
)
return [PodeListener]::new($CancellationToken)
}
function New-PodeListenerSocket {
[CmdletBinding()]
[OutputType([Pode.PodeSocket])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[ipaddress[]]
$Address,
[Parameter(Mandatory = $true)]
[int]
$Port,
[Parameter()]
[System.Security.Authentication.SslProtocols]
$SslProtocols,
[Parameter(Mandatory = $true)]
[PodeProtocolType]
$Type,
[Parameter()]
[X509Certificate]
$Certificate,
[Parameter()]
[bool]
$AllowClientCertificate,
[switch]
$DualMode
)
return [PodeSocket]::new($Name, $Address, $Port, $SslProtocols, $Type, $Certificate, $AllowClientCertificate, 'Implicit', $DualMode.IsPresent)
}
<#
.SYNOPSIS
Displays a customized error page based on the provided error code and additional error details.
.DESCRIPTION
This function is responsible for displaying a custom error page when an error occurs within a Pode web application. It takes an error code, a description, an exception object, and a content type as input. The function then attempts to find a corresponding error page based on the error code and content type. If a custom error page is found, and if exception details are to be shown (as per server settings), it builds a detailed exception message. Finally, it writes the error page to the response stream, displaying the custom error page to the user.
.PARAMETER Code
The HTTP status code of the error. This code is used to find a matching custom error page.
.PARAMETER Description
A descriptive message about the error. This is displayed on the error page if available.
.PARAMETER Exception
The exception object that caused the error. If exception tracing is enabled, details from this object are displayed on the error page.
.PARAMETER ContentType
The content type of the error page to be displayed. This is used to select an appropriate error page format (e.g., HTML, JSON).
.EXAMPLE
Show-PodeErrorPage -Code 404 -Description "Not Found" -ContentType "text/html"
This example shows how to display a custom 404 Not Found error page in HTML format.
.OUTPUTS
None. This function writes the error page directly to the response stream.
.NOTES
- The function uses `Find-PodeErrorPage` to locate a custom error page based on the HTTP status code and content type.
- It checks for server configuration to determine whether to show detailed exception information on the error page.
- The function relies on the global `$PodeContext` variable for server settings and to encode exception and URL details safely.
- `Write-PodeFileResponse` is used to send the custom error page as the response, along with any dynamic data (e.g., exception details, URL).
- This is an internal function and may change in future releases of Pode.
#>
function Show-PodeErrorPage {
param(
[Parameter()]
[int]
$Code,
[Parameter()]
[string]
$Description,
[Parameter()]
$Exception,
[Parameter()]
[string]
$ContentType
)
# error page info
$errorPage = Find-PodeErrorPage -Code $Code -ContentType $ContentType
# if no page found, return
if (Test-PodeIsEmpty $errorPage) {
return
}
# if exception trace showing enabled then build the exception details object
$ex = $null
if (!(Test-PodeIsEmpty $Exception) -and $PodeContext.Server.Web.ErrorPages.ShowExceptions) {
$ex = @{
Message = [System.Web.HttpUtility]::HtmlEncode($Exception.Exception.Message)
StackTrace = [System.Web.HttpUtility]::HtmlEncode($Exception.ScriptStackTrace)
Line = [System.Web.HttpUtility]::HtmlEncode($Exception.InvocationInfo.PositionMessage)
Category = [System.Web.HttpUtility]::HtmlEncode($Exception.CategoryInfo.ToString())
}
}
# setup the data object for dynamic pages
$data = @{
Url = [System.Web.HttpUtility]::HtmlEncode((Get-PodeUrl))
Status = @{
Code = $Code
Description = $Description
}
Exception = $ex
ContentType = $errorPage.ContentType
}
# write the error page to the stream
Write-PodeFileResponse -Path $errorPage.Path -Data $data -ContentType $errorPage.ContentType
}
<#
.SYNOPSIS
Serves files as HTTP responses in a Pode web server, handling both dynamic and static content.
.DESCRIPTION
This function serves files from the server to the client, supporting both static files and files that are dynamically processed by a view engine.
For dynamic content, it uses the server's configured view engine to process the file and returns the rendered content.
For static content, it simply returns the file's content. The function allows for specifying content type, cache control, and HTTP status code.
.PARAMETER Path
The relative path to the file to be served. This path is resolved against the server's root directory.
.PARAMETER Data
A hashtable of data that can be passed to the view engine for dynamic files.
.PARAMETER ContentType
The MIME type of the response. If not provided, it is inferred from the file extension.
.PARAMETER MaxAge
The maximum age (in seconds) for which the response can be cached by the client. Applies only to static content.
.PARAMETER StatusCode
The HTTP status code to accompany the response. Defaults to 200 (OK).
.PARAMETER Cache
A switch to indicate whether the response should include HTTP caching headers. Applies only to static content.
.EXAMPLE
Write-PodeFileResponseInternal -Path 'index.pode' -Data @{ Title = 'Home Page' } -ContentType 'text/html'
Serves the 'index.pode' file as an HTTP response, processing it with the view engine and passing in a title for dynamic content rendering.
.EXAMPLE
Write-PodeFileResponseInternal -Path 'logo.png' -ContentType 'image/png' -Cache
Serves the 'logo.png' file as a static file with the specified content type and caching enabled.
.OUTPUTS
None. The function writes directly to the HTTP response stream.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Write-PodeFileResponseInternal {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNull()]
[string]
$Path,
[Parameter()]
$Data = @{},
[Parameter()]
[string]
$ContentType = $null,
[Parameter()]
[int]
$MaxAge = 3600,
[Parameter()]
[int]
$StatusCode = 200,
[switch]
$Cache,
[switch]
$FileBrowser
)
# Attempt to retrieve information about the path
$pathInfo = Test-PodePath -Path $Path -Force -ReturnItem -FailOnDirectory:(!$FileBrowser)
if (!$pathinfo) {
return
}
# Check if the path is a directory
if ( $pathInfo.PSIsContainer) {
# If directory browsing is enabled, use the directory response function
Write-PodeDirectoryResponseInternal -Path $Path
}
else {
# are we dealing with a dynamic file for the view engine? (ignore html)
# Determine if the file is dynamic and should be processed by the view engine
$mainExt = $pathInfo.Extension.TrimStart('.')
# generate dynamic content
if (![string]::IsNullOrWhiteSpace($mainExt) -and (
($mainExt -ieq 'pode') -or
($mainExt -ieq $PodeContext.Server.ViewEngine.Extension -and $PodeContext.Server.ViewEngine.IsDynamic)
)
) {
# Process dynamic content with the view engine
$content = Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data
# Determine the correct content type for the response
# get the sub-file extension, if empty, use original
$subExt = [System.IO.Path]::GetExtension($pathInfo.BaseName).TrimStart('.')
$subExt = (Protect-PodeValue -Value $subExt -Default $mainExt)
$ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $subExt))
# Write the processed content as the HTTP response
Write-PodeTextResponse -Value $content -ContentType $ContentType -StatusCode $StatusCode
}
# this is a static file
else {
try {
if (Test-PodeIsPSCore) {
$content = (Get-Content -Path $Path -Raw -AsByteStream)
}
else {
$content = (Get-Content -Path $Path -Raw -Encoding byte)
}
# Determine and set the content type for static files
$ContentType = Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $mainExt)
# Write the file content as the HTTP response
Write-PodeTextResponse -Bytes $content -ContentType $ContentType -MaxAge $MaxAge -StatusCode $StatusCode -Cache:$Cache
return
}
catch [System.UnauthorizedAccessException] {
$statusCode = 401
}
catch {
$statusCode = 400
}
# If the file does not exist, set the HTTP response status code appropriately
Set-PodeResponseStatus -Code $StatusCode
}
}
}
<#
.SYNOPSIS
Serves a directory listing as a web page.
.DESCRIPTION
The Write-PodeDirectoryResponseInternal function generates an HTML response that lists the contents of a specified directory,
allowing for browsing of files and directories. It supports both Windows and Unix-like environments by adjusting the
display of file attributes accordingly. If the path is a directory, it generates a browsable HTML view; otherwise, it
serves the file directly.
.PARAMETER Path
The relative path to the directory that should be displayed. This path is resolved and used to generate a list of contents.
.EXAMPLE
# resolve for relative path
$RelativePath = Get-PodeRelativePath -Path './static' -JoinRoot
Write-PodeDirectoryResponseInternal -Path './static'
Generates and serves an HTML page that lists the contents of the './static' directory, allowing users to click through files and directories.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Write-PodeDirectoryResponseInternal {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[string]
$Path
)
if ($WebEvent.Path -eq '/') {
$leaf = '/'
$rootPath = '/'
}
else {
# get leaf of current physical path, and set root path
$leaf = ($Path.Split(':')[1] -split '[\\/]+') -join '/'
$rootPath = $WebEvent.Path -ireplace "$($leaf)$", ''
}
# Determine if the server is running in Windows mode or is running a varsion that support Linux
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem?view=powershell-7.4#example-10-output-for-non-windows-operating-systems
$windowsMode = ((Test-PodeIsWindows) -or ($PSVersionTable.PSVersion -lt [version]'7.1.0') )
# Construct the HTML content for the file browser view
$htmlContent = [System.Text.StringBuilder]::new()
$atoms = $WebEvent.Path -split '/'
$atoms = @(foreach ($atom in $atoms) {
if (![string]::IsNullOrEmpty($atom)) {
[uri]::EscapeDataString($atom)
}
})
if ([string]::IsNullOrWhiteSpace($atoms)) {
$baseLink = ''
}
else {
$baseLink = "/$($atoms -join '/')"
}
# Handle navigation to the parent directory (..)
if ($leaf -ne '/') {
$LastSlash = $baseLink.LastIndexOf('/')
if ($LastSlash -eq -1) {
Set-PodeResponseStatus -Code 404
return
}
$ParentLink = $baseLink.Substring(0, $LastSlash)
if ([string]::IsNullOrWhiteSpace($ParentLink)) {
$ParentLink = '/'
}
$item = Get-Item '..'
if ($windowsMode) {
$htmlContent.Append("<tr> <td class='mode'>")
$htmlContent.Append($item.Mode)
}
else {
$htmlContent.Append("<tr> <td class='unixMode'>")
$htmlContent.Append($item.UnixMode)
$htmlContent.Append("</td> <td class='user'>")
$htmlContent.Append($item.User)
$htmlContent.Append("</td> <td class='group'>")
$htmlContent.Append($item.Group)
}
$htmlContent.Append("</td> <td class='dateTime'>")
$htmlContent.Append($item.CreationTime.ToString('yyyy-MM-dd HH:mm:ss'))
$htmlContent.Append("</td> <td class='dateTime'>")
$htmlContent.Append($item.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))
$htmlContent.Append( "</td> <td class='size'></td> <td class='icon'><i class='bi bi-folder2-open'></td> <td class='name'><a href='")
$htmlContent.Append($ParentLink)
$htmlContent.AppendLine("'>..</a></td> </tr>")
}
# Retrieve the child items of the specified directory
$child = Get-ChildItem -Path $Path -Force
foreach ($item in $child) {
$link = "$baseLink/$([uri]::EscapeDataString($item.Name))"
if ($item.PSIsContainer) {
$size = ''
$icon = '📁'
}
else {
$size = '{0:N2}KB' -f ($item.Length / 1KB)
$icon = '📄'
}
# Format each item as an HTML row
if ($windowsMode) {
$htmlContent.Append("<tr> <td class='mode'>")
$htmlContent.Append($item.Mode)
}
else {
$htmlContent.Append("<tr> <td class='unixMode'>")
$htmlContent.Append($item.UnixMode)
$htmlContent.Append("</td> <td class='user'>")
$htmlContent.Append($item.User)
$htmlContent.Append("</td> <td class='group'>")
$htmlContent.Append($item.Group)
}
$htmlContent.Append("</td> <td class='dateTime'>")
$htmlContent.Append($item.CreationTime.ToString('yyyy-MM-dd HH:mm:ss'))
$htmlContent.Append("</td> <td class='dateTime'>")
$htmlContent.Append($item.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))
$htmlContent.Append("</td> <td class='size'>")
$htmlContent.Append( $size)
$htmlContent.Append( "</td> <td class='icon'>")
$htmlContent.Append( $icon)
$htmlContent.Append( "</td> <td class='name'><a href='")
$htmlContent.Append( $link)
$htmlContent.Append( "'>")
$htmlContent.Append($item.Name )
$htmlContent.AppendLine('</a></td> </tr>' )
}
$Data = @{
RootPath = $RootPath
Path = $leaf.Replace('\', '/')
WindowsMode = $windowsMode.ToString().ToLower()
FileContent = $htmlContent.ToString() # Convert the StringBuilder content to a string
}
$podeRoot = Get-PodeModuleMiscPath
# Write the response
Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, 'default-file-browsing.html.pode')) -Data $Data
}
<#
.SYNOPSIS
Sends a file as an attachment in the response, supporting both file streaming and directory browsing options.
.DESCRIPTION
The Write-PodeAttachmentResponseInternal function is designed to handle HTTP responses for file downloads or directory browsing within a Pode web server. It resolves the given file or directory path, sets the appropriate content type, and configures the response to either download the file as an attachment or list the directory contents if browsing is enabled. The function supports both PowerShell Core and Windows PowerShell environments for file content retrieval.
.PARAMETER Path
The path to the file or directory. This parameter is mandatory and accepts pipeline input. The function resolves relative paths based on the server's root directory.
.PARAMETER ContentType
The MIME type of the file being served. This is validated against a pattern to ensure it's in the format 'type/subtype'. If not specified, the function attempts to determine the content type based on the file extension.
.PARAMETER FileBrowser
A switch parameter that, when present, enables directory browsing. If the path points to a directory and this parameter is enabled, the function will list the directory's contents instead of returning a 404 error.
.EXAMPLE
Write-PodeAttachmentResponseInternal -Path './files/document.pdf' -ContentType 'application/pdf'
Serves the 'document.pdf' file with the 'application/pdf' MIME type as a downloadable attachment.
.EXAMPLE
Write-PodeAttachmentResponseInternal -Path './files' -FileBrowser
Lists the contents of the './files' directory if the FileBrowser switch is enabled; otherwise, returns a 404 error.
.NOTES
- This function integrates with Pode's internal handling of HTTP responses, leveraging other Pode-specific functions like Get-PodeContentType and Set-PodeResponseStatus. It differentiates between streamed and serverless environments to optimize file delivery.
- This is an internal function and may change in future releases of Pode.
#>
function Write-PodeAttachmentResponseInternal {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[switch]
$FileBrowser
)
# Attempt to retrieve information about the path
$pathInfo = Test-PodePath -Path $Path -Force -ReturnItem -FailOnDirectory:(!$FileBrowser)
if (!$pathinfo) {
return
}
# Check if the path exists
if ($null -eq $pathInfo) {
return
}
if ( $pathInfo.PSIsContainer) {
# filebrowsing is enabled, use the directory response function
Write-PodeDirectoryResponseInternal -Path $Path
return
}
try {
# setup the content type and disposition
if (!$ContentType) {
$WebEvent.Response.ContentType = (Get-PodeContentType -Extension $pathInfo.Extension)
}
else {
$WebEvent.Response.ContentType = $ContentType
}
Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($pathInfo.Name)"
# if serverless, get the content raw and return
if (!$WebEvent.Streamed) {
if (Test-PodeIsPSCore) {
$content = (Get-Content -Path $Path -Raw -AsByteStream)
}
else {
$content = (Get-Content -Path $Path -Raw -Encoding byte)
}
$WebEvent.Response.Body = $content
}
# else if normal, stream the content back
else {
# setup the response details and headers
$WebEvent.Response.SendChunked = $false
# set file as an attachment on the response
$buffer = [byte[]]::new(64 * 1024)
$read = 0
# open up the file as a stream
$fs = (Get-Item $Path).OpenRead()
$WebEvent.Response.ContentLength64 = $fs.Length
while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) {
$WebEvent.Response.OutputStream.Write($buffer, 0, $read)
}
}
}
finally {
Close-PodeDisposable -Disposable $fs
}
}
function Test-PodeRouteFromRequest {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', 'SIGNAL', '*')]
[string]
$Method,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Path,
[Parameter()]
[string]
$EndpointName,
[switch]
$CheckWildMethod
)
$route = Find-PodeRoute -Method $Method -Path $Path -EndpointName $EndpointName -CheckWildMethod:$CheckWildMethod
return ($null -ne $route)
}
function Find-PodeRoute {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', 'SIGNAL', '*')]
[string]
$Method,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Path,
[Parameter()]
[string]
$EndpointName,
[switch]
$CheckWildMethod
)
# first, if supplied, check the wildcard method
if ($CheckWildMethod -and ($PodeContext.Server.Routes['*'].Count -ne 0)) {
$found = Find-PodeRoute -Method '*' -Path $Path -EndpointName $EndpointName
if ($null -ne $found) {
return $found
}
}
# first ensure we have the method
$_method = $PodeContext.Server.Routes[$Method]
if ($null -eq $_method) {
return $null
}
# is this a static route?
$isStatic = ($Method -ieq 'static')
# if we have a perfect match for the route, return it if the protocol is right
if (!$isStatic) {
$found = Get-PodeRouteByUrl -Routes $_method[$Path] -EndpointName $EndpointName
if ($null -ne $found) {
return $found
}
}
# otherwise, match the path to routes on regex (first match only)
$paths = @($_method.Keys)
if ($isStatic) {
[array]::Sort($paths)
[array]::Reverse($paths)
}
$valid = @(foreach ($key in $paths) {
if ($Path -imatch "^$($key)$") {
$key
break
}
})[0]
if ($null -eq $valid) {
return $null
}
# is the route valid for any protocols/endpoints?
$found = Get-PodeRouteByUrl -Routes $_method[$valid] -EndpointName $EndpointName
if ($null -eq $found) {
return $null
}
return $found
}
function Find-PodePublicRoute {
param(
[Parameter(Mandatory = $true)]
[string]
$Path
)
$source = $null
$publicPath = $PodeContext.Server.InbuiltDrives['public']
# reutrn null if there is no public directory
if ([string]::IsNullOrWhiteSpace($publicPath)) {
return $source
}
# use the public static directory (but only if path is a file, and a public dir is present)
if (Test-PodePathIsFile $Path) {
$source = [System.IO.Path]::Combine($publicPath, $Path.TrimStart('/', '\'))
if (!(Test-PodePath -Path $source -NoStatus)) {
$source = $null
}
}
# return the route details
return $source
}
<#
.SYNOPSIS
Finds a static route for a given path in a Pode web server application, with optional checks for public routes.
.DESCRIPTION
This function searches for a static route matching the specified path within a Pode web server application. It attempts to resolve the route to a physical file or directory and supports additional checks for public routes as a fallback option. The function returns a hashtable with route details, including whether the route is for a downloadable file, if it's cacheable, and whether it redirects to a default document.
.PARAMETER Path
The URL path for which to find a static route. This parameter is mandatory.
.PARAMETER EndpointName
Optional. Specifies the name of the endpoint to which the route may belong. If not provided, the function searches across all endpoints.
.PARAMETER CheckPublic
A switch parameter. If specified, the function also checks for the route in public routes as a fallback option.
.EXAMPLE
$staticRoute = Find-PodeStaticRoute -Path '/images/logo.png' -CheckPublic
Searches for a static route for '/images/logo.png'. If not found, checks if a public route exists for the same path.
.EXAMPLE
$staticRoute = Find-PodeStaticRoute -Path '/css/style.css' -EndpointName 'WebUI'
Searches for a static route for '/css/style.css' specifically within the 'WebUI' endpoint, without checking public routes.
.OUTPUTS
Hashtable. Returns a hashtable containing the route details, such as the source path, download flag, cacheability, and redirect status.
.NOTES
This is an internal function and may change in future releases of Pode.
#>
function Find-PodeStaticRoute {
[CmdletBinding()]
[OutputType([hashtable])]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName,
[switch]
$CheckPublic
)
# attempt to get a static route for the path
$found = Find-PodeRoute -Method 'static' -Path $Path -EndpointName $EndpointName
$download = ([bool]$found.Download)
$source = $null
$isDefault = $false
$redirectToDefault = ([bool]$found.RedirectToDefault)
# if we have a defined static route, use that
if ($null -ne $found) {
# see if we have a file
$file = [string]::Empty
if ($found.KleeneStar) {
$matchingPath = "$($found.Path -ireplace '.\*', '.+?')$"
}
else {
$matchingPath = "$($found.Path)$"
}
if ($Path -imatch $matchingPath) {
$file = (Protect-PodeValue -Value $Matches['file'] -Default ([string]::Empty))
}
$fileInfo = Get-Item -Path ([System.IO.Path]::Combine($found.Source, $file)) -Force -ErrorAction Ignore
#if $file doesn't exist return $null
if ($null -eq $fileInfo) {
return $null
}
# if there's no file, we need to check defaults
if (!$found.Download -and $fileInfo.PSIsContainer -and (Get-PodeCount @($found.Defaults)) -gt 0) {
foreach ($def in $found.Defaults) {
$fileInfoDefaultFile = Get-Item -Path ([System.IO.Path]::Combine($fileInfo.FullName, $def)) -Force -ErrorAction Ignore
if ($fileInfoDefaultFile) {
$file = $fileInfoDefaultFile.FullName
$isDefault = $true
break
}
}
}
$source = [System.IO.Path]::Combine($found.Source, $file)
}
# check public, if flagged
if ($CheckPublic -and !(Test-PodePath -Path $source -NoStatus)) {
$source = Find-PodePublicRoute -Path $Path
$download = $false
$found = $null
$isDefault = $false
$redirectToDefault = $false
}
# return nothing if no source
if ([string]::IsNullOrWhiteSpace($source)) {
return $null
}
# return the route details
if ($redirectToDefault -and $isDefault) {
$redirectToDefault = $true
}
else {
$redirectToDefault = $false
}
return @{
Content = @{
Source = $source
IsDownload = $download
IsCachable = (Test-PodeRouteValidForCaching -Path $Path)
RedirectToDefault = $redirectToDefault
}
Route = $found
}
}
function Find-PodeSignalRoute {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName
)
# attempt to get a signal route for the path
return (Find-PodeRoute -Method 'signal' -Path $Path -EndpointName $EndpointName)
}
function Test-PodeRouteValidForCaching {
param(
[Parameter(Mandatory = $true)]
[string]
$Path
)
# check current state of caching
$config = $PodeContext.Server.Web.Static.Cache
$caching = $config.Enabled
# if caching, check include/exclude
if ($caching) {
if (($null -ne $config.Exclude) -and ($Path -imatch $config.Exclude)) {
$caching = $false
}
if (($null -ne $config.Include) -and ($Path -inotmatch $config.Include)) {
$caching = $false
}
}
return $caching
}
<#
.SYNOPSIS
Finds and returns a route from an array of routes based on an endpoint name and/or path.
.DESCRIPTION
This function iterates over an array of route definitions to locate a specific route that matches the provided endpoint name and path.
It supports scenarios where only one of the parameters is provided or both. If no matching route is found, or if the routes array is empty or null,
the function returns $null.
.PARAMETER Routes
An array of hashtable objects, each representing a route with potentially defined properties like Root and Endpoint.Name.
.PARAMETER EndpointName
The name of the endpoint to search for within the route definitions. This parameter is optional.
.EXAMPLE
$routes = @(
@{ Root = '/api'; Endpoint = @{ Name = 'GetData' } },
@{ Root = '/home'; Endpoint = @{ Name = 'Index' } }
)
Get-PodeRouteByUrl -Routes $routes -EndpointName 'GetData'
Returns the route for the '/api' endpoint named 'GetData'.
.EXAMPLE
$routes = @(
@{ Root = '/api'; Endpoint = @{ Name = 'GetData' } },
@{ Root = '/home'; Endpoint = @{ Name = 'Index' } }
)
Get-PodeRouteByUrl -Routes $routes -Path '/api'
Returns the route for the '/api' path, regardless of the endpoint name.
.NOTES
The function prioritizes matching both the endpoint name and path but can return a route based on either criterion if the other is unspecified.
#>
function Get-PodeRouteByUrl {
param(
[Parameter()]
[hashtable[]]
$Routes,
[Parameter()]
[string]
$EndpointName
)
# Return null immediately if routes are not defined or empty
if (($null -eq $Routes) -or ($Routes.Length -eq 0)) {
return $null
}
# Handle case when no specific endpoint name is provided
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
foreach ($route in $Routes) {
# Return the first route as a default if no path is specified
return $route
}
}
else {
# Handle case when an endpoint name is provided
foreach ($route in $Routes) {
if ( $route.Endpoint.Name -ieq $EndpointName) {
# Return the first route that matches the endpoint name as a default
return $route
}
}
}
# Last resort check only route with no endpoint name
foreach ($route in $Routes) {
if ([string]::IsNullOrWhiteSpace($route.Endpoint.Name)) {
# Return the first route that matches the endpoint name as a default
return $route
}
}
# Return null if no matching route is found
return $null
}
function ConvertTo-PodeOpenApiRoutePath {
param(
[Parameter(Mandatory = $true)]
[string]
$Path
)
return (Resolve-PodePlaceholders -Path $Path -Pattern '\:(?<tag>[\w]+)' -Prepend '{' -Append '}')
}
function Update-PodeRouteSlashes {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[switch]
$Static,
[switch]
$NoLeadingSlash
)
# ensure route starts with a '/'
if (!$NoLeadingSlash -and !$Path.StartsWith('/')) {
$Path = "/$($Path)"
}
if ($Static) {
# ensure the static route ends with '/{0,1}.*'
$Path = $Path.TrimEnd('/*')
$Path = "$($Path)[/]{0,1}(?<file>*)"
}
# replace * with .*
$Path = ($Path -ireplace '\*', '.*')
return $Path
}
function Split-PodeRouteQuery {
param(
[Parameter(Mandatory = $true)]
[string]
$Path
)
return ($Path -isplit '\?')[0]
}
function ConvertTo-PodeRouteRegex {
param(
[Parameter()]
[string]
$Path
)
if ([string]::IsNullOrWhiteSpace($Path)) {
return [string]::Empty
}
$Path = Protect-PodeValue -Value $Path -Default '/'
$Path = Split-PodeRouteQuery -Path $Path
$Path = Protect-PodeValue -Value $Path -Default '/'
$Path = Update-PodeRouteSlashes -Path $Path
$Path = Resolve-PodePlaceholders -Path $Path
return $Path
}
function Get-PodeStaticRouteDefault {
if (!(Test-PodeIsEmpty $PodeContext.Server.Web.Static.Defaults)) {
return @($PodeContext.Server.Web.Static.Defaults)
}
return @(
'index.html',
'index.htm',
'default.html',
'default.htm'
)
}
function Test-PodeRouteInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Method,
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Address,
[switch]
$ThrowError
)
# check the routes
$found = $false
$routes = @($PodeContext.Server.Routes[$Method][$Path])
foreach ($route in $routes) {
if (($route.Endpoint.Protocol -ieq $Protocol) -and ($route.Endpoint.Address -ieq $Address)) {
$found = $true
break
}
}
# skip if not found
if (!$found) {
return $false
}
# do we want to throw an error if found, or skip?
if (!$ThrowError) {
return $true
}
# throw error
$_url = $Protocol
if (![string]::IsNullOrEmpty($_url) -and ![string]::IsNullOrWhiteSpace($Address)) {
$_url = "$($_url)://$($Address)"
}
elseif (![string]::IsNullOrWhiteSpace($Address)) {
$_url = $Address
}
if ([string]::IsNullOrEmpty($_url)) {
throw "[$($Method)] $($Path): Already defined"
}
throw "[$($Method)] $($Path): Already defined for $($_url)"
}
function Convert-PodeFunctionVerbToHttpMethod {
param(
[Parameter()]
[string]
$Verb
)
# if empty, just return default
switch ($Verb) {
{ $_ -iin @('Find', 'Format', 'Get', 'Join', 'Search', 'Select', 'Split', 'Measure', 'Ping', 'Test', 'Trace') } { 'GET' }
{ $_ -iin @('Set') } { 'PUT' }
{ $_ -iin @('Rename', 'Edit', 'Update') } { 'PATCH' }
{ $_ -iin @('Clear', 'Close', 'Exit', 'Hide', 'Remove', 'Undo', 'Dismount', 'Unpublish', 'Disable', 'Uninstall', 'Unregister') } { 'DELETE' }
Default { 'POST' }
}
}
<#
.SYNOPSIS
Finds and returns the appropriate transfer encoding for a given route path in a Pode server context.
.DESCRIPTION
This function determines the correct transfer encoding for a specified route path within a Pode web server. It checks if a transfer encoding is already specified and returns it; otherwise, it defaults to the server's default transfer encoding. The function searches the server's transfer encoding route settings for a pattern that matches the given path. If a match is found, the corresponding transfer encoding is returned. This is useful for dynamically setting response encodings based on specific route patterns.
.PARAMETER Path
The route path for which the transfer encoding is being determined. This parameter is mandatory.
.PARAMETER TransferEncoding
The current transfer encoding, if already determined. This is an optional parameter. If specified and not null or whitespace, this function returns the given value without further processing.
.EXAMPLE
$encoding = Find-PodeRouteTransferEncoding -Path "/api/data" -TransferEncoding "chunked"
This example determines the transfer encoding for the route "/api/data", with an initial encoding of "chunked". If "/api/data" matches a specific pattern in the server's transfer encoding settings, the corresponding encoding is returned; otherwise, "chunked" is returned.
.OUTPUTS
String. Returns the determined transfer encoding for the given route path. This will be either the input TransferEncoding (if provided and valid), a matched encoding from the server's settings, or the server's default transfer encoding.
.NOTES
- The function uses a case-insensitive match (`-imatch`) to find the first route key pattern that matches the specified path.
- This is an internal function and may change in future releases of Pode.
#>
function Find-PodeRouteTransferEncoding {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$TransferEncoding
)
# if we already have one, return it
if (![string]::IsNullOrWhiteSpace($TransferEncoding)) {
return $TransferEncoding
}
# set the default
$TransferEncoding = $PodeContext.Server.Web.TransferEncoding.Default
# find type by pattern from settings
$matched = $null
foreach ($key in $PodeContext.Server.Web.TransferEncoding.Routes.Keys) {
if ($Path -imatch $key) {
$matched = $key
break
}
}
# if we get a match, set it
if (!(Test-PodeIsEmpty $matched)) {
$TransferEncoding = $PodeContext.Server.Web.TransferEncoding.Routes[$matched]
}
return $TransferEncoding
}
function Find-PodeRouteContentType {
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$ContentType
)
# if we already have one, return it
if (![string]::IsNullOrWhiteSpace($ContentType)) {
return $ContentType
}
# set the default
$ContentType = $PodeContext.Server.Web.ContentType.Default
# find type by pattern from settings
$matched = $null
foreach ($key in $PodeContext.Server.Web.ContentType.Routes.Keys) {
if ($Path -imatch $key) {
$matched = $key
break
}
}
# if we get a match, set it
if (!(Test-PodeIsEmpty $matched)) {
$ContentType = $PodeContext.Server.Web.ContentType.Routes[$matched]
}
return $ContentType
}
function ConvertTo-PodeMiddleware {
[OutputType([hashtable[]])]
param(
[Parameter()]
[object[]]
$Middleware,
[Parameter(Mandatory = $true)]
[System.Management.Automation.SessionState]
$PSSession
)
# return if no middleware
if (Test-PodeIsEmpty $Middleware) {
return $null
}
$Middleware = @($Middleware)
# ensure supplied middlewares are either a scriptblock, or a valid hashtable
foreach ($mid in $Middleware) {
if ($null -eq $mid) {
continue
}
# check middleware is a type valid
if (($mid -isnot [scriptblock]) -and ($mid -isnot [hashtable])) {
throw "One of the Middlewares supplied is an invalid type. Expected either a ScriptBlock or Hashtable, but got: $($mid.GetType().Name)"
}
# if middleware is hashtable, ensure the keys are valid (logic is a scriptblock)
if ($mid -is [hashtable]) {
if ($null -eq $mid.Logic) {
throw 'A Hashtable Middleware supplied has no Logic defined'
}
if ($mid.Logic -isnot [scriptblock]) {
throw "A Hashtable Middleware supplied has an invalid Logic type. Expected ScriptBlock, but got: $($mid.Logic.GetType().Name)"
}
}
}
# if we have middleware, convert scriptblocks to hashtables
$converted = @(for ($i = 0; $i -lt $Middleware.Length; $i++) {
if ($null -eq $Middleware[$i]) {
continue
}
if ($Middleware[$i] -is [scriptblock]) {
$_script, $_usingVars = Convert-PodeScopedVariables -ScriptBlock $Middleware[$i] -PSSession $PSSession
$Middleware[$i] = @{
Logic = $_script
UsingVariables = $_usingVars
}
}
$Middleware[$i]
})
return $converted
}
function Get-PodeRouteIfExistsPreference {
# from route groups
$groupPref = $RouteGroup.IfExists
if (![string]::IsNullOrWhiteSpace($groupPref) -and ($groupPref -ine 'default')) {
return $groupPref
}
# from Use-PodeRoute
if (![string]::IsNullOrWhiteSpace($script:RouteIfExists) -and ($script:RouteIfExists -ine 'default')) {
return $script:RouteIfExists
}
# global preference
$globalPref = $PodeContext.Server.Preferences.Routes.IfExists
if (![string]::IsNullOrWhiteSpace($globalPref) -and ($globalPref -ine 'default')) {
return $globalPref
}
# final global default
return 'Error'
}
function Find-PodeSchedule {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name
)
return $PodeContext.Schedules.Items[$Name]
}
function Test-PodeSchedulesExist {
return (($null -ne $PodeContext.Schedules) -and (($PodeContext.Schedules.Enabled) -or ($PodeContext.Schedules.Items.Count -gt 0)))
}
function Start-PodeScheduleRunspace {
if (!(Test-PodeSchedulesExist)) {
return
}
Add-PodeSchedule -Name '__pode_schedule_housekeeper__' -Cron '@minutely' -ScriptBlock {
if ($PodeContext.Schedules.Processes.Count -eq 0) {
return
}
foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) {
$process = $PodeContext.Schedules.Processes[$key]
# is it completed?
if (!$process.Runspace.Handler.IsCompleted) {
continue
}
# dispose and remove the schedule process
Close-PodeScheduleInternal -Process $process
}
$process = $null
}
$script = {
# select the schedules that trigger on-start
$_now = [DateTime]::Now
$PodeContext.Schedules.Items.Values |
Where-Object {
$_.OnStart
} | ForEach-Object {
Invoke-PodeInternalSchedule -Schedule $_
}
# complete any schedules
Complete-PodeInternalSchedules -Now $_now
# first, sleep for a period of time to get to 00 seconds (start of minute)
Start-Sleep -Seconds (60 - [DateTime]::Now.Second)
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
$_now = [DateTime]::Now
# select the schedules that need triggering
$PodeContext.Schedules.Items.Values |
Where-Object {
!$_.Completed -and
(($null -eq $_.StartTime) -or ($_.StartTime -le $_now)) -and
(($null -eq $_.EndTime) -or ($_.EndTime -ge $_now)) -and
(Test-PodeCronExpressions -Expressions $_.Crons -DateTime $_now)
} | ForEach-Object {
Invoke-PodeInternalSchedule -Schedule $_
}
# complete any schedules
Complete-PodeInternalSchedules -Now $_now
# cron expression only goes down to the minute, so sleep for 1min
Start-Sleep -Seconds (60 - [DateTime]::Now.Second)
}
}
Add-PodeRunspace -Type Main -ScriptBlock $script -NoProfile
}
function Close-PodeScheduleInternal {
param(
[Parameter()]
[hashtable]
$Process
)
if ($null -eq $Process) {
return
}
Close-PodeDisposable -Disposable $Process.Runspace.Pipeline
$null = $PodeContext.Schedules.Processes.Remove($Process.ID)
}
function Complete-PodeInternalSchedules {
param(
[Parameter(Mandatory = $true)]
[datetime]
$Now
)
# add any schedules to remove that have exceeded their end time
$Schedules = @($PodeContext.Schedules.Items.Values |
Where-Object { (($null -ne $_.EndTime) -and ($_.EndTime -lt $Now)) })
if (($null -eq $Schedules) -or ($Schedules.Length -eq 0)) {
return
}
# set any expired schedules as being completed
$Schedules | ForEach-Object {
$_.Completed = $true
}
}
function Invoke-PodeInternalSchedule {
param(
[Parameter(Mandatory = $true)]
$Schedule
)
$Schedule.OnStart = $false
# increment total number of triggers for the schedule
$Schedule.Count++
# set last trigger to current next trigger
if ($null -ne $Schedule.NextTriggerTime) {
$Schedule.LastTriggerTime = $Schedule.NextTriggerTime
}
else {
$Schedule.LastTriggerTime = [datetime]::Now
}
# check if we have hit the limit, and remove
if (($Schedule.Limit -gt 0) -and ($Schedule.Count -ge $Schedule.Limit)) {
$Schedule.Completed = $true
}
# reset the cron and next trigger
if (!$Schedule.Completed) {
$Schedule.Crons = Reset-PodeRandomCronExpressions -Expressions $Schedule.Crons
$Schedule.NextTriggerTime = Get-PodeCronNextEarliestTrigger -Expressions $Schedule.Crons -EndTime $Schedule.EndTime
}
else {
$Schedule.NextTriggerTime = $null
}
# trigger the schedules logic
Invoke-PodeInternalScheduleLogic -Schedule $Schedule
}
function Invoke-PodeInternalScheduleLogic {
param(
[Parameter(Mandatory = $true)]
$Schedule,
[Parameter()]
[hashtable]
$ArgumentList = $null
)
try {
# setup event param
$parameters = @{
Event = @{
Lockable = $PodeContext.Threading.Lockables.Global
Sender = $Schedule
Metadata = @{}
}
}
# add any schedule args
foreach ($key in $Schedule.Arguments.Keys) {
$parameters[$key] = $Schedule.Arguments[$key]
}
# add adhoc schedule invoke args
if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) {
foreach ($key in $ArgumentList.Keys) {
$parameters[$key] = $ArgumentList[$key]
}
}
# add any using variables
if ($null -ne $Schedule.UsingVariables) {
foreach ($usingVar in $Schedule.UsingVariables) {
$parameters[$usingVar.NewName] = $usingVar.Value
}
}
$name = New-PodeGuid
$runspace = Add-PodeRunspace -Type Schedules -ScriptBlock (($Schedule.Script).GetNewClosure()) -Parameters $parameters -PassThru
$PodeContext.Schedules.Processes[$name] = @{
ID = $name
Schedule = $Schedule.Name
Runspace = $runspace
}
}
catch {
$_ | Write-PodeErrorLog
}
}
function Add-PodeScopedVariableInternal {
[CmdletBinding(DefaultParameterSetName = 'Replace')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Replace')]
[string]
$GetReplace,
[Parameter(ParameterSetName = 'Replace')]
[string]
$SetReplace = $null,
[Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
[scriptblock]
$ScriptBlock,
[Parameter(ParameterSetName = 'Internal')]
[switch]
$InternalFunction
)
# lowercase the name
$Name = $Name.ToLowerInvariant()
# check if var already defined
if (Test-PodeScopedVariable -Name $Name) {
throw "Scoped Variable already defined: $($Name)"
}
# add scoped var definition
$PodeContext.Server.ScopedVariables[$Name] = @{
Name = $Name
Type = $PSCmdlet.ParameterSetName.ToLowerInvariant()
ScriptBlock = $ScriptBlock
Get = @{
Pattern = "(?<full>\`$$($Name)\:(?<name>[a-z0-9_\?]+))"
Replace = $GetReplace
}
Set = @{
Pattern = "(?<full>\`$$($Name)\:(?<name>[a-z0-9_\?]+)\s*=)"
Replace = $SetReplace
}
InternalFunction = $InternalFunction.IsPresent
}
}
function Add-PodeScopedVariablesInbuilt {
Add-PodeScopedVariableInbuiltUsing
Add-PodeScopedVariableInbuiltCache
Add-PodeScopedVariableInbuiltSecret
Add-PodeScopedVariableInbuiltSession
Add-PodeScopedVariableInbuiltState
}
function Add-PodeScopedVariableInbuiltCache {
Add-PodeScopedVariable -Name 'cache' `
-SetReplace "Set-PodeCache -Key '{{name}}' -InputObject " `
-GetReplace "Get-PodeCache -Key '{{name}}'"
}
function Add-PodeScopedVariableInbuiltSecret {
Add-PodeScopedVariable -Name 'secret' `
-SetReplace "Update-PodeSecret -Name '{{name}}' -InputObject " `
-GetReplace "Get-PodeSecret -Name '{{name}}'"
}
function Add-PodeScopedVariableInbuiltSession {
Add-PodeScopedVariable -Name 'session' `
-SetReplace "`$WebEvent.Session.Data.'{{name}}'" `
-GetReplace "`$WebEvent.Session.Data.'{{name}}'"
}
function Add-PodeScopedVariableInbuiltState {
Add-PodeScopedVariable -Name 'state' `
-SetReplace "Set-PodeState -Name '{{name}}' -Value " `
-GetReplace "`$PodeContext.Server.State.'{{name}}'.Value"
}
function Add-PodeScopedVariableInbuiltUsing {
Add-PodeScopedVariableInternal -Name 'using' -InternalFunction
}
function Convert-PodeScopedVariableInbuiltUsing {
param(
[Parameter(ValueFromPipeline = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[System.Management.Automation.SessionState]
$PSSession
)
# do nothing if no script or session
if (($null -eq $ScriptBlock) -or ($null -eq $PSSession)) {
return $ScriptBlock, $null
}
# rename any __using_ vars for inner timers, etcs
$strScriptBlock = "$($ScriptBlock)"
$foundInnerUsing = $false
while ($strScriptBlock -imatch '(?<full>\$__using_(?<name>[a-z0-9_\?]+))') {
$foundInnerUsing = $true
$strScriptBlock = $strScriptBlock.Replace($Matches['full'], "`$using:$($Matches['name'])")
}
# just return if there are no $using:
if ($strScriptBlock -inotmatch '\$using:') {
return $ScriptBlock, $null
}
# if we found any inner usings, recreate the scriptblock
if ($foundInnerUsing) {
$ScriptBlock = [scriptblock]::Create($strScriptBlock)
}
# get any using variables
$usingVars = Get-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock
if (($null -eq $usingVars) -or ($usingVars.Count -eq 0)) {
return $ScriptBlock, $null
}
# convert any using vars to use new names
$usingVars = Find-PodeScopedVariableUsingVariableValues -UsingVariables $usingVars -PSSession $PSSession
# now convert the script
$newScriptBlock = Convert-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock -UsingVariables $usingVars
# return converted script
return $newScriptBlock, $usingVars
}
function Get-PodeScopedVariableUsingVariables {
param(
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock
)
return $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.UsingExpressionAst] }, $true)
}
function Find-PodeScopedVariableUsingVariableValues {
param(
[Parameter(Mandatory = $true)]
$UsingVariables,
[Parameter(Mandatory = $true)]
[System.Management.Automation.SessionState]
$PSSession
)
$mapped = @{}
foreach ($usingVar in $UsingVariables) {
# var name
$varName = $usingVar.SubExpression.VariablePath.UserPath
# only retrieve value if new var
if (!$mapped.ContainsKey($varName)) {
# get value, or get __using_ value for child scripts
$value = $PSSession.PSVariable.Get($varName)
if ([string]::IsNullOrEmpty($value)) {
$value = $PSSession.PSVariable.Get("__using_$($varName)")
}
if ([string]::IsNullOrEmpty($value)) {
throw "Value for `$using:$($varName) could not be found"
}
# add to mapped
$mapped[$varName] = @{
OldName = $usingVar.SubExpression.Extent.Text
NewName = "__using_$($varName)"
NewNameWithDollar = "`$__using_$($varName)"
SubExpressions = @()
Value = $value.Value
}
}
# add the vars sub-expression for replacing later
$mapped[$varName].SubExpressions += $usingVar.SubExpression
}
return @($mapped.Values)
}
function Convert-PodeScopedVariableUsingVariables {
param(
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true)]
[hashtable[]]
$UsingVariables
)
$varsList = New-Object 'System.Collections.Generic.List`1[System.Management.Automation.Language.VariableExpressionAst]'
$newParams = New-Object System.Collections.ArrayList
foreach ($usingVar in $UsingVariables) {
foreach ($subExp in $usingVar.SubExpressions) {
$null = $varsList.Add($subExp)
}
}
$null = $newParams.AddRange(@($UsingVariables.NewNameWithDollar))
$newParams = ($newParams -join ', ')
$tupleParams = [tuple]::Create($varsList, $newParams)
$bindingFlags = [System.Reflection.BindingFlags]'Default, NonPublic, Instance'
$_varReplacerMethod = $ScriptBlock.Ast.GetType().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags)
$convertedScriptBlockStr = $_varReplacerMethod.Invoke($ScriptBlock.Ast, @($tupleParams))
if (!$ScriptBlock.Ast.ParamBlock) {
$convertedScriptBlockStr = "param($($newParams))`n$($convertedScriptBlockStr)"
}
$convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr)
if ($convertedScriptBlock.Ast.EndBlock[0].Statements.Extent.Text.StartsWith('$input |')) {
$convertedScriptBlockStr = ($convertedScriptBlockStr -ireplace '\$input \|')
$convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr)
}
return $convertedScriptBlock
}
function Initialize-PodeSecretVault {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$VaultConfig,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock
)
$null = Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Splat -Arguments @($VaultConfig.Parameters)
}
function Register-PodeSecretManagementVault {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$VaultConfig,
[Parameter()]
[string]
$VaultName,
[Parameter(Mandatory = $true)]
[string]
$ModuleName
)
# use the Name for VaultName if not passed
if ([string]::IsNullOrWhiteSpace($VaultName)) {
$VaultName = $VaultConfig.Name
}
# import the modules
$null = Import-Module -Name Microsoft.PowerShell.SecretManagement -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false
$null = Import-Module -Name $ModuleName -Force -DisableNameChecking -Scope Global -ErrorAction Stop -Verbose:$false
# export the modules for pode
Export-PodeModule -Name @('Microsoft.PowerShell.SecretManagement', $ModuleName)
# is this the local SecretStore provider?
$isSecretStore = ($ModuleName -ieq 'Microsoft.PowerShell.SecretStore')
# check if we have an unlock password for local secret store
if ($isSecretStore) {
if ([string]::IsNullOrEmpty($VaultConfig.Unlock.Secret)) {
throw 'An "-UnlockSecret" is required when using Microsoft.PowerShell.SecretStore'
}
}
# does the local secret store already exist?
$secretStoreExists = ($isSecretStore -and (Test-PodeSecretVaultInternal -Name $VaultName))
# do we have vault params?
$hasVaultParams = ($null -ne $VaultConfig.Parameters)
# attempt to register the vault
$registerParams = @{
Name = $VaultName
ModuleName = $ModuleName
Confirm = $false
AllowClobber = $true
ErrorAction = 'Stop'
}
if (!$isSecretStore -and $hasVaultParams) {
$registerParams['VaultParameters'] = $VaultConfig.Parameters
}
$null = Register-SecretVault @registerParams
# all is good, so set the config
$VaultConfig['SecretManagement'] = @{
VaultName = $VaultName
ModuleName = $ModuleName
}
# set local secret store config
if ($isSecretStore) {
if (!$hasVaultParams) {
$VaultConfig.Parameters = @{}
}
$vaultParams = $VaultConfig.Parameters
# remove the password
$vaultParams.Remove('Password')
# set default authentication and interaction flags
if ([string]::IsNullOrEmpty($vaultParams.Authentication)) {
$vaultParams['Authentication'] = 'Password'
}
if ([string]::IsNullOrEmpty($vaultParams.Interaction)) {
$vaultParams['Interaction'] = 'None'
}
# set default password timeout and unlock interval to 1 minute
if ($VaultConfig.Unlock.Interval -le 0) {
$VaultConfig.Unlock.Interval = 1
}
# unlock the vault, and set password
$VaultConfig | Unlock-PodeSecretManagementVault
# set the password timeout for the vault
if (!$secretStoreExists) {
if ($VaultConfig.Parameters.PasswordTimeout -le 0) {
$vaultParams['PasswordTimeout'] = ($VaultConfig.Unlock.Interval * 60) + 10
}
}
# set config
$null = Set-SecretStoreConfiguration @vaultParams -Confirm:$false -ErrorAction Stop
}
}
function Register-PodeSecretCustomVault {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$VaultConfig,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[scriptblock]
$UnlockScriptBlock,
[Parameter()]
[scriptblock]
$RemoveScriptBlock,
[Parameter()]
[scriptblock]
$SetScriptBlock,
[Parameter()]
[scriptblock]
$UnregisterScriptBlock
)
# unlock secret with no script?
if ($VaultConfig.Unlock.Enabled -and (Test-PodeIsEmpty $UnlockScriptBlock)) {
throw 'Unlock secret supplied for custom Secret Vault type, but not Unlock ScriptBlock supplied'
}
# all is good, so set the config
$VaultConfig['Custom'] = @{
Read = $ScriptBlock
Unlock = $UnlockScriptBlock
Remove = $RemoveScriptBlock
Set = $SetScriptBlock
Unregister = $UnregisterScriptBlock
}
}
function Unlock-PodeSecretManagementVault {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$VaultConfig
)
# do we need to unlock the vault?
if (!$VaultConfig.Unlock.Enabled) {
return $null
}
# unlock the vault
$null = Unlock-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Password $VaultConfig.Unlock.Secret -ErrorAction Stop
# interval?
if ($VaultConfig.Unlock.Interval -gt 0) {
return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval))
}
return $null
}
function Unlock-PodeSecretCustomVault {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$VaultConfig
)
# do we need to unlock the vault?
if (!$VaultConfig.Unlock.Enabled) {
return
}
# do we have an unlock scriptblock
if ($null -eq $VaultConfig.Custom.Unlock) {
throw "No Unlock ScriptBlock supplied for unlocking the vault '$($VaultConfig.Name)'"
}
# unlock the vault, and get back an expiry
$expiry = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unlock -Splat -Return -Arguments @(
$VaultConfig.Parameters,
(ConvertFrom-SecureString -SecureString $VaultConfig.Unlock.Secret -AsPlainText)
)
# return expiry if given, otherwise check interval
if ($null -ne $expiry) {
return $expiry
}
if ($VaultConfig.Unlock.Interval -gt 0) {
return ([datetime]::UtcNow.AddMinutes($VaultConfig.Unlock.Interval))
}
return $null
}
function Unregister-PodeSecretManagementVault {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$VaultConfig
)
# do we need to unregister the vault?
if ($VaultConfig.AutoImported) {
return
}
# unregister the vault
$null = Unregister-SecretVault -Name $VaultConfig.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop
}
function Unregister-PodeSecretCustomVault {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$VaultConfig
)
# do we need to unregister the vault?
if ($VaultConfig.AutoImported) {
return
}
# do we have an unregister scriptblock? if not, just do nothing
if ($null -eq $VaultConfig.Custom.Unregister) {
return
}
# unregister the vault
$null = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unregister -Splat -Arguments @(
$VaultConfig.Parameters
)
}
function Get-PodeSecretManagementKey {
param(
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter(Mandatory = $true)]
[string]
$Key
)
# get the vault
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
# fetch the secret
return (Get-Secret -Name $Key -Vault $_vault.SecretManagement.VaultName -AsPlainText -ErrorAction Stop)
}
function Get-PodeSecretCustomKey {
param(
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[object[]]
$ArgumentList
)
# get the vault
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
# fetch the secret
return Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Read -Splat -Return -Arguments (@(
$_vault.Parameters,
$Key
) + $ArgumentList)
}
function Set-PodeSecretManagementKey {
param(
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter(Mandatory = $true)]
[object]
$Value,
[Parameter()]
[hashtable]
$Metadata
)
# get the vault
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
# set the secret
$null = Set-Secret -Name $Key -Secret $Value -Vault $_vault.SecretManagement.VaultName -Metadata $Metadata -Confirm:$false -ErrorAction Stop
}
function Set-PodeSecretCustomKey {
param(
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter(Mandatory = $true)]
[object]
$Value,
[Parameter()]
[hashtable]
$Metadata,
[Parameter()]
[object[]]
$ArgumentList
)
# get the vault
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
# do we have a set scriptblock?
if ($null -eq $_vault.Custom.Set) {
throw "No Set ScriptBlock supplied for updating/creating secrets in the vault '$($_vault.Name)'"
}
# set the secret
$null = Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Set -Splat -Arguments (@(
$_vault.Parameters,
$Key,
$Value,
$Metadata
) + $ArgumentList)
}
function Remove-PodeSecretManagementKey {
param(
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter(Mandatory = $true)]
[string]
$Key
)
# get the vault
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
# remove the secret
$null = Remove-Secret -Name $Key -Vault $_vault.SecretManagement.VaultName -Confirm:$false -ErrorAction Stop
}
function Remove-PodeSecretCustomKey {
param(
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[object[]]
$ArgumentList
)
# get the vault
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
# do we have a remove scriptblock?
if ($null -eq $_vault.Custom.Remove) {
throw "No Remove ScriptBlock supplied for removing secrets from the vault '$($_vault.Name)'"
}
# remove the secret
$null = Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Remove -Splat -Arguments (@(
$_vault.Parameters,
$Key
) + $ArgumentList)
}
function Start-PodeSecretCacheHousekeeper {
if (Test-PodeTimer -Name '__pode_secrets_cache_expiry__') {
return
}
Add-PodeTimer -Name '__pode_secrets_cache_expiry__' -Interval 60 -ScriptBlock {
$now = [datetime]::UtcNow
foreach ($key in $PodeContext.Server.Secrets.Keys.Values) {
if (!$key.Cache.Enabled -or ($null -eq $key.Cache.Expiry) -or ($key.Cache.Expiry -gt $now)) {
continue
}
$key.Cache.Expiry = $null
$key.Cache.Value = $null
}
}
}
function Start-PodeSecretVaultUnlocker {
if (Test-PodeTimer -Name '__pode_secrets_vault_unlock__') {
return
}
Add-PodeTimer -Name '__pode_secrets_vault_unlock__' -Interval 60 -ScriptBlock {
$now = [datetime]::UtcNow
foreach ($vault in $PodeContext.Server.Secrets.Vaults.Values) {
if (!$vault.Unlock.Enabled -or ($null -eq $vault.Unlock.Expiry) -or ($vault.Unlock.Expiry -gt $now)) {
continue
}
Unlock-PodeSecretVault -Name $vault.Name
}
}
}
function Unregister-PodeSecretVaults {
param(
[switch]
$ThrowError
)
if (Test-PodeIsEmpty $PodeContext.Server.Secrets.Vaults) {
return
}
foreach ($vault in $PodeContext.Server.Secrets.Vaults.Values.Name) {
if ([string]::IsNullOrEmpty($vault)) {
continue
}
try {
Unregister-PodeSecretVault -Name $vault
}
catch {
if ($ThrowError) {
throw
}
else {
$_ | Write-PodeErrorLog
}
}
}
}
function Protect-PodeSecretValueType {
param(
[Parameter(Mandatory = $true)]
[object]
$Value
)
if ($Value -is [System.ValueType]) {
$Value = $Value.ToString()
}
if ([string]::IsNullOrEmpty($Value)) {
$Value = [string]::Empty
}
if ($Value -is [ordered]) {
$Value = [hashtable]$Value
}
if (!(
($Value -is [string]) -or
($Value -is [securestring]) -or
($Value -is [hashtable]) -or
($Value -is [byte[]]) -or
($Value -is [pscredential]) -or
($Value -is [System.Management.Automation.OrderedHashtable])
)) {
throw "Value to set secret to is of an invalid type. Expected either String, SecureString, HashTable, Byte[], or PSCredential. But got: $($Value.GetType().Name)"
}
return $Value
}
function Test-PodeSecretVaultInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return ($null -ne (Get-SecretVault -Name $Name -ErrorAction Ignore))
}
using namespace System.Security.Cryptography
function Test-PodeIPLimit {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$IP
)
$type = 'IP'
# get the limit rules and active list
$rules = $PodeContext.Server.Limits.Rules[$type]
$active = $PodeContext.Server.Limits.Active[$type]
# if there are no rules, it's valid
if (($null -eq $rules) -or ($rules.Count -eq 0)) {
return $true
}
# get the ip address in bytes
$IP = @{
String = $IP.IPAddressToString
Family = $IP.AddressFamily
Bytes = $IP.GetAddressBytes()
}
# now
$now = [DateTime]::UtcNow
# is the ip active? (get a direct match, then try grouped subnets)
$_active_ip = $active[$IP.String]
if ($null -eq $_active_ip) {
$_groups = @(foreach ($key in $active.Keys) {
if ($active[$key].Rule.Grouped) {
$active[$key]
}
})
$_active_ip = @(foreach ($_group in $_groups) {
if (Test-PodeIPAddressInRange -IP $IP -LowerIP $_group.Rule.Lower -UpperIP $_group.Rule.Upper) {
$_group
break
}
})[0]
}
# the ip is active, or part of a grouped subnet
if ($null -ne $_active_ip) {
# if limit is -1, always allowed
if ($_active_ip.Rule.Limit -eq -1) {
return $true
}
# check expire time, a reset if needed
if ($now -ge $_active_ip.Expire) {
$_active_ip.Rate = 0
$_active_ip.Expire = $now.AddSeconds($_active_ip.Rule.Seconds)
}
# are we over the limit?
if ($_active_ip.Rate -ge $_active_ip.Rule.Limit) {
return $false
}
# increment the rate
$_active_ip.Rate++
return $true
}
# the ip isn't active
else {
# get the ip's rule
$_rule_ip = @(foreach ($rule in $rules.Values) {
if (Test-PodeIPAddressInRange -IP $IP -LowerIP $rule.Lower -UpperIP $rule.Upper) {
$rule
break
}
})[0]
# if ip not in rules, it's valid
# (add to active list as always allowed - saves running where search everytime)
if ($null -eq $_rule_ip) {
$active[$IP.String] = @{
Rule = @{
Limit = -1
}
}
return $true
}
# add ip to active list (ip if not grouped, else the subnet if it's grouped)
$_ip = (Resolve-PodeValue -Check $_rule_ip.Grouped -TrueValue $_rule_ip.IP -FalseValue $IP.String)
$active[$_ip] = @{
Rule = $_rule_ip
Rate = 1
Expire = $now.AddSeconds($_rule_ip.Seconds)
}
# if limit is 0, it's never allowed
return ($_rule_ip -ne 0)
}
}
function Test-PodeRouteLimit {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[string]
$Path
)
$type = 'Route'
# get the limit rules and active list
$rules = $PodeContext.Server.Limits.Rules[$type]
$active = $PodeContext.Server.Limits.Active[$type]
# if there are no rules, it's valid
if (($null -eq $rules) -or ($rules.Count -eq 0)) {
return $true
}
# now
$now = [DateTime]::UtcNow
# is the route active?
$_active_route = $active[$Path]
# the ip is active, or part of a grouped subnet
if ($null -ne $_active_route) {
# if limit is -1, always allowed
if ($_active_route.Rule.Limit -eq -1) {
return $true
}
# check expire time, a reset if needed
if ($now -ge $_active_route.Expire) {
$_active_route.Rate = 0
$_active_route.Expire = $now.AddSeconds($_active_route.Rule.Seconds)
}
# are we over the limit?
if ($_active_route.Rate -ge $_active_route.Rule.Limit) {
return $false
}
# increment the rate
$_active_route.Rate++
return $true
}
# the route isn't active
else {
# get the route's rule
$_rule_route = $rules[$Path]
# if route not in rules, it's valid (add to active list as always allowed)
if ($null -eq $_rule_route) {
$active[$Path] = @{
Rule = @{
Limit = -1
}
}
return $true
}
# add route to active list
$active[$Path] = @{
Rule = $_rule_route
Rate = 1
Expire = $now.AddSeconds($_rule_route.Seconds)
}
# if limit is 0, it's never allowed
return ($_rule_route -ne 0)
}
}
function Test-PodeEndpointLimit {
param(
[Parameter()]
[string]
$EndpointName
)
$type = 'Endpoint'
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
return $true
}
# get the limit rules and active list
$rules = $PodeContext.Server.Limits.Rules[$type]
$active = $PodeContext.Server.Limits.Active[$type]
# if there are no rules, it's valid
if (($null -eq $rules) -or ($rules.Count -eq 0)) {
return $true
}
# now
$now = [DateTime]::UtcNow
# is the endpoint active?
$_active_endpoint = $active[$EndpointName]
# the endpoint is active
if ($null -ne $_active_endpoint) {
# if limit is -1, always allowed
if ($_active_endpoint.Rule.Limit -eq -1) {
return $true
}
# check expire time, a reset if needed
if ($now -ge $_active_endpoint.Expire) {
$_active_endpoint.Rate = 0
$_active_endpoint.Expire = $now.AddSeconds($_active_endpoint.Rule.Seconds)
}
# are we over the limit?
if ($_active_endpoint.Rate -ge $_active_endpoint.Rule.Limit) {
return $false
}
# increment the rate
$_active_endpoint.Rate++
return $true
}
# the endpoint isn't active
else {
# get the endpoint's rule
$_rule_endpoint = $rules[$EndpointName]
# if endpoint not in rules, it's valid (add to active list as always allowed)
if ($null -eq $_rule_endpoint) {
$active[$EndpointName] = @{
Rule = @{
Limit = -1
}
}
return $true
}
# add endpoint to active list
$active[$EndpointName] = @{
Rule = $_rule_endpoint
Rate = 1
Expire = $now.AddSeconds($_rule_endpoint.Seconds)
}
# if limit is 0, it's never allowed
return ($_rule_endpoint -ne 0)
}
}
function Test-PodeIPAccess {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$IP
)
$type = 'IP'
# get permission lists for ip
$allow = $PodeContext.Server.Access.Allow[$type]
$deny = $PodeContext.Server.Access.Deny[$type]
# are they empty?
$alEmpty = (($null -eq $allow) -or ($allow.Count -eq 0))
$dnEmpty = (($null -eq $deny) -or ($deny.Count -eq 0))
# if both are empty, value is valid
if ($alEmpty -and $dnEmpty) {
return $true
}
# get the ip address in bytes
$IP = @{
Family = $IP.AddressFamily
Bytes = $IP.GetAddressBytes()
}
# if value in allow, it's allowed
if (!$alEmpty) {
$match = @(foreach ($value in $allow.Values) {
if (Test-PodeIPAddressInRange -IP $IP -LowerIP $value.Lower -UpperIP $value.Upper) {
$value
break
}
})[0]
if ($null -ne $match) {
return $true
}
}
# if value in deny, it's disallowed
if (!$dnEmpty) {
$match = @(foreach ($value in $deny.Values) {
if (Test-PodeIPAddressInRange -IP $IP -LowerIP $value.Lower -UpperIP $value.Upper) {
$value
break
}
})[0]
if ($null -ne $match) {
return $false
}
}
# if we have an allow, it's disallowed (because it's not in there)
if (!$alEmpty) {
return $false
}
# otherwise it's allowed (because it's not in the deny)
return $true
}
function Add-PodeIPLimit {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[string]
$IP,
[Parameter(Mandatory = $true)]
[int]
$Limit,
[Parameter(Mandatory = $true)]
[int]
$Seconds,
[switch]
$Group
)
# current limit type
$type = 'IP'
# ensure limit and seconds are non-zero and negative
if ($Limit -le 0) {
throw "Limit value cannot be 0 or less for $($IP)"
}
if ($Seconds -le 0) {
throw "Seconds value cannot be 0 or less for $($IP)"
}
# get current rules
$rules = $PodeContext.Server.Limits.Rules[$type]
# setup up perm type
if ($null -eq $rules) {
$PodeContext.Server.Limits.Rules[$type] = @{}
$PodeContext.Server.Limits.Active[$type] = @{}
$rules = $PodeContext.Server.Limits.Rules[$type]
}
# have we already added the ip?
elseif ($rules.ContainsKey($IP)) {
return
}
# calculate the lower/upper ip bounds
if (Test-PodeIPAddressIsSubnetMask -IP $IP) {
$_tmp = Get-PodeSubnetRange -SubnetMask $IP
$_tmpLo = Get-PodeIPAddress -IP $_tmp.Lower
$_tmpHi = Get-PodeIPAddress -IP $_tmp.Upper
}
elseif (Test-PodeIPAddressAny -IP $IP) {
$_tmpLo = Get-PodeIPAddress -IP '0.0.0.0'
$_tmpHi = Get-PodeIPAddress -IP '255.255.255.255'
}
else {
$_tmpLo = Get-PodeIPAddress -IP $IP
$_tmpHi = $_tmpLo
}
# add limit rule for ip
$rules.Add($IP, @{
Limit = $Limit
Seconds = $Seconds
Grouped = [bool]$Group
IP = $IP
Lower = @{
Family = $_tmpLo.AddressFamily
Bytes = $_tmpLo.GetAddressBytes()
}
Upper = @{
Family = $_tmpHi.AddressFamily
Bytes = $_tmpHi.GetAddressBytes()
}
})
}
function Add-PodeRouteLimit {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[string]
$Path,
[Parameter(Mandatory = $true)]
[int]
$Limit,
[Parameter(Mandatory = $true)]
[int]
$Seconds,
[switch]
$Group
)
# current limit type
$type = 'Route'
# ensure limit and seconds are non-zero and negative
if ($Limit -le 0) {
throw "Limit value cannot be 0 or less for $($IP)"
}
if ($Seconds -le 0) {
throw "Seconds value cannot be 0 or less for $($IP)"
}
# get current rules
$rules = $PodeContext.Server.Limits.Rules[$type]
# setup up perm type
if ($null -eq $rules) {
$PodeContext.Server.Limits.Rules[$type] = @{}
$PodeContext.Server.Limits.Active[$type] = @{}
$rules = $PodeContext.Server.Limits.Rules[$type]
}
# have we already added the route?
elseif ($rules.ContainsKey($Path)) {
return
}
# add limit rule for the route
$rules.Add($Path, @{
Limit = $Limit
Seconds = $Seconds
Grouped = [bool]$Group
Path = $Path
})
}
function Add-PodeEndpointLimit {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[string]
$EndpointName,
[Parameter(Mandatory = $true)]
[int]
$Limit,
[Parameter(Mandatory = $true)]
[int]
$Seconds,
[switch]
$Group
)
# current limit type
$type = 'Endpoint'
# does the endpoint exist?
$endpoint = Get-PodeEndpointByName -Name $EndpointName
if ($null -eq $endpoint) {
throw "Endpoint not found: $($EndpointName)"
}
# ensure limit and seconds are non-zero and negative
if ($Limit -le 0) {
throw "Limit value cannot be 0 or less for $($IP)"
}
if ($Seconds -le 0) {
throw "Seconds value cannot be 0 or less for $($IP)"
}
# get current rules
$rules = $PodeContext.Server.Limits.Rules[$type]
# setup up perm type
if ($null -eq $rules) {
$PodeContext.Server.Limits.Rules[$type] = @{}
$PodeContext.Server.Limits.Active[$type] = @{}
$rules = $PodeContext.Server.Limits.Rules[$type]
}
# have we already added the endpoint?
elseif ($rules.ContainsKey($EndpointName)) {
return
}
# add limit rule for the endpoint
$rules.Add($EndpointName, @{
Limit = $Limit
Seconds = $Seconds
Grouped = [bool]$Group
EndpointName = $EndpointName
})
}
function Add-PodeIPAccess {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Allow', 'Deny')]
[string]
$Access,
[Parameter(Mandatory = $true)]
[string]
$IP
)
# current access type
$type = 'IP'
# get opposite permission
$opp = "$(if ($Access -ieq 'allow') { 'Deny' } else { 'Allow' })"
# get permission lists for type
$permType = $PodeContext.Server.Access[$Access][$type]
$oppType = $PodeContext.Server.Access[$opp][$type]
# setup up perm type
if ($null -eq $permType) {
$PodeContext.Server.Access[$Access][$type] = @{}
$permType = $PodeContext.Server.Access[$Access][$type]
}
# have we already added the ip?
elseif ($permType.ContainsKey($IP)) {
return
}
# remove from opp type
if ($null -ne $oppType -and $oppType.ContainsKey($IP)) {
$oppType.Remove($IP)
}
# calculate the lower/upper ip bounds
if (Test-PodeIPAddressIsSubnetMask -IP $IP) {
$_tmp = Get-PodeSubnetRange -SubnetMask $IP
$_tmpLo = Get-PodeIPAddress -IP $_tmp.Lower
$_tmpHi = Get-PodeIPAddress -IP $_tmp.Upper
}
elseif (Test-PodeIPAddressAny -IP $IP) {
$_tmpLo = Get-PodeIPAddress -IP '0.0.0.0'
$_tmpHi = Get-PodeIPAddress -IP '255.255.255.255'
}
else {
$_tmpLo = Get-PodeIPAddress -IP $IP
$_tmpHi = $_tmpLo
}
# add access rule for ip
$permType.Add($IP, @{
Lower = @{
Family = $_tmpLo.AddressFamily
Bytes = $_tmpLo.GetAddressBytes()
}
Upper = @{
Family = $_tmpHi.AddressFamily
Bytes = $_tmpHi.GetAddressBytes()
}
})
}
function Get-PodeCsrfToken {
# key name to search
$key = $PodeContext.Server.Cookies.Csrf.Name
# check the payload
if (!(Test-PodeIsEmpty $WebEvent.Data[$key])) {
return $WebEvent.Data[$key]
}
# check the query string
if (!(Test-PodeIsEmpty $WebEvent.Query[$key])) {
return $WebEvent.Query[$key]
}
# check the headers
$value = (Get-PodeHeader -Name $key)
if (!(Test-PodeIsEmpty $value)) {
return $value
}
return $null
}
function Test-PodeCsrfToken {
param(
[Parameter()]
[string]
$Secret,
[Parameter()]
[string]
$Token
)
# if there's no token/secret, fail
if ((Test-PodeIsEmpty $Secret) -or (Test-PodeIsEmpty $Token)) {
return $false
}
# the token must start with "t:"
if (!$Token.StartsWith('t:')) {
return $false
}
# get the salt from the token
$_token = $Token.Substring(2)
$periodIndex = $_token.LastIndexOf('.')
if ($periodIndex -eq -1) {
return $false
}
$salt = $_token.Substring(0, $periodIndex)
# ensure the token is valid
if ((Restore-PodeCsrfToken -Secret $Secret -Salt $salt) -ne $Token) {
return $false
}
return $true
}
function New-PodeCsrfSecret {
# see if there's already a secret in session/cookie
$secret = (Get-PodeCsrfSecret)
if (!(Test-PodeIsEmpty $secret)) {
return $secret
}
# otherwise, make a new secret and cache it
$secret = (New-PodeGuid -Secure -Length 16)
Set-PodeCsrfSecret -Secret $secret
return $secret
}
function Get-PodeCsrfSecret {
# key name to get secret
$key = $PodeContext.Server.Cookies.Csrf.Name
# are we getting it from a cookie, or session?
if ($PodeContext.Server.Cookies.Csrf.UseCookies) {
$cookie = Get-PodeCookie `
-Name $PodeContext.Server.Cookies.Csrf.Name `
-Secret $PodeContext.Server.Cookies.Csrf.Secret
return $cookie.Value
}
# on session
else {
return $WebEvent.Session.Data[$key]
}
}
function Set-PodeCsrfSecret {
param(
[Parameter(Mandatory = $true)]
[string]
$Secret
)
# key name to set secret under
$key = $PodeContext.Server.Cookies.Csrf.Name
# are we setting this on a cookie, or session?
if ($PodeContext.Server.Cookies.Csrf.UseCookies) {
$null = Set-PodeCookie `
-Name $PodeContext.Server.Cookies.Csrf.Name `
-Value $Secret `
-Secret $PodeContext.Server.Cookies.Csrf.Secret
}
# on session
else {
$WebEvent.Session.Data[$key] = $Secret
}
}
function Restore-PodeCsrfToken {
param(
[Parameter(Mandatory = $true)]
[string]
$Secret,
[Parameter(Mandatory = $true)]
[string]
$Salt
)
return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
}
function Test-PodeCsrfConfigured {
return (!(Test-PodeIsEmpty $PodeContext.Server.Cookies.Csrf))
}
function Get-PodeCertificateByFile {
param(
[Parameter(Mandatory = $true)]
[string]
$Certificate,
[Parameter()]
[string]
$Password = $null,
[Parameter()]
[string]
$Key = $null
)
# cert + key
if (![string]::IsNullOrWhiteSpace($Key)) {
return (Get-PodeCertificateByPemFile -Certificate $Certificate -Password $Password -Key $Key)
}
$path = Get-PodeRelativePath -Path $Certificate -JoinRoot -Resolve
# cert + password
if (![string]::IsNullOrWhiteSpace($Password)) {
return [X509Certificates.X509Certificate2]::new($path, $Password)
}
# plain cert
return [X509Certificates.X509Certificate2]::new($path)
}
function Get-PodeCertificateByPemFile {
param(
[Parameter(Mandatory = $true)]
[string]
$Certificate,
[Parameter()]
[string]
$Password = $null,
[Parameter()]
[string]
$Key = $null
)
$cert = $null
$certPath = Get-PodeRelativePath -Path $Certificate -JoinRoot -Resolve
$keyPath = Get-PodeRelativePath -Path $Key -JoinRoot -Resolve
# pem's kinda work in .NET3/.NET5
if ([version]$PSVersionTable.PSVersion -ge [version]'7.0.0') {
$cert = [X509Certificates.X509Certificate2]::new($certPath)
$keyText = [System.IO.File]::ReadAllText($keyPath)
$rsa = [RSA]::Create()
# .NET5
if ([version]$PSVersionTable.PSVersion -ge [version]'7.1.0') {
if ([string]::IsNullOrWhiteSpace($Password)) {
$rsa.ImportFromPem($keyText)
}
else {
$rsa.ImportFromEncryptedPem($keyText, $Password)
}
}
# .NET3
else {
$keyBlocks = $keyText.Split('-', [System.StringSplitOptions]::RemoveEmptyEntries)
$keyBytes = [System.Convert]::FromBase64String($keyBlocks[1])
if ($keyBlocks[0] -ieq 'BEGIN PRIVATE KEY') {
$rsa.ImportPkcs8PrivateKey($keyBytes, [ref]$null)
}
elseif ($keyBlocks[0] -ieq 'BEGIN RSA PRIVATE KEY') {
$rsa.ImportRSAPrivateKey($keyBytes, [ref]$null)
}
elseif ($keyBlocks[0] -ieq 'BEGIN ENCRYPTED PRIVATE KEY') {
$rsa.ImportEncryptedPkcs8PrivateKey($Password, $keyBytes, [ref]$null)
}
}
$cert = [X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($cert, $rsa)
$cert = [X509Certificates.X509Certificate2]::new($cert.Export([X509Certificates.X509ContentType]::Pkcs12))
}
# for everything else, there's the openssl way
else {
$tempFile = Join-Path (Split-Path -Parent -Path $certPath) 'temp.pfx'
try {
if ([string]::IsNullOrWhiteSpace($Password)) {
$Password = [string]::Empty
}
$result = openssl pkcs12 -inkey $keyPath -in $certPath -export -passin pass:$Password -password pass:$Password -out $tempFile
if (!$?) {
throw "Failed to create openssl cert: $($result)"
}
$cert = [X509Certificates.X509Certificate2]::new($tempFile, $Password)
}
finally {
$null = Remove-Item $tempFile -Force
}
}
return $cert
}
function Find-PodeCertificateInCertStore {
param(
[Parameter(Mandatory = $true)]
[X509Certificates.X509FindType]
$FindType,
[Parameter(Mandatory = $true)]
[string]
$Query,
[Parameter(Mandatory = $true)]
[X509Certificates.StoreName]
$StoreName,
[Parameter(Mandatory = $true)]
[X509Certificates.StoreLocation]
$StoreLocation
)
# fail if not windows
if (!(Test-PodeIsWindows)) {
throw 'Certificate Thumbprints/Name are only supported on Windows'
}
# open the currentuser\my store
$x509store = [X509Certificates.X509Store]::new($StoreName, $StoreLocation)
try {
# attempt to find the cert
$x509store.Open([X509Certificates.OpenFlags]::ReadOnly)
$x509certs = $x509store.Certificates.Find($FindType, $Query, $false)
}
finally {
# close the store!
if ($null -ne $x509store) {
Close-PodeDisposable -Disposable $x509store -Close
}
}
# fail if no cert found for query
if (($null -eq $x509certs) -or ($x509certs.Count -eq 0)) {
throw "No certificate could be found in $($StoreLocation)\$($StoreName) for '$($Query)'"
}
return ([X509Certificates.X509Certificate2]($x509certs[0]))
}
function Get-PodeCertificateByThumbprint {
param(
[Parameter(Mandatory = $true)]
[string]
$Thumbprint,
[Parameter(Mandatory = $true)]
[X509Certificates.StoreName]
$StoreName,
[Parameter(Mandatory = $true)]
[X509Certificates.StoreLocation]
$StoreLocation
)
return Find-PodeCertificateInCertStore `
-FindType ([X509Certificates.X509FindType]::FindByThumbprint) `
-Query $Thumbprint `
-StoreName $StoreName `
-StoreLocation $StoreLocation
}
function Get-PodeCertificateByName {
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[X509Certificates.StoreName]
$StoreName,
[Parameter(Mandatory = $true)]
[X509Certificates.StoreLocation]
$StoreLocation
)
return Find-PodeCertificateInCertStore `
-FindType ([X509Certificates.X509FindType]::FindBySubjectName) `
-Query $Name `
-StoreName $StoreName `
-StoreLocation $StoreLocation
}
function New-PodeSelfSignedCertificate {
$sanBuilder = [X509Certificates.SubjectAlternativeNameBuilder]::new()
$null = $sanBuilder.AddIpAddress([ipaddress]::Loopback)
$null = $sanBuilder.AddIpAddress([ipaddress]::IPv6Loopback)
$null = $sanBuilder.AddDnsName('localhost')
if (![string]::IsNullOrWhiteSpace($PodeContext.Server.ComputerName)) {
$null = $sanBuilder.AddDnsName($PodeContext.Server.ComputerName)
}
$rsa = [RSA]::Create(2048)
$distinguishedName = [X500DistinguishedName]::new('CN=localhost')
$req = [X509Certificates.CertificateRequest]::new(
$distinguishedName,
$rsa,
[HashAlgorithmName]::SHA256,
[RSASignaturePadding]::Pkcs1
)
$flags = (
[X509Certificates.X509KeyUsageFlags]::DataEncipherment -bor
[X509Certificates.X509KeyUsageFlags]::KeyEncipherment -bor
[X509Certificates.X509KeyUsageFlags]::DigitalSignature
)
$null = $req.CertificateExtensions.Add(
[X509Certificates.X509KeyUsageExtension]::new(
$flags,
$false
)
)
$oid = [OidCollection]::new()
$null = $oid.Add([Oid]::new('1.3.6.1.5.5.7.3.1'))
$req.CertificateExtensions.Add(
[X509Certificates.X509EnhancedKeyUsageExtension]::new(
$oid,
$false
)
)
$null = $req.CertificateExtensions.Add($sanBuilder.Build())
$cert = $req.CreateSelfSigned(
[System.DateTimeOffset]::UtcNow.AddDays(-1),
[System.DateTimeOffset]::UtcNow.AddYears(10)
)
if (Test-PodeIsWindows) {
$cert.FriendlyName = 'localhost'
}
$cert = [X509Certificates.X509Certificate2]::new(
$cert.Export([X509Certificates.X509ContentType]::Pfx, 'self-signed'),
'self-signed'
)
return $cert
}
function Protect-PodeContentSecurityKeyword {
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string[]]
$Value,
[switch]
$Append
)
# cache it
if ($Append -and !(Test-PodeIsEmpty $PodeContext.Server.Security.Cache.ContentSecurity[$Name])) {
$Value += @($PodeContext.Server.Security.Cache.ContentSecurity[$Name])
}
$PodeContext.Server.Security.Cache.ContentSecurity[$Name] = $Value
# do nothing if no value
if (($null -eq $Value) -or ($Value.Length -eq 0)) {
return $null
}
# keywords
$Name = $Name.ToLowerInvariant()
$keywords = @(
'none',
'self',
'unsafe-inline',
'unsafe-eval'
)
$schemes = @(
'http',
'https',
'ws',
'wss',
'data',
'file'
)
# build the value
$values = @(foreach ($v in $Value) {
if ($keywords -icontains $v) {
"'$($v.ToLowerInvariant())'"
continue
}
if ($schemes -icontains $v) {
"$($v.ToLowerInvariant()):"
continue
}
$v
})
return "$($Name) $($values -join ' ')"
}
function Protect-PodePermissionsPolicyKeyword {
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string[]]
$Value,
[switch]
$Append
)
# cache it
if ($Append -and !(Test-PodeIsEmpty $PodeContext.Server.Security.Cache.PermissionsPolicy[$Name])) {
if (($Value.Length -eq 0) -or (@($PodeContext.Server.Security.Cache.PermissionsPolicy[$Name])[0] -ine 'none')) {
$Value += @($PodeContext.Server.Security.Cache.PermissionsPolicy[$Name])
}
}
$PodeContext.Server.Security.Cache.PermissionsPolicy[$Name] = $Value
# do nothing if no value
if (($null -eq $Value) -or ($Value.Length -eq 0)) {
return $null
}
# build value
$Name = $Name.ToLowerInvariant()
if ($Value -icontains 'none') {
return "$($Name)=()"
}
$keywords = @(
'self'
)
$values = @(foreach ($v in $Value) {
if ($keywords -icontains $v) {
$v
continue
}
"`"$($v)`""
})
return "$($Name)=($($values -join ' '))"
}
function Start-PodeInternalServer {
param(
[Parameter()]
$Request,
[switch]
$Browse
)
try {
# Check if the running version of Powershell is EOL
Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID))" -ForegroundColor Cyan
$null = Test-PodeVersionPwshEOL -ReportUntested
# setup temp drives for internal dirs
Add-PodePSInbuiltDrive
# setup inbuilt scoped vars
Add-PodeScopedVariablesInbuilt
# create the shared runspace state
New-PodeRunspaceState
# if iis, setup global middleware to validate token
Initialize-PodeIISMiddleware
# load any secret vaults
Import-PodeSecretVaultsIntoRegistry
# get the server's script and invoke it - to set up routes, timers, middleware, etc
$_script = $PodeContext.Server.Logic
if (Test-PodePath -Path $PodeContext.Server.LogicPath -NoStatus) {
$_script = Convert-PodeFileToScriptBlock -FilePath $PodeContext.Server.LogicPath
}
$_script = Convert-PodeScopedVariables -ScriptBlock $_script -Exclude Session, Using
$null = Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure -Splat
#Validate OpenAPI definitions
Test-PodeOADefinitionInternal
# load any modules/snapins
Import-PodeSnapinsIntoRunspaceState
Import-PodeModulesIntoRunspaceState
# load any functions
Import-PodeFunctionsIntoRunspaceState -ScriptBlock $_script
# run start event hooks
Invoke-PodeEvent -Type Start
# start timer for task housekeeping
Start-PodeTaskHousekeeper
# start the cache housekeeper
Start-PodeCacheHousekeeper
# create timer/schedules for auto-restarting
New-PodeAutoRestartServer
# start the runspace pools for web, schedules, etc
New-PodeRunspacePools
Open-PodeRunspacePools
if (!$PodeContext.Server.IsServerless) {
# start runspace for loggers
Start-PodeLoggingRunspace
# start runspace for timers
Start-PodeTimerRunspace
# start runspace for schedules
Start-PodeScheduleRunspace
# start runspace for gui
Start-PodeGuiRunspace
# start runspace for websockets
Start-PodeWebSocketRunspace
# start runspace for file watchers
Start-PodeFileWatcherRunspace
}
# start the appropriate server
$endpoints = @()
# - service
if ($PodeContext.Server.IsService) {
Start-PodeServiceServer
}
# - serverless
elseif ($PodeContext.Server.IsServerless) {
switch ($PodeContext.Server.ServerlessType.ToUpperInvariant()) {
'AZUREFUNCTIONS' {
Start-PodeAzFuncServer -Data $Request
}
'AWSLAMBDA' {
Start-PodeAwsLambdaServer -Data $Request
}
}
}
# - normal
else {
# start each server type
foreach ($_type in $PodeContext.Server.Types) {
switch ($_type.ToUpperInvariant()) {
'SMTP' {
$endpoints += (Start-PodeSmtpServer)
}
'TCP' {
$endpoints += (Start-PodeTcpServer)
}
'HTTP' {
$endpoints += (Start-PodeWebServer -Browse:$Browse)
}
}
}
# now go back through, and wait for each server type's runspace pool to be ready
foreach ($pool in ($endpoints.Pool | Sort-Object -Unique)) {
$start = [datetime]::Now
Write-Verbose "Waiting for the $($pool) RunspacePool to be Ready"
# wait
while ($PodeContext.RunspacePools[$pool].State -ieq 'Waiting') {
Start-Sleep -Milliseconds 100
}
Write-Verbose "$($pool) RunspacePool $($PodeContext.RunspacePools[$pool].State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]"
# errored?
if ($PodeContext.RunspacePools[$pool].State -ieq 'error') {
throw "$($pool) RunspacePool failed to load"
}
}
}
# set the start time of the server (start and after restart)
$PodeContext.Metrics.Server.StartTime = [datetime]::UtcNow
# run running event hooks
Invoke-PodeEvent -Type Running
# state what endpoints are being listened on
if ($endpoints.Length -gt 0) {
Write-PodeHost "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads.General) thread(s)]:" -ForegroundColor Yellow
$endpoints | ForEach-Object {
$flags = @()
if ($_.DualMode) {
$flags += 'DualMode'
}
if ($flags.Length -eq 0) {
$flags = [string]::Empty
}
else {
$flags = "[$($flags -join ',')]"
}
Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow
}
# state the OpenAPI endpoints for each definition
foreach ($key in $PodeContext.Server.OpenAPI.Definitions.keys) {
$bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks
if ( $bookmarks) {
Write-PodeHost
if (!$OpenAPIHeader) {
Write-PodeHost 'OpenAPI Info:' -ForegroundColor Yellow
$OpenAPIHeader = $true
}
Write-PodeHost " '$key':" -ForegroundColor Yellow
if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) {
Write-PodeHost ' - Specification:' -ForegroundColor Yellow
foreach ($endpoint in $bookmarks.route.Endpoint) {
Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor Yellow
}
Write-PodeHost ' - Documentation:' -ForegroundColor Yellow
foreach ($endpoint in $bookmarks.route.Endpoint) {
Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor Yellow
}
}
else {
Write-PodeHost ' - Specification:' -ForegroundColor Yellow
$endpoints | ForEach-Object {
$url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl)
Write-PodeHost " . $url" -ForegroundColor Yellow
}
Write-PodeHost ' - Documentation:' -ForegroundColor Yellow
$endpoints | ForEach-Object {
$url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path)
Write-PodeHost " . $url" -ForegroundColor Yellow
}
}
}
}
}
}
catch {
throw $_.Exception
}
}
function Restart-PodeInternalServer {
try {
# 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()
# close all current runspaces
Close-PodeRunspaces -ClosePool
# remove all of the pode temp drives
Remove-PodePSDrives
# clear-up modules
$PodeContext.Server.Modules.Clear()
# clear up timers, schedules and loggers
$PodeContext.Server.Routes | Clear-PodeHashtableInnerKeys
$PodeContext.Server.Handlers | Clear-PodeHashtableInnerKeys
$PodeContext.Server.Events | Clear-PodeHashtableInnerKeys
if ($null -ne $PodeContext.Server.Verbs) {
$PodeContext.Server.Verbs.Clear()
}
$PodeContext.Server.Views.Clear()
$PodeContext.Timers.Items.Clear()
$PodeContext.Server.Logging.Types.Clear()
# clear schedules
$PodeContext.Schedules.Items.Clear()
$PodeContext.Schedules.Processes.Clear()
# clear tasks
$PodeContext.Tasks.Items.Clear()
$PodeContext.Tasks.Results.Clear()
# clear file watchers
$PodeContext.Fim.Items.Clear()
# auto-importers
Reset-PodeAutoImportConfiguration
# clear middle/endware
$PodeContext.Server.Middleware = @()
$PodeContext.Server.Endware = @()
# clear body parsers
$PodeContext.Server.BodyParsers.Clear()
# clear security headers
$PodeContext.Server.Security.Headers.Clear()
$PodeContext.Server.Security.Cache | Clear-PodeHashtableInnerKeys
# clear endpoints
$PodeContext.Server.Endpoints.Clear()
$PodeContext.Server.EndpointsMap.Clear()
# clear openapi
$PodeContext.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $PodeContext.Server.Configuration.Web.OpenApi.DefaultDefinitionTag
# clear the sockets
$PodeContext.Server.Signals.Enabled = $false
$PodeContext.Server.Signals.Listener = $null
$PodeContext.Server.Http.Listener = $null
$PodeContext.Listeners = @()
$PodeContext.Receivers = @()
$PodeContext.Watchers = @()
# set view engine back to default
$PodeContext.Server.ViewEngine = @{
Type = 'html'
Extension = 'html'
ScriptBlock = $null
UsingVariables = $null
IsDynamic = $false
}
# clear up cookie sessions
$PodeContext.Server.Sessions.Clear()
# clear up authentication methods
$PodeContext.Server.Authentications.Methods.Clear()
$PodeContext.Server.Authorisations.Methods.Clear()
# clear up shared state
$PodeContext.Server.State.Clear()
# clear scoped variables
$PodeContext.Server.ScopedVariables.Clear()
# clear cache
$PodeContext.Server.Cache.Items.Clear()
$PodeContext.Server.Cache.Storage.Clear()
# clear up secret vaults/cache
Unregister-PodeSecretVaults -ThrowError
$PodeContext.Server.Secrets.Vaults.Clear()
$PodeContext.Server.Secrets.Keys.Clear()
# dispose mutex/semaphores
Clear-PodeLockables
Clear-PodeMutexes
Clear-PodeSemaphores
# clear up output
$PodeContext.Server.Output.Variables.Clear()
# reset type if smtp/tcp
$PodeContext.Server.Types = @()
# recreate the session tokens
Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation
$PodeContext.Tokens.Cancellation = New-Object System.Threading.CancellationTokenSource
Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart
$PodeContext.Tokens.Restart = New-Object System.Threading.CancellationTokenSource
# reload the configuration
$PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext
# done message
Write-PodeHost ' Done' -ForegroundColor Green
# restart the server
$PodeContext.Metrics.Server.RestartCount++
Start-PodeInternalServer
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
}
function Test-PodeServerKeepOpen {
# if we have any timers/schedules/fim - keep open
if ((Test-PodeTimersExist) -or (Test-PodeSchedulesExist) -or (Test-PodeFileWatchersExist)) {
return $true
}
# if not a service, and not any type/serverless - close server
if (!$PodeContext.Server.IsService -and (($PodeContext.Server.Types.Length -eq 0) -or $PodeContext.Server.IsServerless)) {
return $false
}
# keep server open
return $true
}
function Start-PodeAzFuncServer {
param(
[Parameter(Mandatory = $true)]
$Data
)
# setup any inbuilt middleware that works for azure functions
$inbuilt_middleware = @(
(Get-PodeSecurityMiddleware),
(Get-PodePublicMiddleware),
(Get-PodeRouteValidateMiddleware),
(Get-PodeBodyMiddleware),
(Get-PodeCookieMiddleware)
)
$PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)
try {
try {
# get the request
$request = $Data.Request
# setup the response
$response = New-Object -TypeName HttpResponseContext
$response.StatusCode = 200
$response.Headers = @{}
# reset event data
$global:WebEvent = @{
OnEnd = @()
Auth = @{}
Response = $response
Request = $request
Lockable = $PodeContext.Threading.Lockables.Global
Path = [string]::Empty
Method = $request.Method.ToLowerInvariant()
Query = $request.Query
Endpoint = @{
Protocol = ($request.Url -split '://')[0]
Address = $null
Name = $null
}
ContentType = $null
ErrorType = $null
Cookies = @{}
PendingCookies = @{}
Parameters = $null
Data = $null
Files = $null
Streamed = $false
Route = $null
StaticContent = $null
Timestamp = [datetime]::UtcNow
TransferEncoding = $null
AcceptEncoding = $null
Ranges = $null
Metadata = @{}
}
$WebEvent.Endpoint.Address = ((Get-PodeHeader -Name 'host') -split ':')[0]
$WebEvent.ContentType = (Get-PodeHeader -Name 'content-type')
# set the path, using static content query parameter if passed
if (![string]::IsNullOrWhiteSpace($request.Query['static-file'])) {
$WebEvent.Path = $request.Query['static-file']
}
else {
$funcName = $Data.sys.MethodName
if ([string]::IsNullOrWhiteSpace($funcName)) {
$funcName = $Data.FunctionName
}
$WebEvent.Path = "/api/$($funcName)"
}
$WebEvent.Path = [System.Web.HttpUtility]::UrlDecode($WebEvent.Path)
# set pode in server response header
Set-PodeServerHeader -Type 'Kestrel'
# invoke global and route middleware
if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
# invoke the route
if ($null -ne $WebEvent.StaticContent) {
$fileBrowser = $WebEvent.Route.FileBrowser
if ($WebEvent.StaticContent.IsDownload) {
Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser
}
elseif ($WebEvent.StaticContent.RedirectToDefault) {
$file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source)
Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)"
}
else {
$cachable = $WebEvent.StaticContent.IsCachable
Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable -FileBrowser:$fileBrowser
}
}
else {
$null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
}
}
}
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Set-PodeResponseStatus -Code 500 -Exception $_
}
finally {
Update-PodeServerRequestMetrics -WebEvent $WebEvent
}
# invoke endware specifc to the current web event
$_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
Invoke-PodeEndware -Endware $_endware
# close and send the response
Push-OutputBinding -Name Response -Value $response
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
}
function Start-PodeAwsLambdaServer {
param(
[Parameter(Mandatory = $true)]
$Data
)
# setup any inbuilt middleware that works for aws lambda
$inbuilt_middleware = @(
(Get-PodeSecurityMiddleware),
(Get-PodePublicMiddleware),
(Get-PodeRouteValidateMiddleware),
(Get-PodeBodyMiddleware),
(Get-PodeCookieMiddleware)
)
$PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)
try {
try {
# get the request
$request = $Data
# setup the response
$response = @{
StatusCode = 200
Headers = @{}
Body = [string]::Empty
}
# reset event data
$global:WebEvent = @{
OnEnd = @()
Auth = @{}
Response = $response
Request = $request
Lockable = $PodeContext.Threading.Lockables.Global
Path = [System.Web.HttpUtility]::UrlDecode($request.path)
Method = $request.httpMethod.ToLowerInvariant()
Query = $request.queryStringParameters
Endpoint = @{
Protocol = $null
Address = $null
Name = $null
}
ContentType = $null
ErrorType = $null
Cookies = @{}
PendingCookies = @{}
Parameters = $null
Data = $null
Files = $null
Streamed = $false
Route = $null
StaticContent = $null
Timestamp = [datetime]::UtcNow
TransferEncoding = $null
AcceptEncoding = $null
Ranges = $null
Metadata = @{}
}
$WebEvent.Endpoint.Protocol = (Get-PodeHeader -Name 'X-Forwarded-Proto')
$WebEvent.Endpoint.Address = ((Get-PodeHeader -Name 'Host') -split ':')[0]
$WebEvent.ContentType = (Get-PodeHeader -Name 'Content-Type')
# set pode in server response header
Set-PodeServerHeader -Type 'Lambda'
# invoke global and route middleware
if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) {
# invoke the route
if ($null -ne $WebEvent.StaticContent) {
$fileBrowser = $WebEvent.Route.FileBrowser
if ($WebEvent.StaticContent.IsDownload) {
Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser
}
elseif ($WebEvent.StaticContent.RedirectToDefault) {
$file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source)
Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)"
}
else {
$cachable = $WebEvent.StaticContent.IsCachable
Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge `
-Cache:$cachable -FileBrowser:$fileBrowser
}
}
else {
$null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat
}
}
}
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Set-PodeResponseStatus -Code 500 -Exception $_
}
finally {
Update-PodeServerRequestMetrics -WebEvent $WebEvent
}
# invoke endware specifc to the current web event
$_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
Invoke-PodeEndware -Endware $_endware
# close and send the response
if (![string]::IsNullOrWhiteSpace($response.ContentType)) {
Set-PodeHeader -Name 'Content-Type' -Value $response.ContentType
}
return (@{
'statusCode' = $response.StatusCode
'headers' = $response.Headers
'body' = $response.Body
} | ConvertTo-Json -Depth 10 -Compress)
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
}
function Start-PodeServiceServer {
# ensure we have service handlers
if (Test-PodeIsEmpty (Get-PodeHandler -Type Service)) {
throw 'No Service handlers have been defined'
}
# state we're running
Write-PodeHost "Server looping every $($PodeContext.Server.Interval)secs" -ForegroundColor Yellow
# script for the looping server
$serverScript = {
try {
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
# the event object
$ServiceEvent = @{
Lockable = $PodeContext.Threading.Lockables.Global
Metadata = @{}
}
# invoke the service handlers
$handlers = Get-PodeHandler -Type Service
foreach ($name in $handlers.Keys) {
$handler = $handlers[$name]
$null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat
}
# sleep before next run
Start-Sleep -Seconds $PodeContext.Server.Interval
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
}
# start the runspace for the server
Add-PodeRunspace -Type Main -ScriptBlock $serverScript
}
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) {
throw 'there is no session available to set on the response'
}
# 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) {
throw 'No session available to calculate data hash'
}
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 = New-Object -TypeName psobject
# 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
}
}
function Invoke-PodePackageScript {
param(
[Parameter()]
[string]
$ActionScript
)
if ([string]::IsNullOrWhiteSpace($ActionScript)) {
return
}
Invoke-Expression -Command $ActionScript
}
function Install-PodeLocalModules {
param(
[Parameter()]
$Modules = $null
)
if ($null -eq $Modules) {
return
}
$psModules = './ps_modules'
# download modules to ps_modules
$Modules.psobject.properties.name | ForEach-Object {
$_name = $_
# get the module version
$_version = $Modules.$_name.version
if ([string]::IsNullOrWhiteSpace($_version)) {
$_version = $Modules.$_name
}
# get the module repository
$_repository = Protect-PodeValue -Value $Modules.$_name.repository -Default 'PSGallery'
try {
# if version is latest, retrieve current
if ($_version -ieq 'latest') {
$_version = [string]((Find-Module $_name -Repository $_repository -ErrorAction Ignore).Version)
}
Write-Host "=> Downloading $($_name)@$($_version) from $($_repository)... " -NoNewline -ForegroundColor Cyan
# if the current version exists, do nothing
if (!(Test-Path ([System.IO.Path]::Combine($psModules, "$($_name)/$($_version)")))) {
# remove other versions
if (Test-Path ([System.IO.Path]::Combine($psModules, "$($_name)"))) {
$null = Remove-Item -Path ([System.IO.Path]::Combine($psModules, "$($_name)")) -Force -Recurse
}
# download the module
$null = Save-Module -Name $_name -RequiredVersion $_version -Repository $_repository -Path $psModules -Force -ErrorAction Stop
}
Write-Host 'Success' -ForegroundColor Green
}
catch {
Write-Host 'Failed' -ForegroundColor Red
throw "Module or version not found on $($_repository): $($_name)@$($_version)"
}
}
}
using namespace Pode
function Start-PodeSmtpServer {
# ensure we have smtp handlers
if (Test-PodeIsEmpty (Get-PodeHandler -Type Smtp)) {
throw 'No SMTP handlers have been defined'
}
# work out which endpoints to listen on
$endpoints = @()
@(Get-PodeEndpoints -Type Smtp) | ForEach-Object {
# get the ip address
$_ip = [string]($_.Address)
$_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1
$_ip = Get-PodeIPAddress $_ip -DualMode:($_.DualMode)
# dual mode?
$addrs = $_ip
if ($_.DualMode) {
$addrs = Resolve-PodeIPDualMode -IP $_ip
}
# the endpoint
$_endpoint = @{
Name = $_.Name
Key = "$($_ip):$($_.Port)"
Address = $addrs
Hostname = $_.HostName
IsIPAddress = $_.IsIPAddress
Port = $_.Port
Certificate = $_.Certificate.Raw
AllowClientCertificate = $_.Certificate.AllowClientCertificate
TlsMode = $_.Certificate.TlsMode
Url = $_.Url
Protocol = $_.Protocol
Type = $_.Type
Pool = $_.Runspace.PoolName
Acknowledge = $_.Tcp.Acknowledge
SslProtocols = $_.Ssl.Protocols
DualMode = $_.DualMode
}
# add endpoint to list
$endpoints += $_endpoint
}
# create the listener
$listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token)
$listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
$listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
$listener.RequestTimeout = $PodeContext.Server.Request.Timeout
$listener.RequestBodySize = $PodeContext.Server.Request.BodySize
try {
# register endpoints on the listener
$endpoints | ForEach-Object {
$socket = [PodeSocket]::new($_.Name, $_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Smtp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode, $_.DualMode)
$socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout
$socket.AcknowledgeMessage = $_.Acknowledge
if (!$_.IsIPAddress) {
$socket.Hostnames.Add($_.HostName)
}
$listener.Add($socket)
}
$listener.Start()
$PodeContext.Listeners += $listener
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Close-PodeDisposable -Disposable $listener
throw $_.Exception
}
# script for listening out of for incoming requests
$listenScript = {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Listener,
[Parameter(Mandatory = $true)]
[int]
$ThreadId
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
# get email
$context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token))
try {
try {
$Request = $context.Request
$Response = $context.Response
$SmtpEvent = @{
Response = $Response
Request = $Request
Lockable = $PodeContext.Threading.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
}
Endpoint = @{
Protocol = $Request.Scheme
Address = $Request.Address
Name = $context.EndpointName
}
Timestamp = [datetime]::UtcNow
Metadata = @{}
}
# stop now if the request has an error
if ($Request.IsAborted) {
throw $Request.Error
}
# convert the ip
$ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint)
# ensure the request ip is allowed
if (!(Test-PodeIPAccess -IP $ip)) {
$Response.WriteLine('554 Your IP address was rejected', $true)
}
# 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]
$null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
}
finally {
$SmtpEvent = $null
Close-PodeDisposable -Disposable $context
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads.General | ForEach-Object {
Add-PodeRunspace -Type Smtp -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
}
# script to keep smtp server listening until cancelled
$waitScript = {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Listener
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
finally {
Close-PodeDisposable -Disposable $Listener
}
}
Add-PodeRunspace -Type Smtp -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile
# state where we're running
return @(foreach ($endpoint in $endpoints) {
@{
Url = $endpoint.Url
Pool = $endpoint.Pool
DualMode = $endpoint.DualMode
}
})
}
function Read-PodeStreamToEnd {
param(
[Parameter()]
$Stream,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
if ($null -eq $Stream) {
return [string]::Empty
}
return (Use-PodeStream -Stream ([System.IO.StreamReader]::new($Stream, $Encoding)) {
return $args[0].ReadToEnd()
})
}
function Read-PodeByteLineFromByteArray {
param(
[Parameter(Mandatory = $true)]
[byte[]]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8,
[Parameter()]
[int]
$StartIndex = 0,
[switch]
$IncludeNewLine
)
$nlBytes = Get-PodeNewLineBytes -Encoding $Encoding
# attempt to find \n
$index = [array]::IndexOf($Bytes, $nlBytes.NewLine, $StartIndex)
$fIndex = $index
# if not including new line, remove any trailing \r and \n
if (!$IncludeNewLine) {
$fIndex--
if ($Bytes[$fIndex] -eq $nlBytes.Return) {
$fIndex--
}
}
# grab the portion of the bytes array - which is our line
return @{
Bytes = $Bytes[$StartIndex..$fIndex]
StartIndex = $StartIndex
EndIndex = $index
}
}
function Get-PodeByteLinesFromByteArray {
param(
[Parameter(Mandatory = $true)]
[byte[]]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8,
[switch]
$IncludeNewLine
)
# lines
$lines = @()
$nlBytes = Get-PodeNewLineBytes -Encoding $Encoding
# attempt to find \n
$index = 0
while (($nextIndex = [array]::IndexOf($Bytes, $nlBytes.NewLine, $index)) -gt 0) {
$fIndex = $nextIndex
# if not including new line, remove any trailing \r and \n
if (!$IncludeNewLine) {
$fIndex--
if ($Bytes[$fIndex] -eq $nlBytes.Return) {
$fIndex--
}
}
# add the line, and get the next one
$lines += , $Bytes[$index..$fIndex]
$index = $nextIndex + 1
}
return $lines
}
function ConvertFrom-PodeStreamToBytes {
param(
[Parameter(Mandatory = $true)]
$Stream
)
$buffer = [byte[]]::new(64 * 1024)
$ms = New-Object -TypeName System.IO.MemoryStream
$read = 0
while (($read = $Stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$ms.Write($buffer, 0, $read)
}
$ms.Close()
return $ms.ToArray()
}
function ConvertFrom-PodeValueToBytes {
param(
[Parameter()]
[string]
$Value,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
return $Encoding.GetBytes($Value)
}
function ConvertFrom-PodeBytesToString {
param(
[Parameter()]
[byte[]]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8,
[switch]
$RemoveNewLine
)
if (($null -eq $Bytes) -or ($Bytes.Length -eq 0)) {
return $Bytes
}
$value = $Encoding.GetString($Bytes)
if ($RemoveNewLine) {
$value = $value.Trim("`r`n")
}
return $value
}
function Get-PodeNewLineBytes {
param(
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
return @{
NewLine = @($Encoding.GetBytes("`n"))[0]
Return = @($Encoding.GetBytes("`r"))[0]
}
}
function Test-PodeByteArrayIsBoundary {
param(
[Parameter()]
[byte[]]
$Bytes,
[Parameter()]
[string]
$Boundary,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
# if no bytes, return
if ($Bytes.Length -eq 0) {
return $false
}
# if length difference >3, return (ie, 2 offset for `r`n)
if (($Bytes.Length - $Boundary.Length) -gt 3) {
return $false
}
# check if bytes starts with the boundary
return (ConvertFrom-PodeBytesToString $Bytes $Encoding).StartsWith($Boundary)
}
function Remove-PodeNewLineBytesFromArray {
param(
[Parameter()]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
$nlBytes = Get-PodeNewLineBytes -Encoding $Encoding
$length = $Bytes.Length - 1
if ($Bytes[$length] -eq $nlBytes.NewLine) {
$length--
}
if ($Bytes[$length] -eq $nlBytes.Return) {
$length--
}
return $Bytes[0..$length]
}
function Test-PodeTasksExist {
return (($null -ne $PodeContext.Tasks) -and (($PodeContext.Tasks.Enabled) -or ($PodeContext.Tasks.Items.Count -gt 0)))
}
function Start-PodeTaskHousekeeper {
if (!(Test-PodeTasksExist)) {
return
}
Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 30 -ScriptBlock {
if ($PodeContext.Tasks.Results.Count -eq 0) {
return
}
$now = [datetime]::UtcNow
foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) {
$result = $PodeContext.Tasks.Results[$key]
# has it force expired?
if ($result.ExpireTime -lt $now) {
Close-PodeTaskInternal -Result $result
continue
}
# is it completed?
if (!$result.Runspace.Handler.IsCompleted) {
continue
}
# is a completed time set?
if ($null -eq $result.CompletedTime) {
$result.CompletedTime = [datetime]::UtcNow
continue
}
# is it expired by completion? if so, dispose and remove
if ($result.CompletedTime.AddMinutes(1) -lt $now) {
Close-PodeTaskInternal -Result $result
}
}
$result = $null
}
}
function Close-PodeTaskInternal {
param(
[Parameter()]
[hashtable]
$Result
)
if ($null -eq $Result) {
return
}
Close-PodeDisposable -Disposable $Result.Runspace.Pipeline
Close-PodeDisposable -Disposable $Result.Result
$null = $PodeContext.Tasks.Results.Remove($Result.ID)
}
function Invoke-PodeInternalTask {
param(
[Parameter(Mandatory = $true)]
$Task,
[Parameter()]
[hashtable]
$ArgumentList = $null,
[Parameter()]
[int]
$Timeout = -1
)
try {
# setup event param
$parameters = @{
Event = @{
Lockable = $PodeContext.Threading.Lockables.Global
Sender = $Task
Metadata = @{}
}
}
# add any task args
foreach ($key in $Task.Arguments.Keys) {
$parameters[$key] = $Task.Arguments[$key]
}
# add adhoc task invoke args
if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) {
foreach ($key in $ArgumentList.Keys) {
$parameters[$key] = $ArgumentList[$key]
}
}
# add any using variables
if ($null -ne $Task.UsingVariables) {
foreach ($usingVar in $Task.UsingVariables) {
$parameters[$usingVar.NewName] = $usingVar.Value
}
}
$name = New-PodeGuid
$result = [System.Management.Automation.PSDataCollection[psobject]]::new()
$runspace = Add-PodeRunspace -Type Tasks -ScriptBlock (($Task.Script).GetNewClosure()) -Parameters $parameters -OutputStream $result -PassThru
if ($Timeout -ge 0) {
$expireTime = [datetime]::UtcNow.AddSeconds($Timeout)
}
else {
$expireTime = [datetime]::MaxValue
}
$PodeContext.Tasks.Results[$name] = @{
ID = $name
Task = $Task.Name
Runspace = $runspace
Result = $result
CompletedTime = $null
ExpireTime = $expireTime
Timeout = $Timeout
}
return $PodeContext.Tasks.Results[$name]
}
catch {
$_ | Write-PodeErrorLog
}
}
function Wait-PodeNetTaskInternal {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[System.Threading.Tasks.Task]
$Task,
[Parameter()]
[int]
$Timeout = -1
)
# do we need a timeout?
$timeoutTask = $null
if ($Timeout -gt 0) {
$timeoutTask = [System.Threading.Tasks.Task]::Delay($Timeout)
}
# set the check task
if ($null -eq $timeoutTask) {
$checkTask = $Task
}
else {
$checkTask = [System.Threading.Tasks.Task]::WhenAny($Task, $timeoutTask)
}
# is there a cancel token to supply?
if (($null -eq $PodeContext) -or ($null -eq $PodeContext.Tokens.Cancellation.Token)) {
$checkTask.Wait()
}
else {
$checkTask.Wait($PodeContext.Tokens.Cancellation.Token)
}
# if the main task isnt complete, it timed out
if (($null -ne $timeoutTask) -and (!$Task.IsCompleted)) {
throw [System.TimeoutException]::new("Task has timed out after $($Timeout)ms")
}
# only return a value if the result has one
if ($null -ne $Task.Result) {
return $Task.Result
}
}
function Wait-PodeTaskInternal {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[hashtable]
$Task,
[Parameter()]
[int]
$Timeout = -1
)
# timeout needs to be in milliseconds
if ($Timeout -gt 0) {
$Timeout *= 1000
}
# wait for the pipeline to finish processing
$null = $Task.Runspace.Handler.AsyncWaitHandle.WaitOne($Timeout)
# get the current result
$result = $Task.Result.ReadAll()
# close the task
Close-PodeTask -Task $Task
# only return a value if the result has one
if (($null -ne $result) -and ($result.Count -gt 0)) {
return $result
}
}
using namespace Pode
function Start-PodeTcpServer {
# work out which endpoints to listen on
$endpoints = @()
@(Get-PodeEndpoints -Type Tcp) | ForEach-Object {
# get the ip address
$_ip = [string]($_.Address)
$_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1
$_ip = Get-PodeIPAddress $_ip -DualMode:($_.DualMode)
# dual mode?
$addrs = $_ip
if ($_.DualMode) {
$addrs = Resolve-PodeIPDualMode -IP $_ip
}
# the endpoint
$_endpoint = @{
Name = $_.Name
Key = "$($_ip):$($_.Port)"
Address = $addrs
Hostname = $_.HostName
IsIPAddress = $_.IsIPAddress
Port = $_.Port
Certificate = $_.Certificate.Raw
AllowClientCertificate = $_.Certificate.AllowClientCertificate
TlsMode = $_.Certificate.TlsMode
Url = $_.Url
Protocol = $_.Protocol
Type = $_.Type
Pool = $_.Runspace.PoolName
Acknowledge = $_.Tcp.Acknowledge
CRLFMessageEnd = $_.Tcp.CRLFMessageEnd
SslProtocols = $_.Ssl.Protocols
DualMode = $_.DualMode
}
# add endpoint to list
$endpoints += $_endpoint
}
# create the listener
$listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token)
$listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
$listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
$listener.RequestTimeout = $PodeContext.Server.Request.Timeout
$listener.RequestBodySize = $PodeContext.Server.Request.BodySize
try {
# register endpoints on the listener
$endpoints | ForEach-Object {
$socket = [PodeSocket]::new($_.Name, $_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Tcp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode, $_.DualMode)
$socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout
$socket.AcknowledgeMessage = $_.Acknowledge
$socket.CRLFMessageEnd = $_.CRLFMessageEnd
if (!$_.IsIPAddress) {
$socket.Hostnames.Add($_.HostName)
}
$listener.Add($socket)
}
$listener.Start()
$PodeContext.Listeners += $listener
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Close-PodeDisposable -Disposable $listener
throw $_.Exception
}
# script for listening out of for incoming requests
$listenScript = {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Listener,
[Parameter(Mandatory = $true)]
[int]
$ThreadId
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
# get email
$context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token))
try {
try {
$Request = $context.Request
$Response = $context.Response
$TcpEvent = @{
Response = $Response
Request = $Request
Lockable = $PodeContext.Threading.Lockables.Global
Endpoint = @{
Protocol = $Request.Scheme
Address = $Request.Address
Name = $context.EndpointName
}
Parameters = $null
Timestamp = [datetime]::UtcNow
Metadata = @{}
}
# stop now if the request has an error
if ($Request.IsAborted) {
throw $Request.Error
}
# convert the ip
$ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint)
# ensure the request ip is allowed
if (!(Test-PodeIPAccess -IP $ip)) {
$Response.WriteLine('Your IP address was rejected', $true)
Close-PodeTcpClient
continue
}
# has the ip hit the rate limit?
if (!(Test-PodeIPLimit -IP $ip)) {
$Response.WriteLine('Your IP address has hit the rate limit', $true)
Close-PodeTcpClient
continue
}
# deal with tcp call and find the verb, and for the endpoint
if ([string]::IsNullOrEmpty($TcpEvent.Request.Body)) {
continue
}
$verb = Find-PodeVerb -Verb $TcpEvent.Request.Body -EndpointName $TcpEvent.Endpoint.Name
if ($null -eq $verb) {
$verb = Find-PodeVerb -Verb '*' -EndpointName $TcpEvent.Endpoint.Name
}
if ($null -eq $verb) {
continue
}
# set the route parameters
if ($verb.Verb -ine '*') {
$TcpEvent.Parameters = @{}
if ($TcpEvent.Request.Body -imatch "$($verb.Verb)$") {
$TcpEvent.Parameters = $Matches
}
}
# invoke it
if ($null -ne $verb.Logic) {
$null = Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $verb.Arguments -UsingVariables $verb.UsingVariables -Scoped -Splat
}
# is the verb auto-close?
if ($verb.Connection.Close) {
Close-PodeTcpClient
continue
}
# is the verb auto-upgrade to ssl?
if ($verb.Connection.UpgradeToSsl) {
$Request.UpgradeToSSL()
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
}
finally {
$TcpEvent = $null
Close-PodeDisposable -Disposable $context
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads.General | ForEach-Object {
Add-PodeRunspace -Type Tcp -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
}
# script to keep tcp server listening until cancelled
$waitScript = {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Listener
)
try {
while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
finally {
Close-PodeDisposable -Disposable $Listener
}
}
Add-PodeRunspace -Type Tcp -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener } -NoProfile
# state where we're running
return @(foreach ($endpoint in $endpoints) {
@{
Url = $endpoint.Url
Pool = $endpoint.Pool
DualMode = $endpoint.DualMode
}
})
}
function Find-PodeTimer {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name
)
return $PodeContext.Timers.Items[$Name]
}
function Test-PodeTimersExist {
return (($null -ne $PodeContext.Timers) -and (($PodeContext.Timers.Enabled) -or ($PodeContext.Timers.Items.Count -gt 0)))
}
function Start-PodeTimerRunspace {
if (!(Test-PodeTimersExist)) {
return
}
$script = {
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
$_now = [DateTime]::Now
# only run timers that haven't completed, and have a next trigger in the past
$PodeContext.Timers.Items.Values | Where-Object {
!$_.Completed -and ($_.OnStart -or ($_.NextTriggerTime -le $_now))
} | ForEach-Object {
$_.OnStart = $false
$_.Count++
# set last trigger to current next trigger
if ($null -ne $_.NextTriggerTime) {
$_.LastTriggerTime = $_.NextTriggerTime
}
else {
$_.LastTriggerTime = [datetime]::Now
}
# has the timer completed?
if (($_.Limit -gt 0) -and ($_.Count -ge $_.Limit)) {
$_.Completed = $true
}
# next trigger
if (!$_.Completed) {
$_.NextTriggerTime = $_now.AddSeconds($_.Interval)
}
else {
$_.NextTriggerTime = $null
}
# run the timer
Invoke-PodeInternalTimer -Timer $_
}
Start-Sleep -Seconds 1
}
}
Add-PodeRunspace -Type Main -ScriptBlock $script
}
function Invoke-PodeInternalTimer {
param(
[Parameter(Mandatory = $true)]
$Timer,
[Parameter()]
[object[]]
$ArgumentList = $null
)
try {
$global:TimerEvent = @{
Lockable = $PodeContext.Threading.Lockables.Global
Sender = $Timer
Metadata = @{}
}
# add main timer args
$_args = @()
if (($null -ne $Timer.Arguments) -and ($Timer.Arguments.Length -gt 0)) {
$_args += $Timer.Arguments
}
# add adhoc timer invoke args
if (($null -ne $ArgumentList) -and ($ArgumentList.Length -gt 0)) {
$_args += $ArgumentList
}
# invoke timer
$null = Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat
}
catch {
$_ | Write-PodeErrorLog
}
}
function Find-PodeVerb {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Verb,
[Parameter()]
[string]
$EndpointName
)
# if we have a perfect match for the verb, return it
$found = Get-PodeVerbByLiteral -Verbs $PodeContext.Server.Verbs[$Verb] -EndpointName $EndpointName
if ($null -ne $found) {
return $found
}
# otherwise, match regex on the verbs (first match only)
$valid = @(foreach ($key in $PodeContext.Server.Verbs.Keys) {
if (($key -ine '*') -and ($Verb -imatch "^$($key)$")) {
$key
break
}
})[0]
if ($null -eq $valid) {
return $null
}
# is the verb valid for any protocols/endpoints?
$found = Get-PodeVerbByLiteral -Verbs $PodeContext.Server.Verbs[$valid] -EndpointName $EndpointName
if ($null -eq $found) {
return $null
}
return $found
}
function Get-PodeVerbByLiteral {
param(
[Parameter()]
[hashtable[]]
$Verbs,
[Parameter()]
[string]
$EndpointName
)
# if verbs is already null/empty just return
if (($null -eq $Verbs) -or ($Verbs.Length -eq 0)) {
return $null
}
# get the verb
return (Get-PodeVerbsByLiteral -Verbs $Verbs -EndpointName $EndpointName)
}
function Get-PodeVerbsByLiteral {
param(
[Parameter()]
[hashtable[]]
$Verbs,
[Parameter()]
[string]
$EndpointName
)
# see if a verb has the endpoint name
if (![string]::IsNullOrWhiteSpace($EndpointName)) {
foreach ($verb in $Verbs) {
if ($verb.Endpoint.Name -ieq $EndpointName) {
return $verb
}
}
}
# else find first default verb
foreach ($verb in $Verbs) {
if ([string]::IsNullOrWhiteSpace($verb.Endpoint.Name)) {
return $verb
}
}
return $null
}
function Test-PodeVerbAndError {
param(
[Parameter(Mandatory = $true)]
[string]
$Verb,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Address
)
$found = @($PodeContext.Server.Verbs[$Verb])
if (($found | Where-Object { ($_.Endpoint.Protocol -ieq $Protocol) -and ($_.Endpoint.Address -ieq $Address) } | Measure-Object).Count -eq 0) {
return
}
$_url = $Protocol
if (![string]::IsNullOrEmpty($_url) -and ![string]::IsNullOrWhiteSpace($Address)) {
$_url = "$($_url)://$($Address)"
}
elseif (![string]::IsNullOrWhiteSpace($Address)) {
$_url = $Address
}
if ([string]::IsNullOrEmpty($_url)) {
throw "[Verb] $($Verb): Already defined"
}
else {
throw "[Verb] $($Verb): Already defined for $($_url)"
}
}
using namespace Pode
function Test-PodeWebSocketsExist {
return (($null -ne $PodeContext.Server.WebSockets) -and (($PodeContext.Server.WebSockets.Enabled) -or ($PodeContext.Server.WebSockets.Connections.Count -gt 0)))
}
function Find-PodeWebSocket {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.WebSockets.Connections[$Name]
}
function New-PodeWebSocketReceiver {
if ($null -ne $PodeContext.Server.WebSockets.Receiver) {
return
}
try {
$receiver = [PodeReceiver]::new($PodeContext.Tokens.Cancellation.Token)
$receiver.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled)
$receiver.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevels)
$PodeContext.Server.WebSockets.Receiver = $receiver
$PodeContext.Receivers += $receiver
}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
Close-PodeDisposable -Disposable $receiver
throw $_.Exception
}
}
function Start-PodeWebSocketRunspace {
if (!(Test-PodeWebSocketsExist)) {
return
}
# script for listening out of for incoming requests
$receiveScript = {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Receiver,
[Parameter(Mandatory = $true)]
[int]
$ThreadId
)
try {
while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
# get request
$request = (Wait-PodeTask -Task $Receiver.GetWebSocketRequestAsync($PodeContext.Tokens.Cancellation.Token))
try {
try {
$WsEvent = @{
Request = $request
Data = $null
Files = $null
Lockable = $PodeContext.Threading.Lockables.Global
Timestamp = [datetime]::UtcNow
Metadata = @{}
}
# find the websocket definition
$websocket = Find-PodeWebSocket -Name $request.WebSocket.Name
if ($null -eq $websocket.Logic) {
continue
}
# parse data
$result = ConvertFrom-PodeRequestContent -Request $request -ContentType $request.WebSocket.ContentType
$WsEvent.Data = $result.Data
$WsEvent.Files = $result.Files
# invoke websocket script
$null = Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
}
}
finally {
$WsEvent = $null
Close-PodeDisposable -Disposable $request
}
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads.WebSockets | ForEach-Object {
Add-PodeRunspace -Type WebSockets -ScriptBlock $receiveScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver; 'ThreadId' = $_ }
}
# script to keep websocket server receiving until cancelled
$waitScript = {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Receiver
)
try {
while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$_ | Write-PodeErrorLog
$_.Exception | Write-PodeErrorLog -CheckInnerException
throw $_.Exception
}
finally {
Close-PodeDisposable -Disposable $Receiver
}
}
Add-PodeRunspace -Type WebSockets -ScriptBlock $waitScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver } -NoProfile
}
<#
.SYNOPSIS
Create a new type of Access scheme.
.DESCRIPTION
Create a new type of Access scheme, which retrieves the destination/resource's authorisation values which a user needs for access.
.PARAMETER Type
The inbuilt Type of Access this method is for: Role, Group, Scope, User.
.PARAMETER Custom
If supplied, the access Scheme will be flagged as using Custom logic.
.PARAMETER ScriptBlock
An optional ScriptBlock for retrieving authorisation values for the authenticated user, useful if the values reside in an external data store.
This, or Path, is mandatory if using a Custom scheme.
.PARAMETER ArgumentList
An optional array of arguments to supply to the ScriptBlock.
.PARAMETER Path
An optional property Path within the $WebEvent.Auth.User object to extract authorisation values.
The default Path is based on the Access Type, either Roles; Groups; Scopes; or Username.
This, or ScriptBlock, is mandatory if using a Custom scheme.
.EXAMPLE
$role_access = New-PodeAccessScheme -Type Role
.EXAMPLE
$group_access = New-PodeAccessScheme -Type Group -Path 'Metadata.Groups'
.EXAMPLE
$scope_access = New-PodeAccessScheme -Type Scope -Scriptblock { param($user) return @(Get-ExampleAccess -Username $user.Username) }
.EXAMPLE
$custom_access = New-PodeAccessScheme -Custom -Path 'CustomProp'
#>
function New-PodeAccessScheme {
[CmdletBinding(DefaultParameterSetName = 'Type_Path')]
[OutputType([hashtable])]
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Type_Scriptblock')]
[Parameter(Mandatory = $true, ParameterSetName = 'Type_Path')]
[ValidateSet('Role', 'Group', 'Scope', 'User')]
[string]
$Type,
[Parameter(Mandatory = $true, ParameterSetName = 'Custom_Scriptblock')]
[Parameter(Mandatory = $true, ParameterSetName = 'Custom_Path')]
[switch]
$Custom,
[Parameter(Mandatory = $true, ParameterSetName = 'Custom_Scriptblock')]
[Parameter(ParameterSetName = 'Type_Scriptblock')]
[scriptblock]
$ScriptBlock,
[Parameter(ParameterSetName = 'Custom_Scriptblock')]
[Parameter(ParameterSetName = 'Type_Scriptblock')]
[object[]]
$ArgumentList,
[Parameter(Mandatory = $true, ParameterSetName = 'Custom_Path')]
[Parameter(ParameterSetName = 'Type_Path')]
[string]
$Path
)
# for custom access a validator is mandatory
if ($Custom) {
if ([string]::IsNullOrWhiteSpace($Path) -and (Test-PodeIsEmpty $ScriptBlock)) {
throw 'A Path or ScriptBlock is required for sourcing the Custom access values'
}
}
# parse using variables in scriptblock
$scriptObj = $null
if (!(Test-PodeIsEmpty $ScriptBlock)) {
$ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
$scriptObj = @{
Script = $ScriptBlock
UsingVariables = $usingScriptVars
}
}
# default path
if (!$Custom -and (Test-PodeIsEmpty $ScriptBlock) -and [string]::IsNullOrWhiteSpace($Path)) {
if ($Type -ieq 'user') {
$Path = 'Username'
}
else {
$Path = "$($Type)s"
}
}
# return scheme
return @{
Type = $Type
IsCustom = $Custom.IsPresent
ScriptBlock = $scriptObj
Arguments = $ArgumentList
Path = $Path
}
}
<#
.SYNOPSIS
Add an authorisation Access method.
.DESCRIPTION
Add an authorisation Access method for use with Authentication methods, which will authorise access to Routes.
Or they can be used independant of Authentication/Routes for custom scenarios.
.PARAMETER Name
A unique Name for the Access method.
.PARAMETER Description
A short description used by OpenAPI.
.PARAMETER Scheme
The access Scheme to use for retrieving credentials (From New-PodeAccessScheme).
.PARAMETER ScriptBlock
An optional Scriptblock, which can be used to invoke custom validation logic to verify authorisation.
.PARAMETER ArgumentList
An optional array of arguments to supply to the ScriptBlock.
.PARAMETER Match
An optional inbuilt Match method to use when verifying access to a Route, this only applies when no custom Validator scriptblock is supplied. (Default: One)
"One" will allow access if the User has at least one of the Route's access values.
"All" will allow access only if the User has all the values.
"None" will allow access only if the User has none of the values.
.EXAMPLE
New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'Example'
.EXAMPLE
New-PodeAccessScheme -Type Group -Path 'Metadata.Groups' | Add-PodeAccess -Name 'Example' -Match All
.EXAMPLE
New-PodeAccessScheme -Type Scope -Scriptblock { param($user) return @(Get-ExampleAccess -Username $user.Username) } | Add-PodeAccess -Name 'Example'
.EXAMPLE
New-PodeAccessScheme -Custom -Path 'CustomProp' | Add-PodeAccess -Name 'Example' -ScriptBlock { param($userAccess, $customAccess) return $userAccess.Country -ieq $customAccess.Country }
#>
function Add-PodeAccess {
[CmdletBinding(DefaultParameterSetName = 'Match')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[string]
$Description,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Scheme,
[Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
[scriptblock]
$ScriptBlock,
[Parameter(ParameterSetName = 'ScriptBlock')]
[object[]]
$ArgumentList,
[Parameter(ParameterSetName = 'Match')]
[ValidateSet('All', 'One', 'None')]
[string]
$Match = 'One'
)
# check name unique
if (Test-PodeAccessExists -Name $Name) {
throw "Access method already defined: $($Name)"
}
# parse using variables in validator scriptblock
$scriptObj = $null
if (!(Test-PodeIsEmpty $ScriptBlock)) {
$ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
$scriptObj = @{
Script = $ScriptBlock
UsingVariables = $usingScriptVars
}
}
# add access object
$PodeContext.Server.Authorisations.Methods[$Name] = @{
Name = $Name
Description = $Description
Scheme = $Scheme
ScriptBlock = $scriptObj
Arguments = $ArgumentList
Match = $Match.ToLowerInvariant()
Cache = @{}
Merged = $false
Parent = $null
}
}
<#
.SYNOPSIS
Let's you merge multiple Access methods together, into a "single" Access method.
.DESCRIPTION
Let's you merge multiple Access methods together, into a "single" Access method.
You can specify if only One or All of the methods need to pass to allow access, and you can also
merge other merged Access methods for more advanced scenarios.
.PARAMETER Name
A unique Name for the Access method.
.PARAMETER Access
Mutliple Access method Names to be merged.
.PARAMETER Valid
How many of the Access methods are required to be valid, One or All. (Default: One)
.EXAMPLE
Merge-PodeAccess -Name MergedAccess -Access RbacAccess, GbacAccess -Valid All
#>
function Merge-PodeAccess {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string[]]
$Access,
[Parameter()]
[ValidateSet('One', 'All')]
[string]
$Valid = 'One'
)
# ensure the name doesn't already exist
if (Test-PodeAccessExists -Name $Name) {
throw "Access method already defined: $($Name)"
}
# ensure all the access methods exist
foreach ($accName in $Access) {
if (!(Test-PodeAccessExists -Name $accName)) {
throw "Access method does not exist for merging: $($accName)"
}
}
# set parent access
foreach ($accName in $Access) {
$PodeContext.Server.Authorisations.Methods[$accName].Parent = $Name
}
# add auth method to server
$PodeContext.Server.Authorisations.Methods[$Name] = @{
Name = $Name
Access = @($Access)
PassOne = ($Valid -ieq 'one')
Cache = @{}
Merged = $true
Parent = $null
}
}
<#
.SYNOPSIS
Assigns Custom Access value(s) to a Route.
.DESCRIPTION
Assigns Custom Access value(s) to a Route.
.PARAMETER Route
The Route to assign the Custom Access value(s).
.PARAMETER Name
The Name of the Access method the Custom Access value(s) are for.
.PARAMETER Value
The Custom Access Value(s)
.EXAMPLE
Add-PodeRoute -Method Get -Path '/users' -ScriptBlock {} -PassThru | Add-PodeAccessCustom -Name 'Example' -Value @{ Country = 'UK' }
#>
function Add-PodeAccessCustom {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable[]]
$Route,
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[object[]]
$Value
)
begin {
$routes = @()
}
process {
$routes += $Route
}
end {
foreach ($r in $routes) {
if ($r.AccessMeta.Custom.ContainsKey($Name)) {
throw "Route '[$($r.Method)] $($r.Path)' already contains Custom Access with name '$($Name)'"
}
$r.AccessMeta.Custom[$Name] = $Value
}
}
}
<#
.SYNOPSIS
Get one or more Access methods.
.DESCRIPTION
Get one or more Access methods.
.PARAMETER Name
The Name of the Access method. If no name supplied, all methods will be returned.
.EXAMPLE
$methods = Get-PodeAccess
.EXAMPLE
$methods = Get-PodeAccess -Name 'Example'
.EXAMPLE
$methods = Get-PodeAccess -Name 'Example1', 'Example2'
#>
function Get-PodeAccess {
[CmdletBinding()]
[OutputType([object[]])]
param(
[Parameter()]
[string[]]
$Name
)
# return all if no Name
if ([string]::IsNullOrEmpty($Name) -or ($Name.Length -eq 0)) {
return $PodeContext.Server.Authorisations.Methods.Values
}
# return filtered
return @(foreach ($n in $Name) {
$PodeContext.Server.Authorisations.Methods[$n]
})
}
<#
.SYNOPSIS
Test if an Access method exists.
.DESCRIPTION
Test if an Access method exists.
.PARAMETER Name
The Name of the Access method.
.EXAMPLE
if (Test-PodeAccessExists -Name 'Example') { }
#>
function Test-PodeAccessExists {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.Authorisations.Methods.ContainsKey($Name)
}
<#
.SYNOPSIS
Test access values for a Source/Destination against an Access method.
.DESCRIPTION
Test access values for a Source/Destination against an Access method.
.PARAMETER Name
The Name of the Access method to use to verify the access.
.PARAMETER Source
An array of Source access values to pass to the Access method for verification against the Destination access values. (ie: User)
.PARAMETER Destination
An array of Destination access values to pass to the Access method for verification. (ie: Route)
.PARAMETER ArgumentList
An optional array of arguments to supply to the Access Scheme's ScriptBlock for retrieving access values.
.EXAMPLE
if (Test-PodeAccess -Name 'Example' -Source 'Developer' -Destination 'Admin') { }
#>
function Test-PodeAccess {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[object[]]
$Source = $null,
[Parameter()]
[object[]]
$Destination = $null,
[Parameter()]
[object[]]
$ArgumentList = $null
)
# get the access method
$access = $PodeContext.Server.Authorisations.Methods[$Name]
# authorised if no destination values
if (($null -eq $Destination) -or ($Destination.Length -eq 0)) {
return $true
}
# if we have no source values, invoke the scriptblock
if (($null -eq $Source) -or ($Source.Length -eq 0)) {
if ($null -ne $access.Scheme.ScriptBlock) {
$_args = $ArgumentList + @($access.Scheme.Arguments)
$Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat
}
}
# check for custom validator, or use default match logic
if ($null -ne $access.ScriptBlock) {
$_args = @(, $Source) + @(, $Destination) + @($access.Arguments)
return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.ScriptBlock.Script -Arguments $_args -UsingVariables $access.ScriptBlock.UsingVariables -Return -Splat)
}
# not authorised if no source values
if (($access.Match -ne 'none') -and (($null -eq $Source) -or ($Source.Length -eq 0))) {
return $false
}
# one or all match?
else {
switch ($access.Match) {
'one' {
foreach ($item in $Source) {
if ($item -iin $Destination) {
return $true
}
}
}
'all' {
foreach ($item in $Destination) {
if ($item -inotin $Source) {
return $false
}
}
return $true
}
'none' {
foreach ($item in $Source) {
if ($item -iin $Destination) {
return $false
}
}
return $true
}
}
}
# default is not authorised
return $false
}
<#
.SYNOPSIS
Test the currently authenticated User's access against the supplied values.
.DESCRIPTION
Test the currently authenticated User's access against the supplied values. This will be the user in a WebEvent object.
.PARAMETER Name
The Name of the Access method to use to verify the access.
.PARAMETER Value
An array of access values to pass to the Access method for verification against the User.
.EXAMPLE
if (Test-PodeAccessUser -Name 'Example' -Value 'Developer', 'QA') { }
#>
function Test-PodeAccessUser {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[object[]]
$Value
)
# get the access method
$access = $PodeContext.Server.Authorisations.Methods[$Name]
# get the user
$user = $WebEvent.Auth.User
# if there's no scriptblock, try the Path fallback
if ($null -eq $access.Scheme.Scriptblock) {
$userAccess = $user
foreach ($atom in $access.Scheme.Path.Split('.')) {
$userAccess = $userAccess.($atom)
}
}
# otherwise, invoke scriptblock
else {
$_args = @($user) + @($access.Scheme.Arguments)
$userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat
}
# is the user authorised?
return (Test-PodeAccess -Name $Name -Source $userAccess -Destination $Value)
}
<#
.SYNOPSIS
Test the currently authenticated User's access against the access values supplied for the current Route.
.DESCRIPTION
Test the currently authenticated User's access against the access values supplied for the current Route.
.PARAMETER Name
The Name of the Access method to use to verify the access.
.EXAMPLE
if (Test-PodeAccessRoute -Name 'Example') { }
#>
function Test-PodeAccessRoute {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# get the access method
$access = $PodeContext.Server.Authorisations.Methods[$Name]
# get route access values
if ($access.Scheme.IsCustom) {
$routeAccess = $WebEvent.Route.AccessMeta.Custom[$access.Name]
}
else {
$routeAccess = $WebEvent.Route.AccessMeta[$access.Scheme.Type]
}
# if no values then skip
if (($null -eq $routeAccess) -or ($routeAccess.Length -eq 0)) {
return $true
}
# tests values against user
return (Test-PodeAccessUser -Name $Name -Value $routeAccess)
}
<#
.SYNOPSIS
Remove a specific Access method.
.DESCRIPTION
Remove a specific Access method.
.PARAMETER Name
The Name of the Access method.
.EXAMPLE
Remove-PodeAccess -Name 'RBAC'
#>
function Remove-PodeAccess {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name
)
$null = $PodeContext.Server.Authorisations.Methods.Remove($Name)
}
<#
.SYNOPSIS
Clear all defined Access methods.
.DESCRIPTION
Clear all defined Access methods.
.EXAMPLE
Clear-PodeAccess
#>
function Clear-PodeAccess {
[CmdletBinding()]
param()
$PodeContext.Server.Authorisations.Methods.Clear()
}
<#
.SYNOPSIS
Adds an access method as global middleware.
.DESCRIPTION
Adds an access method as global middleware.
.PARAMETER Name
The Name of the Middleware.
.PARAMETER Access
The Name of the Access method to use.
.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.
.EXAMPLE
Add-PodeAccessMiddleware -Name 'GlobalAccess' -Access AccessName
.EXAMPLE
Add-PodeAccessMiddleware -Name 'GlobalAccess' -Access AccessName -Route '/api/*'
#>
function Add-PodeAccessMiddleware {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Access,
[Parameter()]
[string]
$Route
)
if (!(Test-PodeAccessExists -Name $Access)) {
throw "Access method does not exist: $($Access)"
}
Get-PodeAccessMiddlewareScript |
New-PodeMiddleware -ArgumentList @{ Name = $Access } |
Add-PodeMiddleware -Name $Name -Route $Route
}
<#
.SYNOPSIS
Automatically loads access ps1 files
.DESCRIPTION
Automatically loads access ps1 files from either an /access folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeAccess
.EXAMPLE
Use-PodeAccess -Path './my-access'
#>
function Use-PodeAccess {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'access'
}
<#
.SYNOPSIS
Create a new type of Authentication scheme.
.DESCRIPTION
Create a new type of Authentication scheme, which is used to parse the Request for user credentials for validating.
.PARAMETER Basic
If supplied, will use the inbuilt Basic Authentication credentials retriever.
.PARAMETER Encoding
The Encoding to use when decoding the Basic Authorization header.
.PARAMETER HeaderTag
The Tag name used in the Authorization header, ie: Basic, Bearer, Digest.
.PARAMETER Form
If supplied, will use the inbuilt Form Authentication credentials retriever.
.PARAMETER UsernameField
The name of the Username Field in the payload to retrieve the username.
.PARAMETER PasswordField
The name of the Password Field in the payload to retrieve the password.
.PARAMETER Custom
If supplied, will allow you to create a Custom Authentication credentials retriever.
.PARAMETER ScriptBlock
The ScriptBlock is used to parse the request and retieve user credentials and other information.
.PARAMETER ArgumentList
An array of arguments to supply to the Custom Authentication type's ScriptBlock.
.PARAMETER Name
The Name of an Authentication type - such as Basic or NTLM.
.PARAMETER Description
A short description for security scheme. CommonMark syntax MAY be used for rich text representation
.PARAMETER Realm
The name of scope of the protected area.
.PARAMETER Type
The scheme type for custom Authentication types. Default is HTTP.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.
.PARAMETER PostValidator
The PostValidator is a scriptblock that is invoked after user validation.
.PARAMETER Digest
If supplied, will use the inbuilt Digest Authentication credentials retriever.
.PARAMETER Bearer
If supplied, will use the inbuilt Bearer Authentication token retriever.
.PARAMETER ClientCertificate
If supplied, will use the inbuilt Client Certificate Authentication scheme.
.PARAMETER ClientId
The Application ID generated when registering a new app for OAuth2.
.PARAMETER ClientSecret
The Application Secret generated when registering a new app for OAuth2 (this is optional when using PKCE).
.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (default: <host>/oauth2/callback)
.PARAMETER AuthoriseUrl
The OAuth2 Authorisation URL to authenticate a User. This is optional if you're using an InnerScheme like Basic/Form.
.PARAMETER TokenUrl
The OAuth2 Token URL to acquire an access token.
.PARAMETER UserUrl
An optional User profile URL to retrieve a user's details - for OAuth2
.PARAMETER UserUrlMethod
An optional HTTP method to use when calling the User profile URL - for OAuth2 (Default: Post)
.PARAMETER CodeChallengeMethod
An optional method for sending a PKCE code challenge when calling the Authorise URL - for OAuth2 (Default: S256)
.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers - for OAuth2
.PARAMETER OAuth2
If supplied, will use the inbuilt OAuth2 Authentication scheme.
.PARAMETER Scope
An optional array of Scopes for Bearer/OAuth2 Authentication. (These are case-sensitive)
.PARAMETER ApiKey
If supplied, will use the inbuilt API key Authentication scheme.
.PARAMETER Location
The Location to find an API key: Header, Query, or Cookie. (Default: Header)
.PARAMETER LocationName
The Name of the Header, Query, or Cookie to find an API key. (Default depends on Location. Header/Cookie: X-API-KEY, Query: api_key)
.PARAMETER InnerScheme
An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme.
.PARAMETER AsCredential
If supplied, username/password credentials for Basic/Form authentication will instead be supplied as a pscredential object.
.PARAMETER AsJWT
If supplied, the token/key supplied for Bearer/API key authentication will be parsed as a JWT, and the payload supplied instead.
.PARAMETER Secret
An optional Secret, used to sign/verify JWT signatures.
.EXAMPLE
$basic_auth = New-PodeAuthScheme -Basic
.EXAMPLE
$form_auth = New-PodeAuthScheme -Form -UsernameField 'Email'
.EXAMPLE
$custom_auth = New-PodeAuthScheme -Custom -ScriptBlock { /* logic */ }
#>
function New-PodeAuthScheme {
[CmdletBinding(DefaultParameterSetName = 'Basic')]
[OutputType([hashtable])]
param(
[Parameter(ParameterSetName = 'Basic')]
[switch]
$Basic,
[Parameter(ParameterSetName = 'Basic')]
[string]
$Encoding = 'ISO-8859-1',
[Parameter(ParameterSetName = 'Basic')]
[Parameter(ParameterSetName = 'Bearer')]
[Parameter(ParameterSetName = 'Digest')]
[string]
$HeaderTag,
[Parameter(ParameterSetName = 'Form')]
[switch]
$Form,
[Parameter(ParameterSetName = 'Form')]
[string]
$UsernameField = 'username',
[Parameter(ParameterSetName = 'Form')]
[string]
$PasswordField = 'password',
[Parameter(ParameterSetName = 'Custom')]
[switch]
$Custom,
[Parameter(Mandatory = $true, ParameterSetName = 'Custom')]
[ValidateScript({
if (Test-PodeIsEmpty $_) {
throw 'A non-empty ScriptBlock is required for the Custom authentication scheme'
}
return $true
})]
[scriptblock]
$ScriptBlock,
[Parameter(ParameterSetName = 'Custom')]
[hashtable]
$ArgumentList,
[Parameter(ParameterSetName = 'Custom')]
[string]
$Name,
[string]
$Description,
[Parameter()]
[string]
$Realm,
[Parameter(ParameterSetName = 'Custom')]
[ValidateSet('ApiKey', 'Http', 'OAuth2', 'OpenIdConnect')]
[string]
$Type = 'Http',
[Parameter()]
[object[]]
$Middleware,
[Parameter(ParameterSetName = 'Custom')]
[scriptblock]
$PostValidator = $null,
[Parameter(ParameterSetName = 'Digest')]
[switch]
$Digest,
[Parameter(ParameterSetName = 'Bearer')]
[switch]
$Bearer,
[Parameter(ParameterSetName = 'ClientCertificate')]
[switch]
$ClientCertificate,
[Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)]
[string]
$ClientId,
[Parameter(ParameterSetName = 'OAuth2')]
[string]
$ClientSecret,
[Parameter(ParameterSetName = 'OAuth2')]
[string]
$RedirectUrl,
[Parameter(ParameterSetName = 'OAuth2')]
[string]
$AuthoriseUrl,
[Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)]
[string]
$TokenUrl,
[Parameter(ParameterSetName = 'OAuth2')]
[string]
$UserUrl,
[Parameter(ParameterSetName = 'OAuth2')]
[ValidateSet('Get', 'Post')]
[string]
$UserUrlMethod = 'Post',
[Parameter(ParameterSetName = 'OAuth2')]
[ValidateSet('plain', 'S256')]
[string]
$CodeChallengeMethod = 'S256',
[Parameter(ParameterSetName = 'OAuth2')]
[switch]
$UsePKCE,
[Parameter(ParameterSetName = 'OAuth2')]
[switch]
$OAuth2,
[Parameter(ParameterSetName = 'ApiKey')]
[switch]
$ApiKey,
[Parameter(ParameterSetName = 'ApiKey')]
[ValidateSet('Header', 'Query', 'Cookie')]
[string]
$Location = 'Header',
[Parameter(ParameterSetName = 'ApiKey')]
[string]
$LocationName,
[Parameter(ParameterSetName = 'Bearer')]
[Parameter(ParameterSetName = 'OAuth2')]
[string[]]
$Scope,
[Parameter(ValueFromPipeline = $true)]
[hashtable]
$InnerScheme,
[Parameter(ParameterSetName = 'Basic')]
[Parameter(ParameterSetName = 'Form')]
[switch]
$AsCredential,
[Parameter(ParameterSetName = 'Bearer')]
[Parameter(ParameterSetName = 'ApiKey')]
[switch]
$AsJWT,
[Parameter(ParameterSetName = 'Bearer')]
[Parameter(ParameterSetName = 'ApiKey')]
[string]
$Secret
)
# default realm
$_realm = 'User'
# convert any middleware into valid hashtables
$Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)
# configure the auth scheme
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'basic' {
return @{
Name = (Protect-PodeValue -Value $HeaderTag -Default 'Basic')
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
ScriptBlock = @{
Script = (Get-PodeAuthBasicType)
UsingVariables = $null
}
PostValidator = $null
Middleware = $Middleware
InnerScheme = $InnerScheme
Scheme = 'http'
Arguments = @{
Description = $Description
HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Basic')
Encoding = (Protect-PodeValue -Value $Encoding -Default 'ISO-8859-1')
AsCredential = $AsCredential
}
}
}
'clientcertificate' {
return @{
Name = 'Mutual'
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
ScriptBlock = @{
Script = (Get-PodeAuthClientCertificateType)
UsingVariables = $null
}
PostValidator = $null
Middleware = $Middleware
InnerScheme = $InnerScheme
Scheme = 'http'
Arguments = @{}
}
}
'digest' {
return @{
Name = 'Digest'
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
ScriptBlock = @{
Script = (Get-PodeAuthDigestType)
UsingVariables = $null
}
PostValidator = @{
Script = (Get-PodeAuthDigestPostValidator)
UsingVariables = $null
}
Middleware = $Middleware
InnerScheme = $InnerScheme
Scheme = 'http'
Arguments = @{
HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Digest')
}
}
}
'bearer' {
$secretBytes = $null
if (![string]::IsNullOrWhiteSpace($Secret)) {
$secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
}
return @{
Name = 'Bearer'
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
ScriptBlock = @{
Script = (Get-PodeAuthBearerType)
UsingVariables = $null
}
PostValidator = @{
Script = (Get-PodeAuthBearerPostValidator)
UsingVariables = $null
}
Middleware = $Middleware
Scheme = 'http'
InnerScheme = $InnerScheme
Arguments = @{
Description = $Description
HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Bearer')
Scopes = $Scope
AsJWT = $AsJWT
Secret = $secretBytes
}
}
}
'form' {
return @{
Name = 'Form'
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
ScriptBlock = @{
Script = (Get-PodeAuthFormType)
UsingVariables = $null
}
PostValidator = $null
Middleware = $Middleware
InnerScheme = $InnerScheme
Scheme = 'http'
Arguments = @{
Description = $Description
Fields = @{
Username = (Protect-PodeValue -Value $UsernameField -Default 'username')
Password = (Protect-PodeValue -Value $PasswordField -Default 'password')
}
AsCredential = $AsCredential
}
}
}
'oauth2' {
if (($null -ne $InnerScheme) -and ($InnerScheme.Name -inotin @('basic', 'form'))) {
throw "OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: $($InnerScheme.Name)"
}
if (($null -eq $InnerScheme) -and [string]::IsNullOrWhiteSpace($AuthoriseUrl)) {
throw 'OAuth2 requires an Authorise URL to be supplied'
}
if ($UsePKCE -and !(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use OAuth2 with PKCE'
}
if (!$UsePKCE -and [string]::IsNullOrEmpty($ClientSecret)) {
throw 'OAuth2 requires a Client Secret when not using PKCE'
}
return @{
Name = 'OAuth2'
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
ScriptBlock = @{
Script = (Get-PodeAuthOAuth2Type)
UsingVariables = $null
}
PostValidator = $null
Middleware = $Middleware
Scheme = 'oauth2'
InnerScheme = $InnerScheme
Arguments = @{
Description = $Description
Scopes = $Scope
PKCE = @{
Enabled = $UsePKCE
CodeChallenge = @{
Method = $CodeChallengeMethod
}
}
Client = @{
ID = $ClientId
Secret = $ClientSecret
}
Urls = @{
Redirect = $RedirectUrl
Authorise = $AuthoriseUrl
Token = $TokenUrl
User = @{
Url = $UserUrl
Method = (Protect-PodeValue -Value $UserUrlMethod -Default 'Post')
}
}
}
}
}
'apikey' {
# set default location name
if ([string]::IsNullOrWhiteSpace($LocationName)) {
$LocationName = (@{
Header = 'X-API-KEY'
Query = 'api_key'
Cookie = 'X-API-KEY'
})[$Location]
}
$secretBytes = $null
if (![string]::IsNullOrWhiteSpace($Secret)) {
$secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret)
}
return @{
Name = 'ApiKey'
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
ScriptBlock = @{
Script = (Get-PodeAuthApiKeyType)
UsingVariables = $null
}
PostValidator = $null
Middleware = $Middleware
InnerScheme = $InnerScheme
Scheme = 'apiKey'
Arguments = @{
Description = $Description
Location = $Location
LocationName = $LocationName
AsJWT = $AsJWT
Secret = $secretBytes
}
}
}
'custom' {
$ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
if ($null -ne $PostValidator) {
$PostValidator, $usingPostVars = Convert-PodeScopedVariables -ScriptBlock $PostValidator -PSSession $PSCmdlet.SessionState
}
return @{
Name = $Name
Realm = (Protect-PodeValue -Value $Realm -Default $_realm)
InnerScheme = $InnerScheme
Scheme = $Type.ToLowerInvariant()
ScriptBlock = @{
Script = $ScriptBlock
UsingVariables = $usingScriptVars
}
PostValidator = @{
Script = $PostValidator
UsingVariables = $usingPostVars
}
Middleware = $Middleware
Arguments = $ArgumentList
}
}
}
}
<#
.SYNOPSIS
Create an OAuth2 auth scheme for Azure AD.
.DESCRIPTION
A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Azure AD.
.PARAMETER Tenant
The Directory/Tenant ID from registering a new app (default: common).
.PARAMETER ClientId
The Client ID from registering a new app.
.PARAMETER ClientSecret
The Client Secret from registering a new app (this is optional when using PKCE).
.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (default: <host>/oauth2/callback)
.PARAMETER InnerScheme
An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.
.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers.
.EXAMPLE
New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -ClientSecret 1234.abc
.EXAMPLE
New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -UsePKCE
#>
function New-PodeAuthAzureADScheme {
[CmdletBinding()]
param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]
$Tenant = 'common',
[Parameter(Mandatory = $true)]
[string]
$ClientId,
[Parameter()]
[string]
$ClientSecret,
[Parameter()]
[string]
$RedirectUrl,
[Parameter(ValueFromPipeline = $true)]
[hashtable]
$InnerScheme,
[Parameter()]
[object[]]
$Middleware,
[switch]
$UsePKCE
)
return New-PodeAuthScheme `
-OAuth2 `
-ClientId $ClientId `
-ClientSecret $ClientSecret `
-AuthoriseUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/authorize" `
-TokenUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/token" `
-UserUrl 'https://graph.microsoft.com/oidc/userinfo' `
-RedirectUrl $RedirectUrl `
-InnerScheme $InnerScheme `
-Middleware $Middleware `
-UsePKCE:$UsePKCE
}
<#
.SYNOPSIS
Create an OAuth2 auth scheme for Twitter.
.DESCRIPTION
A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Twitter apps.
.PARAMETER ClientId
The Client ID from registering a new app.
.PARAMETER ClientSecret
The Client Secret from registering a new app (this is optional when using PKCE).
.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (default: <host>/oauth2/callback)
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.
.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers.
.EXAMPLE
New-PodeAuthTwitterScheme -ClientId some_id -ClientSecret 1234.abc
.EXAMPLE
New-PodeAuthTwitterScheme -ClientId some_id -UsePKCE
#>
function New-PodeAuthTwitterScheme {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$ClientId,
[Parameter()]
[string]
$ClientSecret,
[Parameter()]
[string]
$RedirectUrl,
[Parameter()]
[object[]]
$Middleware,
[switch]
$UsePKCE
)
return New-PodeAuthScheme `
-OAuth2 `
-ClientId $ClientId `
-ClientSecret $ClientSecret `
-AuthoriseUrl 'https://twitter.com/i/oauth2/authorize' `
-TokenUrl 'https://api.twitter.com/2/oauth2/token' `
-UserUrl 'https://api.twitter.com/2/users/me' `
-UserUrlMethod 'Get' `
-RedirectUrl $RedirectUrl `
-Middleware $Middleware `
-Scope 'tweet.read', 'users.read' `
-UsePKCE:$UsePKCE
}
<#
.SYNOPSIS
Adds a custom Authentication method for verifying users.
.DESCRIPTION
Adds a custom Authentication method for verifying users.
.PARAMETER Name
A unique Name for the Authentication method.
.PARAMETER Scheme
The authentication Scheme to use for retrieving credentials (From New-PodeAuthScheme).
.PARAMETER ScriptBlock
The ScriptBlock defining logic that retrieves and verifys a user.
.PARAMETER ArgumentList
An array of arguments to supply to the Custom Authentication's ScriptBlock.
.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
.PARAMETER FailureMessage
An override Message to throw when authentication fails.
.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.
.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Main' -ScriptBlock { /* logic */ }
#>
function Add-PodeAuth {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Scheme,
[Parameter(Mandatory = $true)]
[ValidateScript({
if (Test-PodeIsEmpty $_) {
throw 'A non-empty ScriptBlock is required for the authentication method'
}
return $true
})]
[scriptblock]
$ScriptBlock,
[Parameter()]
[object[]]
$ArgumentList,
[Parameter()]
[string]
$FailureUrl,
[Parameter()]
[string]
$FailureMessage,
[Parameter()]
[string]
$SuccessUrl,
[switch]
$Sessionless,
[switch]
$SuccessUseOrigin
)
# ensure the name doesn't already exist
if (Test-PodeAuthExists -Name $Name) {
throw "Authentication method already defined: $($Name)"
}
# ensure the Scheme contains a scriptblock
if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
throw "The supplied '$($Scheme.Name)' Scheme for the '$($Name)' authentication validator requires a valid ScriptBlock"
}
# if we're using sessions, ensure sessions have been setup
if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use session persistent authentication'
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add auth method to server
$PodeContext.Server.Authentications.Methods[$Name] = @{
Name = $Name
Scheme = $Scheme
ScriptBlock = $ScriptBlock
UsingVariables = $usingVars
Arguments = $ArgumentList
Sessionless = $Sessionless.IsPresent
Failure = @{
Url = $FailureUrl
Message = $FailureMessage
}
Success = @{
Url = $SuccessUrl
UseOrigin = $SuccessUseOrigin.IsPresent
}
Cache = @{}
Merged = $false
Parent = $null
}
# if the scheme is oauth2, and there's no redirect, set up a default one
if (($Scheme.Name -ieq 'oauth2') -and ($null -eq $Scheme.InnerScheme) -and [string]::IsNullOrWhiteSpace($Scheme.Arguments.Urls.Redirect)) {
$path = '/oauth2/callback'
$Scheme.Arguments.Urls.Redirect = $path
Add-PodeRoute -Method Get -Path $path -Authentication $Name
}
}
<#
.SYNOPSIS
Lets you merge multiple Authentication methods together, into a "single" Authentication method.
.DESCRIPTION
Lets you merge multiple Authentication methods together, into a "single" Authentication method.
You can specify if only One or All of the methods need to pass to allow access, and you can also
merge other merged Authentication methods for more advanced scenarios.
.PARAMETER Name
A unique Name for the Authentication method.
.PARAMETER Authentication
Multiple Autentication method Names to be merged.
.PARAMETER Valid
How many of the Authentication methods are required to be valid, One or All. (Default: One)
.PARAMETER ScriptBlock
This is mandatory, and only used, when $Valid=All. A scriptblock to merge the mutliple users/headers returned by valid authentications into 1 user/header objects.
This scriptblock will receive a hashtable of all result objects returned from Authentication methods. The key for the hashtable will be the authentication names that passed.
.PARAMETER Default
The Default Authentication method to use as a fallback for Failure URLs and other settings.
.PARAMETER MergeDefault
The Default Authentication method's User details result object to use, when $Valid=All.
.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
This will be used as fallback for the merged Authentication methods if not set on them.
.PARAMETER FailureMessage
An override Message to throw when authentication fails.
This will be used as fallback for the merged Authentication methods if not set on them.
.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
This will be used as fallback for the merged Authentication methods if not set on them.
.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.
This will be used as fallback for the merged Authentication methods if not set on them.
.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
This will be used as fallback for the merged Authentication methods if not set on them.
.EXAMPLE
Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -ScriptBlock { ... }
.EXAMPLE
Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -MergeDefault BasicAuth
.EXAMPLE
Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -FailureUrl 'http://localhost:8080/login'
#>
function Merge-PodeAuth {
[CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[Alias('Auth')]
[string[]]
$Authentication,
[Parameter()]
[ValidateSet('One', 'All')]
[string]
$Valid = 'One',
[Parameter(ParameterSetName = 'ScriptBlock')]
[scriptblock]
$ScriptBlock,
[Parameter()]
[string]
$Default,
[Parameter(ParameterSetName = 'MergeDefault')]
[string]
$MergeDefault,
[Parameter()]
[string]
$FailureUrl,
[Parameter()]
[string]
$FailureMessage,
[Parameter()]
[string]
$SuccessUrl,
[switch]
$Sessionless,
[switch]
$SuccessUseOrigin
)
# ensure the name doesn't already exist
if (Test-PodeAuthExists -Name $Name) {
throw "Authentication method already defined: $($Name)"
}
# ensure all the auth methods exist
foreach ($authName in $Authentication) {
if (!(Test-PodeAuthExists -Name $authName)) {
throw "Authentication method does not exist for merging: $($authName)"
}
}
# ensure the merge default is in the auth list
if (![string]::IsNullOrEmpty($MergeDefault) -and ($MergeDefault -inotin @($Authentication))) {
throw "the MergeDefault Authentication '$($MergeDefault)' is not in the Authentication list supplied"
}
# ensure the default is in the auth list
if (![string]::IsNullOrEmpty($Default) -and ($Default -inotin @($Authentication))) {
throw "the Default Authentication '$($Default)' is not in the Authentication list supplied"
}
# set default
if ([string]::IsNullOrEmpty($Default)) {
$Default = $Authentication[0]
}
# get auth for default
$tmpAuth = $PodeContext.Server.Authentications.Methods[$Default]
# check sessionless from default
if (!$Sessionless) {
$Sessionless = $tmpAuth.Sessionless
}
# if we're using sessions, ensure sessions have been setup
if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use session persistent authentication'
}
# check failure url from default
if ([string]::IsNullOrEmpty($FailureUrl)) {
$FailureUrl = $tmpAuth.Failure.Url
}
# check failure message from default
if ([string]::IsNullOrEmpty($FailureMessage)) {
$FailureMessage = $tmpAuth.Failure.Message
}
# check success url from default
if ([string]::IsNullOrEmpty($SuccessUrl)) {
$SuccessUrl = $tmpAuth.Success.Url
}
# check success use origin from default
if (!$SuccessUseOrigin) {
$SuccessUseOrigin = $tmpAuth.Success.UseOrigin
}
# deal with using vars in scriptblock
if (($Valid -ieq 'all') -and [string]::IsNullOrEmpty($MergeDefault)) {
if ($null -eq $ScriptBlock) {
throw 'A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All'
}
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
}
else {
if ($null -ne $ScriptBlock) {
Write-Warning -Message 'The Scriptblock for merged authentications, when Valid=One, will be ignored'
}
}
# set parent auth
foreach ($authName in $Authentication) {
$PodeContext.Server.Authentications.Methods[$authName].Parent = $Name
}
# add auth method to server
$PodeContext.Server.Authentications.Methods[$Name] = @{
Name = $Name
Authentications = @($Authentication)
PassOne = ($Valid -ieq 'one')
ScriptBlock = @{
Script = $ScriptBlock
UsingVariables = $usingVars
}
Default = $Default
MergeDefault = $MergeDefault
Sessionless = $Sessionless.IsPresent
Failure = @{
Url = $FailureUrl
Message = $FailureMessage
}
Success = @{
Url = $SuccessUrl
UseOrigin = $SuccessUseOrigin.IsPresent
}
Cache = @{}
Merged = $true
Parent = $null
}
}
<#
.SYNOPSIS
Gets an Authentication method.
.DESCRIPTION
Gets an Authentication method.
.PARAMETER Name
The Name of an Authentication method.
.EXAMPLE
Get-PodeAuth -Name 'Main'
#>
function Get-PodeAuth {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# ensure the name exists
if (!(Test-PodeAuthExists -Name $Name)) {
throw "Authentication method not defined: $($Name)"
}
# get auth method
return $PodeContext.Server.Authentications.Methods[$Name]
}
<#
.SYNOPSIS
Test if an Authentication method exists.
.DESCRIPTION
Test if an Authentication method exists.
.PARAMETER Name
The Name of the Authentication method.
.EXAMPLE
if (Test-PodeAuthExists -Name BasicAuth) { ... }
#>
function Test-PodeAuthExists {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.Authentications.Methods.ContainsKey($Name)
}
<#
.SYNOPSIS
Test and invoke an Authentication method to verify a user.
.DESCRIPTION
Test and invoke an Authentication method to verify a user. This will verify a user's credentials on the request.
When testing OAuth2 methods, the first attempt will trigger a redirect to the provider and $false will be returned.
.PARAMETER Name
The Name of the Authentication method.
.PARAMETER IgnoreSession
If supplied, authentication will be re-verified on each call even if a valid session exists on the request.
.EXAMPLE
if (Test-PodeAuth -Name 'BasicAuth') { ... }
.EXAMPLE
if (Test-PodeAuth -Name 'FormAuth' -IgnoreSession) { ... }
#>
function Test-PodeAuth {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[switch]
$IgnoreSession
)
# if the session already has a user/isAuth'd, then skip auth - or allow anon
if (!$IgnoreSession -and (Test-PodeSessionsInUse) -and (Test-PodeAuthUser)) {
return $true
}
try {
$result = Invoke-PodeAuthValidation -Name $Name
}
catch {
$_ | Write-PodeErrorLog
return $false
}
# did the auth force a redirect?
if ($result.Redirected) {
return $false
}
# if auth failed, set appropriate response headers/redirects
if (!$result.Success) {
return $false
}
# successful auth
return $true
}
<#
.SYNOPSIS
Adds the inbuilt Windows AD Authentication method for verifying users.
.DESCRIPTION
Adds the inbuilt Windows AD Authentication method for verifying users.
.PARAMETER Name
A unique Name for the Authentication method.
.PARAMETER Scheme
The Scheme to use for retrieving credentials (From New-PodeAuthScheme).
.PARAMETER Fqdn
A custom FQDN for the DNS of the AD you wish to authenticate against. (Alias: Server)
.PARAMETER Domain
(Unix Only) A custom NetBIOS domain name that is prepended onto usernames that are missing it (<Domain>\<Username>).
.PARAMETER SearchBase
(Unix Only) An optional searchbase to refine the LDAP query. This should be the full distinguished name.
.PARAMETER Groups
An array of Group names to only allow access.
.PARAMETER Users
An array of Usernames to only allow access.
.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
.PARAMETER FailureMessage
An override Message to throw when authentication fails.
.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.
.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.
.PARAMETER NoGroups
If supplied, groups will not be retrieved for the user in AD.
.PARAMETER DirectGroups
If supplied, only a user's direct groups will be retrieved rather than all groups recursively.
.PARAMETER OpenLDAP
If supplied, and on Windows, OpenLDAP will be used instead (this is the default for Linux/MacOS).
.PARAMETER ADModule
If supplied, and on Windows, the ActiveDirectory module will be used instead.
.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
.PARAMETER KeepCredential
If suplied pode will save the AD credential as a PSCredential object in $WebEvent.Auth.User.Credential
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth'
.EXAMPLE
New-PodeAuthScheme -Basic | Add-PodeAuthWindowsAd -Name 'WinAuth' -Groups @('Developers')
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth' -NoGroups
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'UnixAuth' -Server 'testdomain.company.com' -Domain 'testdomain'
#>
function Add-PodeAuthWindowsAd {
[CmdletBinding(DefaultParameterSetName = 'Groups')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Scheme,
[Parameter()]
[Alias('Server')]
[string]
$Fqdn,
[Parameter()]
[string]
$Domain,
[Parameter()]
[string]
$SearchBase,
[Parameter(ParameterSetName = 'Groups')]
[string[]]
$Groups,
[Parameter()]
[string[]]
$Users,
[Parameter()]
[string]
$FailureUrl,
[Parameter()]
[string]
$FailureMessage,
[Parameter()]
[string]
$SuccessUrl,
[Parameter()]
[scriptblock]
$ScriptBlock,
[switch]
$Sessionless,
[Parameter(ParameterSetName = 'NoGroups')]
[switch]
$NoGroups,
[Parameter(ParameterSetName = 'Groups')]
[switch]
$DirectGroups,
[switch]
$OpenLDAP,
[switch]
$ADModule,
[switch]
$SuccessUseOrigin,
[switch]
$KeepCredential
)
# ensure the name doesn't already exist
if (Test-PodeAuthExists -Name $Name) {
throw "Windows AD Authentication method already defined: $($Name)"
}
# ensure the Scheme contains a scriptblock
if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
throw "The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock"
}
# if we're using sessions, ensure sessions have been setup
if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use session persistent authentication'
}
# if AD module set, ensure we're on windows and the module is available, then import/export it
if ($ADModule) {
Import-PodeAuthADModule
}
# set server name if not passed
if ([string]::IsNullOrWhiteSpace($Fqdn)) {
$Fqdn = Get-PodeAuthDomainName
if ([string]::IsNullOrWhiteSpace($Fqdn)) {
throw 'No domain server name has been supplied for Windows AD authentication'
}
}
# set the domain if not passed
if ([string]::IsNullOrWhiteSpace($Domain)) {
$Domain = ($Fqdn -split '\.')[0]
}
# if we have a scriptblock, deal with using vars
if ($null -ne $ScriptBlock) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
}
# add Windows AD auth method to server
$PodeContext.Server.Authentications.Methods[$Name] = @{
Name = $Name
Scheme = $Scheme
ScriptBlock = (Get-PodeAuthWindowsADMethod)
Arguments = @{
Server = $Fqdn
Domain = $Domain
SearchBase = $SearchBase
Users = $Users
Groups = $Groups
NoGroups = $NoGroups
DirectGroups = $DirectGroups
KeepCredential = $KeepCredential
Provider = (Get-PodeAuthADProvider -OpenLDAP:$OpenLDAP -ADModule:$ADModule)
ScriptBlock = @{
Script = $ScriptBlock
UsingVariables = $usingVars
}
}
Sessionless = $Sessionless
Failure = @{
Url = $FailureUrl
Message = $FailureMessage
}
Success = @{
Url = $SuccessUrl
UseOrigin = $SuccessUseOrigin
}
Cache = @{}
Merged = $false
Parent = $null
}
}
<#
.SYNOPSIS
Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests.
.DESCRIPTION
Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests.
.PARAMETER Name
A unique Name for the Authentication method.
.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
.PARAMETER FailureMessage
An override Message to throw when authentication fails.
.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.
.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
.EXAMPLE
Add-PodeAuthSession -Name 'SessionAuth' -FailureUrl '/login'
#>
function Add-PodeAuthSession {
[CmdletBinding(DefaultParameterSetName = 'Groups')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string]
$FailureUrl,
[Parameter()]
[string]
$FailureMessage,
[Parameter()]
[string]
$SuccessUrl,
[Parameter()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[object[]]
$Middleware,
[switch]
$SuccessUseOrigin
)
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions have not been configured'
}
# ensure the name doesn't already exist
if (Test-PodeAuthExists -Name $Name) {
throw "Authentication method already defined: $($Name)"
}
# if we have a scriptblock, deal with using vars
if ($null -ne $ScriptBlock) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
}
# create the auth scheme for getting the session
$scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock {
param($options)
# 401 if sessions not used
if (!(Test-PodeSessionsInUse)) {
Revoke-PodeSession
return @{
Message = 'Sessions are not being used'
Code = 401
}
}
# 401 if no authenticated user
if (!(Test-PodeAuthUser)) {
Revoke-PodeSession
return @{
Message = 'Session not authenticated'
Code = 401
}
}
# return user
return @($WebEvent.Session.Data.Auth)
}
# add a custom auth method to return user back
$method = {
param($user, $options)
$result = @{ User = $user }
# call additional scriptblock if supplied
if ($null -ne $options.ScriptBlock.Script) {
$result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables
}
# return user back
return $result
}
$scheme | Add-PodeAuth `
-Name $Name `
-ScriptBlock $method `
-FailureUrl $FailureUrl `
-FailureMessage $FailureMessage `
-SuccessUrl $SuccessUrl `
-SuccessUseOrigin:$SuccessUseOrigin `
-ArgumentList @{
ScriptBlock = @{
Script = $ScriptBlock
UsingVariables = $usingVars
}
}
}
<#
.SYNOPSIS
Remove a specific Authentication method.
.DESCRIPTION
Remove a specific Authentication method.
.PARAMETER Name
The Name of the Authentication method.
.EXAMPLE
Remove-PodeAuth -Name 'Login'
#>
function Remove-PodeAuth {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name
)
$null = $PodeContext.Server.Authentications.Methods.Remove($Name)
}
<#
.SYNOPSIS
Clear all defined Authentication methods.
.DESCRIPTION
Clear all defined Authentication methods.
.EXAMPLE
Clear-PodeAuth
#>
function Clear-PodeAuth {
[CmdletBinding()]
param()
$PodeContext.Server.Authentications.Methods.Clear()
}
<#
.SYNOPSIS
Adds an authentication method as global middleware.
.DESCRIPTION
Adds an authentication method as global middleware.
.PARAMETER Name
The Name of the Middleware.
.PARAMETER Authentication
The Name of the Authentication method to use.
.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.
.PARAMETER OADefinitionTag
An array of string representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
Use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName
.EXAMPLE
Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName -Route '/api/*'
#>
function Add-PodeAuthMiddleware {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string]
$Route,
[string[]]
$OADefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag
if (!(Test-PodeAuthExists -Name $Authentication)) {
throw "Authentication method does not exist: $($Authentication)"
}
Get-PodeAuthMiddlewareScript |
New-PodeMiddleware -ArgumentList @{ Name = $Authentication } |
Add-PodeMiddleware -Name $Name -Route $Route
Set-PodeOAGlobalAuth -DefinitionTag $DefinitionTag -Name $Authentication -Route $Route
}
<#
.SYNOPSIS
Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS.
.DESCRIPTION
Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS.
.PARAMETER Name
A unique Name for the Authentication method.
.PARAMETER Groups
An array of Group names to only allow access.
.PARAMETER Users
An array of Usernames to only allow access.
.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
.PARAMETER FailureMessage
An override Message to throw when authentication fails.
.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.
.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.
.PARAMETER NoGroups
If supplied, groups will not be retrieved for the user in AD.
.PARAMETER DirectGroups
If supplied, only a user's direct groups will be retrieved rather than all groups recursively.
.PARAMETER ADModule
If supplied, and on Windows, the ActiveDirectory module will be used instead.
.PARAMETER NoLocalCheck
If supplied, Pode will not at attempt to retrieve local User/Group information for the authenticated user.
.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
.EXAMPLE
Add-PodeAuthIIS -Name 'IISAuth'
.EXAMPLE
Add-PodeAuthIIS -Name 'IISAuth' -Groups @('Developers')
.EXAMPLE
Add-PodeAuthIIS -Name 'IISAuth' -NoGroups
#>
function Add-PodeAuthIIS {
[CmdletBinding(DefaultParameterSetName = 'Groups')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(ParameterSetName = 'Groups')]
[string[]]
$Groups,
[Parameter()]
[string[]]
$Users,
[Parameter()]
[string]
$FailureUrl,
[Parameter()]
[string]
$FailureMessage,
[Parameter()]
[string]
$SuccessUrl,
[Parameter()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[object[]]
$Middleware,
[switch]
$Sessionless,
[Parameter(ParameterSetName = 'NoGroups')]
[switch]
$NoGroups,
[Parameter(ParameterSetName = 'Groups')]
[switch]
$DirectGroups,
[switch]
$ADModule,
[switch]
$NoLocalCheck,
[switch]
$SuccessUseOrigin
)
# ensure we're on Windows!
if (!(Test-PodeIsWindows)) {
throw 'IIS Authentication support is for Windows only'
}
# ensure the name doesn't already exist
if (Test-PodeAuthExists -Name $Name) {
throw "IIS Authentication method already defined: $($Name)"
}
# if AD module set, ensure we're on windows and the module is available, then import/export it
if ($ADModule) {
Import-PodeAuthADModule
}
# if we have a scriptblock, deal with using vars
if ($null -ne $ScriptBlock) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
}
# create the auth scheme for getting the token header
$scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock {
param($options)
$header = 'MS-ASPNETCORE-WINAUTHTOKEN'
# fail if no header
if (!(Test-PodeHeader -Name $header)) {
return @{
Message = "No $($header) header found"
Code = 401
}
}
# return the header for validation
$token = Get-PodeHeader -Name $header
return @($token)
}
# add a custom auth method to validate the user
$method = Get-PodeAuthWindowsADIISMethod
$scheme | Add-PodeAuth `
-Name $Name `
-ScriptBlock $method `
-FailureUrl $FailureUrl `
-FailureMessage $FailureMessage `
-SuccessUrl $SuccessUrl `
-Sessionless:$Sessionless `
-SuccessUseOrigin:$SuccessUseOrigin `
-ArgumentList @{
Users = $Users
Groups = $Groups
NoGroups = $NoGroups
DirectGroups = $DirectGroups
Provider = (Get-PodeAuthADProvider -ADModule:$ADModule)
NoLocalCheck = $NoLocalCheck
ScriptBlock = @{
Script = $ScriptBlock
UsingVariables = $usingVars
}
}
}
<#
.SYNOPSIS
Adds the inbuilt User File Authentication method for verifying users.
.DESCRIPTION
Adds the inbuilt User File Authentication method for verifying users.
.PARAMETER Name
A unique Name for the Authentication method.
.PARAMETER Scheme
The Scheme to use for retrieving credentials (From New-PodeAuthScheme).
.PARAMETER FilePath
A path to a users JSON file (Default: ./users.json)
.PARAMETER Groups
An array of Group names to only allow access.
.PARAMETER Users
An array of Usernames to only allow access.
.PARAMETER HmacSecret
An optional secret if the passwords are HMAC SHA256 hashed.
.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
.PARAMETER FailureMessage
An override Message to throw when authentication fails.
.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.
.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.
.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login'
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' -FilePath './custom/path/users.json'
#>
function Add-PodeAuthUserFile {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Scheme,
[Parameter()]
[string]
$FilePath,
[Parameter()]
[string[]]
$Groups,
[Parameter()]
[string[]]
$Users,
[Parameter(ParameterSetName = 'Hmac')]
[string]
$HmacSecret,
[Parameter()]
[string]
$FailureUrl,
[Parameter()]
[string]
$FailureMessage,
[Parameter()]
[string]
$SuccessUrl,
[Parameter()]
[scriptblock]
$ScriptBlock,
[switch]
$Sessionless,
[switch]
$SuccessUseOrigin
)
# ensure the name doesn't already exist
if (Test-PodeAuthExists -Name $Name) {
throw "User File Authentication method already defined: $($Name)"
}
# ensure the Scheme contains a scriptblock
if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
throw "The supplied Scheme for the '$($Name)' User File authentication validator requires a valid ScriptBlock"
}
# if we're using sessions, ensure sessions have been setup
if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use session persistent authentication'
}
# set the file path if not passed
if ([string]::IsNullOrWhiteSpace($FilePath)) {
$FilePath = Join-PodeServerRoot -Folder '.' -FilePath 'users.json'
}
else {
$FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -Resolve
}
# ensure the user file exists
if (!(Test-PodePath -Path $FilePath -NoStatus -FailOnDirectory)) {
throw "The user file does not exist: $($FilePath)"
}
# if we have a scriptblock, deal with using vars
if ($null -ne $ScriptBlock) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
}
# add Windows AD auth method to server
$PodeContext.Server.Authentications.Methods[$Name] = @{
Name = $Name
Scheme = $Scheme
ScriptBlock = (Get-PodeAuthUserFileMethod)
Arguments = @{
FilePath = $FilePath
Users = $Users
Groups = $Groups
HmacSecret = $HmacSecret
ScriptBlock = @{
Script = $ScriptBlock
UsingVariables = $usingVars
}
}
Sessionless = $Sessionless
Failure = @{
Url = $FailureUrl
Message = $FailureMessage
}
Success = @{
Url = $SuccessUrl
UseOrigin = $SuccessUseOrigin
}
Cache = @{}
Merged = $false
Parent = $null
}
}
<#
.SYNOPSIS
Adds the inbuilt Windows Local User Authentication method for verifying users.
.DESCRIPTION
Adds the inbuilt Windows Local User Authentication method for verifying users.
.PARAMETER Name
A unique Name for the Authentication method.
.PARAMETER Scheme
The Scheme to use for retrieving credentials (From New-PodeAuthScheme).
.PARAMETER Groups
An array of Group names to only allow access.
.PARAMETER Users
An array of Usernames to only allow access.
.PARAMETER FailureUrl
The URL to redirect to when authentication fails.
.PARAMETER FailureMessage
An override Message to throw when authentication fails.
.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds when logging in.
.PARAMETER ScriptBlock
Optional ScriptBlock that is passed the found user object for further validation.
.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.
.PARAMETER NoGroups
If supplied, groups will not be retrieved for the user.
.PARAMETER SuccessUseOrigin
If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl.
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth'
.EXAMPLE
New-PodeAuthScheme -Basic | Add-PodeAuthWindowsLocal -Name 'WinAuth' -Groups @('Developers')
.EXAMPLE
New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth' -NoGroups
#>
function Add-PodeAuthWindowsLocal {
[CmdletBinding(DefaultParameterSetName = 'Groups')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Scheme,
[Parameter(ParameterSetName = 'Groups')]
[string[]]
$Groups,
[Parameter()]
[string[]]
$Users,
[Parameter()]
[string]
$FailureUrl,
[Parameter()]
[string]
$FailureMessage,
[Parameter()]
[string]
$SuccessUrl,
[Parameter()]
[scriptblock]
$ScriptBlock,
[switch]
$Sessionless,
[Parameter(ParameterSetName = 'NoGroups')]
[switch]
$NoGroups,
[switch]
$SuccessUseOrigin
)
# ensure we're on Windows!
if (!(Test-PodeIsWindows)) {
throw 'Windows Local Authentication support is for Windows only'
}
# ensure the name doesn't already exist
if (Test-PodeAuthExists -Name $Name) {
throw "Windows Local Authentication method already defined: $($Name)"
}
# ensure the Scheme contains a scriptblock
if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
throw "The supplied Scheme for the '$($Name)' Windows Local authentication validator requires a valid ScriptBlock"
}
# if we're using sessions, ensure sessions have been setup
if (!$Sessionless -and !(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use session persistent authentication'
}
# if we have a scriptblock, deal with using vars
if ($null -ne $ScriptBlock) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
}
# add Windows Local auth method to server
$PodeContext.Server.Authentications.Methods[$Name] = @{
Name = $Name
Scheme = $Scheme
ScriptBlock = (Get-PodeAuthWindowsLocalMethod)
Arguments = @{
Users = $Users
Groups = $Groups
NoGroups = $NoGroups
ScriptBlock = @{
Script = $ScriptBlock
UsingVariables = $usingVars
}
}
Sessionless = $Sessionless
Failure = @{
Url = $FailureUrl
Message = $FailureMessage
}
Success = @{
Url = $SuccessUrl
UseOrigin = $SuccessUseOrigin
}
Cache = @{}
Merged = $false
Parent = $null
}
}
<#
.SYNOPSIS
Convert a Header/Payload into a JWT.
.DESCRIPTION
Convert a Header/Payload hashtable into a JWT, with the option to sign it.
.PARAMETER Header
A Hashtable containing the Header information for the JWT.
.PARAMETER Payload
A Hashtable containing the Payload information for the JWT.
.PARAMETER Secret
An Optional Secret for signing the JWT, should be a string or byte[]. This is mandatory if the Header algorithm isn't "none".
.EXAMPLE
ConvertTo-PodeJwt -Header @{ alg = 'none' } -Payload @{ sub = '123'; name = 'John' }
.EXAMPLE
ConvertTo-PodeJwt -Header @{ alg = 'hs256' } -Payload @{ sub = '123'; name = 'John' } -Secret 'abc'
#>
function ConvertTo-PodeJwt {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[hashtable]
$Header,
[Parameter(Mandatory = $true)]
[hashtable]
$Payload,
[Parameter()]
$Secret = $null
)
# validate header
if ([string]::IsNullOrWhiteSpace($Header.alg)) {
throw 'No algorithm supplied in JWT Header'
}
# convert the header
$header64 = ConvertTo-PodeBase64UrlValue -Value ($Header | ConvertTo-Json -Compress)
# convert the payload
$payload64 = ConvertTo-PodeBase64UrlValue -Value ($Payload | ConvertTo-Json -Compress)
# combine
$jwt = "$($header64).$($payload64)"
# convert secret to bytes
if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) {
$Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret)
}
# make the signature
$sig = New-PodeJwtSignature -Algorithm $Header.alg -Token $jwt -SecretBytes $Secret
# add the signature and return
$jwt += ".$($sig)"
return $jwt
}
<#
.SYNOPSIS
Convert and return the payload of a JWT token.
.DESCRIPTION
Convert and return the payload of a JWT token, verifying the signature by default with support to ignore the signature.
.PARAMETER Token
The JWT token.
.PARAMETER Secret
The Secret, as a string or byte[], to verify the token's signature.
.PARAMETER IgnoreSignature
Skip signature verification, and return the decoded payload.
.EXAMPLE
ConvertFrom-PodeJwt -Token "eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY"
#>
function ConvertFrom-PodeJwt {
[CmdletBinding(DefaultParameterSetName = 'Secret')]
param(
[Parameter(Mandatory = $true)]
[string]
$Token,
[Parameter(ParameterSetName = 'Signed')]
$Secret = $null,
[Parameter(ParameterSetName = 'Ignore')]
[switch]
$IgnoreSignature
)
# get the parts
$parts = ($Token -isplit '\.')
# check number of parts (should be 3)
if ($parts.Length -ne 3) {
throw 'Invalid JWT supplied'
}
# convert to header
$header = ConvertFrom-PodeJwtBase64Value -Value $parts[0]
if ([string]::IsNullOrWhiteSpace($header.alg)) {
throw 'Invalid JWT header algorithm supplied'
}
# convert to payload
$payload = ConvertFrom-PodeJwtBase64Value -Value $parts[1]
# get signature
if ($IgnoreSignature) {
return $payload
}
$signature = $parts[2]
# check "none" signature, and return payload if no signature
$isNoneAlg = ($header.alg -ieq 'none')
if ([string]::IsNullOrWhiteSpace($signature) -and !$isNoneAlg) {
throw "No JWT signature supplied for $($header.alg)"
}
if (![string]::IsNullOrWhiteSpace($signature) -and $isNoneAlg) {
throw 'Expected no JWT signature to be supplied'
}
if ($isNoneAlg -and ($null -ne $Secret) -and ($Secret.Length -gt 0)) {
throw "Expected a signed JWT, 'none' algorithm is not allowed"
}
if ($isNoneAlg) {
return $payload
}
# otherwise, we have an alg for the signature, so we need to validate it
if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) {
$Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret)
}
$sig = "$($parts[0]).$($parts[1])"
$sig = New-PodeJwtSignature -Algorithm $header.alg -Token $sig -SecretBytes $Secret
if ($sig -ne $parts[2]) {
throw 'Invalid JWT signature supplied'
}
# it's valid return the payload!
return $payload
}
<#
.SYNOPSIS
Validates JSON Web Tokens (JWT) claims.
.DESCRIPTION
Validates JSON Web Tokens (JWT) claims. Checks time related claims: 'exp' and 'nbf'.
.PARAMETER Payload
Object containing JWT claims. Some of them are:
- exp (expiration time)
- nbf (not before)
.EXAMPLE
Test-PodeJwt @{exp = 2696258821 }
.EXAMPLE
Test-PodeJwt -Payload @{nbf = 1696258821 }
#>
function Test-PodeJwt {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[pscustomobject]
$Payload
)
$now = [datetime]::UtcNow
$unixStart = [datetime]::new(1970, 1, 1, 0, 0, [DateTimeKind]::Utc)
# validate expiry
if (![string]::IsNullOrWhiteSpace($Payload.exp)) {
if ($now -gt $unixStart.AddSeconds($Payload.exp)) {
throw 'The JWT has expired'
}
}
# validate not-before
if (![string]::IsNullOrWhiteSpace($Payload.nbf)) {
if ($now -lt $unixStart.AddSeconds($Payload.nbf)) {
throw 'The JWT is not yet valid for use'
}
}
}
<#
.SYNOPSIS
Automatically loads auth ps1 files
.DESCRIPTION
Automatically loads auth ps1 files from either a /auth folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeAuth
.EXAMPLE
Use-PodeAuth -Path './my-auth'
#>
function Use-PodeAuth {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'auth'
}
<#
.SYNOPSIS
Builds an OAuth2 scheme using an OpenID Connect Discovery URL.
.DESCRIPTION
Builds an OAuth2 scheme using an OpenID Connect Discovery URL.
.PARAMETER Url
The OpenID Connect Discovery URL, this must end with '/.well-known/openid-configuration' (if missing, it will be automatically appended).
.PARAMETER Scope
A list of optional Scopes to use during the OAuth2 request. (Default: the supported list returned)
.PARAMETER ClientId
The Client ID from registering a new app.
.PARAMETER ClientSecret
The Client Secret from registering a new app (this is optional when using PKCE).
.PARAMETER RedirectUrl
An optional OAuth2 Redirect URL (Default: <host>/oauth2/callback)
.PARAMETER InnerScheme
An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock.
.PARAMETER UsePKCE
If supplied, OAuth2 authentication will use PKCE code verifiers.
.EXAMPLE
ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com/.well-known/openid-configuration' -ClientId some_id -UsePKCE
.EXAMPLE
ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com' -ClientId some_id -UsePKCE
#>
function ConvertFrom-PodeOIDCDiscovery {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Url,
[Parameter()]
[string[]]
$Scope,
[Parameter(Mandatory = $true)]
[string]
$ClientId,
[Parameter()]
[string]
$ClientSecret,
[Parameter()]
[string]
$RedirectUrl,
[Parameter(ValueFromPipeline = $true)]
[hashtable]
$InnerScheme,
[Parameter()]
[object[]]
$Middleware,
[switch]
$UsePKCE
)
# get the discovery doc
if (!$Url.EndsWith('/.well-known/openid-configuration')) {
$Url += '/.well-known/openid-configuration'
}
$config = Invoke-RestMethod -Method Get -Uri $Url
# check it supports the code response_type
if ($config.response_types_supported -inotcontains 'code') {
throw "The OAuth2 provider does not support the 'code' response_type"
}
# can we have an InnerScheme?
if (($null -ne $InnerScheme) -and ($config.grant_types_supported -inotcontains 'password')) {
throw "The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme"
}
# scopes
$scopes = $config.scopes_supported
if (($null -ne $Scope) -and ($Scope.Length -gt 0)) {
$scopes = @(foreach ($s in $Scope) {
if ($s -iin $config.scopes_supported) {
$s
}
})
}
# pkce code challenge method
$codeMethod = 'S256'
if ($config.code_challenge_methods_supported -inotcontains $codeMethod) {
$codeMethod = 'plain'
}
return New-PodeAuthScheme `
-OAuth2 `
-ClientId $ClientId `
-ClientSecret $ClientSecret `
-AuthoriseUrl $config.authorization_endpoint `
-TokenUrl $config.token_endpoint `
-UserUrl $config.userinfo_endpoint `
-RedirectUrl $RedirectUrl `
-Scope $scopes `
-InnerScheme $InnerScheme `
-Middleware $Middleware `
-CodeChallengeMethod $codeMethod `
-UsePKCE:$UsePKCE
}
<#
.SYNOPSIS
Test whether the current WebEvent or Session has an authenticated user.
.DESCRIPTION
Test whether the current WebEvent or Session has an authenticated user. Returns true if there is an authenticated user.
.PARAMETER IgnoreSession
If supplied, only the Auth object in the WebEvent will be checked and the Session will be skipped.
.EXAMPLE
if (Test-PodeAuthUser) { ... }
#>
function Test-PodeAuthUser {
[CmdletBinding()]
param(
[switch]
$IgnoreSession
)
# auth middleware
if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) {
$auth = $WebEvent.Auth
}
# session?
elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) {
$auth = $WebEvent.Session.Data.Auth
}
# null?
if (($null -eq $auth) -or ($null -eq $auth.User)) {
return $false
}
return ($null -ne $auth.User)
}
<#
.SYNOPSIS
Get the authenticated user from the WebEvent or Session.
.DESCRIPTION
Get the authenticated user from the WebEvent or Session. This is similar to calling $Webevent.Auth.User.
.PARAMETER IgnoreSession
If supplied, only the Auth object in the WebEvent will be used and the Session will be skipped.
.EXAMPLE
$user = Get-PodeAuthUser
#>
function Get-PodeAuthUser {
[CmdletBinding()]
param(
[switch]
$IgnoreSession
)
# auth middleware
if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) {
$auth = $WebEvent.Auth
}
# session?
elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) {
$auth = $WebEvent.Session.Data.Auth
}
# null?
if (($null -eq $auth) -or ($null -eq $auth.User)) {
return $null
}
return $auth.User
}
<#
.SYNOPSIS
Exports modules that can be auto-imported by Pode, and into its runspaces.
.DESCRIPTION
Exports modules that can be auto-imported by Pode, and into its runspaces.
.PARAMETER Name
The Name(s) of modules to export.
.EXAMPLE
Export-PodeModule -Name Mod1, Mod2
#>
function Export-PodeModule {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]
$Name
)
$PodeContext.Server.AutoImport.Modules.ExportList += @($Name)
$PodeContext.Server.AutoImport.Modules.ExportList = $PodeContext.Server.AutoImport.Modules.ExportList | Sort-Object -Unique
}
<#
.SYNOPSIS
Exports snapins that can be auto-imported by Pode, and into its runspaces.
.DESCRIPTION
Exports snapins that can be auto-imported by Pode, and into its runspaces.
.PARAMETER Name
The Name(s) of snapins to export.
.EXAMPLE
Export-PodeSnapin -Name Mod1, Mod2
#>
function Export-PodeSnapin {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]
$Name
)
# if non-windows or core, fail
if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) {
throw 'Snapins are only supported on Windows PowerShell'
}
$PodeContext.Server.AutoImport.Snapins.ExportList += @($Name)
$PodeContext.Server.AutoImport.Snapins.ExportList = $PodeContext.Server.AutoImport.Snapins.ExportList | Sort-Object -Unique
}
<#
.SYNOPSIS
Exports functions that can be auto-imported by Pode, and into its runspaces.
.DESCRIPTION
Exports functions that can be auto-imported by Pode, and into its runspaces.
.PARAMETER Name
The Name(s) of functions to export.
.EXAMPLE
Export-PodeFunction -Name Mod1, Mod2
#>
function Export-PodeFunction {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]
$Name
)
$PodeContext.Server.AutoImport.Functions.ExportList += @($Name)
$PodeContext.Server.AutoImport.Functions.ExportList = $PodeContext.Server.AutoImport.Functions.ExportList | Sort-Object -Unique
}
<#
.SYNOPSIS
Exports Secret Vaults that can be auto-imported by Pode, and into its runspaces.
.DESCRIPTION
Exports Secret Vaults that can be auto-imported by Pode, and into its runspaces.
.PARAMETER Name
The Name(s) of a Secret Vault to export.
.PARAMETER Type
The Type of the Secret Vault to import - only option currently is SecretManagement (default: SecretManagement)
.EXAMPLE
Export-PodeSecretVault -Name Vault1, Vault2
#>
function Export-PodeSecretVault {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]
$Name,
[Parameter()]
[ValidateSet('SecretManagement')]
[string]
$Type = 'SecretManagement'
)
$PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList += @($Name)
$PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList = $PodeContext.Server.AutoImport.SecretVaults[$Type].ExportList | Sort-Object -Unique
}
<#
.SYNOPSIS
Return the value of a key from the cache. You can use "$value = $cache:key" as well.
.DESCRIPTION
Return the value of a key from the cache, or returns the value plus metadata such as expiry time if required. You can use "$value = $cache:key" as well.
.PARAMETER Key
The Key to be retrieved.
.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)
.PARAMETER Metadata
If supplied, and if supported by the cache storage, an metadata such as expiry times will also be returned.
.EXAMPLE
$value = Get-PodeCache -Key 'ExampleKey'
.EXAMPLE
$value = Get-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage'
.EXAMPLE
$value = Get-PodeCache -Key 'ExampleKey' -Metadata
.EXAMPLE
$value = $cache:ExampleKey
#>
function Get-PodeCache {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[string]
$Storage = $null,
[switch]
$Metadata
)
# inmem or custom storage?
if ([string]::IsNullOrEmpty($Storage)) {
$Storage = $PodeContext.Server.Cache.DefaultStorage
}
# use inmem cache
if ([string]::IsNullOrEmpty($Storage)) {
return (Get-PodeCacheInternal -Key $Key -Metadata:$Metadata)
}
# used custom storage
if (Test-PodeCacheStorage -Key $Storage) {
return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Key, $Metadata.IsPresent) -Splat -Return)
}
# storage not found!
throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Key)'"
}
<#
.SYNOPSIS
Set (create/update) a key in the cache. You can use "$cache:key = 'value'" as well.
.DESCRIPTION
Set (create/update) a key in the cache, with an optional TTL value. You can use "$cache:key = 'value'" as well.
.PARAMETER Key
The Key to be set.
.PARAMETER InputObject
The value of the key to be set, can be any object type.
.PARAMETER Ttl
An optional TTL value, in seconds. The default is whatever "Get-PodeCacheDefaultTtl" retuns, which will be 3600 seconds when not set.
.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)
.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue'
.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Storage 'ExampleStorage'
.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Ttl 300
.EXAMPLE
Set-PodeCache -Key 'ExampleKey' -InputObject @{ Value = 'ExampleValue' }
.EXAMPLE
@{ Value = 'ExampleValue' } | Set-PodeCache -Key 'ExampleKey'
.EXAMPLE
$cache:ExampleKey = 'ExampleValue'
#>
function Set-PodeCache {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]
$InputObject,
[Parameter()]
[int]
$Ttl = 0,
[Parameter()]
[string]
$Storage = $null
)
# use the global settable default here
if ($Ttl -le 0) {
$Ttl = $PodeContext.Server.Cache.DefaultTtl
}
# inmem or custom storage?
if ([string]::IsNullOrEmpty($Storage)) {
$Storage = $PodeContext.Server.Cache.DefaultStorage
}
# use inmem cache
if ([string]::IsNullOrEmpty($Storage)) {
Set-PodeCacheInternal -Key $Key -InputObject $InputObject -Ttl $Ttl
}
# used custom storage
elseif (Test-PodeCacheStorage -Key $Storage) {
$null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat
}
# storage not found!
else {
throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Key)'"
}
}
<#
.SYNOPSIS
Test if a key exists in the cache.
.DESCRIPTION
Test if a key exists in the cache, and isn't expired.
.PARAMETER Key
The Key to test.
.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)
.EXAMPLE
Test-PodeCache -Key 'ExampleKey'
.EXAMPLE
Test-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage'
#>
function Test-PodeCache {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[string]
$Storage = $null
)
# inmem or custom storage?
if ([string]::IsNullOrEmpty($Storage)) {
$Storage = $PodeContext.Server.Cache.DefaultStorage
}
# use inmem cache
if ([string]::IsNullOrEmpty($Storage)) {
return (Test-PodeCacheInternal -Key $Key)
}
# used custom storage
if (Test-PodeCacheStorage -Key $Storage) {
return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Key) -Splat -Return)
}
# storage not found!
throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Key)' exists"
}
<#
.SYNOPSIS
Remove a key from the cache.
.DESCRIPTION
Remove a key from the cache.
.PARAMETER Key
The Key to be removed.
.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)
.EXAMPLE
Remove-PodeCache -Key 'ExampleKey'
.EXAMPLE
Remove-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage'
#>
function Remove-PodeCache {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[string]
$Storage = $null
)
# inmem or custom storage?
if ([string]::IsNullOrEmpty($Storage)) {
$Storage = $PodeContext.Server.Cache.DefaultStorage
}
# use inmem cache
if ([string]::IsNullOrEmpty($Storage)) {
Remove-PodeCacheInternal -Key $Key
}
# used custom storage
elseif (Test-PodeCacheStorage -Key $Storage) {
$null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Key) -Splat
}
# storage not found!
else {
throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Key)'"
}
}
<#
.SYNOPSIS
Clear all keys from the cache.
.DESCRIPTION
Clear all keys from the cache.
.PARAMETER Storage
An optional cache Storage name. (Default: in-memory)
.EXAMPLE
Clear-PodeCache
.EXAMPLE
Clear-PodeCache -Storage 'ExampleStorage'
#>
function Clear-PodeCache {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Storage = $null
)
# inmem or custom storage?
if ([string]::IsNullOrEmpty($Storage)) {
$Storage = $PodeContext.Server.Cache.DefaultStorage
}
# use inmem cache
if ([string]::IsNullOrEmpty($Storage)) {
Clear-PodeCacheInternal
}
# used custom storage
elseif (Test-PodeCacheStorage -Key $Storage) {
$null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear
}
# storage not found!
else {
throw "Cache storage with name '$($Storage)' not found when attempting to clear cached"
}
}
<#
.SYNOPSIS
Add a cache storage.
.DESCRIPTION
Add a cache storage.
.PARAMETER Name
The Name of the cache storage.
.PARAMETER Get
A Get ScriptBlock, to retrieve a key's value from the cache, or the value plus metadata if required. Supplied parameters: Key, Metadata.
.PARAMETER Set
A Set ScriptBlock, to set/create/update a key's value in the cache. Supplied parameters: Key, Value, TTL.
.PARAMETER Remove
A Remove ScriptBlock, to remove a key from the cache. Supplied parameters: Key.
.PARAMETER Test
A Test ScriptBlock, to test if a key exists in the cache. Supplied parameters: Key.
.PARAMETER Clear
A Clear ScriptBlock, to remove all keys from the cache. Use an empty ScriptBlock if not supported.
.PARAMETER Default
If supplied, this cache storage will be set as the default storage.
.EXAMPLE
Add-PodeCacheStorage -Name 'ExampleStorage' -Get {} -Set {} -Remove {} -Test {} -Clear {}
#>
function Add-PodeCacheStorage {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory = $true)]
[scriptblock]
$Get,
[Parameter(Mandatory = $true)]
[scriptblock]
$Set,
[Parameter(Mandatory = $true)]
[scriptblock]
$Remove,
[Parameter(Mandatory = $true)]
[scriptblock]
$Test,
[Parameter(Mandatory = $true)]
[scriptblock]
$Clear,
[switch]
$Default
)
# test if storage already exists
if (Test-PodeCacheStorage -Name $Name) {
throw "Cache Storage with name '$($Name) already exists"
}
# add cache storage
$PodeContext.Server.Cache.Storage[$Name] = @{
Name = $Name
Get = $Get
Set = $Set
Remove = $Remove
Test = $Test
Clear = $Clear
Default = $Default.IsPresent
}
# is default storage?
if ($Default) {
$PodeContext.Server.Cache.DefaultStorage = $Name
}
}
<#
.SYNOPSIS
Remove a cache storage.
.DESCRIPTION
Remove a cache storage.
.PARAMETER Name
The Name of the cache storage.
.EXAMPLE
Remove-PodeCacheStorage -Name 'ExampleStorage'
#>
function Remove-PodeCacheStorage {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$null = $PodeContext.Server.Cache.Storage.Remove($Name)
}
<#
.SYNOPSIS
Returns a cache storage.
.DESCRIPTION
Returns a cache storage.
.PARAMETER Name
The Name of the cache storage.
.EXAMPLE
$storage = Get-PodeCacheStorage -Name 'ExampleStorage'
#>
function Get-PodeCacheStorage {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.Cache.Storage[$Name]
}
<#
.SYNOPSIS
Test if a cache storage has been added/exists.
.DESCRIPTION
Test if a cache storage has been added/exists.
.PARAMETER Name
The Name of the cache storage.
.EXAMPLE
if (Test-PodeCacheStorage -Name 'ExampleStorage') { }
#>
function Test-PodeCacheStorage {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.Cache.Storage.ContainsKey($Name)
}
<#
.SYNOPSIS
Set a default cache storage.
.DESCRIPTION
Set a default cache storage.
.PARAMETER Name
The Name of the default storage to use for caching.
.EXAMPLE
Set-PodeCacheDefaultStorage -Name 'ExampleStorage'
#>
function Set-PodeCacheDefaultStorage {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$PodeContext.Server.Cache.DefaultStorage = $Name
}
<#
.SYNOPSIS
Returns the current default cache Storage name.
.DESCRIPTION
Returns the current default cache Storage name. Empty/null if one isn't set.
.EXAMPLE
$storageName = Get-PodeCacheDefaultStorage
#>
function Get-PodeCacheDefaultStorage {
[CmdletBinding()]
param()
return $PodeContext.Server.Cache.DefaultStorage
}
<#
.SYNOPSIS
Set a default cache TTL.
.DESCRIPTION
Set a default cache TTL.
.PARAMETER Value
A default TTL value, in seconds, to use when setting cache key expiries.
.EXAMPLE
Set-PodeCacheDefaultTtl -Value 3600
#>
function Set-PodeCacheDefaultTtl {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[int]
$Value
)
if ($Value -le 0) {
return
}
$PodeContext.Server.Cache.DefaultTtl = $Value
}
<#
.SYNOPSIS
Returns the current default cache TTL value.
.DESCRIPTION
Returns the current default cache TTL value. 3600 seconds is the default TTL if not set.
.EXAMPLE
$ttl = Get-PodeCacheDefaultTtl
#>
function Get-PodeCacheDefaultTtl {
[CmdletBinding()]
param()
return $PodeContext.Server.Cache.DefaultTtl
}
<#
.SYNOPSIS
Starts a Pode Server with the supplied ScriptBlock.
.DESCRIPTION
Starts a Pode Server with the supplied ScriptBlock.
.PARAMETER ScriptBlock
The main logic for the Server.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Server's logic.
The directory of this file will be used as the Server's root path - unless a specific -RootPath is supplied.
.PARAMETER Interval
For 'Service' type Servers, will invoke the ScriptBlock every X seconds.
.PARAMETER Name
An optional name for the Server (intended for future ideas).
.PARAMETER Threads
The numbers of threads to use for Web, SMTP, and TCP servers.
.PARAMETER RootPath
An override for the Server's root path.
.PARAMETER Request
Intended for Serverless environments, this is Requests details that Pode can parse and use.
.PARAMETER ServerlessType
Optional, this is the serverless type, to define how Pode should run and deal with incoming Requests.
.PARAMETER StatusPageExceptions
An optional value of Show/Hide to control where Stacktraces are shown in the Status Pages.
If supplied this value will override the ShowExceptions setting in the server.psd1 file.
.PARAMETER ListenerType
An optional value to use a custom Socket Listener. The default is Pode's inbuilt listener.
There's the Pode.Kestrel module, so the value here should be "Kestrel" if using that.
.PARAMETER DisableTermination
Disables the ability to terminate the Server.
.PARAMETER Quiet
Disables any output from the Server.
.PARAMETER Browse
Open the web Server's default endpoint in your default browser.
.PARAMETER CurrentPath
Sets the Server's root path to be the current working path - for -FilePath only.
.PARAMETER EnablePool
Tells Pode to configure certain RunspacePools when they're being used adhoc, such as Timers or Schedules.
.PARAMETER EnableBreakpoints
If supplied, any breakpoints created by using Wait-PodeDebugger will be enabled - or disabled if false passed explicitly, or not supplied.
.EXAMPLE
Start-PodeServer { /* logic */ }
.EXAMPLE
Start-PodeServer -Interval 10 { /* logic */ }
.EXAMPLE
Start-PodeServer -Request $LambdaInput -ServerlessType AwsLambda { /* logic */ }
#>
function Start-PodeServer {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[int]
$Interval = 0,
[Parameter()]
[string]
$Name,
[Parameter()]
[int]
$Threads = 1,
[Parameter()]
[string]
$RootPath,
[Parameter()]
$Request,
[Parameter()]
[ValidateSet('', 'AzureFunctions', 'AwsLambda')]
[string]
$ServerlessType = [string]::Empty,
[Parameter()]
[ValidateSet('', 'Hide', 'Show')]
[string]
$StatusPageExceptions = [string]::Empty,
[Parameter()]
[string]
$ListenerType = [string]::Empty,
[Parameter()]
[ValidateSet('Timers', 'Schedules', 'Tasks', 'WebSockets', 'Files')]
[string[]]
$EnablePool,
[switch]
$DisableTermination,
[switch]
$Quiet,
[switch]
$Browse,
[Parameter(ParameterSetName = 'File')]
[switch]
$CurrentPath,
[switch]
$EnableBreakpoints
)
# ensure the session is clean
$PodeContext = $null
$ShowDoneMessage = $true
try {
# if we have a filepath, resolve it - and extract a root path from it
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$FilePath = Get-PodeRelativePath -Path $FilePath -Resolve -TestPath -JoinRoot -RootPath $MyInvocation.PSScriptRoot
# if not already supplied, set root path
if ([string]::IsNullOrWhiteSpace($RootPath)) {
if ($CurrentPath) {
$RootPath = $PWD.Path
}
else {
$RootPath = Split-Path -Parent -Path $FilePath
}
}
}
# configure the server's root path
if (!(Test-PodeIsEmpty $RootPath)) {
$RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath
}
# create main context object
$PodeContext = New-PodeContext `
-ScriptBlock $ScriptBlock `
-FilePath $FilePath `
-Threads $Threads `
-Interval $Interval `
-ServerRoot (Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) `
-ServerlessType $ServerlessType `
-ListenerType $ListenerType `
-EnablePool $EnablePool `
-StatusPageExceptions $StatusPageExceptions `
-DisableTermination:$DisableTermination `
-Quiet:$Quiet `
-EnableBreakpoints:$EnableBreakpoints
# set it so ctrl-c can terminate, unless serverless/iis, or disabled
if (!$PodeContext.Server.DisableTermination -and ($null -eq $psISE)) {
[Console]::TreatControlCAsInput = $true
}
# start the file monitor for interally restarting
Start-PodeFileMonitor
# start the server
Start-PodeInternalServer -Request $Request -Browse:$Browse
# at this point, if it's just a one-one off script, return
if (!(Test-PodeServerKeepOpen)) {
return
}
# sit here waiting for termination/cancellation, or to restart the server
while (!(Test-PodeTerminationPressed -Key $key) -and !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) {
Start-Sleep -Seconds 1
# get the next key presses
$key = Get-PodeConsoleKey
# check for internal restart
if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) {
Restart-PodeInternalServer
}
# check for open browser
if (Test-PodeOpenBrowserPressed -Key $key) {
Invoke-PodeEvent -Type Browser
Start-Process (Get-PodeEndpointUrl)
}
}
if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Shutdown) {
Write-PodeHost '(IIS Shutdown) ' -NoNewline -ForegroundColor Yellow
}
Write-PodeHost 'Terminating...' -NoNewline -ForegroundColor Yellow
Invoke-PodeEvent -Type Terminate
$PodeContext.Tokens.Cancellation.Cancel()
}
catch {
Invoke-PodeEvent -Type Crash
$ShowDoneMessage = $false
throw
}
finally {
Invoke-PodeEvent -Type Stop
# set output values
Set-PodeOutputVariables
# unregister secret vaults
Unregister-PodeSecretVaults
# clean the runspaces and tokens
Close-PodeServerInternal -ShowDoneMessage:$ShowDoneMessage
# clean the session
$PodeContext = $null
}
}
<#
.SYNOPSIS
Closes the Pode server.
.DESCRIPTION
Closes the Pode server.
.EXAMPLE
Close-PodeServer
#>
function Close-PodeServer {
[CmdletBinding()]
param()
$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.
.DESCRIPTION
Helper wrapper function to start a Pode web server for a static website at the current directory.
.PARAMETER Threads
The numbers of threads to use for requests.
.PARAMETER RootPath
An override for the Server's root path.
.PARAMETER Address
The IP/Hostname of the endpoint.
.PARAMETER Port
The Port number of the endpoint.
.PARAMETER Https
Start the server using HTTPS, if no certificate details are supplied a self-signed certificate will be generated.
.PARAMETER Certificate
The path to a certificate that can be use to enable HTTPS.
.PARAMETER CertificatePassword
The password for the certificate referenced in CertificateFile.
.PARAMETER CertificateKey
A key file to be paired with a PEM certificate referenced in CertificateFile
.PARAMETER X509Certificate
The raw X509 certificate that can be use to enable HTTPS.
.PARAMETER Path
The URI path for the static Route.
.PARAMETER Defaults
An array of default pages to display, such as 'index.html'.
.PARAMETER DownloadOnly
When supplied, all static content on this Route will be attached as downloads - rather than rendered.
.PARAMETER FileBrowser
When supplied, If the path is a folder, instead of returning 404, will return A browsable content of the directory.
.PARAMETER Browse
Open the web server's default endpoint in your default browser.
.EXAMPLE
Start-PodeStaticServer
.EXAMPLE
Start-PodeStaticServer -Address '127.0.0.3' -Port 8000
.EXAMPLE
Start-PodeStaticServer -Path '/installers' -DownloadOnly
#>
function Start-PodeStaticServer {
[CmdletBinding()]
param(
[Parameter()]
[int]
$Threads = 3,
[Parameter()]
[string]
$RootPath = $PWD,
[Parameter()]
[string]
$Address = 'localhost',
[Parameter()]
[int]
$Port = 0,
[Parameter()]
[switch]
$Https,
[Parameter()]
[string]
$Certificate = $null,
[Parameter()]
[string]
$CertificatePassword = $null,
[Parameter()]
[string]
$CertificateKey = $null,
[Parameter()]
[X509Certificate]
$X509Certificate = $null,
[Parameter()]
[string]
$Path = '/',
[Parameter()]
[string[]]
$Defaults,
[switch]
$DownloadOnly,
[switch]
$FileBrowser,
[switch]
$Browse
)
Start-PodeServer -RootPath $RootPath -Threads $Threads -Browse:$Browse -ScriptBlock {
# add either an http or https endpoint
if ($Https) {
if ($null -ne $X509Certificate) {
Add-PodeEndpoint -Address $Address -Port $Port -Protocol Https -X509Certificate $X509Certificate
}
elseif (![string]::IsNullOrWhiteSpace($Certificate)) {
Add-PodeEndpoint -Address $Address -Port $Port -Protocol Https -Certificate $Certificate -CertificatePassword $CertificatePassword -CertificateKey $CertificateKey
}
else {
Add-PodeEndpoint -Address $Address -Port $Port -Protocol Https -SelfSigned
}
}
else {
Add-PodeEndpoint -Address $Address -Port $Port -Protocol Http
}
# add the static route
Add-PodeStaticRoute -Path $Path -Source (Get-PodeServerPath) -Defaults $Defaults -DownloadOnly:$DownloadOnly -FileBrowser:$FileBrowser
}
}
<#
.SYNOPSIS
A default server secret that can be for signing values like Session, Cookies, or SSE IDs.
.DESCRIPTION
A default server secret that can be for signing values like Session, Cookies, or SSE IDs. This secret is regenerated
on every server start and restart.
.EXAMPLE
$secret = Get-PodeServerDefaultSecret
#>
function Get-PodeServerDefaultSecret {
[CmdletBinding()]
param()
return $PodeContext.Server.DefaultSecret
}
<#
.SYNOPSIS
The CLI for Pode, to initialise, build and start your Server.
.DESCRIPTION
The CLI for Pode, to initialise, build and start your Server.
.PARAMETER Action
The action to invoke on your Server.
.PARAMETER Dev
Supply when running "pode install", this will install any dev packages defined in your package.json.
.EXAMPLE
pode install -dev
.EXAMPLE
pode build
.EXAMPLE
pode start
#>
function Pode {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('init', 'test', 'start', 'install', 'build')]
[Alias('a')]
[string]
$Action,
[switch]
[Alias('d')]
$Dev
)
# default config file name and content
$file = './package.json'
$name = Split-Path -Leaf -Path $pwd
$data = $null
# default config data that's used to populate on init
$map = @{
'name' = $name
'version' = '1.0.0'
'description' = ''
'main' = './server.ps1'
'scripts' = @{
'start' = './server.ps1'
'install' = 'yarn install --force --ignore-scripts --modules-folder pode_modules'
'build' = 'psake'
'test' = 'invoke-pester ./tests/*.ps1'
}
'author' = ''
'license' = 'MIT'
}
# check and load config if already exists
if (Test-Path $file) {
$data = (Get-Content $file | ConvertFrom-Json)
}
# quick check to see if the data is required
if ($Action -ine 'init') {
if ($null -eq $data) {
Write-Host 'package.json file not found' -ForegroundColor Red
return
}
else {
$actionScript = $data.scripts.$Action
if ([string]::IsNullOrWhiteSpace($actionScript) -and $Action -ieq 'start') {
$actionScript = $data.main
}
if ([string]::IsNullOrWhiteSpace($actionScript) -and $Action -ine 'install') {
Write-Host "package.json does not contain a script for the $($Action) action" -ForegroundColor Yellow
return
}
}
}
else {
if ($null -ne $data) {
Write-Host 'package.json already exists' -ForegroundColor Yellow
return
}
}
switch ($Action.ToLowerInvariant()) {
'init' {
$v = Read-Host -Prompt "name ($($map.name))"
if (![string]::IsNullOrWhiteSpace($v)) { $map.name = $v }
$v = Read-Host -Prompt "version ($($map.version))"
if (![string]::IsNullOrWhiteSpace($v)) { $map.version = $v }
$map.description = Read-Host -Prompt 'description'
$v = Read-Host -Prompt "entry point ($($map.main))"
if (![string]::IsNullOrWhiteSpace($v)) { $map.main = $v; $map.scripts.start = $v }
$map.author = Read-Host -Prompt 'author'
$v = Read-Host -Prompt "license ($($map.license))"
if (![string]::IsNullOrWhiteSpace($v)) { $map.license = $v }
$map | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 -Force
Write-Host 'Success, saved package.json' -ForegroundColor Green
}
'test' {
Invoke-PodePackageScript -ActionScript $actionScript
}
'start' {
Invoke-PodePackageScript -ActionScript $actionScript
}
'install' {
if ($Dev) {
Install-PodeLocalModules -Modules $data.devModules
}
Install-PodeLocalModules -Modules $data.modules
Invoke-PodePackageScript -ActionScript $actionScript
}
'build' {
Invoke-PodePackageScript -ActionScript $actionScript
}
}
}
<#
.SYNOPSIS
Opens a Web Server up as a Desktop Application.
.DESCRIPTION
Opens a Web Server up as a Desktop Application.
.PARAMETER Title
The title of the Application's window.
.PARAMETER Icon
A path to an icon image for the Application.
.PARAMETER WindowState
The state the Application's window starts, such as Minimized.
.PARAMETER WindowStyle
The border style of the Application's window.
.PARAMETER ResizeMode
Specifies if the Application's window is resizable.
.PARAMETER Height
The height of the window.
.PARAMETER Width
The width of the window.
.PARAMETER EndpointName
The specific endpoint name to use, if you are listening on multiple endpoints.
.PARAMETER HideFromTaskbar
Stops the Application from appearing on the taskbar.
.EXAMPLE
Show-PodeGui -Title 'MyApplication' -WindowState 'Maximized'
#>
function Show-PodeGui {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Title,
[Parameter()]
[string]
$Icon,
[Parameter()]
[ValidateSet('Normal', 'Maximized', 'Minimized')]
[string]
$WindowState = 'Normal',
[Parameter()]
[ValidateSet('None', 'SingleBorderWindow', 'ThreeDBorderWindow', 'ToolWindow')]
[string]
$WindowStyle = 'SingleBorderWindow',
[Parameter()]
[ValidateSet('CanResize', 'CanMinimize', 'NoResize')]
[string]
$ResizeMode = 'CanResize',
[Parameter()]
[int]
$Height = 0,
[Parameter()]
[int]
$Width = 0,
[Parameter()]
[string]
$EndpointName,
[switch]
$HideFromTaskbar
)
# error if serverless
Test-PodeIsServerless -FunctionName 'Show-PodeGui' -ThrowError
# only valid for Windows PowerShell
if ((Test-PodeIsPSCore) -and ($PSVersionTable.PSVersion.Major -eq 6)) {
throw 'Show-PodeGui is currently only available for Windows PowerShell, and PowerShell 7+ on Windows'
}
# enable the gui and set general settings
$PodeContext.Server.Gui.Enabled = $true
$PodeContext.Server.Gui.Title = $Title
$PodeContext.Server.Gui.ShowInTaskbar = !$HideFromTaskbar
$PodeContext.Server.Gui.WindowState = $WindowState
$PodeContext.Server.Gui.WindowStyle = $WindowStyle
$PodeContext.Server.Gui.ResizeMode = $ResizeMode
# set the window's icon path
if (![string]::IsNullOrWhiteSpace($Icon)) {
$PodeContext.Server.Gui.Icon = Get-PodeRelativePath -Path $Icon -JoinRoot -Resolve
if (!(Test-Path $PodeContext.Server.Gui.Icon)) {
throw "Path to icon for GUI does not exist: $($PodeContext.Server.Gui.Icon)"
}
}
# set the height of the window
$PodeContext.Server.Gui.Height = $Height
if ($PodeContext.Server.Gui.Height -le 0) {
$PodeContext.Server.Gui.Height = 'auto'
}
# set the width of the window
$PodeContext.Server.Gui.Width = $Width
if ($PodeContext.Server.Gui.Width -le 0) {
$PodeContext.Server.Gui.Width = 'auto'
}
# set the gui to use a specific listener
$PodeContext.Server.Gui.EndpointName = $EndpointName
if (![string]::IsNullOrWhiteSpace($EndpointName)) {
if (!$PodeContext.Server.Endpoints.ContainsKey($EndpointName)) {
throw "Endpoint with name '$($EndpointName)' does not exist"
}
$PodeContext.Server.Gui.Endpoint = $PodeContext.Server.Endpoints[$EndpointName]
}
}
<#
.SYNOPSIS
Bind an endpoint to listen for incoming Requests.
.DESCRIPTION
Bind an endpoint to listen for incoming Requests. The endpoints can be HTTP, HTTPS, TCP or SMTP, with the option to bind certificates.
.PARAMETER Address
The IP/Hostname of the endpoint (Default: localhost).
.PARAMETER Port
The Port number of the endpoint.
.PARAMETER Hostname
An optional hostname for the endpoint, specifying a hostname restricts access to just the hostname.
.PARAMETER Protocol
The protocol of the supplied endpoint.
.PARAMETER Certificate
The path to a certificate that can be use to enable HTTPS
.PARAMETER CertificatePassword
The password for the certificate file referenced in Certificate
.PARAMETER CertificateKey
A key file to be paired with a PEM certificate file referenced in Certificate
.PARAMETER CertificateThumbprint
A certificate thumbprint to bind onto HTTPS endpoints (Windows).
.PARAMETER CertificateName
A certificate subject name to bind onto HTTPS endpoints (Windows).
.PARAMETER CertificateStoreName
The name of a certifcate store where a certificate can be found (Default: My) (Windows).
.PARAMETER CertificateStoreLocation
The location of a certifcate store where a certificate can be found (Default: CurrentUser) (Windows).
.PARAMETER X509Certificate
The raw X509 certificate that can be use to enable HTTPS
.PARAMETER TlsMode
The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit).
.PARAMETER Name
An optional name for the endpoint, that can be used with other functions (Default: GUID).
.PARAMETER RedirectTo
The Name of another Endpoint to automatically generate a redirect route for all traffic.
.PARAMETER Description
A quick description of the Endpoint - normally used in OpenAPI.
.PARAMETER Acknowledge
An optional Acknowledge message to send to clients when they first connect, for TCP and SMTP endpoints only.
.PARAMETER SslProtocol
One or more optional SSL Protocols this endpoints supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS).
.PARAMETER CRLFMessageEnd
If supplied, TCP endpoints will expect incoming data to end with CRLF.
.PARAMETER Force
Ignore Adminstrator checks for non-localhost endpoints.
.PARAMETER SelfSigned
Create and bind a self-signed certifcate for HTTPS endpoints.
.PARAMETER AllowClientCertificate
Allow for client certificates to be sent on requests.
.PARAMETER PassThru
If supplied, the endpoint created will be returned.
.PARAMETER LookupHostname
If supplied, a supplied Hostname will have its IP Address looked up from host file or DNS.
.PARAMETER DualMode
If supplied, this endpoint will listen on both the IPv4 and IPv6 versions of the supplied -Address.
For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 address.
.PARAMETER Default
If supplied, this endpoint will be the default one used for internally generating URLs.
.EXAMPLE
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http
.EXAMPLE
Add-PodeEndpoint -Address localhost -Protocol Smtp
.EXAMPLE
Add-PodeEndpoint -Address dev.pode.com -Port 8443 -Protocol Https -SelfSigned
.EXAMPLE
Add-PodeEndpoint -Address 127.0.0.2 -Hostname dev.pode.com -Port 8443 -Protocol Https -SelfSigned
.EXAMPLE
Add-PodeEndpoint -Address live.pode.com -Protocol Https -CertificateThumbprint '2A9467F7D3940243D6C07DE61E7FCCE292'
#>
function Add-PodeEndpoint {
[CmdletBinding(DefaultParameterSetName = 'Default')]
[OutputType([hashtable])]
param(
[Parameter()]
[string]
$Address = 'localhost',
[Parameter()]
[int]
$Port = 0,
[Parameter()]
[string]
$Hostname,
[Parameter()]
[ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
[string]
$Protocol,
[Parameter(Mandatory = $true, ParameterSetName = 'CertFile')]
[string]
$Certificate = $null,
[Parameter(ParameterSetName = 'CertFile')]
[string]
$CertificatePassword = $null,
[Parameter(ParameterSetName = 'CertFile')]
[string]
$CertificateKey = $null,
[Parameter(Mandatory = $true, ParameterSetName = 'CertThumb')]
[string]
$CertificateThumbprint,
[Parameter(Mandatory = $true, ParameterSetName = 'CertName')]
[string]
$CertificateName,
[Parameter(ParameterSetName = 'CertName')]
[Parameter(ParameterSetName = 'CertThumb')]
[System.Security.Cryptography.X509Certificates.StoreName]
$CertificateStoreName = 'My',
[Parameter(ParameterSetName = 'CertName')]
[Parameter(ParameterSetName = 'CertThumb')]
[System.Security.Cryptography.X509Certificates.StoreLocation]
$CertificateStoreLocation = 'CurrentUser',
[Parameter(Mandatory = $true, ParameterSetName = 'CertRaw')]
[X509Certificate]
$X509Certificate = $null,
[Parameter(ParameterSetName = 'CertFile')]
[Parameter(ParameterSetName = 'CertThumb')]
[Parameter(ParameterSetName = 'CertName')]
[Parameter(ParameterSetName = 'CertRaw')]
[Parameter(ParameterSetName = 'CertSelf')]
[ValidateSet('Implicit', 'Explicit')]
[string]
$TlsMode = 'Implicit',
[Parameter()]
[string]
$Name = $null,
[Parameter()]
[string]
$RedirectTo = $null,
[Parameter()]
[string]
$Description,
[Parameter()]
[string]
$Acknowledge,
[Parameter()]
[ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')]
[string[]]
$SslProtocol = $null,
[switch]
$CRLFMessageEnd,
[switch]
$Force,
[Parameter(ParameterSetName = 'CertSelf')]
[switch]
$SelfSigned,
[switch]
$AllowClientCertificate,
[switch]
$PassThru,
[switch]
$LookupHostname,
[switch]
$DualMode,
[switch]
$Default
)
# error if serverless
Test-PodeIsServerless -FunctionName 'Add-PodeEndpoint' -ThrowError
# if RedirectTo is supplied, then a Name is mandatory
if (![string]::IsNullOrWhiteSpace($RedirectTo) -and [string]::IsNullOrWhiteSpace($Name)) {
throw 'A Name is required for the endpoint if the RedirectTo parameter is supplied'
}
# get the type of endpoint
$type = Get-PodeEndpointType -Protocol $Protocol
# are we running as IIS for HTTP/HTTPS? (if yes, force the port, address and protocol)
$isIIS = ((Test-PodeIsIIS) -and (@('Http', 'Ws') -icontains $type))
if ($isIIS) {
$Port = [int]$env:ASPNETCORE_PORT
$Address = '127.0.0.1'
$Hostname = [string]::Empty
$Protocol = $type
}
# are we running as Heroku for HTTP/HTTPS? (if yes, force the port, address and protocol)
$isHeroku = ((Test-PodeIsHeroku) -and (@('Http') -icontains $type))
if ($isHeroku) {
$Port = [int]$env:PORT
$Address = '0.0.0.0'
$Hostname = [string]::Empty
$Protocol = $type
}
# parse the endpoint for host/port info
if (![string]::IsNullOrWhiteSpace($Hostname) -and !(Test-PodeHostname -Hostname $Hostname)) {
throw "Invalid hostname supplied: $($Hostname)"
}
if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) {
$Hostname = $Address
$Address = 'localhost'
}
if (![string]::IsNullOrWhiteSpace($Hostname) -and $LookupHostname) {
$Address = (Get-PodeIPAddressesForHostname -Hostname $Hostname -Type All | Select-Object -First 1)
}
$_endpoint = Get-PodeEndpointInfo -Address "$($Address):$($Port)"
# if no name, set to guid, then check uniqueness
if ([string]::IsNullOrWhiteSpace($Name)) {
$Name = New-PodeGuid -Secure
}
if ($PodeContext.Server.Endpoints.ContainsKey($Name)) {
throw "An endpoint with the name '$($Name)' has already been defined"
}
# protocol must be https for client certs, or hosted behind a proxy like iis
if (($Protocol -ine 'https') -and !(Test-PodeIsHosted) -and $AllowClientCertificate) {
throw 'Client certificates are only supported on HTTPS endpoints'
}
# explicit tls is only supported for smtp/tcp
if (($type -inotin @('smtp', 'tcp')) -and ($TlsMode -ieq 'explicit')) {
throw 'The Explicit TLS mode is only supported on SMTPS and TCPS endpoints'
}
# ack message is only for smtp/tcp
if (($type -inotin @('smtp', 'tcp')) -and ![string]::IsNullOrEmpty($Acknowledge)) {
throw 'The Acknowledge message is only supported on SMTP and TCP endpoints'
}
# crlf message end is only for tcp
if (($type -ine 'tcp') -and $CRLFMessageEnd) {
throw 'The CRLF message end check is only supported on TCP endpoints'
}
# new endpoint object
$obj = @{
Name = $Name
Description = $Description
DualMode = $DualMode
Address = $null
RawAddress = $null
Port = $null
IsIPAddress = $true
HostName = $Hostname
FriendlyName = $Hostname
Url = $null
Ssl = @{
Enabled = (@('https', 'wss', 'smtps', 'tcps') -icontains $Protocol)
Protocols = $PodeContext.Server.Sockets.Ssl.Protocols
}
Protocol = $Protocol.ToLowerInvariant()
Type = $type.ToLowerInvariant()
Runspace = @{
PoolName = (Get-PodeEndpointRunspacePoolName -Protocol $Protocol)
}
Default = $Default.IsPresent
Certificate = @{
Raw = $X509Certificate
SelfSigned = $SelfSigned
AllowClientCertificate = $AllowClientCertificate
TlsMode = $TlsMode
}
Tcp = @{
Acknowledge = $Acknowledge
CRLFMessageEnd = $CRLFMessageEnd
}
}
# set ssl protocols
if (!(Test-PodeIsEmpty $SslProtocol)) {
$obj.Ssl.Protocols = (ConvertTo-PodeSslProtocols -Protocols $SslProtocol)
}
# set the ip for the context (force to localhost for IIS)
$obj.Address = Get-PodeIPAddress $_endpoint.Host -DualMode:$DualMode
$obj.IsIPAddress = [string]::IsNullOrWhiteSpace($obj.HostName)
if ($obj.IsIPAddress) {
if (!(Test-PodeIPAddressLocalOrAny -IP $obj.Address)) {
$obj.FriendlyName = "$($obj.Address)"
}
else {
$obj.FriendlyName = 'localhost'
}
}
# set the port for the context, if 0 use a default port for protocol
$obj.Port = $_endpoint.Port
if (([int]$obj.Port) -eq 0) {
$obj.Port = Get-PodeDefaultPort -Protocol $Protocol -TlsMode $TlsMode
}
if ($obj.IsIPAddress) {
$obj.RawAddress = "$($obj.Address):$($obj.Port)"
}
else {
$obj.RawAddress = "$($obj.FriendlyName):$($obj.Port)"
}
# set the url of this endpoint
$obj.Url = "$($obj.Protocol)://$($obj.FriendlyName):$($obj.Port)/"
# if the address is non-local, then check admin privileges
if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and !(Test-PodeIsAdminUser)) {
throw 'Must be running with administrator priviledges to listen on non-localhost addresses'
}
# has this endpoint been added before? (for http/https we can just not add it again)
$exists = ($PodeContext.Server.Endpoints.Values | Where-Object {
($_.FriendlyName -ieq $obj.FriendlyName) -and ($_.Port -eq $obj.Port) -and ($_.Ssl.Enabled -eq $obj.Ssl.Enabled) -and ($_.Type -ieq $obj.Type)
} | Measure-Object).Count
# if we're dealing with a certificate, attempt to import it
if (!(Test-PodeIsHosted) -and ($PSCmdlet.ParameterSetName -ilike 'cert*')) {
# fail if protocol is not https
if (@('https', 'wss', 'smtps', 'tcps') -inotcontains $Protocol) {
throw 'Certificate supplied for non-HTTPS/WSS endpoint'
}
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'certfile' {
$obj.Certificate.Raw = Get-PodeCertificateByFile -Certificate $Certificate -Password $CertificatePassword -Key $CertificateKey
}
'certthumb' {
$obj.Certificate.Raw = Get-PodeCertificateByThumbprint -Thumbprint $CertificateThumbprint -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation
}
'certname' {
$obj.Certificate.Raw = Get-PodeCertificateByName -Name $CertificateName -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation
}
'certself' {
$obj.Certificate.Raw = New-PodeSelfSignedCertificate
}
}
# fail if the cert is expired
if ($obj.Certificate.Raw.NotAfter -lt [datetime]::Now) {
throw "The certificate '$($obj.Certificate.Raw.Subject)' has expired: $($obj.Certificate.Raw.NotAfter)"
}
}
if (!$exists) {
# set server type
$_type = $type
if ($_type -iin @('http', 'ws')) {
$_type = 'http'
}
if ($PodeContext.Server.Types -inotcontains $_type) {
$PodeContext.Server.Types += $_type
}
# add the new endpoint
$PodeContext.Server.Endpoints[$Name] = $obj
$PodeContext.Server.EndpointsMap["$($obj.Protocol)|$($obj.RawAddress)"] = $Name
}
# if RedirectTo is set, attempt to build a redirecting route
if (!(Test-PodeIsHosted) -and ![string]::IsNullOrWhiteSpace($RedirectTo)) {
$redir_endpoint = $PodeContext.Server.Endpoints[$RedirectTo]
# ensure the name exists
if (Test-PodeIsEmpty $redir_endpoint) {
throw "An endpoint with the name '$($RedirectTo)' has not been defined for redirecting"
}
# build the redirect route
Add-PodeRoute -Method * -Path * -EndpointName $obj.Name -ArgumentList $redir_endpoint -ScriptBlock {
param($endpoint)
Move-PodeResponseUrl -EndpointName $endpoint.Name
}
}
# return the endpoint?
if ($PassThru) {
return $obj
}
}
<#
.SYNOPSIS
Get an Endpoint(s).
.DESCRIPTION
Get an Endpoint(s).
.PARAMETER Address
An Address to filter the endpoints.
.PARAMETER Port
A Port to filter the endpoints.
.PARAMETER Hostname
A Hostname to filter the endpoints.
.PARAMETER Protocol
A Protocol to filter the endpoints.
.PARAMETER Name
Any endpoints Names to filter endpoints.
.EXAMPLE
Get-PodeEndpoint -Address 127.0.0.1
.EXAMPLE
Get-PodeEndpoint -Protocol Http
.EXAMPLE
Get-PodeEndpoint -Name Admin, User
#>
function Get-PodeEndpoint {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Address,
[Parameter()]
[int]
$Port = 0,
[Parameter()]
[string]
$Hostname,
[Parameter()]
[ValidateSet('', 'Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')]
[string]
$Protocol,
[Parameter()]
[string[]]
$Name
)
if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) {
$Hostname = $Address
$Address = 'localhost'
}
$endpoints = $PodeContext.Server.Endpoints.Values
# if we have an address, filter
if (![string]::IsNullOrWhiteSpace($Address)) {
if (($Address -eq '*') -or $PodeContext.Server.IsHeroku) {
$Address = '0.0.0.0'
}
if ($PodeContext.Server.IsIIS -or ($Address -ieq 'localhost')) {
$Address = '127.0.0.1'
}
$endpoints = @(foreach ($endpoint in $endpoints) {
if ($endpoint.Address.ToString() -ine $Address) {
continue
}
$endpoint
})
}
# if we have a hostname, filter
if (![string]::IsNullOrWhiteSpace($Hostname)) {
$endpoints = @(foreach ($endpoint in $endpoints) {
if ($endpoint.Hostname.ToString() -ine $Hostname) {
continue
}
$endpoint
})
}
# if we have a port, filter
if ($Port -gt 0) {
if ($PodeContext.Server.IsIIS) {
$Port = [int]$env:ASPNETCORE_PORT
}
if ($PodeContext.Server.IsHeroku) {
$Port = [int]$env:PORT
}
$endpoints = @(foreach ($endpoint in $endpoints) {
if ($endpoint.Port -ne $Port) {
continue
}
$endpoint
})
}
# if we have a protocol, filter
if (![string]::IsNullOrWhiteSpace($Protocol)) {
if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) {
$Protocol = 'Http'
}
$endpoints = @(foreach ($endpoint in $endpoints) {
if ($endpoint.Protocol -ine $Protocol) {
continue
}
$endpoint
})
}
# further filter by endpoint names
if (($null -ne $Name) -and ($Name.Length -gt 0)) {
$endpoints = @(foreach ($_name in $Name) {
foreach ($endpoint in $endpoints) {
if ($endpoint.Name -ine $_name) {
continue
}
$endpoint
}
})
}
# return
return $endpoints
}
<#
.SYNOPSIS
Sets the path for a specified default folder type in the Pode server context.
.DESCRIPTION
This function configures the path for one of the Pode server's default folder types: Views, Public, or Errors.
It updates the server's configuration to reflect the new path for the specified folder type.
The function first checks if the provided path exists and is a directory;
if so, it updates the `Server.DefaultFolders` dictionary with the new path.
If the path does not exist or is not a directory, the function throws an error.
The purpose of this function is to allow dynamic configuration of the server's folder paths, which can be useful during server setup or when altering the server's directory structure at runtime.
.PARAMETER Type
The type of the default folder to set the path for. Must be one of 'Views', 'Public', or 'Errors'.
This parameter determines which default folder's path is being set.
.PARAMETER Path
The new file system path for the specified default folder type. This path must exist and be a directory; otherwise, an exception is thrown.
.EXAMPLE
Set-PodeDefaultFolder -Type 'Views' -Path 'C:\Pode\Views'
This example sets the path for the server's default 'Views' folder to 'C:\Pode\Views', assuming this path exists and is a directory.
.EXAMPLE
Set-PodeDefaultFolder -Type 'Public' -Path 'C:\Pode\Public'
This example sets the path for the server's default 'Public' folder to 'C:\Pode\Public'.
#>
function Set-PodeDefaultFolder {
[CmdletBinding()]
param (
[Parameter()]
[ValidateSet('Views', 'Public', 'Errors')]
[string]
$Type,
[Parameter()]
[string]
$Path
)
if (Test-Path -Path $Path -PathType Container) {
$PodeContext.Server.DefaultFolders[$Type] = $Path
}
else {
throw "Folder $Path doesn't exist"
}
}
<#
.SYNOPSIS
Retrieves the path of a specified default folder type from the Pode server context.
.DESCRIPTION
This function returns the path for one of the Pode server's default folder types: Views, Public, or Errors. It accesses the server's configuration stored in the `$PodeContext` variable and retrieves the path for the specified folder type from the `DefaultFolders` dictionary. This function is useful for scripts or modules that need to dynamically access server resources based on the server's current configuration.
.PARAMETER Type
The type of the default folder for which to retrieve the path. The valid options are 'Views', 'Public', or 'Errors'. This parameter determines which folder's path will be returned by the function.
.EXAMPLE
$path = Get-PodeDefaultFolder -Type 'Views'
This example retrieves the current path configured for the server's 'Views' folder and stores it in the `$path` variable.
.EXAMPLE
$path = Get-PodeDefaultFolder -Type 'Public'
This example retrieves the current path configured for the server's 'Public' folder.
.OUTPUTS
String. The file system path of the specified default folder.
#>
function Get-PodeDefaultFolder {
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter()]
[ValidateSet('Views', 'Public', 'Errors')]
[string]
$Type
)
return $PodeContext.Server.DefaultFolders[$Type]
}
<#
.SYNOPSIS
Attaches a breakpoint which can be used for debugging.
.DESCRIPTION
Attaches a breakpoint which can be used for debugging.
.EXAMPLE
Wait-PodeDebugger
#>
function Wait-PodeDebugger {
[CmdletBinding()]
param()
if (!$PodeContext.Server.Debug.Breakpoints.Enabled) {
return
}
Wait-Debugger
}
<#
.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', 'Crash', 'Stop', 'Running')]
[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 for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -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', 'Crash', 'Stop', 'Running')]
[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
$null = $PodeContext.Server.Events[$Type].Remove($Name)
}
<#
.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', 'Crash', 'Stop', 'Running')]
[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', 'Crash', 'Stop', 'Running')]
[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', 'Crash', 'Stop', 'Running')]
[string]
$Type
)
$null = $PodeContext.Server.Events[$Type].Clear()
}
<#
.SYNOPSIS
Automatically loads event ps1 files
.DESCRIPTION
Automatically loads event ps1 files from either a /events folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeEvents
.EXAMPLE
Use-PodeEvents -Path './my-events'
#>
function Use-PodeEvents {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'events'
}
<#
.SYNOPSIS
Adds a new File Watcher to monitor file changes in a directory.
.DESCRIPTION
Adds a new File Watcher to monitor file changes in a directory.
.PARAMETER Name
An optional Name for the File Watcher. (Default: GUID)
.PARAMETER EventName
An optional EventName to be monitored. Note: '*' refers to all event names. (Default: Changed, Created, Deleted, Renamed)
.PARAMETER Path
The Path to a directory which contains the files to be monitored.
.PARAMETER ScriptBlock
The ScriptBlock defining logic to be run when events are triggered.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the File Watcher's logic.
.PARAMETER ArgumentList
A hashtable of arguments to supply to the File Watcher's ScriptBlock.
.PARAMETER NotifyFilter
The attributes on files to monitor and notify about. (Default: FileName, DirectoryName, LastWrite, CreationTime)
.PARAMETER Exclude
An optional array of file patterns to be excluded.
.PARAMETER Include
An optional array of file patterns to be included. (Default: *.*)
.PARAMETER InternalBufferSize
The InternalBufferSize of the file monitor, used when temporarily storing events. (Default: 8kb)
.PARAMETER NoSubdirectories
If supplied, the File Watcher will only monitor files in the specified directory path, and not in all sub-directories as well.
.PARAMETER PassThru
If supplied, the File Watcher object registered will be returned.
.EXAMPLE
Add-PodeFileWatcher -Path 'C:/Projects/:project/src' -Include '*.ps1' -ScriptBlock {}
.EXAMPLE
Add-PodeFileWatcher -Path 'C:/Websites/:site' -Include '*.config' -EventName Changed -ScriptBlock {}
.EXAMPLE
Add-PodeFileWatcher -Path '/temp/logs' -EventName Created -NotifyFilter CreationTime -ScriptBlock {}
.EXAMPLE
$watcher = Add-PodeFileWatcher -Path '/temp/logs' -Exclude *.txt -ScriptBlock {} -PassThru
#>
function Add-PodeFileWatcher {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter()]
[string]
$Name = $null,
[Parameter()]
[ValidateSet('Changed', 'Created', 'Deleted', 'Renamed', 'Existed', '*')]
[string[]]
$EventName = @('Changed', 'Created', 'Deleted', 'Renamed'),
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter(Mandatory = $true, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[object[]]
$ArgumentList,
[Parameter()]
[System.IO.NotifyFilters[]]
$NotifyFilter = @('FileName', 'DirectoryName', 'LastWrite', 'CreationTime'),
[Parameter()]
[string[]]
$Exclude,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string[]]
$Include = '*.*',
[Parameter()]
[ValidateRange(4kb, 64kb)]
[int]
$InternalBufferSize = 8kb,
[switch]
$NoSubdirectories,
[switch]
$PassThru
)
# set random name
if ([string]::IsNullOrEmpty($Name)) {
$Name = New-PodeGuid -Secure
}
# set all for * event
if ('*' -iin $EventName) {
$EventName = @('Changed', 'Created', 'Deleted', 'Renamed', 'Existed')
}
# resolve path if relative
if (!(Test-PodeIsPSCore)) {
$Path = Convert-PodePlaceholders -Path $Path -Prepend '%' -Append '%'
}
$Path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve
if (!(Test-PodeIsPSCore)) {
$Path = Convert-PodePlaceholders -Path $Path -Pattern '\%(?<tag>[\w]+)\%' -Prepend ':' -Append ([string]::Empty)
}
# resolve path, and test it
$hasPlaceholders = Test-PodePlaceholders -Path $Path
if ($hasPlaceholders) {
$rgxPath = Update-PodeRouteSlashes -Path $Path -NoLeadingSlash
$rgxPath = Resolve-PodePlaceholders -Path $rgxPath -Slashes
$Path = $Path -ireplace (Get-PodePlaceholderRegex), '*'
}
# test path to make sure it exists
if (!(Test-PodePath $Path -NoStatus)) {
throw "The path does not exist: $($Path)"
}
# test if we have the file watcher already
if (Test-PodeFileWatcher -Name $Name) {
throw "A File Watcher with the name '$($Name)' has already been defined"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# enable the file watcher threads
$PodeContext.Fim.Enabled = $true
# resolve the path's widacards if any
$paths = @($Path)
if ($Path.Contains('*')) {
$paths = @(Get-ChildItem -Path $Path -Directory -Force | Select-Object -ExpandProperty FullName)
}
# add the file watcher
$PodeContext.Fim.Items[$Name] = @{
Name = $Name
Events = @($EventName)
Path = $Path
Placeholders = @{
Path = $rgxPath
Exist = $hasPlaceholders
}
Script = $ScriptBlock
UsingVariables = $usingVars
Arguments = $ArgumentList
NotifyFilters = @($NotifyFilter)
IncludeSubdirectories = !$NoSubdirectories.IsPresent
InternalBufferSize = $InternalBufferSize
Exclude = $Exclude
Include = $Include
Paths = $paths
}
# return?
if ($PassThru) {
return $PodeContext.Fim.Items[$Name]
}
}
<#
.SYNOPSIS
Tests whether the passed File Watcher exists.
.DESCRIPTION
Tests whether the passed File Watcher exists by its name.
.PARAMETER Name
The Name of the File Watcher.
.EXAMPLE
if (Test-PodeFileWatcher -Name WatcherName) { }
#>
function Test-PodeFileWatcher {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return (($null -ne $PodeContext.Fim.Items) -and $PodeContext.Fim.Items.ContainsKey($Name))
}
<#
.SYNOPSIS
Returns any defined File Watchers.
.DESCRIPTION
Returns any defined File Watchers.
.PARAMETER Name
An optional File Watcher Name(s) to be returned.
.EXAMPLE
Get-PodeFileWatcher
.EXAMPLE
Get-PodeFileWatcher -Name Name1, Name2
#>
function Get-PodeFileWatcher {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Name
)
$watchers = $PodeContext.Fim.Items.Values
# further filter by file watcher names
if (($null -ne $Name) -and ($Name.Length -gt 0)) {
$watchers = @(foreach ($_name in $Name) {
foreach ($watcher in $watchers) {
if ($watcher.Name -ine $_name) {
continue
}
$watcher
}
})
}
# return
return $watchers
}
<#
.SYNOPSIS
Removes a specific File Watchers.
.DESCRIPTION
Removes a specific File Watchers.
.PARAMETER Name
The Name of the File Watcher to be removed.
.EXAMPLE
Remove-PodeFileWatcher -Name 'Logs'
#>
function Remove-PodeFileWatcher {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$null = $PodeContext.Fim.Items.Remove($Name)
}
<#
.SYNOPSIS
Removes all File Watchers.
.DESCRIPTION
Removes all File Watchers.
.EXAMPLE
Clear-PodeFileWatchers
#>
function Clear-PodeFileWatchers {
[CmdletBinding()]
param()
$PodeContext.Fim.Items.Clear()
}
<#
.SYNOPSIS
Automatically loads File Watchers ps1 files
.DESCRIPTION
Automatically loads File Watchers ps1 files from either a /filewatcher folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeFileWatchers
.EXAMPLE
Use-PodeFileWatchers -Path './my-watchers'
#>
function Use-PodeFileWatchers {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'filewatchers'
}
<#
.SYNOPSIS
Appends a message to the current flash messages stored in the session.
.DESCRIPTION
Appends a message to the current flash messages stored in the session for the supplied name.
The messages per name are stored as an array.
.PARAMETER Name
The name of the flash message to be appended.
.PARAMETER Message
The message to append.
.EXAMPLE
Add-PodeFlashMessage -Name 'error' -Message 'There was an error'
#>
function Add-PodeFlashMessage {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Message
)
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use Flash messages'
}
# append the message against the key
if ($null -eq $WebEvent.Session.Data.Flash) {
$WebEvent.Session.Data.Flash = @{}
}
if ($null -eq $WebEvent.Session.Data.Flash[$Name]) {
$WebEvent.Session.Data.Flash[$Name] = @($Message)
}
else {
$WebEvent.Session.Data.Flash[$Name] += @($Message)
}
}
<#
.SYNOPSIS
Clears all flash messages.
.DESCRIPTION
Clears all of the flash messages currently stored in the session.
.EXAMPLE
Clear-PodeFlashMessages
#>
function Clear-PodeFlashMessages {
[CmdletBinding()]
param()
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use Flash messages'
}
# clear all keys
if ($null -ne $WebEvent.Session.Data.Flash) {
$WebEvent.Session.Data.Flash = @{}
}
}
<#
.SYNOPSIS
Returns all flash messages stored against a name, and the clears the messages.
.DESCRIPTION
Returns all of the flash messages, as an array, currently stored for the name within the session.
Once retrieved, the messages are removed from storage.
.PARAMETER Name
The name of the flash messages to return.
.EXAMPLE
Get-PodeFlashMessage -Name 'error'
#>
function Get-PodeFlashMessage {
[CmdletBinding()]
[OutputType([string[]])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use Flash messages'
}
# retrieve messages from session, then delete it
if ($null -eq $WebEvent.Session.Data.Flash) {
return @()
}
$v = @($WebEvent.Session.Data.Flash[$Name])
$WebEvent.Session.Data.Flash.Remove($Name)
if (Test-PodeIsEmpty $v) {
return @()
}
return @($v)
}
<#
.SYNOPSIS
Returns all of the names for each of the messages currently being stored.
.DESCRIPTION
Returns all of the names for each of the messages currently being stored. This does not clear the messages.
.EXAMPLE
Get-PodeFlashMessageNames
#>
function Get-PodeFlashMessageNames {
[CmdletBinding()]
[OutputType([string[]])]
param()
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use Flash messages'
}
# return list of all current keys
if ($null -eq $WebEvent.Session.Data.Flash) {
return @()
}
return @($WebEvent.Session.Data.Flash.Keys)
}
<#
.SYNOPSIS
Removes flash messages for the supplied name currently being stored.
.DESCRIPTION
Removes flash messages for the supplied name currently being stored.
.PARAMETER Name
The name of the flash messages to remove.
.EXAMPLE
Remove-PodeFlashMessage -Name 'error'
#>
function Remove-PodeFlashMessage {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use Flash messages'
}
# remove key from flash messages
if ($null -ne $WebEvent.Session.Data.Flash) {
$WebEvent.Session.Data.Flash.Remove($Name)
}
}
<#
.SYNOPSIS
Tests if there are any flash messages currently being stored for a supplied name.
.DESCRIPTION
Tests if there are any flash messages currently being stored for a supplied name.
.PARAMETER Name
The name of the flash message to check.
.EXAMPLE
Test-PodeFlashMessage -Name 'error'
#>
function Test-PodeFlashMessage {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use Flash messages'
}
# return if a key exists as a flash message
if ($null -eq $WebEvent.Session.Data.Flash) {
return $false
}
return $WebEvent.Session.Data.Flash.ContainsKey($Name)
}
<#
.SYNOPSIS
Adds a Handler of a specific Type.
.DESCRIPTION
Adds a Handler of a specific Type.
.PARAMETER Type
The Type of the Handler.
.PARAMETER Name
The Name of the Handler.
.PARAMETER ScriptBlock
The ScriptBlock for the Handler's main logic.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Handler's main logic.
.PARAMETER ArgumentList
An array of arguments to supply to the Handler's ScriptBlock.
.EXAMPLE
Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeHandler -Type Service -Name 'Looper' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeHandler {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Service', 'Smtp')]
[string]
$Type,
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[object[]]
$ArgumentList
)
# error if serverless
Test-PodeIsServerless -FunctionName 'Add-PodeHandler' -ThrowError
# ensure handler isn't already set
if ($PodeContext.Server.Handlers[$Type].ContainsKey($Name)) {
throw "[$($Type)] $($Name): Handler already defined"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add the handler
Write-Verbose "Adding Handler: [$($Type)] $($Name)"
$PodeContext.Server.Handlers[$Type][$Name] += @(@{
Logic = $ScriptBlock
UsingVariables = $usingVars
Arguments = $ArgumentList
})
}
<#
.SYNOPSIS
Remove a specific Handler.
.DESCRIPTION
Remove a specific Handler.
.PARAMETER Type
The type of the Handler to be removed.
.PARAMETER Name
The name of the Handler to be removed.
.EXAMPLE
Remove-PodeHandler -Type Smtp -Name 'Main'
#>
function Remove-PodeHandler {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Service', 'Smtp')]
[string]
$Type,
[Parameter(Mandatory = $true)]
[string]
$Name
)
# ensure handler does exist
if (!$PodeContext.Server.Handlers[$Type].ContainsKey($Name)) {
return
}
# remove the handler
$null = $PodeContext.Server.Handlers[$Type].Remove($Name)
}
<#
.SYNOPSIS
Removes all added Handlers, or Handlers of a specific Type.
.DESCRIPTION
Removes all added Handlers, or Handlers of a specific Type.
.PARAMETER Type
The Type of Handlers to remove.
.EXAMPLE
Clear-PodeHandlers -Type Smtp
#>
function Clear-PodeHandlers {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('', 'Service', 'Smtp')]
[string]
$Type
)
if (![string]::IsNullOrWhiteSpace($Type)) {
$PodeContext.Server.Handlers[$Type].Clear()
}
else {
$PodeContext.Server.Handlers.Keys.Clone() | ForEach-Object {
$PodeContext.Server.Handlers[$_].Clear()
}
}
}
<#
.SYNOPSIS
Automatically loads handler ps1 files
.DESCRIPTION
Automatically loads handler ps1 files from either a /handler folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeHandlers
.EXAMPLE
Use-PodeHandlers -Path './my-handlers'
#>
function Use-PodeHandlers {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'handlers'
}
<#
.SYNOPSIS
Appends a header against the Response.
.DESCRIPTION
Appends a header against the Response. If the current context is serverless, then this function acts like Set-PodeHeader.
.PARAMETER Name
The name of the header.
.PARAMETER Value
The value to set against the header.
.PARAMETER Secret
If supplied, the secret with which to sign the header's value.
.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
.EXAMPLE
Add-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33'
#>
function Add-PodeHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Value,
[Parameter()]
[string]
$Secret,
[switch]
$Strict
)
# sign the value if we have a secret
if (![string]::IsNullOrWhiteSpace($Secret)) {
$Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict)
}
# add the header to the response
if ($PodeContext.Server.IsServerless) {
$WebEvent.Response.Headers[$Name] = $Value
}
else {
$WebEvent.Response.Headers.Add($Name, $Value)
}
}
<#
.SYNOPSIS
Appends multiple headers against the Response.
.DESCRIPTION
Appends multiple headers against the Response. If the current context is serverless, then this function acts like Set-PodeHeaderBulk.
.PARAMETER Values
A hashtable of headers to be appended.
.PARAMETER Secret
If supplied, the secret with which to sign the header values.
.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
.EXAMPLE
Add-PodeHeaderBulk -Values @{ Name1 = 'Value1'; Name2 = 'Value2' }
#>
function Add-PodeHeaderBulk {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[hashtable]
$Values,
[Parameter()]
[string]
$Secret,
[switch]
$Strict
)
foreach ($key in $Values.Keys) {
$value = $Values[$key]
# sign the value if we have a secret
if (![string]::IsNullOrWhiteSpace($Secret)) {
$value = (Invoke-PodeValueSign -Value $value -Secret $Secret -Strict:$Strict)
}
# add the header to the response
if ($PodeContext.Server.IsServerless) {
$WebEvent.Response.Headers[$key] = $value
}
else {
$WebEvent.Response.Headers.Add($key, $value)
}
}
}
<#
.SYNOPSIS
Tests if a header is present on the Request.
.DESCRIPTION
Tests if a header is present on the Request.
.PARAMETER Name
The name of the header to test.
.EXAMPLE
Test-PodeHeader -Name 'X-AuthToken'
#>
function Test-PodeHeader {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$header = (Get-PodeHeader -Name $Name)
return (![string]::IsNullOrWhiteSpace($header))
}
<#
.SYNOPSIS
Retrieves the value of a header from the Request.
.DESCRIPTION
Retrieves the value of a header from the Request.
.PARAMETER Name
The name of the header to retrieve.
.PARAMETER Secret
The secret used to unsign the header's value.
.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
.EXAMPLE
Get-PodeHeader -Name 'X-AuthToken'
#>
function Get-PodeHeader {
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string]
$Secret,
[switch]
$Strict
)
# get the value for the header from the request
$header = $WebEvent.Request.Headers.$Name
# if a secret was supplied, attempt to unsign the header's value
if (![string]::IsNullOrWhiteSpace($Secret)) {
$header = (Invoke-PodeValueUnsign -Value $header -Secret $Secret -Strict:$Strict)
}
return $header
}
<#
.SYNOPSIS
Sets a header on the Response, clearing all current values for the header.
.DESCRIPTION
Sets a header on the Response, clearing all current values for the header.
.PARAMETER Name
The name of the header.
.PARAMETER Value
The value to set against the header.
.PARAMETER Secret
If supplied, the secret with which to sign the header's value.
.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
.EXAMPLE
Set-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33'
#>
function Set-PodeHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Value,
[Parameter()]
[string]
$Secret,
[switch]
$Strict
)
# sign the value if we have a secret
if (![string]::IsNullOrWhiteSpace($Secret)) {
$Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict)
}
# set the header on the response
if ($PodeContext.Server.IsServerless) {
$WebEvent.Response.Headers[$Name] = $Value
}
else {
$WebEvent.Response.Headers.Set($Name, $Value)
}
}
<#
.SYNOPSIS
Sets multiple headers on the Response, clearing all current values for the header.
.DESCRIPTION
Sets multiple headers on the Response, clearing all current values for the header.
.PARAMETER Values
A hashtable of headers to be set.
.PARAMETER Secret
If supplied, the secret with which to sign the header values.
.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
.EXAMPLE
Set-PodeHeaderBulk -Values @{ Name1 = 'Value1'; Name2 = 'Value2' }
#>
function Set-PodeHeaderBulk {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[hashtable]
$Values,
[Parameter()]
[string]
$Secret,
[switch]
$Strict
)
foreach ($key in $Values.Keys) {
$value = $Values[$key]
# sign the value if we have a secret
if (![string]::IsNullOrWhiteSpace($Secret)) {
$value = (Invoke-PodeValueSign -Value $value -Secret $Secret -Strict:$Strict)
}
# set the header on the response
if ($PodeContext.Server.IsServerless) {
$WebEvent.Response.Headers[$key] = $value
}
else {
$WebEvent.Response.Headers.Set($key, $value)
}
}
}
<#
.SYNOPSIS
Tests if a header on the Request is validly signed.
.DESCRIPTION
Tests if a header on the Request is validly signed, by attempting to unsign it using some secret.
.PARAMETER Name
The name of the header to test.
.PARAMETER Secret
A secret to use for attempting to unsign the header's value.
.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
.EXAMPLE
Test-PodeHeaderSigned -Name 'X-Header-Name' -Secret 'hunter2'
#>
function Test-PodeHeaderSigned {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string]
$Secret,
[switch]
$Strict
)
$header = Get-PodeHeader -Name $Name
return Test-PodeValueSigned -Value $header -Secret $Secret -Strict:$Strict
}
<#
.SYNOPSIS
Create a new method of outputting logs.
.DESCRIPTION
Create a new method of outputting logs.
.PARAMETER Terminal
If supplied, will use the inbuilt Terminal logging output method.
.PARAMETER File
If supplied, will use the inbuilt File logging output method.
.PARAMETER Path
The File Path of where to store the logs.
.PARAMETER Name
The File Name to prepend new log files using.
.PARAMETER EventViewer
If supplied, will use the inbuilt Event Viewer logging output method.
.PARAMETER EventLogName
Optional Log Name for the Event Viewer (Default: Application)
.PARAMETER Source
Optional Source for the Event Viewer (Default: Pode)
.PARAMETER EventID
Optional EventID for the Event Viewer (Default: 0)
.PARAMETER Batch
An optional batch size to write log items in bulk (Default: 1)
.PARAMETER BatchTimeout
An optional batch timeout, in seconds, to send items off for writing if a log item isn't received (Default: 0)
.PARAMETER MaxDays
The maximum number of days to keep logs, before Pode automatically removes them.
.PARAMETER MaxSize
The maximum size of a log file, before Pode starts writing to a new log file.
.PARAMETER Custom
If supplied, will allow you to create a Custom Logging output method.
.PARAMETER ScriptBlock
The ScriptBlock that defines how to output a log item.
.PARAMETER ArgumentList
An array of arguments to supply to the Custom Logging output method's ScriptBlock.
.EXAMPLE
$term_logging = New-PodeLoggingMethod -Terminal
.EXAMPLE
$file_logging = New-PodeLoggingMethod -File -Path ./logs -Name 'requests'
.EXAMPLE
$custom_logging = New-PodeLoggingMethod -Custom -ScriptBlock { /* logic */ }
#>
function New-PodeLoggingMethod {
[CmdletBinding(DefaultParameterSetName = 'Terminal')]
[OutputType([hashtable])]
param(
[Parameter(ParameterSetName = 'Terminal')]
[switch]
$Terminal,
[Parameter(ParameterSetName = 'File')]
[switch]
$File,
[Parameter(ParameterSetName = 'File')]
[string]
$Path = './logs',
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$Name,
[Parameter(ParameterSetName = 'EventViewer')]
[switch]
$EventViewer,
[Parameter(ParameterSetName = 'EventViewer')]
[string]
$EventLogName = 'Application',
[Parameter(ParameterSetName = 'EventViewer')]
[string]
$Source = 'Pode',
[Parameter(ParameterSetName = 'EventViewer')]
[int]
$EventID = 0,
[Parameter()]
[int]
$Batch = 1,
[Parameter()]
[int]
$BatchTimeout = 0,
[Parameter(ParameterSetName = 'File')]
[ValidateScript({
if ($_ -lt 0) {
throw "MaxDays must be 0 or greater, but got: $($_)s"
}
return $true
})]
[int]
$MaxDays = 0,
[Parameter(ParameterSetName = 'File')]
[ValidateScript({
if ($_ -lt 0) {
throw "MaxSize must be 0 or greater, but got: $($_)s"
}
return $true
})]
[int]
$MaxSize = 0,
[Parameter(ParameterSetName = 'Custom')]
[switch]
$Custom,
[Parameter(Mandatory = $true, ParameterSetName = 'Custom')]
[ValidateScript({
if (Test-PodeIsEmpty $_) {
throw 'A non-empty ScriptBlock is required for the Custom logging output method'
}
return $true
})]
[scriptblock]
$ScriptBlock,
[Parameter(ParameterSetName = 'Custom')]
[object[]]
$ArgumentList
)
# batch details
$batchInfo = @{
Size = $Batch
Timeout = $BatchTimeout
LastUpdate = $null
Items = @()
RawItems = @()
}
# return info on appropriate logging type
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'terminal' {
return @{
ScriptBlock = (Get-PodeLoggingTerminalMethod)
Batch = $batchInfo
Arguments = @{}
}
}
'file' {
$Path = (Protect-PodeValue -Value $Path -Default './logs')
$Path = (Get-PodeRelativePath -Path $Path -JoinRoot)
$null = New-Item -Path $Path -ItemType Directory -Force
return @{
ScriptBlock = (Get-PodeLoggingFileMethod)
Batch = $batchInfo
Arguments = @{
Name = $Name
Path = $Path
MaxDays = $MaxDays
MaxSize = $MaxSize
FileId = 0
Date = $null
NextClearDown = [datetime]::Now.Date
}
}
}
'eventviewer' {
# only windows
if (!(Test-PodeIsWindows)) {
throw 'Event Viewer logging only supported on Windows'
}
# create source
if (![System.Diagnostics.EventLog]::SourceExists($Source)) {
$null = [System.Diagnostics.EventLog]::CreateEventSource($Source, $EventLogName)
}
return @{
ScriptBlock = (Get-PodeLoggingEventViewerMethod)
Batch = $batchInfo
Arguments = @{
LogName = $EventLogName
Source = $Source
ID = $EventID
}
}
}
'custom' {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
return @{
ScriptBlock = $ScriptBlock
UsingVariables = $usingVars
Batch = $batchInfo
Arguments = $ArgumentList
}
}
}
}
<#
.SYNOPSIS
Enables Request Logging using a supplied output method.
.DESCRIPTION
Enables Request Logging using a supplied output method.
.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).
.PARAMETER UsernameProperty
An optional property path within the $WebEvent.Auth.User object for the user's Username. (Default: Username).
.PARAMETER Raw
If supplied, the log item returned will be the raw Request item as a hashtable and not a string (for Custom methods).
.EXAMPLE
New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
#>
function Enable-PodeRequestLogging {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Method,
[Parameter()]
[string]
$UsernameProperty,
[switch]
$Raw
)
Test-PodeIsServerless -FunctionName 'Enable-PodeRequestLogging' -ThrowError
$name = Get-PodeRequestLoggingName
# error if it's already enabled
if ($PodeContext.Server.Logging.Types.Contains($name)) {
throw 'Request Logging has already been enabled'
}
# ensure the Method contains a scriptblock
if (Test-PodeIsEmpty $Method.ScriptBlock) {
throw 'The supplied output Method for Request Logging requires a valid ScriptBlock'
}
# username property
if ([string]::IsNullOrWhiteSpace($UsernameProperty)) {
$UsernameProperty = 'Username'
}
# add the request logger
$PodeContext.Server.Logging.Types[$name] = @{
Method = $Method
ScriptBlock = (Get-PodeLoggingInbuiltType -Type Requests)
Properties = @{
Username = $UsernameProperty
}
Arguments = @{
Raw = $Raw
}
}
}
<#
.SYNOPSIS
Disables Request Logging.
.DESCRIPTION
Disables Request Logging.
.EXAMPLE
Disable-PodeRequestLogging
#>
function Disable-PodeRequestLogging {
[CmdletBinding()]
param()
Remove-PodeLogger -Name (Get-PodeRequestLoggingName)
}
<#
.SYNOPSIS
Enables Error Logging using a supplied output method.
.DESCRIPTION
Enables Error Logging using a supplied output method.
.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).
.PARAMETER Levels
The Levels of errors that should be logged (default is Error).
.PARAMETER Raw
If supplied, the log item returned will be the raw Error item as a hashtable and not a string (for Custom methods).
.EXAMPLE
New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
#>
function Enable-PodeErrorLogging {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Method,
[Parameter()]
[ValidateNotNullOrEmpty()]
[ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug', '*')]
[string[]]
$Levels = @('Error'),
[switch]
$Raw
)
$name = Get-PodeErrorLoggingName
# error if it's already enabled
if ($PodeContext.Server.Logging.Types.Contains($name)) {
throw 'Error Logging has already been enabled'
}
# ensure the Method contains a scriptblock
if (Test-PodeIsEmpty $Method.ScriptBlock) {
throw 'The supplied output Method for Error Logging requires a valid ScriptBlock'
}
# all errors?
if ($Levels -contains '*') {
$Levels = @('Error', 'Warning', 'Informational', 'Verbose', 'Debug')
}
# add the error logger
$PodeContext.Server.Logging.Types[$name] = @{
Method = $Method
ScriptBlock = (Get-PodeLoggingInbuiltType -Type Errors)
Arguments = @{
Raw = $Raw
Levels = $Levels
}
}
}
<#
.SYNOPSIS
Disables Error Logging.
.DESCRIPTION
Disables Error Logging.
.EXAMPLE
Disable-PodeErrorLogging
#>
function Disable-PodeErrorLogging {
[CmdletBinding()]
param()
Remove-PodeLogger -Name (Get-PodeErrorLoggingName)
}
<#
.SYNOPSIS
Adds a custom Logging method for parsing custom log items.
.DESCRIPTION
Adds a custom Logging method for parsing custom log items.
.PARAMETER Name
A unique Name for the Logging method.
.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).
.PARAMETER ScriptBlock
The ScriptBlock defining logic that transforms an item, and returns it for outputting.
.PARAMETER ArgumentList
An array of arguments to supply to the Custom Logger's ScriptBlock.
.EXAMPLE
New-PodeLoggingMethod -Terminal | Add-PodeLogger -Name 'Main' -ScriptBlock { /* logic */ }
#>
function Add-PodeLogger {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Method,
[Parameter(Mandatory = $true)]
[ValidateScript({
if (Test-PodeIsEmpty $_) {
throw 'A non-empty ScriptBlock is required for the logging method'
}
return $true
})]
[scriptblock]
$ScriptBlock,
[Parameter()]
[object[]]
$ArgumentList
)
# ensure the name doesn't already exist
if ($PodeContext.Server.Logging.Types.ContainsKey($Name)) {
throw "Logging method already defined: $($Name)"
}
# ensure the Method contains a scriptblock
if (Test-PodeIsEmpty $Method.ScriptBlock) {
throw "The supplied output Method for the '$($Name)' Logging method requires a valid ScriptBlock"
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add logging method to server
$PodeContext.Server.Logging.Types[$Name] = @{
Method = $Method
ScriptBlock = $ScriptBlock
UsingVariables = $usingVars
Arguments = $ArgumentList
}
}
<#
.SYNOPSIS
Removes a configured Logging method.
.DESCRIPTION
Removes a configured Logging method.
.PARAMETER Name
The Name of the Logging method.
.EXAMPLE
Remove-PodeLogger -Name 'LogName'
#>
function Remove-PodeLogger {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name
)
$null = $PodeContext.Server.Logging.Types.Remove($Name)
}
<#
.SYNOPSIS
Clears all Logging methods that have been configured.
.DESCRIPTION
Clears all Logging methods that have been configured.
.EXAMPLE
Clear-PodeLoggers
#>
function Clear-PodeLoggers {
[CmdletBinding()]
param()
$PodeContext.Server.Logging.Types.Clear()
}
<#
.SYNOPSIS
Writes and Exception or ErrorRecord using the inbuilt error logging.
.DESCRIPTION
Writes and Exception or ErrorRecord using the inbuilt error logging.
.PARAMETER Exception
An Exception to write.
.PARAMETER ErrorRecord
An ErrorRecord to write.
.PARAMETER Level
The Level of the error being logged.
.PARAMETER CheckInnerException
If supplied, any exceptions are check for inner exceptions. If one is present, this is also logged.
.EXAMPLE
try { /* logic */ } catch { $_ | Write-PodeErrorLog }
.EXAMPLE
[System.Exception]::new('error message') | Write-PodeErrorLog
#>
function Write-PodeErrorLog {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')]
[System.Exception]
$Exception,
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Error')]
[System.Management.Automation.ErrorRecord]
$ErrorRecord,
[Parameter()]
[ValidateNotNullOrEmpty()]
[ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug')]
[string]
$Level = 'Error',
[Parameter(ParameterSetName = 'Exception')]
[switch]
$CheckInnerException
)
# do nothing if logging is disabled, or error logging isn't setup
$name = Get-PodeErrorLoggingName
if (!(Test-PodeLoggerEnabled -Name $name)) {
return
}
# do nothing if the error level isn't present
$levels = @(Get-PodeErrorLoggingLevels)
if ($levels -inotcontains $Level) {
return
}
# build error object for what we need
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'exception' {
$item = @{
Category = $Exception.Source
Message = $Exception.Message
StackTrace = $Exception.StackTrace
}
}
'error' {
$item = @{
Category = $ErrorRecord.CategoryInfo.ToString()
Message = $ErrorRecord.Exception.Message
StackTrace = $ErrorRecord.ScriptStackTrace
}
}
}
# add general info
$item['Server'] = $PodeContext.Server.ComputerName
$item['Level'] = $Level
$item['Date'] = [datetime]::Now
$item['ThreadId'] = [int]$ThreadId
# add the item to be processed
$null = $PodeContext.LogsToProcess.Add(@{
Name = $name
Item = $item
})
# for exceptions, check the inner exception
if ($CheckInnerException -and ($null -ne $Exception.InnerException) -and ![string]::IsNullOrWhiteSpace($Exception.InnerException.Message)) {
$Exception.InnerException | Write-PodeErrorLog
}
}
<#
.SYNOPSIS
Write an object to a configured custom Logging method.
.DESCRIPTION
Write an object to a configured custom Logging method.
.PARAMETER Name
The Name of the Logging method.
.PARAMETER InputObject
The Object to write.
.EXAMPLE
$object | Write-PodeLog -Name 'LogName'
#>
function Write-PodeLog {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]
$InputObject
)
# do nothing if logging is disabled, or logger isn't setup
if (!(Test-PodeLoggerEnabled -Name $Name)) {
return
}
# add the item to be processed
$null = $PodeContext.LogsToProcess.Add(@{
Name = $Name
Item = $InputObject
})
}
<#
.SYNOPSIS
Masks values within a log item to protect sensitive information.
.DESCRIPTION
Masks values within a log item, or any string, to protect sensitive information.
Patterns, and the Mask, can be configured via the server.psd1 configuration file.
.PARAMETER Item
The string Item to mask values.
.EXAMPLE
$value = Protect-PodeLogItem -Item 'Username=Morty, Password=Hunter2'
#>
function Protect-PodeLogItem {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[string]
$Item
)
# do nothing if there are no masks
if (Test-PodeIsEmpty $PodeContext.Server.Logging.Masking.Patterns) {
return $item
}
# attempt to apply each mask
foreach ($mask in $PodeContext.Server.Logging.Masking.Patterns) {
if ($Item -imatch $mask) {
# has both keep before/after
if ($Matches.ContainsKey('keep_before') -and $Matches.ContainsKey('keep_after')) {
$Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}")
}
# has just keep before
elseif ($Matches.ContainsKey('keep_before')) {
$Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)")
}
# has just keep after
elseif ($Matches.ContainsKey('keep_after')) {
$Item = ($Item -ireplace $mask, "$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}")
}
# normal mask
else {
$Item = ($Item -ireplace $mask, $PodeContext.Server.Logging.Masking.Mask)
}
}
}
return $Item
}
<#
.SYNOPSIS
Automatically loads logging ps1 files
.DESCRIPTION
Automatically loads logging ps1 files from either a /logging folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeLogging
.EXAMPLE
Use-PodeLogging -Path './my-logging'
#>
function Use-PodeLogging {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'logging'
}
<#
.SYNOPSIS
Returns the uptime of the server in milliseconds.
.DESCRIPTION
Returns the uptime of the server in milliseconds. You can optionally return the total uptime regardless of server restarts.
.PARAMETER Total
If supplied, the total uptime of the server will be returned, regardless of restarts.
.EXAMPLE
$currentUptime = Get-PodeServerUptime
.EXAMPLE
$totalUptime = Get-PodeServerUptime -Total
#>
function Get-PodeServerUptime {
[CmdletBinding()]
param(
[switch]
$Total
)
$time = $PodeContext.Metrics.Server.StartTime
if ($Total) {
$time = $PodeContext.Metrics.Server.InitialLoadTime
}
return [long]([datetime]::UtcNow - $time).TotalMilliseconds
}
<#
.SYNOPSIS
Returns the number of times the server has restarted.
.DESCRIPTION
Returns the number of times the server has restarted.
.EXAMPLE
$restarts = Get-PodeServerRestartCount
#>
function Get-PodeServerRestartCount {
[CmdletBinding()]
param()
return $PodeContext.Metrics.Server.RestartCount
}
<#
.SYNOPSIS
Returns the total number of requests/per status code the Server has receieved.
.DESCRIPTION
Returns the total number of requests/per status code the Server has receieved.
.PARAMETER StatusCode
If supplied, will return the total number of requests for a specific StatusCode.
.PARAMETER Total
If supplied, will return the Total number of Requests.
.EXAMPLE
$totalReqs = Get-PodeServerRequestMetric -Total
.EXAMPLE
$statusReqs = Get-PodeServerRequestMetric
.EXAMPLE
$404Reqs = Get-PodeServerRequestMetric -StatusCode 404
#>
function Get-PodeServerRequestMetric {
[CmdletBinding(DefaultParameterSetName = 'StatusCode')]
param(
[Parameter(ParameterSetName = 'StatusCode')]
[int]
$StatusCode = 0,
[Parameter(ParameterSetName = 'Total')]
[switch]
$Total
)
if ($Total) {
return $PodeContext.Metrics.Requests.Total
}
if (($StatusCode -le 0)) {
return $PodeContext.Metrics.Requests.StatusCodes
}
$strCode = "$($StatusCode)"
if (!$PodeContext.Metrics.Requests.StatusCodes.ContainsKey($strCode)) {
return 0
}
return $PodeContext.Metrics.Requests.StatusCodes[$strCode]
}
<#
.SYNOPSIS
Returns the total number of Signal requests the Server has receieved.
.DESCRIPTION
Returns the total number of Signal requests the Server has receieved.
.EXAMPLE
$totalReqs = Get-PodeServerSignalMetric
#>
function Get-PodeServerSignalMetric {
[CmdletBinding()]
param()
return $PodeContext.Metrics.Signals.Total
}
<#
.SYNOPSIS
Returns the count of active requests.
.DESCRIPTION
Returns the count of all, processing, or queued active requests.
.PARAMETER CountType
The count type to return. (Default: Total)
.EXAMPLE
Get-PodeServerActiveRequestMetric
.EXAMPLE
Get-PodeServerActiveRequestMetric -CountType Queued
#>
function Get-PodeServerActiveRequestMetric {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Total', 'Queued', 'Processing')]
[string]
$CountType = 'Total'
)
switch ($CountType.ToLowerInvariant()) {
'total' {
return $PodeContext.Server.Signals.Listener.Contexts.Count
}
'queued' {
return $PodeContext.Server.Signals.Listener.Contexts.QueuedCount
}
'processing' {
return $PodeContext.Server.Signals.Listener.Contexts.ProcessingCount
}
}
}
<#
.SYNOPSIS
Returns the count of active signals.
.DESCRIPTION
Returns the count of all, processing, or queued active signals; for either server or client signals.
.PARAMETER Type
The type of signal to return. (Default: Total)
.PARAMETER CountType
The count type to return. (Default: Total)
.EXAMPLE
Get-PodeServerActiveSignalMetric
.EXAMPLE
Get-PodeServerActiveSignalMetric -Type Client -CountType Queued
#>
function Get-PodeServerActiveSignalMetric {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Total', 'Server', 'Client')]
[string]
$Type = 'Total',
[Parameter()]
[ValidateSet('Total', 'Queued', 'Processing')]
[string]
$CountType = 'Total'
)
switch ($Type.ToLowerInvariant()) {
'total' {
switch ($CountType.ToLowerInvariant()) {
'total' {
return $PodeContext.Server.Signals.Listener.ServerSignals.Count + $PodeContext.Server.Signals.Listener.ClientSignals.Count
}
'queued' {
return $PodeContext.Server.Signals.Listener.ServerSignals.QueuedCount + $PodeContext.Server.Signals.Listener.ClientSignals.QueuedCount
}
'processing' {
return $PodeContext.Server.Signals.Listener.ServerSignals.ProcessingCount + $PodeContext.Server.Signals.Listener.ClientSignals.ProcessingCount
}
}
}
'server' {
switch ($CountType.ToLowerInvariant()) {
'total' {
return $PodeContext.Server.Signals.Listener.ServerSignals.Count
}
'queued' {
return $PodeContext.Server.Signals.Listener.ServerSignals.QueuedCount
}
'processing' {
return $PodeContext.Server.Signals.Listener.ServerSignals.ProcessingCount
}
}
}
'client' {
switch ($CountType.ToLowerInvariant()) {
'total' {
return $PodeContext.Server.Signals.Listener.ClientSignals.Count
}
'queued' {
return $PodeContext.Server.Signals.Listener.ClientSignals.QueuedCount
}
'processing' {
return $PodeContext.Server.Signals.Listener.ClientSignals.ProcessingCount
}
}
}
}
}
<#
.SYNOPSIS
Adds an access rule to allow or deny IP addresses.
.DESCRIPTION
Adds an access rule to allow or deny IP addresses.
.PARAMETER Access
The type of access to enable.
.PARAMETER Type
What type of request are we configuring?
.PARAMETER Values
A single, or an array of values.
.EXAMPLE
Add-PodeAccessRule -Access Allow -Type IP -Values '127.0.0.1'
.EXAMPLE
Add-PodeAccessRule -Access Deny -Type IP -Values @('192.168.1.1', '10.10.1.0/24')
#>
function Add-PodeAccessRule {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Allow', 'Deny')]
[string]
$Access,
[Parameter(Mandatory = $true)]
[ValidateSet('IP')]
[string]
$Type,
[Parameter(Mandatory = $true)]
[string[]]
$Values
)
# error if serverless
Test-PodeIsServerless -FunctionName 'Add-PodeAccessRule' -ThrowError
# call the appropriate access method
switch ($Type.ToLowerInvariant()) {
'ip' {
foreach ($ip in $Values) {
Add-PodeIPAccess -Access $Access -IP $ip
}
}
}
}
<#
.SYNOPSIS
Adds rate limiting rules for an IP addresses, Routes, or Endpoints.
.DESCRIPTION
Adds rate limiting rules for an IP addresses, Routes, or Endpoints.
.PARAMETER Type
What type of request is being rate limited: IP, Route, or Endpoint?
.PARAMETER Values
A single, or an array of values.
.PARAMETER Limit
The maximum number of requests to allow.
.PARAMETER Seconds
The number of seconds to count requests before restarting the count.
.PARAMETER Group
If supplied, groups of IPs in a subnet will be considered as one IP.
.EXAMPLE
Add-PodeLimitRule -Type IP -Values '127.0.0.1' -Limit 10 -Seconds 1
.EXAMPLE
Add-PodeLimitRule -Type IP -Values @('192.168.1.1', '10.10.1.0/24') -Limit 50 -Seconds 1 -Group
.EXAMPLE
Add-PodeLimitRule -Type Route -Values '/downloads' -Limit 5 -Seconds 1
#>
function Add-PodeLimitRule {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('IP', 'Route', 'Endpoint')]
[string]
$Type,
[Parameter(Mandatory = $true)]
[string[]]
$Values,
[Parameter(Mandatory = $true)]
[int]
$Limit,
[Parameter(Mandatory = $true)]
[int]
$Seconds,
[switch]
$Group
)
# call the appropriate limit method
foreach ($value in $Values) {
switch ($Type.ToLowerInvariant()) {
'ip' {
Test-PodeIsServerless -FunctionName 'Add-PodeLimitRule' -ThrowError
Add-PodeIPLimit -IP $value -Limit $Limit -Seconds $Seconds -Group:$Group
}
'route' {
Add-PodeRouteLimit -Path $value -Limit $Limit -Seconds $Seconds -Group:$Group
}
'endpoint' {
Add-PodeEndpointLimit -EndpointName $value -Limit $Limit -Seconds $Seconds -Group:$Group
}
}
}
}
<#
.SYNOPSIS
Creates and returns a new secure token for use with CSRF.
.DESCRIPTION
Creates and returns a new secure token for use with CSRF.
.EXAMPLE
$token = New-PodeCsrfToken
#>
function New-PodeCsrfToken {
[CmdletBinding()]
[OutputType([string])]
param()
# fail if the csrf logic hasn't been initialised
if (!(Test-PodeCsrfConfigured)) {
throw 'CSRF Middleware has not been initialised'
}
# generate a new secret and salt
$Secret = New-PodeCsrfSecret
$Salt = (New-PodeSalt -Length 8)
# return a new token
return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
}
<#
.SYNOPSIS
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.
.DESCRIPTION
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.
.EXAMPLE
$csrf = Get-PodeCsrfMiddleware
Add-PodeRoute -Method Get -Path '/cpu' -Middleware $csrf -ScriptBlock { /* logic */ }
#>
function Get-PodeCsrfMiddleware {
[CmdletBinding()]
[OutputType([hashtable])]
param()
# fail if the csrf logic hasn't been initialised
if (!(Test-PodeCsrfConfigured)) {
throw 'CSRF Middleware has not been initialised'
}
# return scriptblock for the csrf route middleware to test tokens
$script = {
# if there's not a secret, generate and store it
$secret = New-PodeCsrfSecret
# verify the token on the request, if invalid, throw a 403
$token = Get-PodeCsrfToken
if (!(Test-PodeCsrfToken -Secret $secret -Token $token)) {
Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
return $false
}
# token is valid, move along
return $true
}
return (New-PodeMiddleware -ScriptBlock $script)
}
<#
.SYNOPSIS
Initialises CSRF within Pode for adhoc usage.
.DESCRIPTION
Initialises CSRF within Pode for adhoc usage, with configurable HTTP methods to ignore verification.
.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.
.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.
.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.
.EXAMPLE
Initialize-PodeCsrf -IgnoreMethods @('Get', 'Trace')
.EXAMPLE
Initialize-PodeCsrf -Secret 'some-secret' -UseCookies
#>
function Initialize-PodeCsrf {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
[string[]]
$IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),
[Parameter()]
[string]
$Secret,
[switch]
$UseCookies
)
# check that csrf logic hasn't already been intialised
if (Test-PodeCsrfConfigured) {
return
}
# if sessions haven't been setup and we're not using cookies, error
if (!$UseCookies -and !(Test-PodeSessionsEnabled)) {
throw 'Sessions are required to use CSRF unless you want to use cookies'
}
# if we're using cookies, ensure a global secret exists
if ($UseCookies) {
$Secret = (Protect-PodeValue -Value $Secret -Default (Get-PodeCookieSecret -Global))
if (Test-PodeIsEmpty $Secret) {
throw "When using cookies for CSRF, a Secret is required. You can either supply a Secret, or set the Cookie global secret - (Set-PodeCookieSecret '<value>' -Global)"
}
}
# set the options against the server context
$PodeContext.Server.Cookies.Csrf = @{
Name = 'pode.csrf'
UseCookies = $UseCookies
Secret = $Secret
IgnoredMethods = $IgnoreMethods
}
}
<#
.SYNOPSIS
Enables Middleware for verifying CSRF tokens on Requests.
.DESCRIPTION
Enables Middleware for verifying CSRF tokens on Requests, with configurable HTTP methods to ignore verification.
.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.
.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.
.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.
.EXAMPLE
Enable-PodeCsrfMiddleware -IgnoreMethods @('Get', 'Trace')
.EXAMPLE
Enable-PodeCsrfMiddleware -Secret 'some-secret' -UseCookies
#>
function Enable-PodeCsrfMiddleware {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
[string[]]
$IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),
[Parameter(ParameterSetName = 'Cookies')]
[string]
$Secret,
[Parameter(ParameterSetName = 'Cookies')]
[switch]
$UseCookies
)
Initialize-PodeCsrf -IgnoreMethods $IgnoreMethods -Secret $Secret -UseCookies:$UseCookies
# return scriptblock for the csrf middleware
$script = {
# if the current route method is ignored, just return
$ignored = @($PodeContext.Server.Cookies.Csrf.IgnoredMethods)
if (!(Test-PodeIsEmpty $ignored) -and ($ignored -icontains $WebEvent.Method)) {
return $true
}
# if there's not a secret, generate and store it
$secret = New-PodeCsrfSecret
# verify the token on the request, if invalid, throw a 403
$token = Get-PodeCsrfToken
if (!(Test-PodeCsrfToken -Secret $secret -Token $token)) {
Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
return $false
}
# token is valid, move along
return $true
}
(New-PodeMiddleware -ScriptBlock $script) | Add-PodeMiddleware -Name '__pode_mw_csrf__'
}
<#
.SYNOPSIS
Adds a custom body parser middleware.
.DESCRIPTION
Adds a custom body parser middleware script for a content-type, which will be used if a payload is sent with a Request.
.PARAMETER ContentType
The ContentType of the custom body parser.
.PARAMETER ScriptBlock
The ScriptBlock that will parse the body content, and return the result.
.EXAMPLE
Add-PodeBodyParser -ContentType 'application/json' -ScriptBlock { param($body) /* parsing logic */ }
#>
function Add-PodeBodyParser {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^\w+\/[\w\.\+-]+$')]
[string]
$ContentType,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[scriptblock]
$ScriptBlock
)
# if a parser for the type already exists, fail
if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
throw "There is already a body parser defined for the $($ContentType) content-type"
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
$PodeContext.Server.BodyParsers[$ContentType] = @{
ScriptBlock = $ScriptBlock
UsingVariables = $usingVars
}
}
<#
.SYNOPSIS
Removes a custom body parser.
.DESCRIPTION
Removes a custom body parser middleware script for a content-type.
.PARAMETER ContentType
The ContentType of the custom body parser.
.EXAMPLE
Remove-PodeBodyParser -ContentType 'application/json'
#>
function Remove-PodeBodyParser {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidatePattern('^\w+\/[\w\.\+-]+$')]
[string]
$ContentType
)
# if there's no parser for the type, return
if (!$PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
return
}
$null = $PodeContext.Server.BodyParsers.Remove($ContentType)
}
<#
.SYNOPSIS
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. 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. 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.
.PARAMETER Route
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 */ }
.EXAMPLE
Add-PodeMiddleware -Name 'CheckEmailOnApi' -Route '/api/*' -ScriptBlock { /* logic */ }
#>
function Add-PodeMiddleware {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'Input', ValueFromPipeline = $true)]
[hashtable]
$InputObject,
[Parameter()]
[string]
$Route,
[Parameter()]
[object[]]
$ArgumentList
)
# ensure name doesn't already exist
if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) {
throw "[Middleware] $($Name): Middleware already defined"
}
# if it's a script - call New-PodeMiddleware
if ($PSCmdlet.ParameterSetName -ieq 'script') {
$InputObject = (New-PodeMiddlewareInternal `
-ScriptBlock $ScriptBlock `
-Route $Route `
-ArgumentList $ArgumentList `
-PSSession $PSCmdlet.SessionState)
}
else {
$Route = ConvertTo-PodeRouteRegex -Path $Route
$InputObject.Route = Protect-PodeValue -Value $Route -Default $InputObject.Route
$InputObject.Options = Protect-PodeValue -Value $Options -Default $InputObject.Options
}
# ensure we have a script to run
if (Test-PodeIsEmpty $InputObject.Logic) {
throw '[Middleware]: No logic supplied in ScriptBlock'
}
# set name, and override route/args
$InputObject.Name = $Name
# add the logic to array of middleware that needs to be run
$PodeContext.Server.Middleware += $InputObject
}
<#
.SYNOPSIS
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. ScriptBlock should return $true to continue execution, or $false to stop.
.PARAMETER ScriptBlock
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.
.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'
#>
function New-PodeMiddleware {
[CmdletBinding()]
[OutputType([hashtable])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[string]
$Route,
[Parameter()]
[object[]]
$ArgumentList
)
return New-PodeMiddlewareInternal `
-ScriptBlock $ScriptBlock `
-Route $Route `
-ArgumentList $ArgumentList `
-PSSession $PSCmdlet.SessionState
}
<#
.SYNOPSIS
Removes a specific user defined Middleware.
.DESCRIPTION
Removes a specific user defined Middleware.
.PARAMETER Name
The Name of the Middleware to be removed.
.EXAMPLE
Remove-PodeMiddleware -Name 'Sessions'
#>
function Remove-PodeMiddleware {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$PodeContext.Server.Middleware = @($PodeContext.Server.Middleware | Where-Object { $_.Name -ine $Name })
}
<#
.SYNOPSIS
Removes all user defined Middleware.
.DESCRIPTION
Removes all user defined Middleware.
.EXAMPLE
Clear-PodeMiddleware
#>
function Clear-PodeMiddleware {
[CmdletBinding()]
param()
$PodeContext.Server.Middleware = @()
}
<#
.SYNOPSIS
Automatically loads middleware ps1 files
.DESCRIPTION
Automatically loads middleware ps1 files from either a /middleware folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeMiddleware
.EXAMPLE
Use-PodeMiddleware -Path './my-middleware'
#>
function Use-PodeMiddleware {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'middleware'
}
<#
.SYNOPSIS
Adds a reusable component for responses.
.DESCRIPTION
Adds a reusable component for responses.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.LINK
https://swagger.io/docs/specification/serialization/
.PARAMETER Name
The reference Name of the response.
.PARAMETER Content
The content-types and schema the response returns (the schema is created using the Property functions).
.PARAMETER Headers
The header name and schema the response returns (the schema is created using the Add-PodeOAComponentHeader cmdlet).
.PARAMETER Description
The Description of the response.
.PARAMETER Reference
A Reference Name of an existing component response to use.
.PARAMETER Links
A Response link definition
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAComponentResponse -Name 'OKResponse' -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
.EXAMPLE
Add-PodeOAComponentResponse -Name 'ErrorResponse' -Content @{ 'application/json' = 'ErrorSchema' }
#>
function Add-PodeOAComponentResponse {
[CmdletBinding(DefaultParameterSetName = 'Schema')]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(ParameterSetName = 'Schema')]
[Alias('ContentSchemas')]
[hashtable]
$Content,
[Parameter(ParameterSetName = 'Schema')]
[Alias('HeaderSchemas')]
[AllowEmptyString()]
[ValidateNotNullOrEmpty()]
[ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] })]
$Headers,
[Parameter(ParameterSetName = 'Schema')]
[string]
$Description,
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[string]
$Reference,
[Parameter(ParameterSetName = 'Schema')]
[System.Collections.Specialized.OrderedDictionary ]
$Links,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$PodeContext.Server.OpenAPI.Definitions[$tag].components.responses[$Name] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters
}
}
<#
.SYNOPSIS
Adds a reusable component schema
.DESCRIPTION
Adds a reusable component schema.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.LINK
https://swagger.io/docs/specification/serialization/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER Name
The reference Name of the schema.
.PARAMETER Component
The Component definition (the schema is created using the Property functions).
.PARAMETER Description
A description of the schema
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAComponentSchema -Name 'UserIdSchema' -Component (New-PodeOAIntProperty -Name 'userId' -Object)
#>
function Add-PodeOAComponentSchema {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[Alias('Schema')]
[hashtable]
$Component,
[string]
$Description,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name] = ($Component | ConvertTo-PodeOASchemaProperty -DefinitionTag $tag)
if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaValidation) {
try {
$modifiedComponent = ($Component | ConvertTo-PodeOASchemaProperty -DefinitionTag $tag) | Resolve-PodeOAReference -DefinitionTag $tag
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson[$Name] = @{
'available' = $true
'schema' = $modifiedComponent
'json' = $modifiedComponent | ConvertTo-Json -depth $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.depth
}
}
catch {
if ($_.ToString().StartsWith('Validation of schema with')) {
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.schemaJson[$Name] = @{
'available' = $false
}
}
}
}
if ($Description) {
$PodeContext.Server.OpenAPI.Definitions[$tag].components.schemas[$Name].description = $Description
}
}
}
<#
.SYNOPSIS
Adds a reusable component for a Header schema.
.DESCRIPTION
Adds a reusable component for a Header schema.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.LINK
https://swagger.io/docs/specification/serialization/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER Name
The reference Name of the schema.
.PARAMETER Schema
The Schema definition (the schema is created using the Property functions).
.PARAMETER Description
A description of the header
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAComponentHeader -Name 'UserIdSchema' -Schema (New-PodeOAIntProperty -Name 'userId' -Object)
#>
function Add-PodeOAComponentHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter()]
[string]
$Description,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Schema,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$param = [ordered]@{
'schema' = ($Schema | ConvertTo-PodeOASchemaProperty -NoDescription -DefinitionTag $tag)
}
if ( $Description) {
$param['description'] = $Description
}
$PodeContext.Server.OpenAPI.Definitions[$tag].components.headers[$Name] = $param
}
}
<#
.SYNOPSIS
Adds a reusable component for a request body.
.DESCRIPTION
Adds a reusable component for a request body.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.LINK
https://swagger.io/docs/specification/describing-request-body/
.PARAMETER Name
The reference Name of the request body.
.PARAMETER Content
The content-types and schema the request body accepts (the schema is created using the Property functions).
.PARAMETER Description
A Description of the request body.
.PARAMETER Required
If supplied, the request body will be flagged as required.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAComponentRequestBody -Name 'UserIdBody' -ContentSchemas @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
.EXAMPLE
Add-PodeOAComponentRequestBody -Name 'UserIdBody' -ContentSchemas @{ 'application/json' = 'UserIdSchema' }
#>
function Add-PodeOAComponentRequestBody {
[CmdletBinding()]
[OutputType([System.Collections.Specialized.OrderedDictionary])]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[Alias('ContentSchemas')]
[hashtable]
$Content,
[Parameter()]
[string]
$Description ,
[Parameter()]
[switch]
$Required,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$param = [ordered]@{ content = ($Content | ConvertTo-PodeOAObjectSchema -DefinitionTag $tag) }
if ($Required.IsPresent) {
$param['required'] = $Required.IsPresent
}
if ( $Description) {
$param['description'] = $Description
}
$PodeContext.Server.OpenAPI.Definitions[$tag].components.requestBodies[$Name] = $param
}
}
<#
.SYNOPSIS
Adds a reusable component for a request parameter.
.DESCRIPTION
Adds a reusable component for a request parameter.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.LINK
https://swagger.io/docs/specification/describing-parameters/
.PARAMETER Name
The reference Name of the parameter.
.PARAMETER Parameter
The Parameter to use for the component (from ConvertTo-PodeOAParameter)
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
New-PodeOAIntProperty -Name 'userId' | ConvertTo-PodeOAParameter -In Query | Add-PodeOAComponentParameter -Name 'UserIdParam'
#>
function Add-PodeOAComponentParameter {
[CmdletBinding()]
param(
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Parameter,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
if ([string]::IsNullOrWhiteSpace($Name)) {
if ($Parameter.name) {
$Name = $Parameter.name
}
else {
throw 'The Parameter has no name. Please provide a name to this component using -Name property'
}
}
$PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters[$Name] = $Parameter
}
}
<#
.SYNOPSIS
Adds a reusable example component.
.DESCRIPTION
Adds a reusable example component.
.PARAMETER Name
The Name of the Example.
.PARAMETER Summary
Short description for the example
.PARAMETER Description
Long description for the example.
.PARAMETER Value
Embedded literal example. The value Parameter and ExternalValue parameter are mutually exclusive.
To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary.
.PARAMETER ExternalValue
A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents.
The -Value parameter and -ExternalValue parameter are mutually exclusive.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. |
.EXAMPLE
Add-PodeOAComponentExample -name 'frog-example' -Summary "An example of a frog with a cat's name" -Value @{name = 'Jaguar'; petType = 'Panthera'; color = 'Lion'; gender = 'Male'; breed = 'Mantella Baroni' }
#>
function Add-PodeOAComponentExample {
[CmdletBinding(DefaultParameterSetName = 'Value')]
param(
[Parameter(Mandatory = $true)]
[Alias('Title')]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[string]
$Summary,
[Parameter()]
[string]
$Description,
[Parameter(Mandatory = $true, ParameterSetName = 'Value')]
[object]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'ExternalValue')]
[string]
$ExternalValue,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$Example = [ordered]@{ }
if ($Summary) {
$Example.summary = $Summary
}
if ($Description) {
$Example.description = $Description
}
if ($Value) {
$Example.value = $Value
}
elseif ($ExternalValue) {
$Example.externalValue = $ExternalValue
}
$PodeContext.Server.OpenAPI.Definitions[$tag].components.examples[$Name] = $Example
}
}
<#
.SYNOPSIS
Adds a reusable response link.
.DESCRIPTION
The Add-PodeOAComponentResponseLink function is designed to add a new reusable response link
.PARAMETER Name
Mandatory. A unique name for the response link.
Must be a valid string composed of alphanumeric characters, periods (.), hyphens (-), and underscores (_).
.PARAMETER Description
A brief description of the response link. CommonMark syntax may be used for rich text representation.
For more information on CommonMark syntax, see [CommonMark Specification](https://spec.commonmark.org/).
.PARAMETER OperationId
The name of an existing, resolvable OpenAPI Specification (OAS) operation, as defined with a unique `operationId`.
This parameter is mandatory when using the 'OperationId' parameter set and is mutually exclusive of the `OperationRef` field. It is used to specify the unique identifier of the operation the link is associated with.
.PARAMETER OperationRef
A relative or absolute URI reference to an OAS operation.
This parameter is mandatory when using the 'OperationRef' parameter set and is mutually exclusive of the `OperationId` field.
It MUST point to an Operation Object. Relative `operationRef` values MAY be used to locate an existing Operation Object in the OpenAPI specification.
.PARAMETER Parameters
A map representing parameters to pass to an operation as specified with `operationId` or identified via `operationRef`.
The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and passed to the linked operation.
Parameter names can be qualified using the parameter location syntax `[{in}.]{name}` for operations that use the same parameter name in different locations (e.g., path.id).
.PARAMETER RequestBody
A string representing the request body to use as a request body when calling the target.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAComponentResponseLink -Name 'address' -OperationId 'getUserByName' -Parameters @{'username' = '$request.path.username'}
Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User'} -Links 'address'
This example demonstrates creating and adding a link named 'address' associated with the operation 'getUserByName' to an OrderedDictionary of links. The updated dictionary is then used in the 'Add-PodeOAResponse' function to define a response with a status code of 200.
.NOTES
The function supports adding links either by specifying an 'OperationId' or an 'OperationRef', making it versatile for different OpenAPI specification needs.
It's important to match the parameters and response structures as per the OpenAPI specification to ensure the correct functionality of the API documentation.
#>
function Add-PodeOAComponentResponseLink {
[CmdletBinding(DefaultParameterSetName = 'OperationId')]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter()]
[string]
$Description,
[Parameter(Mandatory = $true, ParameterSetName = 'OperationId')]
[string]
$OperationId,
[Parameter(Mandatory = $true, ParameterSetName = 'OperationRef')]
[string]
$OperationRef,
[Parameter()]
[hashtable]
$Parameters,
[Parameter()]
[string]
$RequestBody,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$PodeContext.Server.OpenAPI.Definitions[$tag].components.links[$Name] = New-PodeOAResponseLinkInternal -Params $PSBoundParameters
}
}
<#
.SYNOPSIS
Adds OpenAPI reusable callback configurations.
.DESCRIPTION
The Add-PodeOACallBack function is used for defining OpenAPI callback configurations for routes in a Pode server.
It enables setting up API specifications including detailed parameters, request body schemas, and response structures for various HTTP methods.
.PARAMETER Path
Specifies the callback path, usually a relative URL.
The key that identifies the Path Item Object is a runtime expression evaluated in the context of a runtime HTTP request/response to identify the URL for the callback request.
A simple example is `$request.body#/url`.
The runtime expression allows complete access to the HTTP message, including any part of a body that a JSON Pointer (RFC6901) can reference.
More information on JSON Pointer can be found at [RFC6901](https://datatracker.ietf.org/doc/html/rfc6901).
.PARAMETER Name
Alias for 'Name'. A unique identifier for the callback.
It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).
.PARAMETER Method
Defines the HTTP method for the callback (e.g., GET, POST, PUT). Supports standard HTTP methods and a wildcard (*) for all methods.
.PARAMETER Parameters
The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).
.PARAMETER RequestBody
Defines the schema of the request body. Can be set using New-PodeOARequestBody.
.PARAMETER Responses
Defines the possible responses for the callback. Can be set using New-PodeOAResponse.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAComponentCallBack -Title 'test' -Path '{$request.body#/id}' -Method Post `
-RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id')}) `
-Response (
New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array)
New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' |
New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' |
New-PodeOAResponse -Default -Description 'Something is wrong'
)
Add-PodeOACallBack -Reference 'test'
This example demonstrates adding a POST callback to handle a request body and define various responses based on different status codes.
.NOTES
Ensure that the provided parameters match the expected schema and formats of Pode and OpenAPI specifications.
The function is useful for dynamically configuring and documenting API callbacks in a Pode server environment.
#>
function Add-PodeOAComponentCallBack {
param (
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter(Mandatory = $true)]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[hashtable[]]
$Parameters,
[hashtable]
$RequestBody,
[hashtable]
$Responses,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$PodeContext.Server.OpenAPI.Definitions[$tag].components.callbacks.$Name = New-PodeOAComponentCallBackInternal -Params $PSBoundParameters -DefinitionTag $tag
}
}
<#
.SYNOPSIS
Sets metadate for the supplied route.
.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.
.LINK
https://swagger.io/docs/specification/paths-and-operations/
.PARAMETER Name
Alias for 'Name'. A unique identifier for the route.
It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).
.PARAMETER Path
The URI path for the Route.
.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.
.PARAMETER Servers
A list of external endpoint. created with New-PodeOAServerEndpoint
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAExternalRoute -PassThru -Method Get -Path '/peta/:id' -Servers (
New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
) |
Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
(New-PodeOAStringProperty -Name 'id' -Description 'ID of pet to use' -array | ConvertTo-PodeOAParameter -In Path -Style Simple -Required )) |
Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOASchemaProperty -ComponentSchema 'Pet' -array }) -PassThru |
Add-PodeOAResponse -Default -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
.EXAMPLE
Add-PodeOAComponentPathItem -PassThru -Method Get -Path '/peta/:id' -ScriptBlock {
Write-PodeJsonResponse -Value 'done' -StatusCode 200
} | Add-PodeOAExternalRoute -PassThru -Servers (
New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
) |
Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
(New-PodeOAStringProperty -Name 'id' -Description 'ID of pet to use' -array | ConvertTo-PodeOAParameter -In Path -Style Simple -Required )) |
Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOASchemaProperty -ComponentSchema 'Pet' -array }) -PassThru |
Add-PodeOAResponse -Default -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
#>
function Add-PodeOAComponentPathItem {
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true )]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[switch]
$PassThru,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
$refRoute = @{
Method = $Method.ToLower()
NotPrepared = $true
OpenApi = @{
Responses = $null
Parameters = $null
RequestBody = $null
callbacks = [ordered]@{}
Authentication = @()
}
}
foreach ($tag in $DefinitionTag) {
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $tag ) {
throw 'The feature reusable component pathItems is not available in OpenAPI v3.0.x'
}
#add the default OpenApi responses
if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses) {
$refRoute.OpenApi.Responses = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone()
}
$PodeContext.Server.OpenAPI.Definitions[$tag].components.pathItems[$Name] = $refRoute
}
if ($PassThru) {
return $refRoute
}
}
<#
.SYNOPSIS
Check the OpenAPI version
.DESCRIPTION
Check the OpenAPI version for a specific OpenAPI Definition
.PARAMETER Version
The version number to compare
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Test-PodeOAVersion -Version 3.1 -DefinitionTag 'default'
#>
function Test-PodeOAVersion {
param (
[Parameter(Mandatory = $true)]
[ValidateSet( 3.1 , 3.0 )]
[decimal]
$Version,
[Parameter(Mandatory = $true)]
[string[] ]
$DefinitionTag
)
return $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.version -eq $Version
}
<#
.SYNOPSIS
Check the OpenAPI component exist
.DESCRIPTION
Check the OpenAPI component exist
.PARAMETER Field
The component type
.PARAMETER Name
The component Name
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Test-PodeOAComponent -Field 'responses' -Name 'myresponse' -DefinitionTag 'default'
#>
function Test-PodeOAComponent {
param(
[Parameter(Mandatory = $true)]
[ValidateSet( 'schemas' , 'responses' , 'parameters' , 'examples' , 'requestBodies' , 'headers' , 'securitySchemes' , 'links' , 'callbacks' , 'pathItems' )]
[string]
$Field,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
if (!($PodeContext.Server.OpenAPI.Definitions[$tag].components[$field].keys -ccontains $Name)) {
return $false
}
}
if (!$ThrowException.IsPresent) {
return $true
}
}
<#
.SYNOPSIS
Remove an OpenAPI component if exist
.DESCRIPTION
Remove an OpenAPI component if exist
.PARAMETER Field
The component type
.PARAMETER Name
The component Name
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Remove-PodeOAComponent -Field 'responses' -Name 'myresponse' -DefinitionTag 'default'
#>
function Remove-PodeOAComponent {
param(
[Parameter(Mandatory = $true)]
[ValidateSet( 'schemas' , 'responses' , 'parameters' , 'examples' , 'requestBodies' , 'headers' , 'securitySchemes' , 'links' , 'callbacks' , 'pathItems' )]
[string]
$Field,
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
if (!($PodeContext.Server.OpenAPI.Definitions[$tag].components[$field ].keys -ccontains $Name)) {
$PodeContext.Server.OpenAPI.Definitions[$tag].components[$field ].remove($Name)
}
}
}
if (!(Test-Path Alias:Enable-PodeOpenApiViewer)) {
New-Alias Enable-PodeOpenApiViewer -Value Enable-PodeOAViewer
}
if (!(Test-Path Alias:Enable-PodeOA)) {
New-Alias Enable-PodeOA -Value Enable-PodeOpenApi
}
if (!(Test-Path Alias:Get-PodeOpenApiDefinition)) {
New-Alias Get-PodeOpenApiDefinition -Value Get-PodeOADefinition
}
<#
.SYNOPSIS
Creates a new OpenAPI New-PodeOAMultiTypeProperty property.
.DESCRIPTION
Creates a new OpenAPI multi type property, for Schemas or Parameters.
OpenAPI version 3.1 is required to use this cmdlet.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER Name
The Name of the property.
.PARAMETER Type
The parameter types
.PARAMETER Format
The inbuilt OpenAPI Format . (Default: Any)
.PARAMETER CustomFormat
The name of a custom OpenAPI Format . (Default: None)
(String type only)
.PARAMETER Default
The default value of the property. (Default: $null)
.PARAMETER Pattern
A Regex pattern that the string must match.
(String type only)
.PARAMETER Description
A Description of the property.
.PARAMETER Minimum
The minimum value of the number.
(Integer,Number types only)
.PARAMETER Maximum
The maximum value of the number.
(Integer,Number types only)
.PARAMETER ExclusiveMaximum
Specifies an exclusive upper limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMaximum attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified maximum value.
This parameter is typically paired with a -Maximum parameter to define the upper bound.
(Integer,Number types only)
.PARAMETER ExclusiveMinimum
Specifies an exclusive lower limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMinimun attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified minimun value.
This parameter is typically paired with a -Minimum parameter to define the lower bound.
(Integer,Number types only)
.PARAMETER MultiplesOf
The number must be in multiples of the supplied value.
(Integer,Number types only)
.PARAMETER Properties
An array of other int/string/etc properties wrap up as an object.
(Object type only)
.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER Example
An example of a parameter value
.PARAMETER Enum
An optional array of values that this property can only be set to.
.PARAMETER Required
If supplied, the string will be treated as Required where supported.
.PARAMETER Deprecated
If supplied, the string will be treated as Deprecated where supported.
.PARAMETER Object
If supplied, the string will be automatically wrapped in an object.
.PARAMETER Nullable
If supplied, the string will be treated as Nullable.
.PARAMETER ReadOnly
If supplied, the string will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the string will be included in a request but not in a response
.PARAMETER MinLength
If supplied, the string will be restricted to minimal length of characters.
.PARAMETER MaxLength
If supplied, the string will be restricted to maximal length of characters.
.PARAMETER NoProperties
If supplied, no properties are allowed in the object.
If no properties are assigned to the object and the NoProperties parameter is not set the object accept any property.(Object type only)
.PARAMETER MinProperties
If supplied, will restrict the minimun number of properties allowed in an object.
(Object type only)
.PARAMETER MaxProperties
If supplied, will restrict the maximum number of properties allowed in an object.
(Object type only)
.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.
.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique
.PARAMETER MinItems
If supplied, specify minimum length of an array
.PARAMETER MaxItems
If supplied, specify maximum length of an array
.PARAMETER DiscriminatorProperty
If supplied, specifies the name of the property used to distinguish between different subtypes in a polymorphic schema in OpenAPI.
This string value represents the property in the payload that indicates which specific subtype schema should be applied.
It's essential in scenarios where an API endpoint handles data that conforms to one of several derived schemas from a common base schema.
.PARAMETER DiscriminatorMapping
If supplied, define a mapping between the values of the discriminator property and the corresponding subtype schemas.
This parameter accepts a HashTable where each key-value pair maps a discriminator value to a specific subtype schema name.
It's used in conjunction with the -DiscriminatorProperty to provide complete discrimination logic in polymorphic scenarios.
.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.
.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.
.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.
.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.
.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.
.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.
.EXAMPLE
New-PodeOAMultiTypeProperty -Name 'userType' -type integer,boolean
.EXAMPLE
New-PodeOAMultiTypeProperty -Name 'password' -type string,object -Format Password -Properties (New-PodeOAStringProperty -Name 'password' -Format Password)
#>
function New-PodeOAMultiTypeProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true )]
[hashtable[]]
$ParamsList,
[Parameter(Mandatory)]
[ValidateSet( 'integer', 'number', 'string', 'object', 'boolean' )]
[string]
$Type,
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[Alias('Title')]
[string]
$Name,
[Parameter( ParameterSetName = 'Array')]
[Parameter(ParameterSetName = 'Inbuilt')]
[ValidateSet('', 'Int32', 'Int64', 'Double', 'Float', 'Binary', 'Base64', 'Byte', 'Date', 'Date-Time', 'Password', 'Email', 'Uuid', 'Uri', 'Hostname', 'Ipv4', 'Ipv6')]
[string]
$Format,
[Parameter( ParameterSetName = 'Array')]
[Parameter(ParameterSetName = 'Custom')]
[string]
$CustomFormat,
[Parameter()]
$Default,
[Parameter()]
[string]
$Pattern,
[Parameter()]
[hashtable[]]
$Properties,
[Parameter()]
[string]
$Description,
[Parameter()]
[double]
$Minimum,
[Parameter()]
[double]
$Maximum,
[Parameter()]
[switch]
$ExclusiveMaximum,
[Parameter()]
[switch]
$ExclusiveMinimum,
[Parameter()]
[double]
$MultiplesOf,
[Parameter()]
[string]
$ExternalDoc,
[Parameter()]
[object]
$Example,
[Parameter()]
[object[]]
$Enum,
[switch]
$Required,
[switch]
$Deprecated,
[switch]
$Object,
[switch]
$Nullable,
[switch]
$ReadOnly,
[switch]
$WriteOnly,
[Parameter()]
[int]
$MinLength,
[Parameter()]
[int]
$MaxLength,
[switch]
$NoProperties,
[int]
$MinProperties,
[int]
$MaxProperties,
[switch]
$NoAdditionalProperties,
[hashtable]
$AdditionalProperties,
[string]
$XmlName,
[string]
$XmlNamespace,
[string]
$XmlPrefix,
[switch]
$XmlAttribute,
[Parameter( ParameterSetName = 'Array')]
[string]
$XmlItemName,
[Parameter( ParameterSetName = 'Array')]
[switch]
$XmlWrapped,
[Parameter(Mandatory = $true, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems,
[string]
$DiscriminatorProperty,
[hashtable]
$DiscriminatorMapping
)
begin {
$param = New-PodeOAPropertyInternal -Params $PSBoundParameters
if ($type -contains 'string') {
if (![string]::IsNullOrWhiteSpace($CustomFormat)) {
$_format = $CustomFormat
}
elseif ($Format) {
$_format = $Format
}
if ($Format -or $CustomFormat) {
$param.format = $_format.ToLowerInvariant()
}
}
if ($type -contains 'object') {
if ($NoProperties) {
if ($Properties -or $MinProperties -or $MaxProperties) {
throw '-NoProperties is not compatible with -Properties, -MinProperties and -MaxProperties'
}
$param.properties = @($null)
}
elseif ($Properties) {
$param.properties = $Properties
}
else {
$param.properties = @()
}
if ($DiscriminatorProperty) {
$param.discriminator = @{
'propertyName' = $DiscriminatorProperty
}
if ($DiscriminatorMapping) {
$param.discriminator.mapping = $DiscriminatorMapping
}
}
elseif ($DiscriminatorMapping) {
throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters'
}
}
if ($type -contains 'boolean') {
if ($Default) {
if ([bool]::TryParse($Default, [ref]$null) -or $Enum -icontains $Default) {
$param.default = $Default
}
else {
throw "The default value is not a boolean and it's not part of the enum"
}
}
}
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ParamsList) {
$collectedInput.AddRange($ParamsList)
}
}
end {
if ($collectedInput) {
return $collectedInput + $param
}
else {
return $param
}
}
}
<#
.SYNOPSIS
Creates a new OpenAPI integer property.
.DESCRIPTION
Creates a new OpenAPI integer property, for Schemas or Parameters.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER Name
The Name of the property.
.PARAMETER Format
The inbuilt OpenAPI Format of the integer. (Default: Any)
.PARAMETER Default
The default value of the property. (Default: 0)
.PARAMETER Minimum
The minimum value of the integer. (Default: Int.Min)
.PARAMETER Maximum
The maximum value of the integer. (Default: Int.Max)
.PARAMETER ExclusiveMaximum
Specifies an exclusive upper limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMaximum attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified maximum value.
This parameter is typically paired with a -Maximum parameter to define the upper bound.
.PARAMETER ExclusiveMinimum
Specifies an exclusive lower limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMinimun attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified minimun value.
This parameter is typically paired with a -Minimum parameter to define the lower bound.
.PARAMETER MultiplesOf
The integer must be in multiples of the supplied value.
.PARAMETER Description
A Description of the property.
.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER Example
An example of a parameter value
.PARAMETER Enum
An optional array of values that this property can only be set to.
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
.PARAMETER Object
If supplied, the integer will be automatically wrapped in an object.
.PARAMETER Nullable
If supplied, the integer will be treated as Nullable.
.PARAMETER ReadOnly
If supplied, the integer will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the integer will be included in a request but not in a response
.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.
.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique
.PARAMETER MinItems
If supplied, specify minimum length of an array
.PARAMETER MaxItems
If supplied, specify maximum length of an array
.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.
.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.
.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.
.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.
.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.
.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.
.EXAMPLE
New-PodeOAIntProperty -Name 'age' -Required
Creates a required integer property named 'age'.
.EXAMPLE
New-PodeOAIntProperty -Name 'count' -Minimum 0 -Maximum 10 -Default 5 -Description 'Item count'
Creates an integer property 'count' with a minimum value of 0, maximum of 10, default value of 5, and a description.
.EXAMPLE
New-PodeOAIntProperty -Name 'quantity' -XmlName 'Quantity' -XmlNamespace 'http://example.com/quantity' -XmlPrefix 'q'
Creates an integer property 'quantity' with a custom XML element name 'Quantity', using a specified namespace and prefix.
.EXAMPLE
New-PodeOAIntProperty -Array -XmlItemName 'unit' -XmlName 'units' | Add-PodeOAComponentSchema -Name 'Units'
Generates a schema where the integer property is treated as an array, with each array item named 'unit' in XML, and the array itself represented with the XML name 'units'.
#>
function New-PodeOAIntProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
[OutputType([System.Collections.Specialized.OrderedDictionary])]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true)]
[hashtable[]]
$ParamsList,
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[Alias('Title')]
[string]
$Name,
[Parameter()]
[ValidateSet('', 'Int32', 'Int64')]
[string]
$Format,
[Parameter()]
[int]
$Default,
[Parameter()]
[int]
$Minimum,
[Parameter()]
[int]
$Maximum,
[Parameter()]
[switch]
$ExclusiveMaximum,
[Parameter()]
[switch]
$ExclusiveMinimum,
[Parameter()]
[int]
$MultiplesOf,
[Parameter()]
[string]
$Description,
[Parameter()]
[string]
$ExternalDoc,
[Parameter()]
[object]
$Example,
[Parameter()]
[int[]]
$Enum,
[switch]
$Required,
[switch]
$Deprecated,
[switch]
$Object,
[switch]
$Nullable,
[switch]
$ReadOnly,
[switch]
$WriteOnly,
[switch]
$NoAdditionalProperties,
[hashtable]
$AdditionalProperties,
[string]
$XmlName,
[string]
$XmlNamespace,
[string]
$XmlPrefix,
[switch]
$XmlAttribute,
[Parameter( ParameterSetName = 'Array')]
[string]
$XmlItemName,
[Parameter( ParameterSetName = 'Array')]
[switch]
$XmlWrapped,
[Parameter(Mandatory = $true, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems
)
begin {
$param = New-PodeOAPropertyInternal -type 'integer' -Params $PSBoundParameters
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ParamsList) {
$collectedInput.AddRange($ParamsList)
}
}
end {
if ($collectedInput) {
return $collectedInput + $param
}
else {
return $param
}
}
}
<#
.SYNOPSIS
Creates a new OpenAPI number property.
.DESCRIPTION
Creates a new OpenAPI number property, for Schemas or Parameters.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER Name
The Name of the property.
.PARAMETER Format
The inbuilt OpenAPI Format of the number. (Default: Any)
.PARAMETER Default
The default value of the property. (Default: 0)
.PARAMETER Minimum
The minimum value of the number. (Default: Double.Min)
.PARAMETER Maximum
The maximum value of the number. (Default: Double.Max)
.PARAMETER ExclusiveMaximum
Specifies an exclusive upper limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMaximum attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified maximum value.
This parameter is typically paired with a -Maximum parameter to define the upper bound.
.PARAMETER ExclusiveMinimum
Specifies an exclusive lower limit for a numeric property in the OpenAPI schema.
When this parameter is used, it sets the exclusiveMinimun attribute in the OpenAPI definition to true, indicating that the numeric value must be strictly less than the specified minimun value.
This parameter is typically paired with a -Minimum parameter to define the lower bound.
.PARAMETER MultiplesOf
The number must be in multiples of the supplied value.
.PARAMETER Description
A Description of the property.
.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER Example
An example of a parameter value
.PARAMETER Enum
An optional array of values that this property can only be set to.
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
.PARAMETER Object
If supplied, the number will be automatically wrapped in an object.
.PARAMETER Nullable
If supplied, the number will be treated as Nullable.
.PARAMETER ReadOnly
If supplied, the number will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the number will be included in a request but not in a response
.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.
.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique
.PARAMETER MinItems
If supplied, specify minimum length of an array
.PARAMETER MaxItems
If supplied, specify maximum length of an array
.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.
.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.
.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.
.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.
.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.
.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.
.EXAMPLE
New-PodeOANumberProperty -Name 'gravity' -Default 9.8
#>
function New-PodeOANumberProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true )]
[hashtable[]]
$ParamsList,
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[Alias('Title')]
[string]
$Name,
[Parameter()]
[ValidateSet('', 'Double', 'Float')]
[string]
$Format,
[Parameter()]
[double]
$Default,
[Parameter()]
[double]
$Minimum,
[Parameter()]
[double]
$Maximum,
[Parameter()]
[switch]
$ExclusiveMaximum,
[Parameter()]
[switch]
$ExclusiveMinimum,
[Parameter()]
[double]
$MultiplesOf,
[Parameter()]
[string]
$Description,
[Parameter()]
[string]
$ExternalDoc,
[Parameter()]
[object]
$Example,
[Parameter()]
[double[]]
$Enum,
[switch]
$Required,
[switch]
$Deprecated,
[switch]
$Object,
[switch]
$Nullable,
[switch]
$ReadOnly,
[switch]
$WriteOnly,
[switch]
$NoAdditionalProperties,
[hashtable]
$AdditionalProperties,
[string]
$XmlName,
[string]
$XmlNamespace,
[string]
$XmlPrefix,
[switch]
$XmlAttribute,
[Parameter( ParameterSetName = 'Array')]
[string]
$XmlItemName,
[Parameter( ParameterSetName = 'Array')]
[switch]
$XmlWrapped,
[Parameter(Mandatory = $true, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems
)
begin {
$param = New-PodeOAPropertyInternal -type 'number' -Params $PSBoundParameters
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ParamsList) {
$collectedInput.AddRange($ParamsList)
}
}
end {
if ($collectedInput) {
return $collectedInput + $param
}
else {
return $param
}
}
}
<#
.SYNOPSIS
Creates a new OpenAPI string property.
.DESCRIPTION
Creates a new OpenAPI string property, for Schemas or Parameters.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER Name
The Name of the property.
.PARAMETER Format
The inbuilt OpenAPI Format of the string. (Default: Any)
.PARAMETER CustomFormat
The name of a custom OpenAPI Format of the string. (Default: None)
.PARAMETER Default
The default value of the property. (Default: $null)
.PARAMETER Pattern
A Regex pattern that the string must match.
.PARAMETER Description
A Description of the property.
.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER Example
An example of a parameter value
.PARAMETER Enum
An optional array of values that this property can only be set to.
.PARAMETER Required
If supplied, the string will be treated as Required where supported.
.PARAMETER Deprecated
If supplied, the string will be treated as Deprecated where supported.
.PARAMETER Object
If supplied, the string will be automatically wrapped in an object.
.PARAMETER Nullable
If supplied, the string will be treated as Nullable.
.PARAMETER ReadOnly
If supplied, the string will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the string will be included in a request but not in a response
.PARAMETER MinLength
If supplied, the string will be restricted to minimal length of characters.
.PARAMETER MaxLength
If supplied, the string will be restricted to maximal length of characters.
.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.
.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique
.PARAMETER MinItems
If supplied, specify minimum length of an array
.PARAMETER MaxItems
If supplied, specify maximum length of an array
.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.
.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.
.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.
.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.
.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.
.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.
.EXAMPLE
New-PodeOAStringProperty -Name 'userType' -Default 'admin'
.EXAMPLE
New-PodeOAStringProperty -Name 'password' -Format Password
#>
function New-PodeOAStringProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true )]
[hashtable[]]
$ParamsList,
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[Alias('Title')]
[string]
$Name,
[Parameter( ParameterSetName = 'Array')]
[Parameter(ParameterSetName = 'Inbuilt')]
[ValidateSet('', 'Binary', 'Base64', 'Byte', 'Date', 'Date-Time', 'Password', 'Email', 'Uuid', 'Uri', 'Hostname', 'Ipv4', 'Ipv6')]
[string]
$Format,
[Parameter( ParameterSetName = 'Array')]
[Parameter(ParameterSetName = 'Custom')]
[string]
$CustomFormat,
[Parameter()]
[string]
$Default,
[Parameter()]
[string]
$Pattern,
[Parameter()]
[string]
$Description,
[Parameter()]
[string]
$ExternalDoc,
[Parameter()]
[object]
$Example,
[Parameter()]
[string[]]
$Enum,
[switch]
$Required,
[switch]
$Deprecated,
[switch]
$Object,
[switch]
$Nullable,
[switch]
$ReadOnly,
[switch]
$WriteOnly,
[Parameter()]
[int]
$MinLength,
[Parameter()]
[int]
$MaxLength,
[switch]
$NoAdditionalProperties,
[hashtable]
$AdditionalProperties,
[string]
$XmlName,
[string]
$XmlNamespace,
[string]
$XmlPrefix,
[switch]
$XmlAttribute,
[Parameter( ParameterSetName = 'Array')]
[string]
$XmlItemName,
[Parameter( ParameterSetName = 'Array')]
[switch]
$XmlWrapped,
[Parameter(Mandatory = $true, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems
)
begin {
if (![string]::IsNullOrWhiteSpace($CustomFormat)) {
$_format = $CustomFormat
}
elseif ($Format) {
$_format = $Format
}
$param = New-PodeOAPropertyInternal -type 'string' -Params $PSBoundParameters
if ($Format -or $CustomFormat) {
$param.format = $_format.ToLowerInvariant()
}
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ParamsList) {
$collectedInput.AddRange($ParamsList)
}
}
end {
if ($collectedInput) {
return $collectedInput + $param
}
else {
return $param
}
}
}
<#
.SYNOPSIS
Creates a new OpenAPI boolean property.
.DESCRIPTION
Creates a new OpenAPI boolean property, for Schemas or Parameters.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER Name
The Name of the property.
.PARAMETER Default
The default value of the property. (Default: $false)
.PARAMETER Description
A Description of the property.
.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER Example
An example of a parameter value
.PARAMETER Enum
An optional array of values that this property can only be set to.
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
.PARAMETER Object
If supplied, the boolean will be automatically wrapped in an object.
.PARAMETER Nullable
If supplied, the boolean will be treated as Nullable.
.PARAMETER ReadOnly
If supplied, the boolean will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the boolean will be included in a request but not in a response
.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.
.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique
.PARAMETER MinItems
If supplied, specify minimum length of an array
.PARAMETER MaxItems
If supplied, specify maximum length of an array
.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.
.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.
.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.
.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.
.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.
.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.
.EXAMPLE
New-PodeOABoolProperty -Name 'enabled' -Required
#>
function New-PodeOABoolProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true)]
[hashtable[]]
$ParamsList,
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[Alias('Title')]
[string]
$Name,
[Parameter()]
[string]
$Default,
[Parameter()]
[string]
$Description,
[Parameter()]
[string]
$ExternalDoc,
[Parameter()]
[object]
$Example,
[Parameter()]
[string[]]
$Enum,
[switch]
$Required,
[switch]
$Deprecated,
[switch]
$Object,
[switch]
$Nullable,
[switch]
$ReadOnly,
[switch]
$WriteOnly,
[switch]
$NoAdditionalProperties,
[hashtable]
$AdditionalProperties,
[string]
$XmlName,
[string]
$XmlNamespace,
[string]
$XmlPrefix,
[switch]
$XmlAttribute,
[Parameter( ParameterSetName = 'Array')]
[string]
$XmlItemName,
[Parameter( ParameterSetName = 'Array')]
[switch]
$XmlWrapped,
[Parameter(Mandatory = $true, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems
)
begin {
$param = New-PodeOAPropertyInternal -type 'boolean' -Params $PSBoundParameters
if ($Default) {
if ([bool]::TryParse($Default, [ref]$null) -or $Enum -icontains $Default) {
$param.default = $Default
}
else {
throw "The default value is not a boolean and it's not part of the enum"
}
}
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ParamsList) {
$collectedInput.AddRange($ParamsList)
}
}
end {
if ($collectedInput) {
return $collectedInput + $param
}
else {
return $param
}
}
}
<#
.SYNOPSIS
Creates a new OpenAPI object property from other properties.
.DESCRIPTION
Creates a new OpenAPI object property from other properties, for Schemas or Parameters.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER Name
The Name of the property.
.PARAMETER Properties
An array of other int/string/etc properties wrap up as an object.
.PARAMETER Description
A Description of the property.
.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER Example
An example of a parameter value
.PARAMETER Deprecated
If supplied, the object will be treated as Deprecated where supported.
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
.PARAMETER Nullable
If supplied, the object will be treated as Nullable.
.PARAMETER ReadOnly
If supplied, the object will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the object will be included in a request but not in a response
.PARAMETER NoProperties
If supplied, no properties are allowed in the object. If no properties are assigned to the object and the NoProperties parameter is not set the object accept any property
.PARAMETER MinProperties
If supplied, will restrict the minimun number of properties allowed in an object.
.PARAMETER MaxProperties
If supplied, will restrict the maximum number of properties allowed in an object.
.PARAMETER NoAdditionalProperties
If supplied, will configure the OpenAPI property additionalProperties to false.
This means that the defined object will not allow any properties beyond those explicitly declared in its schema.
If any additional properties are provided, they will be considered invalid.
Use this switch to enforce a strict schema definition, ensuring that objects contain only the specified set of properties and no others.
.PARAMETER AdditionalProperties
Define a set of additional properties for the OpenAPI schema. This parameter accepts a HashTable where each key-value pair represents a property name and its corresponding schema.
The schema for each property can include type, format, description, and other OpenAPI specification attributes.
When specified, these additional properties are included in the OpenAPI definition, allowing for more flexible and dynamic object structures.
.PARAMETER Array
If supplied, the object will be treated as an array of objects.
.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique
.PARAMETER MinItems
If supplied, specify minimum length of an array
.PARAMETER MaxItems
If supplied, specify maximum length of an array
.PARAMETER DiscriminatorProperty
If supplied, specifies the name of the property used to distinguish between different subtypes in a polymorphic schema in OpenAPI.
This string value represents the property in the payload that indicates which specific subtype schema should be applied.
It's essential in scenarios where an API endpoint handles data that conforms to one of several derived schemas from a common base schema.
.PARAMETER DiscriminatorMapping
If supplied, define a mapping between the values of the discriminator property and the corresponding subtype schemas.
This parameter accepts a HashTable where each key-value pair maps a discriminator value to a specific subtype schema name.
It's used in conjunction with the -DiscriminatorProperty to provide complete discrimination logic in polymorphic scenarios.
.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.
.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.
.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.
.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.
.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.
.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.
.EXAMPLE
New-PodeOAObjectProperty -Name 'user' -Properties @('<ARRAY_OF_PROPERTIES>')
.EXAMPLE
New-PodeOABoolProperty -Name 'enabled' -Required|
New-PodeOAObjectProperty -Name 'extraProperties' -AdditionalProperties [ordered]@{
"property1" = @{ "type" = "string"; "description" = "Description for property1" };
"property2" = @{ "type" = "integer"; "format" = "int32" }
}
#>
function New-PodeOAObjectProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true , Position = 0 )]
[hashtable[]]
$ParamsList,
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[Alias('Title')]
[string]
$Name,
[Parameter()]
[hashtable[]]
$Properties,
[Parameter()]
[string]
$Description,
[Parameter()]
[string]
$ExternalDoc,
[Parameter()]
[object]
$Example,
[switch]
$Deprecated,
[switch]
$Required,
[switch]
$Nullable,
[switch]
$ReadOnly,
[switch]
$WriteOnly,
[switch]
$NoProperties,
[int]
$MinProperties,
[int]
$MaxProperties,
[switch]
$NoAdditionalProperties,
[hashtable]
$AdditionalProperties,
[string]
$XmlName,
[string]
$XmlNamespace,
[string]
$XmlPrefix,
[switch]
$XmlAttribute,
[Parameter( ParameterSetName = 'Array')]
[string]
$XmlItemName,
[Parameter( ParameterSetName = 'Array')]
[switch]
$XmlWrapped,
[Parameter( Mandatory, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems,
[string]
$DiscriminatorProperty,
[hashtable]
$DiscriminatorMapping
)
begin {
$param = New-PodeOAPropertyInternal -type 'object' -Params $PSBoundParameters
if ($NoProperties) {
if ($Properties -or $MinProperties -or $MaxProperties) {
throw '-NoProperties is not compatible with -Properties, -MinProperties and -MaxProperties'
}
$param.properties = @($null)
$PropertiesFromPipeline = $false
}
elseif ($Properties) {
$param.properties = $Properties
$PropertiesFromPipeline = $false
}
else {
$param.properties = @()
$PropertiesFromPipeline = $true
}
if ($DiscriminatorProperty) {
$param.discriminator = @{
'propertyName' = $DiscriminatorProperty
}
if ($DiscriminatorMapping) {
$param.discriminator.mapping = $DiscriminatorMapping
}
}
elseif ($DiscriminatorMapping) {
throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters'
}
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ParamsList) {
if ($PropertiesFromPipeline) {
$param.properties += $ParamsList
}
else {
$collectedInput.AddRange($ParamsList)
}
}
}
end {
if ($PropertiesFromPipeline) {
return $param
}
elseif ($collectedInput) {
return $collectedInput + $param
}
else {
return $param
}
}
}
<#
.SYNOPSIS
Creates a new OpenAPI object combining schemas and properties.
.DESCRIPTION
Creates a new OpenAPI object combining schemas and properties.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline an object definition
.PARAMETER Type
Define the type of validation between the objects
oneOf – validates the value against exactly one of the subschemas
allOf – validates the value against all the subschemas
anyOf – validates the value against any (one or more) of the subschemas
.PARAMETER ObjectDefinitions
An array of object definitions that are used for independent validation but together compose a single object.
.PARAMETER DiscriminatorProperty
If supplied, specifies the name of the property used to distinguish between different subtypes in a polymorphic schema in OpenAPI.
This string value represents the property in the payload that indicates which specific subtype schema should be applied.
It's essential in scenarios where an API endpoint handles data that conforms to one of several derived schemas from a common base schema.
.PARAMETER DiscriminatorMapping
If supplied, define a mapping between the values of the discriminator property and the corresponding subtype schemas.
This parameter accepts a HashTable where each key-value pair maps a discriminator value to a specific subtype schema name.
It's used in conjunction with the -DiscriminatorProperty to provide complete discrimination logic in polymorphic scenarios.
.EXAMPLE
Add-PodeOAComponentSchema -Name 'Pets' -Component ( Merge-PodeOAProperty -Type OneOf -ObjectDefinitions @( 'Cat','Dog') -Discriminator "petType")
.EXAMPLE
Add-PodeOAComponentSchema -Name 'Cat' -Component (
Merge-PodeOAProperty -Type AllOf -ObjectDefinitions @( 'Pet', ( New-PodeOAObjectProperty -Properties @(
(New-PodeOAStringProperty -Name 'huntingSkill' -Description 'The measured skill for hunting' -Enum @( 'clueless', 'lazy', 'adventurous', 'aggressive'))
))
))
#>
function Merge-PodeOAProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
[OutputType([System.Collections.Specialized.OrderedDictionary])]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true )]
[hashtable[]]
$ParamsList,
[Parameter(Mandatory)]
[ValidateSet('OneOf', 'AnyOf', 'AllOf')]
[string]
$Type,
[Parameter()]
[System.Object[]]
$ObjectDefinitions,
[string]
$DiscriminatorProperty,
[hashtable]
$DiscriminatorMapping
)
begin {
$param = [ordered]@{}
switch ($type.ToLower()) {
'oneof' {
$param.type = 'oneOf'
}
'anyof' {
$param.type = 'anyOf'
}
'allof' {
$param.type = 'allOf'
}
}
$param.schemas = @()
if ($ObjectDefinitions) {
foreach ($schema in $ObjectDefinitions) {
if ($schema -is [System.Object[]] -or ($schema -is [hashtable] -and
(($schema.type -ine 'object') -and !$schema.object))) {
throw "Only properties of type Object can be associated with $($param.type)"
}
$param.schemas += $schema
}
}
if ($DiscriminatorProperty) {
if ($type.ToLower() -eq 'allof' ) {
throw 'Discriminator parameter is not compatible with allOf'
}
$param.discriminator = @{
'propertyName' = $DiscriminatorProperty
}
if ($DiscriminatorMapping) {
$param.discriminator.mapping = $DiscriminatorMapping
}
}
elseif ($DiscriminatorMapping) {
throw 'Parameter -DiscriminatorMapping requires the -DiscriminatorProperty parameters'
}
}
process {
if ($ParamsList) {
if ($ParamsList.type -ine 'object' -and !$ParamsList.object) {
throw "Only properties of type Object can be associated with $type"
}
$param.schemas += $ParamsList
}
}
end {
return $param
}
}
<#
.SYNOPSIS
Creates a OpenAPI schema reference property.
.DESCRIPTION
Creates a new OpenAPI component schema reference from another OpenAPI schema.
.LINK
https://swagger.io/docs/specification/basic-structure/
.LINK
https://swagger.io/docs/specification/data-models/
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER Name
The Name of the property.
.PARAMETER Reference
An component schema name.
.PARAMETER Description
A Description of the property.
.PARAMETER Example
An example of a parameter value
.PARAMETER Deprecated
If supplied, the schema will be treated as Deprecated where supported.
.PARAMETER Required
If supplied, the object will be treated as Required where supported.
.PARAMETER Array
If supplied, the schema will be treated as an array of objects.
.PARAMETER Nullable
If supplied, the schema will be treated as Nullable.
.PARAMETER ReadOnly
If supplied, the schema will be included in a response but not in a request
.PARAMETER WriteOnly
If supplied, the schema will be included in a request but not in a response
.PARAMETER MinProperties
If supplied, will restrict the minimun number of properties allowed in an schema.
.PARAMETER MaxProperties
If supplied, will restrict the maximum number of properties allowed in an schema.
.PARAMETER Array
If supplied, the schema will be treated as an array of objects.
.PARAMETER UniqueItems
If supplied, specify that all items in the array must be unique
.PARAMETER MinItems
If supplied, specify minimum length of an array
.PARAMETER MaxItems
If supplied, specify maximum length of an array
.PARAMETER XmlName
By default, XML elements get the same names that fields in the API declaration have. This property change the XML name of the property
reflecting the 'xml.name' attribute in the OpenAPI specification.
.PARAMETER XmlNamespace
Defines a specific XML namespace for the property, corresponding to the 'xml.namespace' attribute in OpenAPI.
.PARAMETER XmlPrefix
Sets a prefix for the XML element name, aligning with the 'xml.prefix' attribute in OpenAPI.
.PARAMETER XmlAttribute
Indicates whether the property should be serialized as an XML attribute, equivalent to the 'xml.attribute' attribute in OpenAPI.
.PARAMETER XmlItemName
Specifically for properties treated as arrays, it defines the XML name for each item in the array. This parameter aligns with the 'xml.name' attribute under 'items' in OpenAPI.
.PARAMETER XmlWrapped
Indicates whether array items should be wrapped in an XML element, similar to the 'xml.wrapped' attribute in OpenAPI.
.EXAMPLE
New-PodeOAComponentSchemaProperty -Name 'Config' -Component "MyConfigSchema"
#>
function New-PodeOAComponentSchemaProperty {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true )]
[hashtable[]]
$ParamsList,
[Parameter()]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Reference,
[Parameter( ParameterSetName = 'Array')]
[string]
$Description,
[string]
$XmlName,
[string]
$XmlNamespace,
[string]
$XmlPrefix,
[switch]
$XmlAttribute,
[Parameter( ParameterSetName = 'Array')]
[string]
$XmlItemName,
[Parameter( ParameterSetName = 'Array')]
[switch]
$XmlWrapped,
[Parameter(ParameterSetName = 'Array')]
[object]
$Example,
[Parameter(ParameterSetName = 'Array')]
[switch]
$Deprecated,
[Parameter(ParameterSetName = 'Array')]
[switch]
$Required,
[Parameter(ParameterSetName = 'Array')]
[switch]
$Nullable,
[Parameter(ParameterSetName = 'Array')]
[switch]
$ReadOnly,
[Parameter(ParameterSetName = 'Array')]
[switch]
$WriteOnly,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinProperties,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxProperties,
[Parameter(Mandatory = $true, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems
)
begin {
$param = New-PodeOAPropertyInternal -type 'schema' -Params $PSBoundParameters
if (! $param.Name) {
$param.Name = $Reference
}
$param.schema = $Reference
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ParamsList) {
$collectedInput.AddRange($ParamsList)
}
}
end {
if ($collectedInput) {
return $collectedInput + $param
}
else {
return $param
}
}
}
if (!(Test-Path Alias:New-PodeOASchemaProperty)) {
New-Alias New-PodeOASchemaProperty -Value New-PodeOAComponentSchemaProperty
}
<#
.SYNOPSIS
Enables the OpenAPI default route in Pode.
.DESCRIPTION
Enables the OpenAPI default route in Pode, as well as setting up details like Title and API Version.
.PARAMETER Path
An optional custom route path to access the OpenAPI definition. (Default: /openapi)
.PARAMETER Title
The Title of the API. (Deprecated - Use Add-PodeOAInfo)
.PARAMETER Version
The Version of the API. (Deprecated - Use Add-PodeOAInfo)
The OpenAPI Specification is versioned using Semantic Versioning 2.0.0 (semver) and follows the semver specification.
https://semver.org/spec/v2.0.0.html
.PARAMETER Description
A short description of the API. (Deprecated - Use Add-PodeOAInfo)
CommonMark syntax MAY be used for rich text representation.
https://spec.commonmark.org/
.PARAMETER OpenApiVersion
Specify OpenApi Version (default: 3.0.3)
.PARAMETER RouteFilter
An optional route filter for routes that should be included in the definition. (Default: /*)
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to the route.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to bind the static Route against.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER RestrictRoutes
If supplied, only routes that are available on the Requests URI will be used to generate the OpenAPI definition.
.PARAMETER ServerEndpoint
If supplied, will be used as URL base to generate the OpenAPI definition.
The parameter is created by New-PodeOpenApiServerEndpoint
.PARAMETER Mode
Define the way the OpenAPI definition file is accessed, the value can be View or Download. (Default: View)
.PARAMETER NoCompress
If supplied, generate the OpenApi Json version in human readible form.
.PARAMETER MarkupLanguage
Define the default markup language for the OpenApi spec ('Json', 'Json-Compress', 'Yaml')
.PARAMETER EnableSchemaValidation
If supplied enable Test-PodeOAJsonSchemaCompliance cmdlet that provide support for opeapi parameter schema validation
.PARAMETER Depth
Define the default depth used by any JSON,YAML OpenAPI conversion (default 20)
.PARAMETER DisableMinimalDefinitions
If supplied the OpenApi decument will include only the route validated by Set-PodeOARouteInfo. Any other not OpenApi route will be excluded.
.PARAMETER NoDefaultResponses
If supplied, it will disable the default OpenAPI response with the new provided.
.PARAMETER DefaultResponses
If supplied, it will replace the default OpenAPI response with the new provided.(Default: @{'200' = @{ description = 'OK' };'default' = @{ description = 'Internal server error' }} )
.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Enable-PodeOpenApi -Title 'My API' -Version '1.0.0' -RouteFilter '/api/*'
.EXAMPLE
Enable-PodeOpenApi -Title 'My API' -Version '1.0.0' -RouteFilter '/api/*' -RestrictRoutes
.EXAMPLE
Enable-PodeOpenApi -Path '/docs/openapi' -NoCompress -Mode 'Donwload' -DisableMinimalDefinitions
#>
function Enable-PodeOpenApi {
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string]
$Path = '/openapi',
[Parameter(ParameterSetName = 'Deprecated')]
[string]
$Title,
[Parameter(ParameterSetName = 'Deprecated')]
[ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$')]
[string]
$Version,
[Parameter(ParameterSetName = 'Deprecated')]
[string]
$Description,
[ValidateSet('3.1.0', '3.0.3', '3.0.2', '3.0.1', '3.0.0')]
[string]
$OpenApiVersion = '3.0.3',
[ValidateNotNullOrEmpty()]
[string]
$RouteFilter = '/*',
[string[]]
$EndpointName,
[object[]]
$Middleware,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[switch]
$RestrictRoutes,
[ValidateSet('View', 'Download')]
[String]
$Mode = 'view',
[ValidateSet('Json', 'Json-Compress', 'Yaml')]
[String]
$MarkupLanguage = 'Json',
[switch]
$EnableSchemaValidation,
[ValidateRange(1, 100)]
[int]
$Depth = 20,
[switch]
$DisableMinimalDefinitions,
[Parameter(Mandatory, ParameterSetName = 'DefaultResponses')]
[hashtable]
$DefaultResponses,
[Parameter(Mandatory, ParameterSetName = 'NoDefaultResponses')]
[switch]
$NoDefaultResponses,
[string]
$DefinitionTag
)
if (Test-PodeIsEmpty -Value $DefinitionTag) {
$DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
}
if ($Description -or $Version -or $Title) {
if (! $Version) {
$Version = '0.0.0'
}
Write-PodeHost -ForegroundColor Yellow "WARNING: Title, Version, and Description on 'Enable-PodeOpenApi' are deprecated. Please use 'Add-PodeOAInfo' instead."
}
if ( $DefinitionTag -ine $PodeContext.Server.OpenAPI.DefaultDefinitionTag ) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag] = Get-PodeOABaseObject
}
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.enableMinimalDefinitions = !$DisableMinimalDefinitions.IsPresent
# initialise openapi info
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].Version = $OpenApiVersion
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].Path = $Path
if ($OpenApiVersion.StartsWith('3.0')) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.version = 3.0
}
elseif ($OpenApiVersion.StartsWith('3.1')) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.version = 3.1
}
$meta = @{
RouteFilter = $RouteFilter
RestrictRoutes = $RestrictRoutes
NoCompress = ($MarkupLanguage -ine 'Json-Compress')
Mode = $Mode
MarkupLanguage = $MarkupLanguage
DefinitionTag = $DefinitionTag
}
if ( $Title) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.title = $Title
}
if ($Version) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.version = $Version
}
if ($Description ) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.description = $Description
}
if ( $EnableSchemaValidation.IsPresent) {
#Test-Json has been introduced with version 6.1.0
if ($PSVersionTable.PSVersion -ge [version]'6.1.0') {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaValidation = $EnableSchemaValidation.IsPresent
}
else {
throw 'Schema validation required Powershell version 6.1.0 or greater'
}
}
if ( $Depth) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth = $Depth
}
$openApiCreationScriptBlock = {
param($meta)
$format = $WebEvent.Query['format']
$mode = $WebEvent.Query['mode']
$DefinitionTag = $meta.DefinitionTag
if (!$mode) {
$mode = $meta.Mode
}
elseif (@('download', 'view') -inotcontains $mode) {
Write-PodeHtmlResponse -Value "Mode $mode not valid" -StatusCode 400
return
}
if ($WebEvent.path -ilike '*.json') {
if ($format) {
Show-PodeErrorPage -Code 400 -ContentType 'text/html' -Description 'Format query not valid when the file extension is used'
return
}
$format = 'json'
}
elseif ($WebEvent.path -ilike '*.yaml') {
if ($format) {
Show-PodeErrorPage -Code 400 -ContentType 'text/html' -Description 'Format query not valid when the file extension is used'
return
}
$format = 'yaml'
}
elseif (!$format) {
$format = $meta.MarkupLanguage.ToLower()
}
elseif (@('yaml', 'json', 'json-Compress') -inotcontains $format) {
Show-PodeErrorPage -Code 400 -ContentType 'text/html' -Description "Format $format not valid"
return
}
if (($mode -ieq 'download') ) {
# Set-PodeResponseAttachment -Path
Add-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=openapi.$format"
}
# generate the openapi definition
$def = Get-PodeOpenApiDefinitionInternal `
-EndpointName $WebEvent.Endpoint.Name `
-DefinitionTag $DefinitionTag `
-MetaInfo $meta
# write the openapi definition
if ($format -ieq 'yaml') {
if ($mode -ieq 'view') {
Write-PodeTextResponse -Value (ConvertTo-PodeYaml -InputObject $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth) -ContentType 'text/x-yaml; charset=utf-8'
}
else {
Write-PodeYamlResponse -Value $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
}
}
else {
Write-PodeJsonResponse -Value $def -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -NoCompress:$meta.NoCompress
}
}
# add the OpenAPI route
Add-PodeRoute -Method Get -Path $Path -ArgumentList $meta -Middleware $Middleware `
-ScriptBlock $openApiCreationScriptBlock -EndpointName $EndpointName `
-Authentication $Authentication -Role $Role -Scope $Scope -Group $Group
Add-PodeRoute -Method Get -Path "$Path.json" -ArgumentList $meta -Middleware $Middleware `
-ScriptBlock $openApiCreationScriptBlock -EndpointName $EndpointName `
-Authentication $Authentication -Role $Role -Scope $Scope -Group $Group
Add-PodeRoute -Method Get -Path "$Path.yaml" -ArgumentList $meta -Middleware $Middleware `
-ScriptBlock $openApiCreationScriptBlock -EndpointName $EndpointName `
-Authentication $Authentication -Role $Role -Scope $Scope -Group $Group
#set new DefaultResponses
if ($NoDefaultResponses.IsPresent) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses = @{}
}
elseif ($DefaultResponses) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses = $DefaultResponses
}
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.enabled = $true
}
<#
.SYNOPSIS
Creates an OpenAPI Server Object.
.DESCRIPTION
Creates an OpenAPI Server Object.
.PARAMETER Url
A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served.
Variable substitutions will be made when a variable is named in `{`brackets`}`.
.PARAMETER Description
An optional string describing the host designated by the URL. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation.
.PARAMETER Variables
A map between a variable name and its value. The value is used for substitution in the server's URL template.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAServerEndpoint -Url 'https://myserver.io/api' -Description 'My test server'
.EXAMPLE
Add-PodeOAServerEndpoint -Url '/api' -Description 'My local server'
.EXAMPLE
Add-PodeOAServerEndpoint -Url "https://{username}.gigantic-server.com:{port}/{basePath}" -Description "The production API server" `
-Variable @{
username = @{
default = 'demo'
description = 'this value is assigned by the service provider, in this example gigantic-server.com'
}
port = @{
enum = @('System.Object[]') # Assuming 'System.Object[]' is a placeholder for actual values
default = 8443
}
basePath = @{
default = 'v2'
}
}
}
#>
function Add-PodeOAServerEndpoint {
param (
[Parameter(Mandatory = $true)]
[ValidatePattern('^(https?://|/).+')]
[string]
$Url,
[string]
$Description,
[System.Collections.Specialized.OrderedDictionary]
$Variables,
[string[]]
$DefinitionTag
)
if (Test-PodeIsEmpty -Value $DefinitionTag) {
$DefinitionTag = @($PodeContext.Server.OpenAPI.SelectedDefinitionTag)
}
foreach ($tag in $DefinitionTag) {
if (! $PodeContext.Server.OpenAPI.Definitions[$tag].servers) {
$PodeContext.Server.OpenAPI.Definitions[$tag].servers = @()
}
$lUrl = [ordered]@{url = $Url }
if ($Description) {
$lUrl.description = $Description
}
if ($Variables) {
$lUrl.variables = $Variables
}
$PodeContext.Server.OpenAPI.Definitions[$tag].servers += $lUrl
}
}
<#
.SYNOPSIS
Gets the OpenAPI definition.
.DESCRIPTION
Gets the OpenAPI definition for custom use in routes, or other functions.
.PARAMETER Format
Return the definition in a specific format 'Json', 'Json-Compress', 'Yaml', 'HashTable'
.PARAMETER Title
The Title of the API. (Default: the title supplied in Enable-PodeOpenApi)
.PARAMETER Version
The Version of the API. (Default: the version supplied in Enable-PodeOpenApi)
.PARAMETER Description
A Description of the API. (Default: the description supplied into Enable-PodeOpenApi)
.PARAMETER RouteFilter
An optional route filter for routes that should be included in the definition. (Default: /*)
.PARAMETER RestrictRoutes
If supplied, only routes that are available on the Requests URI will be used to generate the OpenAPI definition.
.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
$defInJson = Get-PodeOADefinition -Json
#>
function Get-PodeOADefinition {
[CmdletBinding()]
param(
[ValidateSet('Json', 'Json-Compress', 'Yaml', 'HashTable')]
[string]
$Format = 'HashTable',
[string]
$Title,
[string]
$Version,
[string]
$Description,
[ValidateNotNullOrEmpty()]
[string]
$RouteFilter = '/*',
[switch]
$RestrictRoutes,
[string]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
$meta = @{
RouteFilter = $RouteFilter
RestrictRoutes = $RestrictRoutes
}
if ($RestrictRoutes) {
$meta = @{
RouteFilter = $RouteFilter
RestrictRoutes = $RestrictRoutes
}
}
else {
$meta = @{}
}
if ($Title) {
$meta.Title = $Title
}
if ($Version) {
$meta.Version = $Version
}
if ($Description) {
$meta.Description = $Description
}
$oApi = Get-PodeOpenApiDefinitionInternal -MetaInfo $meta -EndpointName $WebEvent.Endpoint.Name -DefinitionTag $DefinitionTag
switch ($Format.ToLower()) {
'json' {
return ConvertTo-Json -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
}
'json-compress' {
return ConvertTo-Json -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth -Compress
}
'yaml' {
return ConvertTo-PodeYaml -InputObject $oApi -depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
}
Default {
return $oApi
}
}
}
<#
.SYNOPSIS
Adds a response definition to the supplied route.
.DESCRIPTION
Adds a response definition to the supplied route.
.PARAMETER Route
The route to add the response definition, usually from -PassThru on Add-PodeRoute.
.PARAMETER StatusCode
The HTTP StatusCode for the response.To define a range of response codes, this field MAY contain the uppercase wildcard character `X`.
For example, `2XX` represents all response codes between `[200-299]`. Only the following range definitions are allowed: `1XX`, `2XX`, `3XX`, `4XX`, and `5XX`.
If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code.
.PARAMETER Content
The content-types and schema the response returns (the schema is created using the Property functions).
Alias: ContentSchemas
.PARAMETER Headers
The header name and schema the response returns (the schema is created using Add-PodeOAComponentHeader cmd-let).
Alias: HeaderSchemas
.PARAMETER Description
A Description of the response. (Default: the HTTP StatusCode description)
.PARAMETER Reference
A Reference Name of an existing component response to use.
.PARAMETER Links
A Response link definition
.PARAMETER Default
If supplied, the response will be used as a default response - this overrides the StatusCode supplied.
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = 'UserIdSchema' }
.EXAMPLE
Add-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Reference 'OKResponse'
#>
function Add-PodeOAResponse {
[CmdletBinding(DefaultParameterSetName = 'Schema')]
[OutputType([hashtable[]])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[hashtable[]]
$Route,
[Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[ValidatePattern('^([1-5][0-9][0-9]|[1-5]XX)$')]
[string]
$StatusCode,
[Parameter(ParameterSetName = 'Schema')]
[Parameter(ParameterSetName = 'SchemaDefault')]
[Alias('ContentSchemas')]
[hashtable]
$Content,
[Alias('HeaderSchemas')]
[AllowEmptyString()]
[ValidateNotNullOrEmpty()]
[ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] -or $_ -is [ordered] })]
$Headers,
[Parameter(Mandatory = $false, ParameterSetName = 'Schema')]
[Parameter(Mandatory = $false, ParameterSetName = 'SchemaDefault')]
[string]
$Description,
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[Parameter(ParameterSetName = 'ReferenceDefault')]
[string]
$Reference,
[Parameter(Mandatory = $true, ParameterSetName = 'ReferenceDefault')]
[Parameter(Mandatory = $true, ParameterSetName = 'SchemaDefault')]
[switch]
$Default,
[Parameter(ParameterSetName = 'Schema')]
[Parameter(ParameterSetName = 'SchemaDefault')]
[System.Collections.Specialized.OrderedDictionary ]
$Links,
[switch]
$PassThru,
[string[]]
$DefinitionTag
)
if ($null -eq $Route) { throw 'Add-PodeOAResponse - The parameter -Route cannot be NULL.' }
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
# override status code with default
if ($Default) {
$code = 'default'
}
else {
$code = "$($StatusCode)"
}
# add the respones to the routes
foreach ($r in @($Route)) {
foreach ($tag in $DefinitionTag) {
if (! $r.OpenApi.Responses.$tag) {
$r.OpenApi.Responses.$tag = @{}
}
$r.OpenApi.Responses.$tag[$code] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters
}
}
if ($PassThru) {
return $Route
}
}
<#
.SYNOPSIS
Remove a response definition from the supplied route.
.DESCRIPTION
Remove a response definition from the supplied route.
.PARAMETER Route
The route to remove the response definition, usually from -PassThru on Add-PodeRoute.
.PARAMETER StatusCode
The HTTP StatusCode for the response to remove.
.PARAMETER Default
If supplied, the response will be used as a default response - this overrides the StatusCode supplied.
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.EXAMPLE
Add-PodeRoute -PassThru | Remove-PodeOAResponse -StatusCode 200
.EXAMPLE
Add-PodeRoute -PassThru | Remove-PodeOAResponse -StatusCode 201 -Default
#>
function Remove-PodeOAResponse {
[CmdletBinding()]
[OutputType([hashtable[]])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[hashtable[]]
$Route,
[Parameter(Mandatory = $true)]
[int]
$StatusCode,
[switch]
$Default,
[switch]
$PassThru
)
if ($null -eq $Route) { throw 'The parameter -Route cannot be NULL.' }
# override status code with default
$code = "$($StatusCode)"
if ($Default) {
$code = 'default'
}
# remove the respones from the routes
foreach ($r in @($Route)) {
if ($r.OpenApi.Responses.ContainsKey($code)) {
$null = $r.OpenApi.Responses.Remove($code)
}
}
if ($PassThru) {
return $Route
}
}
<#
.SYNOPSIS
Sets the definition of a request for a route.
.DESCRIPTION
Sets the definition of a request for a route.
.PARAMETER Route
The route to set a request definition, usually from -PassThru on Add-PodeRoute.
.PARAMETER Parameters
The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).
.PARAMETER RequestBody
The Request Body definition the request uses (from New-PodeOARequestBody).
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.EXAMPLE
Add-PodeRoute -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Schema 'UserIdBody')
#>
function Set-PodeOARequest {
[CmdletBinding()]
[OutputType([hashtable[]])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[hashtable[]]
$Route,
[hashtable[]]
$Parameters,
[hashtable]
$RequestBody,
[switch]
$PassThru
)
if ($null -eq $Route) { throw 'Set-PodeOARequest - The parameter -Route cannot be NULL.' }
foreach ($r in @($Route)) {
if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) {
$r.OpenApi.Parameters = @($Parameters)
}
if ($null -ne $RequestBody) {
$r.OpenApi.RequestBody = $RequestBody
}
}
if ($PassThru) {
return $Route
}
}
<#
.SYNOPSIS
Creates a Request Body definition for routes.
.DESCRIPTION
Creates a Request Body definition for routes from the supplied content-types and schemas.
.PARAMETER Reference
A reference name from an existing component request body.
Alias: Reference
.PARAMETER Content
The content of the request body. The key is a media type or media type range and the value describes it.
For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/*
Alias: ContentSchemas
.PARAMETER Description
A brief description of the request body. This could contain examples of use. CommonMark syntax MAY be used for rich text representation.
.PARAMETER Required
Determines if the request body is required in the request. Defaults to false.
.PARAMETER Properties
Use to force the use of the properties keyword under a schema. Commonly used to specify a multipart/form-data multi file
.PARAMETER Examples
Supplied an Example of the media type. The example object SHOULD be in the correct format as specified by the media type.
The `example` field is mutually exclusive of the `examples` field.
Furthermore, if referencing a `schema` which contains an example, the `example` value SHALL _override_ the example provided by the schema.
.PARAMETER Encoding
This parameter give you control over the serialization of parts of multipart request bodies.
This attribute is only applicable to multipart and application/x-www-form-urlencoded request bodies.
Use New-PodeOAEncodingObject to define the encode
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
New-PodeOARequestBody -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) }
.EXAMPLE
New-PodeOARequestBody -Content @{ 'application/json' = 'UserIdSchema' }
.EXAMPLE
New-PodeOARequestBody -Reference 'UserIdBody'
.EXAMPLE
New-PodeOARequestBody -Content @{'multipart/form-data' =
New-PodeOAStringProperty -name 'id' -format 'uuid' |
New-PodeOAObjectProperty -name 'address' -NoProperties |
New-PodeOAObjectProperty -name 'historyMetadata' -Description 'metadata in XML format' -NoProperties |
New-PodeOAStringProperty -name 'profileImage' -Format Binary |
New-PodeOAObjectProperty
} -Encoding (
New-PodeOAEncodingObject -Name 'historyMetadata' -ContentType 'application/xml; charset=utf-8' |
New-PodeOAEncodingObject -Name 'profileImage' -ContentType 'image/png, image/jpeg' -Headers (
New-PodeOAIntProperty -name 'X-Rate-Limit-Limit' -Description 'The number of allowed requests in the current period' -Default 3 -Enum @(1,2,3)
)
)
#>
function New-PodeOARequestBody {
[CmdletBinding(DefaultParameterSetName = 'BuiltIn' )]
[OutputType([hashtable])]
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[string]
$Reference,
[Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')]
[Alias('ContentSchemas')]
[hashtable]
$Content,
[Parameter(ParameterSetName = 'BuiltIn')]
[string]
$Description,
[Parameter(ParameterSetName = 'BuiltIn')]
[switch]
$Required,
[Parameter(ParameterSetName = 'BuiltIn')]
[switch]
$Properties,
[System.Collections.Specialized.OrderedDictionary]
$Examples,
[hashtable[]]
$Encoding,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
if ($Example -and $Examples) {
throw 'Parameter -Examples and -Example are mutually exclusive'
}
$result = @{}
foreach ($tag in $DefinitionTag) {
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'builtin' {
$param = @{content = ConvertTo-PodeOAObjectSchema -DefinitionTag $tag -Content $Content -Properties:$Properties }
if ($Required.IsPresent) {
$param['required'] = $Required.IsPresent
}
if ( $Description) {
$param['description'] = $Description
}
if ($Examples) {
if ( $Examples.'*/*') {
$Examples['"*/*"'] = $Examples['*/*']
$Examples.Remove('*/*')
}
foreach ($k in $Examples.Keys ) {
if (!$param.content.ContainsKey($k)) {
$param.content[$k] = @{}
}
$param.content.$k.examples = $Examples.$k
}
}
}
'reference' {
Test-PodeOAComponentInternal -Field requestBodies -DefinitionTag $tag -Name $Reference -PostValidation
$param = @{
'$ref' = "#/components/requestBodies/$Reference"
}
}
}
if ($Encoding) {
if (([string]$Content.keys[0]) -match '(?i)^(multipart.*|application\/x-www-form-urlencoded)$' ) {
$r = @{}
foreach ( $e in $Encoding) {
$key = [string]$e.Keys
$elems = @{}
foreach ($v in $e[$key].Keys) {
if ($v -ieq 'headers') {
$elems.headers = ConvertTo-PodeOAHeaderProperty -Headers $e[$key].headers
}
else {
$elems.$v = $e[$key].$v
}
}
$r.$key = $elems
}
$param.Content.$($Content.keys[0]).encoding = $r
}
else {
throw 'The encoding attribute is only applicable to multipart and application/x-www-form-urlencoded request bodies.'
}
}
$result[$tag] = $param
}
return $result
}
<#
.SYNOPSIS
Validate a parameter with a provided schema.
.DESCRIPTION
Validate the parameter of a method against it's own schema
.PARAMETER Json
The object in Json format to validate
.PARAMETER SchemaReference
The schema name to use to validate the property.
.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.OUTPUTS
result: true if the object is validate positively
message: any validation issue
.EXAMPLE
$UserInfo = Test-PodeOAJsonSchemaCompliance -Json $UserInfo -SchemaReference 'UserIdSchema'}
#>
function Test-PodeOAJsonSchemaCompliance {
param (
[Parameter(Mandatory = $true)]
[System.Object]
$Json,
[Parameter(Mandatory = $true)]
[string]
$SchemaReference,
[string]
$DefinitionTag
)
if ($DefinitionTag) {
if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $DefinitionTag)) {
throw "DefinitionTag $DefinitionTag is not defined"
}
}
else {
$DefinitionTag = $PodeContext.Server.OpenAPI.DefaultDefinitionTag
}
if ($Json -isnot [string]) {
$json = ConvertTo-Json -InputObject $Json -Depth $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.depth
}
if (!$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaValidation) {
throw 'Test-PodeOAComponentchema need to be enabled using `Enable-PodeOpenApi -EnableSchemaValidation` '
}
if (!(Test-PodeOAComponentSchemaJson -Name $SchemaReference -DefinitionTag $DefinitionTag)) {
throw "The OpenApi component schema in Json doesn't exist: $SchemaReference"
}
if ($PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson[$SchemaReference].available) {
[string[]] $message = @()
$result = Test-Json -Json $Json -Schema $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaJson[$SchemaReference].json -ErrorVariable jsonValidationErrors -ErrorAction SilentlyContinue
if ($jsonValidationErrors) {
foreach ($item in $jsonValidationErrors) {
$message += $item
}
}
}
else {
$result = $false
$message = 'Validation of schema with oneof or anyof is not supported'
}
return @{result = $result; message = $message }
}
<#
.SYNOPSIS
Converts an OpenAPI property into a Request Parameter.
.DESCRIPTION
Converts an OpenAPI property (such as from New-PodeOAIntProperty) into a Request Parameter.
.PARAMETER In
Where in the Request can the parameter be found?
.PARAMETER Property
The Property that need converting (such as from New-PodeOAIntProperty).
.PARAMETER Reference
The name of an existing component parameter to be reused.
Alias: ComponentParameter
.PARAMETER Name
Assign a name to the parameter
.PARAMETER ContentType
The content-types to be use with component schema
.PARAMETER Schema
The component schema to use.
.PARAMETER Description
A Description of the property.
.PARAMETER Explode
If supplied, controls how arrays are serialized in query parameters
.PARAMETER AllowReserved
If supplied, determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without percent-encoding.
This property only applies to parameters with an in value of query. The default value is false.
.PARAMETER Required
If supplied, the object will be treated as Required where supported.(Applicable only to ContentSchema)
.PARAMETER AllowEmptyValue
If supplied, allow the parameter to be empty
.PARAMETER Style
If supplied, defines how multiple values are delimited. Possible styles depend on the parameter location: path, query, header or cookie.
.PARAMETER Deprecated
If supplied, specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is false.
.PARAMETER Example
Example of the parameter's potential value. The example SHOULD match the specified schema and encoding properties if present.
The Example parameter is mutually exclusive of the Examples parameter.
Furthermore, if referencing a Schema that contains an example, the Example value SHALL _override_ the example provided by the schema.
To represent examples of media types that cannot naturally be represented in JSON or YAML, a string value can contain the example with escaping where necessary.
.PARAMETER Examples
Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding.
The Examples parameter is mutually exclusive of the Example parameter.
Furthermore, if referencing a Schema that contains an example, the Examples value SHALL _override_ the example provided by the schema.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
New-PodeOAIntProperty -Name 'userId' | ConvertTo-PodeOAParameter -In Query
.EXAMPLE
ConvertTo-PodeOAParameter -Reference 'UserIdParam'
.EXAMPLE
ConvertTo-PodeOAParameter -In Header -ContentSchemas @{ 'application/json' = 'UserIdSchema' }
#>
function ConvertTo-PodeOAParameter {
[CmdletBinding(DefaultParameterSetName = 'Reference')]
param(
[Parameter( Mandatory = $true, ParameterSetName = 'Schema')]
[Parameter(Mandatory = $true, ParameterSetName = 'Properties')]
[Parameter(Mandatory = $true, ParameterSetName = 'ContentSchema')]
[Parameter( Mandatory = $true, ParameterSetName = 'ContentProperties')]
[ValidateSet('Cookie', 'Header', 'Path', 'Query')]
[string]
$In,
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Properties')]
[Parameter( Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ContentProperties')]
[ValidateNotNull()]
[hashtable]
$Property,
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[Alias('ComponentParameter')]
[string]
$Reference,
[Parameter( ParameterSetName = 'Schema')]
[Parameter(ParameterSetName = 'Properties')]
[Parameter(ParameterSetName = 'ContentSchema')]
[Parameter( ParameterSetName = 'ContentProperties')]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
[Parameter(Mandatory = $true, ParameterSetName = 'ContentSchema')]
[Alias('ComponentSchema')]
[String]
$Schema,
[Parameter( Mandatory = $true, ParameterSetName = 'ContentSchema')]
[Parameter( Mandatory = $true, ParameterSetName = 'ContentProperties')]
[String]
$ContentType,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'ContentSchema')]
[Parameter( ParameterSetName = 'Properties')]
[Parameter( ParameterSetName = 'ContentProperties')]
[String]
$Description,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'Properties')]
[Switch]
$Explode,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'ContentSchema')]
[Parameter( ParameterSetName = 'Properties')]
[Parameter( ParameterSetName = 'ContentProperties')]
[Switch]
$Required,
[Parameter( ParameterSetName = 'ContentSchema')]
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'Properties')]
[Switch]
$AllowEmptyValue,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'Properties')]
[Switch]
$AllowReserved,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'ContentSchema')]
[Parameter( ParameterSetName = 'Properties')]
[Parameter( ParameterSetName = 'ContentProperties')]
[object]
$Example,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'ContentSchema')]
[Parameter( ParameterSetName = 'Properties')]
[Parameter( ParameterSetName = 'ContentProperties')]
[System.Collections.Specialized.OrderedDictionary]
$Examples,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'Properties')]
[ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )]
[string]
$Style,
[Parameter( ParameterSetName = 'Schema')]
[Parameter( ParameterSetName = 'ContentSchema')]
[Parameter( ParameterSetName = 'Properties')]
[Parameter( ParameterSetName = 'ContentProperties')]
[Switch]
$Deprecated,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
if ($PSCmdlet.ParameterSetName -ieq 'ContentSchema' -or $PSCmdlet.ParameterSetName -ieq 'Schema') {
if (Test-PodeIsEmpty $Schema) {
return $null
}
Test-PodeOAComponentInternal -Field schemas -DefinitionTag $DefinitionTag -Name $Schema -PostValidation
if (!$Name ) {
$Name = $Schema
}
$prop = [ordered]@{
in = $In.ToLowerInvariant()
name = $Name
}
if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders) {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Schema -Append
}
if ($AllowEmptyValue.IsPresent ) {
$prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent
}
if ($Required.IsPresent ) {
$prop['required'] = $Required.IsPresent
}
if ($Description ) {
$prop.description = $Description
}
if ($Deprecated.IsPresent ) {
$prop.deprecated = $Deprecated.IsPresent
}
if ($ContentType ) {
# ensure all content types are valid
if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') {
throw "Invalid content-type found for schema: $($type)"
}
$prop.content = [ordered]@{
$ContentType = [ordered]@{
schema = [ordered]@{
'$ref' = "#/components/schemas/$($Schema )"
}
}
}
if ($Example ) {
$prop.content.$ContentType.example = $Example
}
elseif ($Examples) {
$prop.content.$ContentType.examples = $Examples
}
}
else {
$prop.schema = [ordered]@{
'$ref' = "#/components/schemas/$($Schema )"
}
if ($Style) {
switch ($in.ToLower()) {
'path' {
if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
'query' {
if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
'header' {
if (@('Simple' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
'cookie' {
if (@('Form' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
}
$prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1)
}
if ($Explode.IsPresent ) {
$prop['explode'] = $Explode.IsPresent
}
if ($AllowEmptyValue.IsPresent ) {
$prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent
}
if ($AllowReserved.IsPresent) {
$prop['allowReserved'] = $AllowReserved.IsPresent
}
}
}
elseif ($PSCmdlet.ParameterSetName -ieq 'Reference') {
# return a reference
Test-PodeOAComponentInternal -Field parameters -DefinitionTag $DefinitionTag -Name $Reference -PostValidation
$prop = [ordered]@{
'$ref' = "#/components/parameters/$Reference"
}
foreach ($tag in $DefinitionTag) {
if ($PodeContext.Server.OpenAPI.Definitions[$tag].components.parameters.$Reference.In -eq 'Header' -and $PodeContext.Server.Security.autoHeaders) {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Reference -Append
}
}
}
else {
if (!$Name ) {
if ($Property.name) {
$Name = $Property.name
}
else {
throw 'Parameter requires a Name'
}
}
if ($In -ieq 'Header' -and $PodeContext.Server.Security.autoHeaders -and $Name ) {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value $Name -Append
}
# build the base parameter
$prop = [ordered]@{
in = $In.ToLowerInvariant()
name = $Name
}
$sch = [ordered]@{}
if ($Property.array) {
$sch.type = 'array'
$sch.items = [ordered]@{
type = $Property.type
}
if ($Property.format) {
$sch.items.format = $Property.format
}
}
else {
$sch.type = $Property.type
if ($Property.format) {
$sch.format = $Property.format
}
}
if ($ContentType) {
if ($ContentType -inotmatch '^[\w-]+\/[\w\.\+-]+$') {
throw "Invalid content-type found for schema: $($type)"
}
$prop.content = [ordered]@{
$ContentType = [ordered] @{
schema = $sch
}
}
}
else {
$prop.schema = $sch
}
if ($Example -and $Examples) {
throw '-Example and -Examples are mutually exclusive'
}
if ($AllowEmptyValue.IsPresent ) {
$prop['allowEmptyValue'] = $AllowEmptyValue.IsPresent
}
if ($Description ) {
$prop.description = $Description
}
elseif ($Property.description) {
$prop.description = $Property.description
}
if ($Required.IsPresent ) {
$prop.required = $Required.IsPresent
}
elseif ($Property.required) {
$prop.required = $Property.required
}
if ($Deprecated.IsPresent ) {
$prop.deprecated = $Deprecated.IsPresent
}
elseif ($Property.deprecated) {
$prop.deprecated = $Property.deprecated
}
if (!$ContentType) {
if ($Style) {
switch ($in.ToLower()) {
'path' {
if (@('Simple', 'Label', 'Matrix' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
'query' {
if (@('Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
'header' {
if (@('Simple' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
'cookie' {
if (@('Form' ) -inotcontains $Style) {
throw "OpenApi request Style cannot be $Style for a $in parameter"
}
break
}
}
$prop['style'] = $Style.Substring(0, 1).ToLower() + $Style.Substring(1)
}
if ($Explode.IsPresent ) {
$prop['explode'] = $Explode.IsPresent
}
if ($AllowReserved.IsPresent) {
$prop['allowReserved'] = $AllowReserved.IsPresent
}
if ($Example ) {
$prop['example'] = $Example
}
elseif ($Examples) {
$prop['examples'] = $Examples
}
if ($Property.default -and !$prop.required ) {
$prop.schema['default'] = $Property.default
}
if ($Property.enum) {
if ($Property.array) {
$prop.schema.items['enum'] = $Property.enum
}
else {
$prop.schema['enum'] = $Property.enum
}
}
}
else {
if ($Example ) {
$prop.content.$ContentType.example = $Example
}
elseif ($Examples) {
$prop.content.$ContentType.examples = $Examples
}
}
}
if ($In -ieq 'Path' -and !$prop.required ) {
Throw "If the parameter location is 'Path', the switch parameter `-Required` is required"
}
return $prop
}
<#
.SYNOPSIS
Sets metadate for the supplied route.
.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.
.PARAMETER Route
The route to update info, usually from -PassThru on Add-PodeRoute.
.PARAMETER Summary
A quick Summary of the route.
.PARAMETER Description
A longer Description of the route.
.PARAMETER ExternalDoc
If supplied, add an additional external documentation for this operation.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER OperationId
Sets the OperationId of the route.
.PARAMETER Tags
An array of Tags for the route, mostly for grouping.
.PARAMETER Deprecated
If supplied, the route will be flagged as deprecated.
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeRoute -PassThru | Set-PodeOARouteInfo -Summary 'A quick summary' -Tags 'Admin'
#>
function Set-PodeOARouteInfo {
[CmdletBinding()]
[OutputType([hashtable[]])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[hashtable[]]
$Route,
[string]
$Summary,
[string]
$Description,
[System.Collections.Specialized.OrderedDictionary]
$ExternalDoc,
[string]
$OperationId,
[string[]]
$Tags,
[switch]
$Deprecated,
[switch]
$PassThru,
[string[]]
$DefinitionTag
)
if ($null -eq $Route) { throw 'Set-PodeOARouteInfo - The parameter -Route cannot be NULL.' }
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($r in @($Route)) {
$r.OpenApi.DefinitionTag = $DefinitionTag
if ($Summary) {
$r.OpenApi.Summary = $Summary
}
if ($Description) {
$r.OpenApi.Description = $Description
}
if ($OperationId) {
if ($Route.Count -gt 1) {
throw "OperationID:$OperationId has to be unique and cannot be applied to an array."
}
foreach ($tag in $DefinitionTag) {
if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $OperationId) {
throw "OperationID:$OperationId has to be unique."
}
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId += $OperationId
}
$r.OpenApi.OperationId = $OperationId
}
if ($Tags) {
$r.OpenApi.Tags = $Tags
}
if ($ExternalDocs) {
$r.OpenApi.ExternalDocs = $ExternalDoc
}
$r.OpenApi.Swagger = $true
if ($Deprecated.IsPresent) {
$r.OpenApi.Deprecated = $Deprecated.IsPresent
}
}
if ($PassThru) {
return $Route
}
}
<#
.SYNOPSIS
Adds a route that enables a viewer to display OpenAPI docs, such as Swagger, ReDoc, RapiDoc, StopLight, Explorer, RapiPdf or Bookmarks.
.DESCRIPTION
Adds a route that enables a viewer to display OpenAPI docs, such as Swagger, ReDoc, RapiDoc, StopLight, Explorer, RapiPdf or Bookmarks.
.LINK
https://github.com/mrin9/RapiPdf
.LINK
https://github.com/Authress-Engineering/openapi-explorer
.LINK
https://github.com/stoplightio/elements
.LINK
https://github.com/rapi-doc/RapiDoc
.LINK
https://github.com/Redocly/redoc
.LINK
https://github.com/swagger-api/swagger-ui
.PARAMETER Type
The Type of OpenAPI viewer to use.
.PARAMETER Path
The route Path where the docs can be accessed. (Default: "/$Type")
.PARAMETER OpenApiUrl
The URL where the OpenAPI definition can be retrieved. (Default is the OpenAPI path from Enable-PodeOpenApi)
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied.
.PARAMETER Title
The title of the web page. (Default is the OpenAPI title from Enable-PodeOpenApi)
.PARAMETER DarkMode
If supplied, the page will be rendered using a dark theme (this is not supported for all viewers).
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to bind the static Route against.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Bookmarks
If supplied, create a new documentation bookmarks page
.PARAMETER Editor
If supplied, enable the Swagger-Editor
.PARAMETER NoAdvertise
If supplied, it is not going to state the documentation URL at the startup of the server
.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Enable-PodeOAViewer -Type Swagger -DarkMode
.EXAMPLE
Enable-PodeOAViewer -Type ReDoc -Title 'Some Title' -OpenApi 'http://some-url/openapi'
.EXAMPLE
Enable-PodeOAViewer -Bookmarks
Adds a route that enables a viewer to display with links to any documentation tool associated with the OpenApi.
#>
function Enable-PodeOAViewer {
[CmdletBinding(DefaultParameterSetName = 'Doc')]
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Doc')]
[ValidateSet('Swagger', 'ReDoc', 'RapiDoc', 'StopLight', 'Explorer', 'RapiPdf' )]
[string]
$Type,
[string]
$Path,
[string]
$OpenApiUrl,
[object[]]
$Middleware,
[string]
$Title,
[switch]
$DarkMode,
[string[]]
$EndpointName,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[Parameter(Mandatory = $true, ParameterSetName = 'Bookmarks')]
[switch]
$Bookmarks,
[Parameter( ParameterSetName = 'Bookmarks')]
[switch]
$NoAdvertise,
[Parameter(Mandatory = $true, ParameterSetName = 'Editor')]
[switch]
$Editor,
[string]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
# error if there's no OpenAPI URL
$OpenApiUrl = Protect-PodeValue -Value $OpenApiUrl -Default $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].Path
if ([string]::IsNullOrWhiteSpace($OpenApiUrl)) {
throw "No OpenAPI URL supplied for $($Type)"
}
# fail if no title
$Title = Protect-PodeValue -Value $Title -Default $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.Title
if ([string]::IsNullOrWhiteSpace($Title)) {
throw "No title supplied for $($Type) page"
}
if ($Editor.IsPresent) {
# set a default path
$Path = Protect-PodeValue -Value $Path -Default '/editor'
if ([string]::IsNullOrWhiteSpace($Title)) {
throw "No route path supplied for $($Type) page"
}
if (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag) {
throw "This version on Swagger-Editor doesn't support OpenAPI 3.1"
}
# setup meta info
$meta = @{
Title = $Title
OpenApi = "$($OpenApiUrl)?format=yaml"
DarkMode = $DarkMode
DefinitionTag = $DefinitionTag
SwaggerEditorDist = 'https://unpkg.com/swagger-editor-dist@4'
}
Add-PodeRoute -Method Get -Path $Path `
-Middleware $Middleware -ArgumentList $meta `
-EndpointName $EndpointName -Authentication $Authentication `
-Role $Role -Scope $Scope -Group $Group `
-ScriptBlock {
param($meta)
$Data = @{
Title = $meta.Title
OpenApi = $meta.OpenApi
SwaggerEditorDist = $meta.SwaggerEditorDist
}
$podeRoot = Get-PodeModuleMiscPath
Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, 'default-swagger-editor.html.pode')) -Data $Data
}
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer['editor'] = $Path
}
elseif ($Bookmarks.IsPresent) {
# set a default path
$Path = Protect-PodeValue -Value $Path -Default '/bookmarks'
if ([string]::IsNullOrWhiteSpace($Title)) {
throw "No route path supplied for $($Type) page"
}
# setup meta info
$meta = @{
Title = $Title
OpenApi = $OpenApiUrl
DarkMode = $DarkMode
DefinitionTag = $DefinitionTag
}
$route = Add-PodeRoute -Method Get -Path $Path `
-Middleware $Middleware -ArgumentList $meta `
-EndpointName $EndpointName -Authentication $Authentication `
-Role $Role -Scope $Scope -Group $Group `
-PassThru -ScriptBlock {
param($meta)
$Data = @{
Title = $meta.Title
OpenApi = $meta.OpenApi
}
$DefinitionTag = $meta.DefinitionTag
foreach ($type in $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer.Keys) {
$Data[$type] = $true
$Data["$($type)_path"] = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer[$type]
}
$podeRoot = Get-PodeModuleMiscPath
Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, 'default-doc-bookmarks.html.pode')) -Data $Data
}
if (! $NoAdvertise.IsPresent) {
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.bookmarks = @{
path = $Path
route = @()
openApiUrl = $OpenApiUrl
}
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.bookmarks.route += $route
}
}
else {
if ($Type -ieq 'RapiPdf' -and (Test-PodeOAVersion -Version 3.1 -DefinitionTag $DefinitionTag)) {
throw "The Document tool RapidPdf doesn't support OpenAPI 3.1"
}
# set a default path
$Path = Protect-PodeValue -Value $Path -Default "/$($Type.ToLowerInvariant())"
if ([string]::IsNullOrWhiteSpace($Title)) {
throw "No route path supplied for $($Type) page"
}
# setup meta info
$meta = @{
Type = $Type.ToLowerInvariant()
Title = $Title
OpenApi = $OpenApiUrl
DarkMode = $DarkMode
}
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.viewer[$($meta.Type)] = $Path
# add the viewer route
Add-PodeRoute -Method Get -Path $Path -Middleware $Middleware -ArgumentList $meta `
-EndpointName $EndpointName -Authentication $Authentication `
-Role $Role -Scope $Scope -Group $Group `
-ScriptBlock {
param($meta)
$podeRoot = Get-PodeModuleMiscPath
if ( $meta.DarkMode) { $Theme = 'dark' } else { $Theme = 'light' }
Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, "default-$($meta.Type).html.pode")) -Data @{
Title = $meta.Title
OpenApi = $meta.OpenApi
DarkMode = $meta.DarkMode
Theme = $Theme
}
}
}
}
<#
.SYNOPSIS
Define an external docs reference.
.DESCRIPTION
Define an external docs reference.
.PARAMETER url
The link to the external documentation
.PARAMETER Description
A Description of the external documentation.
.EXAMPLE
$swaggerDoc = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io'
Add-PodeRoute -PassThru | Set-PodeOARouteInfo -Summary 'A quick summary' -Tags 'Admin' -ExternalDoc $swaggerDoc
.EXAMPLE
$swaggerDoc = New-PodeOAExternalDoc -Description 'Find out more about Swagger' -Url 'http://swagger.io'
Add-PodeOATag -Name 'user' -Description 'Operations about user' -ExternalDoc $swaggerDoc
#>
function New-PodeOAExternalDoc {
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ $_ -imatch '^https?://.+' })]
$Url,
[string]
$Description
)
$param = [ordered]@{}
if ($Description) {
$param.description = $Description
}
$param['url'] = $Url
return $param
}
<#
.SYNOPSIS
Add an external docs reference to the OpenApi document.
.DESCRIPTION
Add an external docs reference to the OpenApi document.
.PARAMETER ExternalDoc
An externalDoc object
.PARAMETER Name
The Name of the reference.
.PARAMETER url
The link to the external documentation
.PARAMETER Description
A Description of the external documentation.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAExternalDoc -Name 'SwaggerDocs' -Description 'Find out more about Swagger' -Url 'http://swagger.io'
.EXAMPLE
$ExtDoc = New-PodeOAExternalDoc -Name 'SwaggerDocs' -Description 'Find out more about Swagger' -Url 'http://swagger.io'
$ExtDoc|Add-PodeOAExternalDoc
#>
function Add-PodeOAExternalDoc {
[CmdletBinding(DefaultParameterSetName = 'Pipe')]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Pipe')]
[System.Collections.Specialized.OrderedDictionary ]
$ExternalDoc,
[Parameter(Mandatory = $true, ParameterSetName = 'NewRef')]
[ValidateScript({ $_ -imatch '^https?://.+' })]
$Url,
[Parameter(ParameterSetName = 'NewRef')]
[string]
$Description,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
if ($PSCmdlet.ParameterSetName -ieq 'NewRef') {
$param = [ordered]@{url = $Url }
if ($Description) {
$param.description = $Description
}
$PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $param
}
else {
$PodeContext.Server.OpenAPI.Definitions[$tag].externalDocs = $ExternalDoc
}
}
}
<#
.SYNOPSIS
Creates a OpenAPI Tag reference property.
.DESCRIPTION
Creates a new OpenAPI tag reference.
.PARAMETER Name
The Name of the tag.
.PARAMETER Description
A Description of the tag.
.PARAMETER ExternalDoc
If supplied, the tag references an existing external documentation reference.
The parameter is created by Add-PodeOAExternalDoc
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOATag -Name 'store' -Description 'Access to Petstore orders' -ExternalDoc 'SwaggerDocs'
#>
function Add-PodeOATag {
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[string]
$Description,
[System.Collections.Specialized.OrderedDictionary]
$ExternalDoc,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($tag in $DefinitionTag) {
$param = [ordered]@{
'name' = $Name
}
if ($Description) {
$param.description = $Description
}
if ($ExternalDoc) {
$param.externalDocs = $ExternalDoc
}
$PodeContext.Server.OpenAPI.Definitions[$tag].tags[$Name] = $param
}
}
<#
.SYNOPSIS
Creates an OpenAPI metadata.
.DESCRIPTION
Creates an OpenAPI metadata like TermOfService, license and so on.
The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience.
.PARAMETER Title
The Title of the API.
.PARAMETER Version
The Version of the API.
The OpenAPI Specification is versioned using Semantic Versioning 2.0.0 (semver) and follows the semver specification.
https://semver.org/spec/v2.0.0.html
.PARAMETER Description
A short description of the API.
CommonMark syntax MAY be used for rich text representation.
https://spec.commonmark.org/
.PARAMETER TermsOfService
A URL to the Terms of Service for the API. MUST be in the format of a URL.
.PARAMETER LicenseName
The license name used for the API.
.PARAMETER LicenseUrl
A URL to the license used for the API. MUST be in the format of a URL.
.PARAMETER ContactName
The identifying name of the contact person/organization.
.PARAMETER ContactEmail
The email address of the contact person/organization. MUST be in the format of an email address.
.PARAMETER ContactUrl
The URL pointing to the contact information. MUST be in the format of a URL.
.PARAMETER DefinitionTag
A string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAInfo -TermsOfService 'http://swagger.io/terms/' -License 'Apache 2.0' -LicenseUrl 'http://www.apache.org/licenses/LICENSE-2.0.html' -ContactName 'API Support' -ContactEmail '[email protected]' -ContactUrl 'http://example.com/support'
#>
function Add-PodeOAInfo {
param(
[string]
$Title,
[ValidatePattern('^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$')]
[string]
$Version ,
[string]
$Description,
[ValidateScript({ $_ -imatch '^https?://.+' })]
[string]
$TermsOfService,
[string]
$LicenseName,
[ValidateScript({ $_ -imatch '^https?://.+' })]
[string]
$LicenseUrl,
[string]
$ContactName,
[ValidateScript({ $_ -imatch '^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$' })]
[string]
$ContactEmail,
[ValidateScript({ $_ -imatch '^https?://.+' })]
[string]
$ContactUrl,
[string]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
$Info = [ordered]@{}
if ($LicenseName) {
$Info.license = [ordered]@{
'name' = $LicenseName
}
}
if ($LicenseUrl) {
if ( $Info.license ) {
$Info.license.url = $LicenseUrl
}
else {
throw 'The OpenAPI property license.name is required. Use -LicenseName'
}
}
if ($Title) {
$Info.title = $Title
}
elseif ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.title) {
$Info.title = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.title
}
if ($Version) {
$Info.version = $Version
}
elseif ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.version) {
$Info.version = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.version
}
else {
$Info.version = '1.0.0'
}
if ($Description ) {
$Info.description = $Description
}
elseif ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.description) {
$Info.description = $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info.description
}
if ($TermsOfService) {
$Info['termsOfService'] = $TermsOfService
}
if ($ContactName -or $ContactEmail -or $ContactUrl ) {
$Info['contact'] = [ordered]@{}
if ($ContactName) {
$Info['contact'].name = $ContactName
}
if ($ContactEmail) {
$Info['contact'].email = $ContactEmail
}
if ($ContactUrl) {
$Info['contact'].url = $ContactUrl
}
}
$PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].info = $Info
}
<#
.SYNOPSIS
Creates a new OpenAPI example.
.DESCRIPTION
Creates a new OpenAPI example.
.PARAMETER ParamsList
Used to pipeline multiple properties
.PARAMETER MediaType
The Media Type associated with the Example.
.PARAMETER Name
The Name of the Example.
.PARAMETER Summary
Short description for the example
.PARAMETER Description
Long description for the example.
.PARAMETER Reference
A reference to a reusable component example
.PARAMETER Value
Embedded literal example. The value Parameter and ExternalValue parameter are mutually exclusive.
To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary.
.PARAMETER ExternalValue
A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents.
The -Value parameter and -ExternalValue parameter are mutually exclusive. |
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
New-PodeOAExample -ContentMediaType 'text/plain' -Name 'user' -Summary = 'User Example in Plain text' -ExternalValue = 'http://foo.bar/examples/user-example.txt'
.EXAMPLE
$example =
New-PodeOAExample -ContentMediaType 'application/json' -Name 'user' -Summary = 'User Example' -ExternalValue = 'http://foo.bar/examples/user-example.json' |
New-PodeOAExample -ContentMediaType 'application/xml' -Name 'user' -Summary = 'User Example in XML' -ExternalValue = 'http://foo.bar/examples/user-example.xml'
#>
function New-PodeOAExample {
[CmdletBinding(DefaultParameterSetName = 'Inbuilt')]
[OutputType([System.Collections.Specialized.OrderedDictionary ])]
param(
[Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Inbuilt')]
[Parameter(ValueFromPipeline = $true, DontShow = $true, ParameterSetName = 'Reference')]
[System.Collections.Specialized.OrderedDictionary ]
$ParamsList,
[string]
$MediaType,
[Parameter(Mandatory = $true, ParameterSetName = 'Inbuilt')]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter( ParameterSetName = 'Inbuilt')]
[string]
$Summary,
[Parameter( ParameterSetName = 'Inbuilt')]
[string]
$Description,
[Parameter( ParameterSetName = 'Inbuilt')]
[object]
$Value,
[Parameter( ParameterSetName = 'Inbuilt')]
[string]
$ExternalValue,
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Reference,
[string[]]
$DefinitionTag
)
begin {
if (Test-PodeIsEmpty -Value $DefinitionTag) {
$DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
}
if ($PSCmdlet.ParameterSetName -ieq 'Reference') {
Test-PodeOAComponentInternal -Field examples -DefinitionTag $DefinitionTag -Name $Reference -PostValidation
$Name = $Reference
$Example = [ordered]@{'$ref' = "#/components/examples/$Reference" }
}
else {
if ( $ExternalValue -and $Value) {
throw '-Value or -ExternalValue are mutually exclusive'
}
$Example = [ordered]@{ }
if ($Summary) {
$Example.summary = $Summary
}
if ($Description) {
$Example.description = $Description
}
if ($Value) {
$Example.value = $Value
}
elseif ($ExternalValue) {
$Example.externalValue = $ExternalValue
}
else {
throw '-Value or -ExternalValue are mandatory'
}
}
$param = [ordered]@{}
if ($MediaType) {
$param.$MediaType = [ordered]@{
$Name = $Example
}
}
else {
$param.$Name = $Example
}
}
process {
}
end {
if ($ParamsList) {
if ($ParamsList.keys -contains $param.Keys[0]) {
$param.Values[0].GetEnumerator() | ForEach-Object { $ParamsList[$param.Keys[0]].$($_.Key) = $_.Value }
}
else {
$param.GetEnumerator() | ForEach-Object { $ParamsList[$_.Key] = $_.Value }
}
return $ParamsList
}
else {
return [System.Collections.Specialized.OrderedDictionary] $param
}
}
}
<#
.SYNOPSIS
Adds a single encoding definition applied to a single schema property.
.DESCRIPTION
A single encoding definition applied to a single schema property.
.PARAMETER EncodingList
Used by pipe
.PARAMETER Title
The Name of the associated encoded property .
.PARAMETER ContentType
Content-Type for encoding a specific property. Default value depends on the property type: for `string` with `format` being `binary` – `application/octet-stream`;
for other primitive types – `text/plain`; for `object` - `application/json`; for `array` – the default is defined based on the inner type.
The value can be a specific media type (e.g. `application/json`), a wildcard media type (e.g. `image/*`), or a comma-separated list of the two types.
.PARAMETER Headers
A map allowing additional information to be provided as headers, for example `Content-Disposition`.
`Content-Type` is described separately and SHALL be ignored in this section.
This property SHALL be ignored if the request body media type is not a `multipart`.
.PARAMETER Style
Describes how a specific property value will be serialized depending on its type. See [Parameter Object](#parameterObject) for details on the [`style`](#parameterStyle) property.
The behavior follows the same values as `query` parameters, including default values.
This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`.
.PARAMETER Explode
When enabled, property values of type `array` or `object` generate separate parameters for each value of the array, or key-value-pair of the map. For other types of properties this property has no effect.
When [`style`](#encodingStyle) is `form`, the `Explode` is set to `true`.
This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`.
.PARAMETER AllowReserved
Determines whether the parameter value SHOULD allow reserved characters, as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) `:/?#[]@!$&'()*+,;=` to be included without percent-encoding.
This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`.
.EXAMPLE
New-PodeOAEncodingObject -Name 'profileImage' -ContentType 'image/png, image/jpeg' -Headers (
New-PodeOAIntProperty -name 'X-Rate-Limit-Limit' -Description 'The number of allowed requests in the current period' -Default 3 -Enum @(1,2,3) -Maximum 3
)
#>
function New-PodeOAEncodingObject {
param (
[Parameter(ValueFromPipeline = $true, DontShow = $true )]
[hashtable[]]
$EncodingList,
[Parameter(Mandatory = $true)]
[Alias('Name')]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Title,
[string]
$ContentType,
[hashtable[]]
$Headers,
[ValidateSet('Simple', 'Label', 'Matrix', 'Query', 'Form', 'SpaceDelimited', 'PipeDelimited', 'DeepObject' )]
[string]
$Style,
[switch]
$Explode,
[switch]
$AllowReserved
)
begin {
$encoding = [ordered]@{
$Title = [ordered]@{}
}
if ($ContentType) {
$encoding.$Title.contentType = $ContentType
}
if ($Style) {
$encoding.$Title.style = $Style
}
if ($Headers) {
$encoding.$Title.headers = $Headers
}
if ($Explode.IsPresent ) {
$encoding.$Title.explode = $Explode.IsPresent
}
if ($AllowReserved.IsPresent ) {
$encoding.$Title.allowReserved = $AllowReserved.IsPresent
}
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($EncodingList) {
$collectedInput.AddRange($EncodingList)
}
}
end {
if ($collectedInput) {
return $collectedInput + $encoding
}
else {
return $encoding
}
}
}
<#
.SYNOPSIS
Adds OpenAPI callback configurations to routes in a Pode web application.
.PARAMETER Route
The route to update info, usually from -PassThru on Add-PodeRoute.
.DESCRIPTION
The Add-PodeOACallBack function is used for defining OpenAPI callback configurations for routes in a Pode server.
It enables setting up API specifications including detailed parameters, request body schemas, and response structures for various HTTP methods.
.PARAMETER Path
Specifies the callback path, usually a relative URL.
The key that identifies the Path Item Object is a runtime expression evaluated in the context of a runtime HTTP request/response to identify the URL for the callback request.
A simple example is `$request.body#/url`.
The runtime expression allows complete access to the HTTP message, including any part of a body that a JSON Pointer (RFC6901) can reference.
More information on JSON Pointer can be found at [RFC6901](https://datatracker.ietf.org/doc/html/rfc6901).
.PARAMETER Name
Alias for 'Name'. A unique identifier for the callback.
It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).
.PARAMETER Reference
A reference to a reusable CallBack component.
.PARAMETER Method
Defines the HTTP method for the callback (e.g., GET, POST, PUT). Supports standard HTTP methods and a wildcard (*) for all methods.
.PARAMETER Parameters
The Parameter definitions the request uses (from ConvertTo-PodeOAParameter).
.PARAMETER RequestBody
Defines the schema of the request body. Can be set using New-PodeOARequestBody.
.PARAMETER Responses
Defines the possible responses for the callback. Can be set using New-PodeOAResponse.
.PARAMETER DefinitionTag
A array of string representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.EXAMPLE
Add-PodeOACallBack -Title 'test' -Path '{$request.body#/id}' -Method Post `
-RequestBody (New-PodeOARequestBody -Content @{'*/*' = (New-PodeOAStringProperty -Name 'id')}) `
-Response (
New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array)
New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' |
New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' |
New-PodeOAResponse -Default -Description 'Something is wrong'
)
This example demonstrates adding a POST callback to handle a request body and define various responses based on different status codes.
.NOTES
Ensure that the provided parameters match the expected schema and formats of Pode and OpenAPI specifications.
The function is useful for dynamically configuring and documenting API callbacks in a Pode server environment.
#>
function Add-PodeOACallBack {
[CmdletBinding(DefaultParameterSetName = 'inbuilt')]
[OutputType([hashtable[]])]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[hashtable[]]
$Route,
[Parameter(Mandatory = $true , ParameterSetName = 'inbuilt')]
[Parameter(Mandatory = $false, ParameterSetName = 'Reference')]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[string]
$Reference,
[Parameter(Mandatory = $true , ParameterSetName = 'inbuilt')]
[string]
$Path,
[Parameter(Mandatory = $true, ParameterSetName = 'inbuilt')]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[Parameter(ParameterSetName = 'inbuilt')]
[hashtable[]]
$Parameters,
[Parameter(ParameterSetName = 'inbuilt')]
[hashtable]
$RequestBody,
[Parameter(ParameterSetName = 'inbuilt')]
[hashtable]
$Responses,
[switch]
$PassThru,
[string[]]
$DefinitionTag
)
if ($null -eq $Route) { throw 'Add-PodeOACallBack - The parameter -Route cannot be NULL.' }
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
foreach ($r in @($Route)) {
foreach ($tag in $DefinitionTag) {
if ($Reference) {
Test-PodeOAComponentInternal -Field callbacks -DefinitionTag $tag -Name $Reference -PostValidation
if (!$Name) {
$Name = $Reference
}
if (! $r.OpenApi.CallBacks.ContainsKey($tag)) {
$r.OpenApi.CallBacks[$tag] = [ordered]@{}
}
$r.OpenApi.CallBacks[$tag].$Name = @{
'$ref' = "#/components/callbacks/$Reference"
}
}
else {
if (! $r.OpenApi.CallBacks.ContainsKey($tag)) {
$r.OpenApi.CallBacks[$tag] = [ordered]@{}
}
$r.OpenApi.CallBacks[$tag].$Name = New-PodeOAComponentCallBackInternal -Params $PSBoundParameters -DefinitionTag $tag
}
}
}
if ($PassThru) {
return $Route
}
}
<#
.SYNOPSIS
Adds a response definition to the Callback.
.DESCRIPTION
Adds a response definition to the Callback.
.PARAMETER ResponseList
Hidden parameter used to pipe multiple CallBacksResponses
.PARAMETER StatusCode
The HTTP StatusCode for the response.To define a range of response codes, this field MAY contain the uppercase wildcard character `X`.
For example, `2XX` represents all response codes between `[200-299]`. Only the following range definitions are allowed: `1XX`, `2XX`, `3XX`, `4XX`, and `5XX`.
If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code.
.PARAMETER Content
The content-types and schema the response returns (the schema is created using the Property functions).
Alias: ContentSchemas
.PARAMETER Headers
The header name and schema the response returns (the schema is created using Add-PodeOAComponentHeader cmd-let).
Alias: HeaderSchemas
.PARAMETER Description
A Description of the response. (Default: the HTTP StatusCode description)
.PARAMETER Reference
A Reference Name of an existing component response to use.
.PARAMETER Links
A Response link definition
.PARAMETER Default
If supplied, the response will be used as a default response - this overrides the StatusCode supplied.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
New-PodeOAResponse -StatusCode 200 -Content ( New-PodeOAContentMediaType -ContentMediaType 'application/json' -Content(New-PodeOAIntProperty -Name 'userId' -Object) )
.EXAMPLE
New-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = 'UserIdSchema' }
.EXAMPLE
New-PodeOAResponse -StatusCode 200 -Reference 'OKResponse'
.EXAMPLE
Add-PodeOACallBack -Title 'test' -Path '$request.body#/id' -Method Post -RequestBody (
New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentMediaType '*/*' -Content (New-PodeOAStringProperty -Name 'id'))
) `
-Response (
New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array) |
New-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' |
New-PodeOAResponse -StatusCode 404 -Description 'Pet not found' |
New-PodeOAResponse -Default -Description 'Something is wrong'
)
#>
function New-PodeOAResponse {
[CmdletBinding(DefaultParameterSetName = 'Schema')]
[OutputType([hashtable])]
param(
[Parameter(ValueFromPipeline = $true , DontShow = $true )]
[hashtable]
$ResponseList,
[Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[ValidatePattern('^([1-5][0-9][0-9]|[1-5]XX)$')]
[string]
$StatusCode,
[Parameter(ParameterSetName = 'Schema')]
[Parameter(ParameterSetName = 'SchemaDefault')]
[Alias('ContentSchemas')]
[hashtable]
$Content,
[Alias('HeaderSchemas')]
[AllowEmptyString()]
[ValidateNotNullOrEmpty()]
[ValidateScript({ $_ -is [string] -or $_ -is [string[]] -or $_ -is [hashtable] })]
$Headers,
[Parameter(Mandatory = $true, ParameterSetName = 'Schema')]
[Parameter(Mandatory = $true, ParameterSetName = 'SchemaDefault')]
[string]
$Description ,
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[Parameter(ParameterSetName = 'ReferenceDefault')]
[string]
$Reference,
[Parameter(Mandatory = $true, ParameterSetName = 'ReferenceDefault')]
[Parameter(Mandatory = $true, ParameterSetName = 'SchemaDefault')]
[switch]
$Default,
[Parameter(ParameterSetName = 'Schema')]
[Parameter(ParameterSetName = 'SchemaDefault')]
[System.Collections.Specialized.OrderedDictionary ]
$Links,
[string[]]
$DefinitionTag
)
begin {
if (Test-PodeIsEmpty -Value $DefinitionTag) {
$DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
}
# override status code with default
if ($Default) {
$code = 'default'
}
else {
$code = "$($StatusCode)"
}
$response = @{}
}
process {
foreach ($tag in $DefinitionTag) {
if (! $response.$tag) {
$response.$tag = [ordered] @{}
}
$response[$tag][$code] = New-PodeOResponseInternal -DefinitionTag $tag -Params $PSBoundParameters
}
}
end {
if ($ResponseList) {
foreach ($tag in $DefinitionTag) {
if (! $ResponseList.ContainsKey( $tag) ) {
$ResponseList[$tag] = [ordered] @{}
}
$response[$tag].GetEnumerator() | ForEach-Object { $ResponseList[$tag][$_.Key] = $_.Value }
}
return $ResponseList
}
else {
return $response
}
}
}
<#
.SYNOPSIS
Creates media content type definitions for OpenAPI specifications.
.DESCRIPTION
The New-PodeOAContentMediaType function generates media content type definitions suitable for use in OpenAPI specifications. It supports various media types and allows for the specification of content as either a single object or an array of objects.
.PARAMETER MediaType
An array of strings specifying the media types to be defined. Media types should conform to standard MIME types (e.g., 'application/json', 'image/png'). The function validates these media types against a regular expression to ensure they are properly formatted.
.PARAMETER Content
The content definition for the media type. This could be an object representing the structure of the content expected for the specified media types.
.PARAMETER Array
A switch parameter, used in the 'Array' parameter set, to indicate that the content should be treated as an array.
.PARAMETER UniqueItems
A switch parameter, used in the 'Array' parameter set, to specify that items in the array should be unique.
.PARAMETER MinItems
Used in the 'Array' parameter set to specify the minimum number of items that should be present in the array.
.PARAMETER MaxItems
Used in the 'Array' parameter set to specify the maximum number of items that should be present in the array.
.PARAMETER Title
Used in the 'Array' parameter set to provide a title for the array content.
.PARAMETER Upload
If provided configure the media for an upload changing the result based on the OpenApi version
.PARAMETER ContentEncoding
Define the content encoding for upload (Default Binary)
.PARAMETER PartContentMediaType
Define the content encoding for multipart upload
.EXAMPLE
Add-PodeRoute -PassThru -Method get -Path '/pet/findByStatus' -Authentication 'Login-OAuth2' -Scope 'read' -ScriptBlock {
Write-PodeJsonResponse -Value 'done' -StatusCode 200
} | Set-PodeOARouteInfo -Summary 'Finds Pets by status' -Description 'Multiple status values can be provided with comma separated strings' -Tags 'pet' -OperationId 'findPetsByStatus' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
(New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query)
) |
Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru |
Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value'
This example demonstrates the use of New-PodeOAContentMediaType in defining a GET route '/pet/findByStatus' in an OpenAPI specification. The route includes request parameters and responses with media content types for 'application/json' and 'application/xml'.
.EXAMPLE
$content = @{ type = 'string' }
$mediaType = 'application/json'
New-PodeOAContentMediaType -MediaType $mediaType -Content $content
This example creates a media content type definition for 'application/json' with a simple string content type.
.EXAMPLE
$content = @{ type = 'object'; properties = @{ name = @{ type = 'string' } } }
$mediaTypes = 'application/json', 'application/xml'
New-PodeOAContentMediaType -MediaType $mediaTypes -Content $content -Array -MinItems 1 -MaxItems 5 -Title 'UserList'
This example demonstrates defining an array of objects for both 'application/json' and 'application/xml' media types, with a specified range for the number of items and a title.
.EXAMPLE
Add-PodeRoute -PassThru -Method get -Path '/pet/findByStatus' -Authentication 'Login-OAuth2' -Scope 'read' -ScriptBlock {
Write-PodeJsonResponse -Value 'done' -StatusCode 200
} | Set-PodeOARouteInfo -Summary 'Finds Pets by status' -Description 'Multiple status values can be provided with comma separated strings' -Tags 'pet' -OperationId 'findPetsByStatus' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
(New-PodeOAStringProperty -Name 'status' -Description 'Status values that need to be considered for filter' -Default 'available' -Enum @('available', 'pending', 'sold') | ConvertTo-PodeOAParameter -In Query)
) |
Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -ContentMediaType 'application/json','application/xml' -Content 'Pet' -Array -UniqueItems) -PassThru |
Add-PodeOAResponse -StatusCode 400 -Description 'Invalid status value'
This example demonstrates the use of New-PodeOAContentMediaType in defining a GET route '/pet/findByStatus' in an OpenAPI specification. The route includes request parameters and responses with media content types for 'application/json' and 'application/xml'.
.NOTES
This function is useful for dynamically creating media type specifications in OpenAPI documentation, providing flexibility in defining the expected content structure for different media types.
#>
function New-PodeOAContentMediaType {
[CmdletBinding(DefaultParameterSetName = 'inbuilt')]
[OutputType([System.Collections.Specialized.OrderedDictionary])]
param (
[string[]]
$MediaType = '*/*',
[object]
$Content,
[Parameter( Mandatory = $true, ParameterSetName = 'Array')]
[switch]
$Array,
[Parameter(ParameterSetName = 'Array')]
[switch]
$UniqueItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MinItems,
[Parameter(ParameterSetName = 'Array')]
[int]
$MaxItems,
[Parameter(ParameterSetName = 'Array')]
[string]
$Title,
[Parameter(Mandatory = $true, ParameterSetName = 'Upload')]
[switch]
$Upload,
[Parameter( ParameterSetName = 'Upload')]
[ValidateSet('Binary', 'Base64')]
[string]
$ContentEncoding = 'Binary',
[Parameter( ParameterSetName = 'Upload')]
[string]
$PartContentMediaType
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
$props = [ordered]@{}
foreach ($media in $MediaType) {
if ($media -inotmatch '^(application|audio|image|message|model|multipart|text|video|\*)\/[\w\.\-\*]+(;[\s]*(charset|boundary)=[\w\.\-\*]+)*$') {
throw "Invalid content-type found for schema: $($media)"
}
if ($Upload.IsPresent) {
if ( $media -ieq 'multipart/form-data' -and $Content) {
$Content = @{'__upload' = @{
'content' = $Content
'partContentMediaType' = $PartContentMediaType
}
}
}
else {
$Content = @{'__upload' = @{
'contentEncoding' = $ContentEncoding
}
}
}
}
else {
if ($null -eq $Content ) {
$Content = @{}
}
}
if ($Array.IsPresent) {
$props[$media] = @{
__array = $true
__content = $Content
__upload = $Upload
}
if ($MinItems) {
$props[$media].__minItems = $MinItems
}
if ($MaxItems) {
$props[$media].__maxItems = $MaxItems
}
if ($Title) {
$props[$media].__title = $Title
}
if ($UniqueItems.IsPresent) {
$props[$media].__uniqueItems = $UniqueItems.IsPresent
}
}
else {
$props[$media] = $Content
}
}
return $props
}
<#
.SYNOPSIS
Adds a response link to an existing list of OpenAPI response links.
.DESCRIPTION
The New-PodeOAResponseLink function is designed to add a new response link to an existing OrderedDictionary of OpenAPI response links.
It can be used to define complex response structures with links to other operations or references, and it supports adding multiple links through pipeline input.
.PARAMETER LinkList
An OrderedDictionary of existing response links.
This parameter is intended for use with pipeline input, allowing the function to add multiple links to the collection.
It is hidden from standard help displays to emphasize its use primarily in pipeline scenarios.
.PARAMETER Name
Mandatory. A unique name for the response link.
Must be a valid string composed of alphanumeric characters, periods (.), hyphens (-), and underscores (_).
.PARAMETER Description
A brief description of the response link. CommonMark syntax may be used for rich text representation.
For more information on CommonMark syntax, see [CommonMark Specification](https://spec.commonmark.org/).
.PARAMETER OperationId
The name of an existing, resolvable OpenAPI Specification (OAS) operation, as defined with a unique `operationId`.
This parameter is mandatory when using the 'OperationId' parameter set and is mutually exclusive of the `OperationRef` field. It is used to specify the unique identifier of the operation the link is associated with.
.PARAMETER OperationRef
A relative or absolute URI reference to an OAS operation.
This parameter is mandatory when using the 'OperationRef' parameter set and is mutually exclusive of the `OperationId` field.
It MUST point to an Operation Object. Relative `operationRef` values MAY be used to locate an existing Operation Object in the OpenAPI specification.
.PARAMETER Reference
A Reference Name of an existing component link to use.
.PARAMETER Parameters
A map representing parameters to pass to an operation as specified with `operationId` or identified via `operationRef`.
The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and passed to the linked operation.
Parameter names can be qualified using the parameter location syntax `[{in}.]{name}` for operations that use the same parameter name in different locations (e.g., path.id).
.PARAMETER RequestBody
A string representing the request body to use as a request body when calling the target.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
$links = New-PodeOAResponseLink -LinkList $links -Name 'address' -OperationId 'getUserByName' -Parameters @{'username' = '$request.path.username'}
Add-PodeOAResponse -StatusCode 200 -Content @{'application/json' = 'User'} -Links $links
This example demonstrates creating and adding a link named 'address' associated with the operation 'getUserByName' to an OrderedDictionary of links. The updated dictionary is then used in the 'Add-PodeOAResponse' function to define a response with a status code of 200.
.NOTES
The function supports adding links either by specifying an 'OperationId' or an 'OperationRef', making it versatile for different OpenAPI specification needs.
It's important to match the parameters and response structures as per the OpenAPI specification to ensure the correct functionality of the API documentation.
#>
function New-PodeOAResponseLink {
[CmdletBinding(DefaultParameterSetName = 'OperationId')]
[OutputType([System.Collections.Specialized.OrderedDictionary])]
param(
[Parameter(ValueFromPipeline = $true , DontShow = $true )]
[System.Collections.Specialized.OrderedDictionary ]
$LinkList,
[Parameter( Mandatory = $false, ParameterSetName = 'Reference')]
[Parameter( Mandatory = $true, ParameterSetName = 'OperationRef')]
[Parameter( Mandatory = $true, ParameterSetName = 'OperationId')]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter( ParameterSetName = 'OperationRef')]
[Parameter( ParameterSetName = 'OperationId')]
[string]
$Description,
[Parameter(Mandatory = $true, ParameterSetName = 'OperationId')]
[string]
$OperationId,
[Parameter(Mandatory = $true, ParameterSetName = 'OperationRef')]
[string]
$OperationRef,
[Parameter( ParameterSetName = 'OperationRef')]
[Parameter( ParameterSetName = 'OperationId')]
[hashtable]
$Parameters,
[Parameter( ParameterSetName = 'OperationRef')]
[Parameter( ParameterSetName = 'OperationId')]
[string]
$RequestBody,
[Parameter(Mandatory = $true, ParameterSetName = 'Reference')]
[string]
$Reference,
[string[]]
$DefinitionTag
)
begin {
if (Test-PodeIsEmpty -Value $DefinitionTag) {
$DefinitionTag = $PodeContext.Server.OpenAPI.SelectedDefinitionTag
}
if ($Reference) {
Test-PodeOAComponentInternal -Field links -DefinitionTag $DefinitionTag -Name $Reference -PostValidation
if (!$Name) {
$Name = $Reference
}
$link = [ordered]@{
$Name = @{
'$ref' = "#/components/links/$Reference"
}
}
}
else {
$link = [ordered]@{
$Name = New-PodeOAResponseLinkInternal -Params $PSBoundParameters
}
}
}
process {
}
end {
if ($LinkList) {
$link.GetEnumerator() | ForEach-Object { $LinkList[$_.Key] = $_.Value }
return $LinkList
}
else {
return [System.Collections.Specialized.OrderedDictionary] $link
}
}
}
<#
.SYNOPSIS
Sets metadate for the supplied route.
.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.
.PARAMETER Route
The route to update info, usually from -PassThru on Add-PodeRoute.
.PARAMETER Path
The URI path for the Route.
.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.
.PARAMETER Servers
A list of external endpoint. created with New-PodeOAServerEndpoint
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAExternalRoute -PassThru -Method Get -Path '/peta/:id' -Servers (
New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
) |
Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
(New-PodeOAStringProperty -Name 'id' -Description 'ID of pet to use' -array | ConvertTo-PodeOAParameter -In Path -Style Simple -Required )) |
Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOASchemaProperty -ComponentSchema 'Pet' -array }) -PassThru |
Add-PodeOAResponse -Default -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
.EXAMPLE
Add-PodeRoute -PassThru -Method Get -Path '/peta/:id' -ScriptBlock {
Write-PodeJsonResponse -Value 'done' -StatusCode 200
} | Add-PodeOAExternalRoute -PassThru -Servers (
New-PodeOAServerEndpoint -Url 'http://ext.server.com/api/v12' -Description 'ext test server' |
New-PodeOAServerEndpoint -Url 'http://ext13.server.com/api/v12' -Description 'ext test server 13'
) |
Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
(New-PodeOAStringProperty -Name 'id' -Description 'ID of pet to use' -array | ConvertTo-PodeOAParameter -In Path -Style Simple -Required )) |
Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOASchemaProperty -ComponentSchema 'Pet' -array }) -PassThru |
Add-PodeOAResponse -Default -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
#>
function Add-PodeOAExternalRoute {
[CmdletBinding(DefaultParameterSetName = 'Pipeline')]
[OutputType([hashtable[]], ParameterSetName = 'Pipeline')]
[OutputType([hashtable], ParameterSetName = 'builtin')]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Pipeline')]
[ValidateNotNullOrEmpty()]
[hashtable[]]
$Route,
[Parameter(Mandatory = $true , ParameterSetName = 'BuiltIn')]
[string]
$Path,
[Parameter(Mandatory = $true)]
[ValidateScript({ $_.Count -gt 0 })]
[hashtable[]]
$Servers,
[Parameter(Mandatory = $true, ParameterSetName = 'BuiltIn')]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[switch]
$PassThru,
[Parameter( ParameterSetName = 'BuiltIn')]
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'builtin' {
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlashes -Path $Path
$OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
$Path = Resolve-PodePlaceholders -Path $Path
$extRoute = @{
Method = $Method.ToLower()
Path = $Path
Local = $false
OpenApi = @{
Path = $OpenApiPath
Responses = $null
Parameters = $null
RequestBody = $null
callbacks = [ordered]@{}
Authentication = @()
Servers = $Servers
DefinitionTag = $DefinitionTag
}
}
foreach ($tag in $DefinitionTag) {
#add the default OpenApi responses
if ( $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses) {
$extRoute.OpenApi.Responses = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone()
}
if (! (Test-PodeOAComponentExternalPath -DefinitionTag $tag -Name $Path)) {
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath[$Path] = @{}
}
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.externalPath.$Path[$Method] = $extRoute
}
if ($PassThru) {
return $extRoute
}
}
'pipeline' {
if ($null -eq $Route) { throw 'Add-PodeOAExternalRoute - The parameter -Route cannot be NULL.' }
foreach ($r in @($Route)) {
$r.OpenApi.Servers = $Servers
}
if ($PassThru) {
return $Route
}
}
}
}
<#
.SYNOPSIS
Creates an OpenAPI Server Object.
.DESCRIPTION
Creates an OpenAPI Server Object to use with Add-PodeOAExternalRoute
.PARAMETER ServerEndpointList
Used for piping
.PARAMETER Url
A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served.
Variable substitutions will be made when a variable is named in `{`brackets`}`.
.PARAMETER Description
An optional string describing the host designated by the URL. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation.
.EXAMPLE
New-PodeOAServerEndpoint -Url 'https://myserver.io/api' -Description 'My test server'
.EXAMPLE
New-PodeOAServerEndpoint -Url '/api' -Description 'My local server'
}
#>
function New-PodeOAServerEndpoint {
param (
[Parameter(ValueFromPipeline = $true , DontShow = $true )]
[hashtable[]]
$ServerEndpointList,
[Parameter(Mandatory = $true)]
[ValidatePattern('^(https?://|/).+')]
[string]
$Url,
[string]
$Description
)
begin {
$lUrl = [ordered]@{url = $Url }
if ($Description) {
$lUrl.description = $Description
}
$collectedInput = [System.Collections.Generic.List[hashtable]]::new()
}
process {
if ($ServerEndpointList) {
$collectedInput.AddRange($ServerEndpointList)
}
}
end {
if ($ServerEndpointList) {
return $collectedInput + $lUrl
}
else {
return $lUrl
}
}
}
<#
.SYNOPSIS
Sets metadate for the supplied route.
.DESCRIPTION
Sets metadate for the supplied route, such as Summary and Tags.
.PARAMETER Name
Alias for 'Name'. A unique identifier for the webhook.
It must be a valid string of alphanumeric characters, periods (.), hyphens (-), and underscores (_).
.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.
.PARAMETER PassThru
If supplied, the route passed in will be returned for further chaining.
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeOAWebhook -PassThru -Method Get |
Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru |
Set-PodeOARequest -PassThru -Parameters @(
(New-PodeOAStringProperty -Name 'id' -Description 'ID of pet to use' -array | ConvertTo-PodeOAParameter -In Path -Style Simple -Required )) |
Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOASchemaProperty -ComponentSchema 'Pet' -array }) -PassThru |
Add-PodeOAResponse -Default -Description 'error payload' -Content (@{'text/html' = 'ErrorModel' }) -PassThru
#>
function Add-PodeOAWebhook {
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[a-zA-Z0-9\.\-_]+$')]
[string]
$Name,
[Parameter(Mandatory = $true )]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[switch]
$PassThru,
[string[]]
$DefinitionTag
)
$DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag
$refRoute = @{
Method = $Method.ToLower()
NotPrepared = $true
OpenApi = @{
Responses = @{}
Parameters = $null
RequestBody = $null
callbacks = [ordered]@{}
Authentication = @()
}
}
foreach ($tag in $DefinitionTag) {
if (Test-PodeOAVersion -Version 3.0 -DefinitionTag $tag ) {
throw 'The feature reusable component webhook is not available in OpenAPI v3.0.x'
}
$PodeContext.Server.OpenAPI.Definitions[$tag].webhooks[$Name] = $refRoute
}
if ($PassThru) {
return $refRoute
}
}
<#
.SYNOPSIS
Select a group of OpenAPI Definions for modification.
.DESCRIPTION
Select a group of OpenAPI Definions for modification.
.PARAMETER Tag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
If Tag is empty or null the default Definition is selected
.PARAMETER ScriptBlock
The ScriptBlock that will modified the group.
.EXAMPLE
Select-PodeOADefinition -Tag 'v3', 'v3.1' -Script {
New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -Required |
New-PodeOAIntProperty -Name 'petId' -Format Int64 -Example 198772 -Required |
New-PodeOAIntProperty -Name 'quantity' -Format Int32 -Example 7 -Required |
New-PodeOAStringProperty -Name 'shipDate' -Format Date-Time |
New-PodeOAStringProperty -Name 'status' -Description 'Order Status' -Required -Example 'approved' -Enum @('placed', 'approved', 'delivered') |
New-PodeOABoolProperty -Name 'complete' |
New-PodeOAObjectProperty -XmlName 'order' |
Add-PodeOAComponentSchema -Name 'Order'
New-PodeOAContentMediaType -ContentMediaType 'application/json', 'application/xml' -Content 'Pet' |
Add-PodeOAComponentRequestBody -Name 'Pet' -Description 'Pet object that needs to be added to the store'
}
#>
function Select-PodeOADefinition {
[CmdletBinding()]
param(
[string[]]
$Tag,
[Parameter(Mandatory = $true)]
[scriptblock]
$Scriptblock
)
if (Test-PodeIsEmpty $Scriptblock) {
throw 'No scriptblock for -Scriptblock passed'
}
if (Test-PodeIsEmpty -Value $Tag) {
$Tag = $PodeContext.Server.OpenAPI.DefaultDefinitionTag
}
else {
$Tag = Test-PodeOADefinitionTag -Tag $Tag
}
# check for scoped vars
$Scriptblock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Scriptblock -PSSession $PSCmdlet.SessionState
$PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Push($PodeContext.Server.OpenAPI.SelectedDefinitionTag)
$PodeContext.Server.OpenAPI.SelectedDefinitionTag = $Tag
$null = Invoke-PodeScriptBlock -ScriptBlock $Scriptblock -UsingVariables $usingVars -Splat
$PodeContext.Server.OpenAPI.SelectedDefinitionTag = $PodeContext.Server.OpenApi.DefinitionTagSelectionStack.Pop()
}
<#
.SYNOPSIS
Check if a Definition exist
.DESCRIPTION
Check if a Definition exist. If the parameter Tag is empty or Null $PodeContext.Server.OpenAPI.SelectedDefinitionTag is returned
.PARAMETER Tag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Test-PodeOADefinitionTag -Tag 'v3', 'v3.1'
#>
function Test-PodeOADefinitionTag {
param (
[Parameter(Mandatory = $false)]
[string[]]
$Tag
)
if ($Tag -and $Tag.Count -gt 0) {
foreach ($t in $Tag) {
if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $t)) {
throw "DefinitionTag $t is not defined"
}
}
return $Tag
}
else {
return $PodeContext.Server.OpenAPI.SelectedDefinitionTag
}
}
<#
.SYNOPSIS
Validate the OpenAPI definition if all Reference are satisfied
.DESCRIPTION
Validate the OpenAPI definition if all Reference are satisfied
.PARAMETER DefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps distinguish between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
if ((Test-PodeOADefinition -DefinitionTag 'v3').count -eq 0){
Write-PodeHost "The OpenAPI definition is valid"
}
#>
function Test-PodeOADefinition {
param (
[string[]]
$DefinitionTag
)
if (! ($DefinitionTag -and $DefinitionTag.Count -gt 0)) {
$DefinitionTag = $PodeContext.Server.OpenAPI.Definitions.keys
}
$result = @{
valid = $true
issues = @{
}
}
foreach ($tag in $DefinitionTag) {
if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.enabled) {
if ([string]::IsNullOrWhiteSpace( $PodeContext.Server.OpenAPI.Definitions[$tag].info.title) -or [string]::IsNullOrWhiteSpace( $PodeContext.Server.OpenAPI.Definitions[$tag].info.version)) {
$result.valid = $false
}
$result.issues[$tag] = @{
title = [string]::IsNullOrWhiteSpace( $PodeContext.Server.OpenAPI.Definitions[$tag].info.title)
version = [string]::IsNullOrWhiteSpace( $PodeContext.Server.OpenAPI.Definitions[$tag].info.version)
components = @{}
definition = ''
}
foreach ($field in $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation.keys) {
foreach ($name in $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field].keys) {
if (! (Test-PodeOAComponentInternal -DefinitionTag $tag -Field $field -Name $name)) {
$result.issues[$tag].components["#/components/$field/$name"] = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.postValidation[$field][$name]
$result.valid = $false
}
}
}
try {
Get-PodeOADefinition -DefinitionTag $tag | Out-Null
}
catch {
$result.issues[$tag].definition = $_.Exception.Message
}
}
}
return $result
}
<#
.SYNOPSIS
Attaches a file onto the Response for downloading.
.DESCRIPTION
Attaches a file from the "/public", and static Routes, onto the Response for downloading.
If the supplied path is not in the Static Routes but is a literal/relative path, then this file is used instead.
.PARAMETER Path
The Path to a static file relative to the "/public" directory, or a static Route.
If the supplied Path doesn't match any custom static Route, then Pode will look in the "/public" directory.
Failing this, if the file path exists as a literal/relative file, then this file is used as a fall back.
.PARAMETER ContentType
Manually specify the content type of the response rather than infering it from the attachment's file extension.
The supplied value must match the valid ContentType format, e.g. application/json
.PARAMETER EndpointName
Optional EndpointName that the static route was creating under.
.PARAMETER FileBrowser
If the path is a folder, instead of returning 404, will return A browsable content of the directory.
.EXAMPLE
Set-PodeResponseAttachment -Path 'downloads/installer.exe'
.EXAMPLE
Set-PodeResponseAttachment -Path './image.png'
.EXAMPLE
Set-PodeResponseAttachment -Path 'c:/content/accounts.xlsx'
.EXAMPLE
Set-PodeResponseAttachment -Path './data.txt' -ContentType 'application/json'
.EXAMPLE
Set-PodeResponseAttachment -Path '/assets/data.txt' -EndpointName 'Example'
#>
function Set-PodeResponseAttachment {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Path,
[ValidatePattern('^\w+\/[\w\.\+-]+$')]
[string]
$ContentType,
[Parameter()]
[string]
$EndpointName,
[switch]
$FileBrowser
)
# already sent? skip
if ($WebEvent.Response.Sent) {
return
}
# only attach files from public/static-route directories when path is relative
$route = (Find-PodeStaticRoute -Path $Path -CheckPublic -EndpointName $EndpointName)
if ($route) {
$_path = $route.Content.Source
}
else {
$_path = Get-PodeRelativePath -Path $Path -JoinRoot
}
#call internal Attachment function
Write-PodeAttachmentResponseInternal -Path $_path -ContentType $ContentType -FileBrowser:$fileBrowser
}
<#
.SYNOPSIS
Writes a String or a Byte[] to the Response.
.DESCRIPTION
Writes a String or a Byte[] to the Response, as some specified content type. This value can also be cached.
.PARAMETER Value
A String value to write.
.PARAMETER Bytes
An array of Bytes to write.
.PARAMETER ContentType
The content type of the data being written.
.PARAMETER MaxAge
The maximum age to cache the value on the browser, in seconds.
.PARAMETER StatusCode
The status code to set against the response.
.PARAMETER Cache
Should the value be cached by browsers, or not?
.EXAMPLE
Write-PodeTextResponse -Value 'Leeeeeerrrooooy Jeeeenkiiins!'
.EXAMPLE
Write-PodeTextResponse -Value '{"name": "Rick"}' -ContentType 'application/json'
.EXAMPLE
Write-PodeTextResponse -Bytes (Get-Content -Path ./some/image.png -Raw -AsByteStream) -Cache -MaxAge 1800
.EXAMPLE
Write-PodeTextResponse -Value 'Untitled Text Response' -StatusCode 418
#>
function Write-PodeTextResponse {
[CmdletBinding(DefaultParameterSetName = 'String')]
param (
[Parameter(ParameterSetName = 'String', ValueFromPipeline = $true, Position = 0)]
[string]
$Value,
[Parameter(ParameterSetName = 'Bytes')]
[byte[]]
$Bytes,
[Parameter()]
[string]
$ContentType = 'text/plain',
[Parameter()]
[int]
$MaxAge = 3600,
[Parameter()]
[int]
$StatusCode = 200,
[switch]
$Cache
)
$isStringValue = ($PSCmdlet.ParameterSetName -ieq 'string')
$isByteValue = ($PSCmdlet.ParameterSetName -ieq 'bytes')
# set the status code of the response, but only if it's not 200 (to prevent overriding)
if ($StatusCode -ne 200) {
Set-PodeResponseStatus -Code $StatusCode -NoErrorPage
}
# if there's nothing to write, return
if ($isStringValue -and [string]::IsNullOrWhiteSpace($Value)) {
return
}
if ($isByteValue -and (($null -eq $Bytes) -or ($Bytes.Length -eq 0))) {
return
}
# if the response stream isn't writable or already sent, return
$res = $WebEvent.Response
if (($null -eq $res) -or ($WebEvent.Streamed -and (($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite -or $res.Sent))) {
return
}
# set a cache value
if ($Cache) {
Set-PodeHeader -Name 'Cache-Control' -Value "max-age=$($MaxAge), must-revalidate"
Set-PodeHeader -Name 'Expires' -Value ([datetime]::UtcNow.AddSeconds($MaxAge).ToString('r', [CultureInfo]::InvariantCulture))
}
# specify the content-type if supplied (adding utf-8 if missing)
if (![string]::IsNullOrWhiteSpace($ContentType)) {
$charset = 'charset=utf-8'
if ($ContentType -inotcontains $charset) {
$ContentType = "$($ContentType); $($charset)"
}
$res.ContentType = $ContentType
}
# if we're serverless, set the string as the body
if (!$WebEvent.Streamed) {
if ($isStringValue) {
$res.Body = $Value
}
else {
$res.Body = $Bytes
}
}
else {
# convert string to bytes
if ($isStringValue) {
$Bytes = ConvertFrom-PodeValueToBytes -Value $Value
}
# check if we only need a range of the bytes
if (($null -ne $WebEvent.Ranges) -and ($WebEvent.Response.StatusCode -eq 200) -and ($StatusCode -eq 200)) {
$lengths = @()
$size = $Bytes.Length
$Bytes = @(foreach ($range in $WebEvent.Ranges) {
# ensure range not invalid
if (([int]$range.Start -lt 0) -or ([int]$range.Start -ge $size) -or ([int]$range.End -lt 0)) {
Set-PodeResponseStatus -Code 416 -NoErrorPage
return
}
# skip start bytes only
if ([string]::IsNullOrWhiteSpace($range.End)) {
$Bytes[$range.Start..($size - 1)]
$lengths += "$($range.Start)-$($size - 1)/$($size)"
}
# end bytes only
elseif ([string]::IsNullOrWhiteSpace($range.Start)) {
if ([int]$range.End -gt $size) {
$range.End = $size
}
if ([int]$range.End -gt 0) {
$Bytes[$($size - $range.End)..($size - 1)]
$lengths += "$($size - $range.End)-$($size - 1)/$($size)"
}
else {
$lengths += "0-0/$($size)"
}
}
# normal range
else {
if ([int]$range.End -ge $size) {
Set-PodeResponseStatus -Code 416 -NoErrorPage
return
}
$Bytes[$range.Start..$range.End]
$lengths += "$($range.Start)-$($range.End)/$($size)"
}
})
Set-PodeHeader -Name 'Content-Range' -Value "bytes $($lengths -join ', ')"
if ($StatusCode -eq 200) {
Set-PodeResponseStatus -Code 206 -NoErrorPage
}
}
# check if we need to compress the response
if ($PodeContext.Server.Web.Compression.Enabled -and ![string]::IsNullOrWhiteSpace($WebEvent.AcceptEncoding)) {
try {
$ms = New-Object -TypeName System.IO.MemoryStream
$stream = New-Object "System.IO.Compression.$($WebEvent.AcceptEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Compress, $true)
$stream.Write($Bytes, 0, $Bytes.Length)
$stream.Close()
$ms.Position = 0
$Bytes = $ms.ToArray()
}
finally {
if ($null -ne $stream) {
$stream.Close()
}
if ($null -ne $ms) {
$ms.Close()
}
}
# set content encoding header
Set-PodeHeader -Name 'Content-Encoding' -Value $WebEvent.AcceptEncoding
}
# write the content to the response stream
$res.ContentLength64 = $Bytes.Length
try {
$ms = New-Object -TypeName System.IO.MemoryStream
$ms.Write($Bytes, 0, $Bytes.Length)
$ms.WriteTo($res.OutputStream)
}
catch {
if ((Test-PodeValidNetworkFailure $_.Exception)) {
return
}
$_ | Write-PodeErrorLog
throw
}
finally {
if ($null -ne $ms) {
$ms.Close()
}
}
}
}
<#
.SYNOPSIS
Renders the content of a static, or dynamic, file on the Response.
.DESCRIPTION
Renders the content of a static, or dynamic, file on the Response.
You can set browser's to cache the content, and also override the file's content type.
.PARAMETER Path
The path to a file.
.PARAMETER Data
A HashTable of dynamic data to supply to a dynamic file.
.PARAMETER ContentType
The content type of the file's contents - this overrides the file's extension.
.PARAMETER MaxAge
The maximum age to cache the file's content on the browser, in seconds.
.PARAMETER StatusCode
The status code to set against the response.
.PARAMETER Cache
Should the file's content be cached by browsers, or not?
.PARAMETER FileBrowser
If the path is a folder, instead of returning 404, will return A browsable content of the directory.
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt'
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -Cache -MaxAge 1800
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -ContentType 'application/json'
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Views/Index.pode' -Data @{ Counter = 2 }
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -StatusCode 201
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/' -FileBrowser
#>
function Write-PodeFileResponse {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNull()]
[string]
$Path,
[Parameter()]
$Data = @{},
[Parameter()]
[string]
$ContentType = $null,
[Parameter()]
[int]
$MaxAge = 3600,
[Parameter()]
[int]
$StatusCode = 200,
[switch]
$Cache,
[switch]
$FileBrowser
)
# resolve for relative path
$RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot
Write-PodeFileResponseInternal -Path $RelativePath -Data $Data -ContentType $ContentType -MaxAge $MaxAge `
-StatusCode $StatusCode -Cache:$Cache -FileBrowser:$FileBrowser
}
<#
.SYNOPSIS
Serves a directory listing as a web page.
.DESCRIPTION
The Write-PodeDirectoryResponse function generates an HTML response that lists the contents of a specified directory,
allowing for browsing of files and directories. It supports both Windows and Unix-like environments by adjusting the
display of file attributes accordingly. If the path is a directory, it generates a browsable HTML view; otherwise, it
serves the file directly.
.PARAMETER Path
The path to the directory that should be displayed. This path is resolved and used to generate a list of contents.
.EXAMPLE
Write-PodeDirectoryResponse -Path './static'
Generates and serves an HTML page that lists the contents of the './static' directory, allowing users to click through files and directories.
#>
function Write-PodeDirectoryResponse {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[ValidateNotNull()]
[string]
$Path
)
# resolve for relative path
$RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot
if (Test-Path -Path $RelativePath -PathType Container) {
Write-PodeDirectoryResponseInternal -Path $RelativePath
}
else {
Set-PodeResponseStatus -Code 404
}
}
<#
.SYNOPSIS
Writes CSV data to the Response.
.DESCRIPTION
Writes CSV data to the Response, setting the content type accordingly.
.PARAMETER Value
A String, PSObject, or HashTable value.
.PARAMETER Path
The path to a CSV file.
.PARAMETER StatusCode
The status code to set against the response.
.EXAMPLE
Write-PodeCsvResponse -Value "Name`nRick"
.EXAMPLE
Write-PodeCsvResponse -Value @{ Name = 'Rick' }
.EXAMPLE
Write-PodeCsvResponse -Path 'E:/Files/Names.csv'
#>
function Write-PodeCsvResponse {
[CmdletBinding(DefaultParameterSetName = 'Value')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$Path,
[Parameter()]
[int]
$StatusCode = 200
)
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'file' {
if (Test-PodePath $Path) {
$Value = Get-PodeFileContent -Path $Path
}
}
'value' {
if ($Value -isnot [string]) {
$Value = @(foreach ($v in $Value) {
New-Object psobject -Property $v
})
if (Test-PodeIsPSCore) {
$Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false)
}
else {
$Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation)
}
$Value = ($Value -join ([environment]::NewLine))
}
}
}
if ([string]::IsNullOrWhiteSpace($Value)) {
$Value = [string]::Empty
}
Write-PodeTextResponse -Value $Value -ContentType 'text/csv' -StatusCode $StatusCode
}
<#
.SYNOPSIS
Writes HTML data to the Response.
.DESCRIPTION
Writes HTML data to the Response, setting the content type accordingly.
.PARAMETER Value
A String, PSObject, or HashTable value.
.PARAMETER Path
The path to a HTML file.
.PARAMETER StatusCode
The status code to set against the response.
.EXAMPLE
Write-PodeHtmlResponse -Value "Raw HTML can be placed here"
.EXAMPLE
Write-PodeHtmlResponse -Value @{ Message = 'Hello, all!' }
.EXAMPLE
Write-PodeHtmlResponse -Path 'E:/Site/About.html'
#>
function Write-PodeHtmlResponse {
[CmdletBinding(DefaultParameterSetName = 'Value')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$Path,
[Parameter()]
[int]
$StatusCode = 200
)
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'file' {
if (Test-PodePath $Path) {
$Value = Get-PodeFileContent -Path $Path
}
}
'value' {
if ($Value -isnot [string]) {
$Value = ($Value | ConvertTo-Html)
$Value = ($Value -join ([environment]::NewLine))
}
}
}
if ([string]::IsNullOrWhiteSpace($Value)) {
$Value = [string]::Empty
}
Write-PodeTextResponse -Value $Value -ContentType 'text/html' -StatusCode $StatusCode
}
<#
.SYNOPSIS
Writes Markdown data to the Response.
.DESCRIPTION
Writes Markdown data to the Response, with the option to render it as HTML.
.PARAMETER Value
A String, PSObject, or HashTable value.
.PARAMETER Path
The path to a Markdown file.
.PARAMETER StatusCode
The status code to set against the response.
.PARAMETER AsHtml
If supplied, the Markdown will be converted to HTML. (This is only supported in PS7+)
.EXAMPLE
Write-PodeMarkdownResponse -Value '# Hello, world!' -AsHtml
.EXAMPLE
Write-PodeMarkdownResponse -Path 'E:/Site/About.md'
#>
function Write-PodeMarkdownResponse {
[CmdletBinding(DefaultParameterSetName = 'Value')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$Path,
[Parameter()]
[int]
$StatusCode = 200,
[switch]
$AsHtml
)
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'file' {
if (Test-PodePath $Path) {
$Value = Get-PodeFileContent -Path $Path
}
}
}
if ([string]::IsNullOrWhiteSpace($Value)) {
$Value = [string]::Empty
}
$mimeType = 'text/markdown'
if ($AsHtml) {
if ($PSVersionTable.PSVersion.Major -ge 7) {
$mimeType = 'text/html'
$Value = ($Value | ConvertFrom-Markdown).Html
}
}
Write-PodeTextResponse -Value $Value -ContentType $mimeType -StatusCode $StatusCode
}
<#
.SYNOPSIS
Writes JSON data to the Response.
.DESCRIPTION
Writes JSON data to the Response, setting the content type accordingly.
.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.
.PARAMETER Path
The path to a JSON file.
.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.
.PARAMETER StatusCode
The status code to set against the response.
.PARAMETER NoCompress
The JSON document is not compressed (Human readable form)
.EXAMPLE
Write-PodeJsonResponse -Value '{"name": "Rick"}'
.EXAMPLE
Write-PodeJsonResponse -Value @{ Name = 'Rick' } -StatusCode 201
.EXAMPLE
Write-PodeJsonResponse -Path 'E:/Files/Names.json'
#>
function Write-PodeJsonResponse {
[CmdletBinding(DefaultParameterSetName = 'Value')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
[AllowNull()]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$Path,
[Parameter(ParameterSetName = 'Value')]
[ValidateRange(0, 100)]
[int]
$Depth = 10,
[Parameter()]
[int]
$StatusCode = 200,
[Parameter(ParameterSetName = 'Value')]
[switch]
$NoCompress
)
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'file' {
if (Test-PodePath $Path) {
$Value = Get-PodeFileContent -Path $Path
}
if ([string]::IsNullOrWhiteSpace($Value)) {
$Value = '{}'
}
}
'value' {
if ($Value -isnot [string]) {
if ($Depth -le 0) {
$Value = (ConvertTo-Json -InputObject $Value -Compress:(!$NoCompress))
}
else {
$Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress:(!$NoCompress))
}
}
}
}
if ([string]::IsNullOrWhiteSpace($Value)) {
$Value = '{}'
}
Write-PodeTextResponse -Value $Value -ContentType 'application/json' -StatusCode $StatusCode
}
<#
.SYNOPSIS
Writes XML data to the Response.
.DESCRIPTION
Writes XML data to the Response, setting the content type accordingly.
.PARAMETER Value
A String, PSObject, or HashTable value.
.PARAMETER Path
The path to an XML file.
.PARAMETER StatusCode
The status code to set against the response.
.EXAMPLE
Write-PodeXmlResponse -Value '<root><name>Rick</name></root>'
.EXAMPLE
Write-PodeXmlResponse -Value @{ Name = 'Rick' } -StatusCode 201
.EXAMPLE
Write-PodeXmlResponse -Path 'E:/Files/Names.xml'
#>
function Write-PodeXmlResponse {
[CmdletBinding(DefaultParameterSetName = 'Value')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
[AllowNull()]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$Path,
[Parameter()]
[int]
$StatusCode = 200
)
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'file' {
if (Test-PodePath $Path) {
$Value = Get-PodeFileContent -Path $Path
}
}
'value' {
if ($Value -isnot [string]) {
$Value = @(foreach ($v in $Value) {
New-Object psobject -Property $v
})
$Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation)
}
}
}
if ([string]::IsNullOrWhiteSpace($Value)) {
$Value = [string]::Empty
}
Write-PodeTextResponse -Value $Value -ContentType 'text/xml' -StatusCode $StatusCode
}
<#
.SYNOPSIS
Writes YAML data to the Response.
.DESCRIPTION
Writes YAML data to the Response, setting the content type accordingly.
.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to YAML.
.PARAMETER Path
The path to a YAML file.
.PARAMETER ContentType
Because JSON content has not yet an official content type. one custom can be specified here (Default: 'application/x-yaml' )
.PARAMETER Depth
The Depth to generate the YAML document - the larger this value the worse performance gets.
.PARAMETER StatusCode
The status code to set against the response.
.EXAMPLE
Write-PodeYamlResponse -Value '{"name": "Rick"}'
.EXAMPLE
Write-PodeYamlResponse -Value @{ Name = 'Rick' } -StatusCode 201
.EXAMPLE
Write-PodeYamlResponse -Path 'E:/Files/Names.json'
#>
function Write-PodeYamlResponse {
[CmdletBinding(DefaultParameterSetName = 'Value')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Value', ValueFromPipeline = $true, Position = 0)]
[AllowNull()]
$Value,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$Path,
[Parameter()]
[ValidatePattern('^\w+\/[\w\.\+-]+$')]
[ValidateNotNullOrEmpty()]
[string]
$ContentType = 'application/x-yaml',
[Parameter(ParameterSetName = 'Value')]
[ValidateRange(0, 100)]
[int]
$Depth = 10,
[Parameter()]
[int]
$StatusCode = 200
)
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'file' {
if (Test-PodePath $Path) {
$Value = Get-PodeFileContent -Path $Path
}
}
'value' {
if ($Value -isnot [string]) {
if ( $Depth -gt 0) {
$Value = ConvertTo-PodeYaml -InputObject $Value -Depth $Depth
}
else {
$Value = ConvertTo-PodeYaml -InputObject $Value
}
}
}
}
if ([string]::IsNullOrWhiteSpace($Value)) {
$Value = '[]'
}
Write-PodeTextResponse -Value $Value -ContentType $ContentType -StatusCode $StatusCode
}
<#
.SYNOPSIS
Renders a dynamic, or static, View on the Response.
.DESCRIPTION
Renders a dynamic, or static, View on the Response; allowing for dynamic data to be supplied.
.PARAMETER Path
The path to a View, relative to the "/views" directory. (Extension is optional).
.PARAMETER Data
Any dynamic data to supply to a dynamic View.
.PARAMETER StatusCode
The status code to set against the response.
.PARAMETER Folder
If supplied, a custom views folder will be used.
.PARAMETER FlashMessages
Automatically supply all Flash messages in the current session to the View.
.EXAMPLE
Write-PodeViewResponse -Path 'index'
.EXAMPLE
Write-PodeViewResponse -Path 'accounts/profile_page' -Data @{ Username = 'Morty' }
.EXAMPLE
Write-PodeViewResponse -Path 'login' -FlashMessages
#>
function Write-PodeViewResponse {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Path,
[Parameter()]
[hashtable]
$Data = @{},
[Parameter()]
[int]
$StatusCode = 200,
[Parameter()]
[string]
$Folder,
[switch]
$FlashMessages
)
# default data if null
if ($null -eq $Data) {
$Data = @{}
}
# add path to data as "pagename" - unless key already exists
if (!$Data.ContainsKey('pagename')) {
$Data['pagename'] = $Path
}
# load all flash messages if needed
if ($FlashMessages -and ($null -ne $WebEvent.Session.Data.Flash)) {
$Data['flash'] = @{}
foreach ($name in (Get-PodeFlashMessageNames)) {
$Data.flash[$name] = (Get-PodeFlashMessage -Name $name)
}
}
elseif ($null -eq $Data['flash']) {
$Data['flash'] = @{}
}
# add view engine extension
$ext = Get-PodeFileExtension -Path $Path
if ([string]::IsNullOrWhiteSpace($ext)) {
$Path += ".$($PodeContext.Server.ViewEngine.Extension)"
}
# only look in the view directories
$viewFolder = $PodeContext.Server.InbuiltDrives['views']
if (![string]::IsNullOrWhiteSpace($Folder)) {
$viewFolder = $PodeContext.Server.Views[$Folder]
}
$Path = [System.IO.Path]::Combine($viewFolder, $Path)
# test the file path, and set status accordingly
if (!(Test-PodePath $Path)) {
return
}
# run any engine logic and render it
$engine = (Get-PodeViewEngineType -Path $Path)
$value = (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)
switch ($engine.ToLowerInvariant()) {
'md' {
Write-PodeMarkdownResponse -Value $value -StatusCode $StatusCode -AsHtml
}
default {
Write-PodeHtmlResponse -Value $value -StatusCode $StatusCode
}
}
}
<#
.SYNOPSIS
Sets the Status Code of the Response, and controls rendering error pages.
.DESCRIPTION
Sets the Status Code of the Response, and controls rendering error pages.
.PARAMETER Code
The Status Code to set on the Response.
.PARAMETER Description
An optional Status Description.
.PARAMETER Exception
An exception to use when detailing error information on error pages.
.PARAMETER ContentType
The content type of the error page to use.
.PARAMETER NoErrorPage
Don't render an error page when the Status Code is 400+.
.EXAMPLE
Set-PodeResponseStatus -Code 404
.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception
.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception -ContentType 'application/json'
#>
function Set-PodeResponseStatus {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[int]
$Code,
[Parameter()]
[string]
$Description,
[Parameter()]
$Exception,
[Parameter()]
[string]
$ContentType = $null,
[switch]
$NoErrorPage
)
# already sent? skip
if ($WebEvent.Response.Sent) {
return
}
# set the code
$WebEvent.Response.StatusCode = $Code
# set an appropriate description (mapping if supplied is blank)
if ([string]::IsNullOrWhiteSpace($Description)) {
$Description = (Get-PodeStatusDescription -StatusCode $Code)
}
if (!$PodeContext.Server.IsServerless -and ![string]::IsNullOrWhiteSpace($Description)) {
$WebEvent.Response.StatusDescription = $Description
}
# if the status code is >=400 then attempt to load error page
if (!$NoErrorPage -and ($Code -ge 400)) {
Show-PodeErrorPage -Code $Code -Description $Description -Exception $Exception -ContentType $ContentType
}
}
<#
.SYNOPSIS
Redirecting a user to a new URL.
.DESCRIPTION
Redirecting a user to a new URL, or the same URL as the Request but a different Protocol - or other components.
.PARAMETER Url
Redirect the user to a new URL, or a relative path.
.PARAMETER EndpointName
The Name of an Endpoint to redirect to.
.PARAMETER Port
Change the port of the current Request before redirecting.
.PARAMETER Protocol
Change the protocol of the current Request before redirecting.
.PARAMETER Address
Change the domain address of the current Request before redirecting.
.PARAMETER Moved
Set the Status Code as "301 Moved", rather than "302 Redirect".
.EXAMPLE
Move-PodeResponseUrl -Url 'https://google.com'
.EXAMPLE
Move-PodeResponseUrl -Url '/about'
.EXAMPLE
Move-PodeResponseUrl -Protocol HTTPS
.EXAMPLE
Move-PodeResponseUrl -Port 9000 -Moved
#>
function Move-PodeResponseUrl {
[CmdletBinding(DefaultParameterSetName = 'Url')]
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Url')]
[string]
$Url,
[Parameter(ParameterSetName = 'Endpoint')]
[string]
$EndpointName,
[Parameter(ParameterSetName = 'Components')]
[int]
$Port = 0,
[Parameter(ParameterSetName = 'Components')]
[ValidateSet('', 'Http', 'Https')]
[string]
$Protocol,
[Parameter(ParameterSetName = 'Components')]
[string]
$Address,
[switch]
$Moved
)
# build the url
if ($PSCmdlet.ParameterSetName -ieq 'components') {
$uri = $WebEvent.Request.Url
# set the protocol
$Protocol = $Protocol.ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($Protocol)) {
$Protocol = $uri.Scheme
}
# set the domain
if ([string]::IsNullOrWhiteSpace($Address)) {
$Address = $uri.Host
}
# set the port
if ($Port -le 0) {
$Port = $uri.Port
}
$PortStr = [string]::Empty
if (@(80, 443) -notcontains $Port) {
$PortStr = ":$($Port)"
}
# combine to form the url
$Url = "$($Protocol)://$($Address)$($PortStr)$($uri.PathAndQuery)"
}
# build the url from an endpoint
elseif ($PSCmdlet.ParameterSetName -ieq 'endpoint') {
$endpoint = Get-PodeEndpointByName -Name $EndpointName -ThrowError
# set the port
$PortStr = [string]::Empty
if (@(80, 443) -notcontains $endpoint.Port) {
$PortStr = ":$($endpoint.Port)"
}
$Url = "$($endpoint.Protocol)://$($endpoint.FriendlyName)$($PortStr)$($WebEvent.Request.Url.PathAndQuery)"
}
Set-PodeHeader -Name 'Location' -Value $Url
if ($Moved) {
Set-PodeResponseStatus -Code 301 -Description 'Moved'
}
else {
Set-PodeResponseStatus -Code 302 -Description 'Redirect'
}
}
<#
.SYNOPSIS
Writes data to a TCP socket stream.
.DESCRIPTION
Writes data to a TCP socket stream.
.PARAMETER Message
The message to write
.EXAMPLE
Write-PodeTcpClient -Message '250 OK'
#>
function Write-PodeTcpClient {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[string]
$Message
)
$TcpEvent.Response.WriteLine($Message, $true)
}
<#
.SYNOPSIS
Reads data from a TCP socket stream.
.DESCRIPTION
Reads data from a TCP socket stream.
.PARAMETER Timeout
An optional Timeout in milliseconds.
.PARAMETER CheckBytes
An optional array of bytes to check at the end of a receievd data stream, to determine if the data is complete.
.PARAMETER CRLFMessageEnd
If supplied, the CheckBytes will be set to 13 and 10 to make sure a message ends with CR and LF.
.EXAMPLE
$data = Read-PodeTcpClient
.EXAMPLE
$data = Read-PodeTcpClient -CRLFMessageEnd
#>
function Read-PodeTcpClient {
[CmdletBinding(DefaultParameterSetName = 'default')]
[OutputType([string])]
param(
[Parameter()]
[int]
$Timeout = 0,
[Parameter(ParameterSetName = 'CheckBytes')]
[byte[]]
$CheckBytes = $null,
[Parameter(ParameterSetName = 'CRLF')]
[switch]
$CRLFMessageEnd
)
$cBytes = $CheckBytes
if ($CRLFMessageEnd) {
$cBytes = [byte[]]@(13, 10)
}
return (Wait-PodeTask -Task $TcpEvent.Request.Read($cBytes, $PodeContext.Tokens.Cancellation.Token) -Timeout $Timeout)
}
<#
.SYNOPSIS
Close an open TCP client connection
.DESCRIPTION
Close an open TCP client connection
.EXAMPLE
Close-PodeTcpClient
#>
function Close-PodeTcpClient {
[CmdletBinding()]
param()
$TcpEvent.Request.Close()
}
<#
.SYNOPSIS
Saves any uploaded files on the Request to the File System.
.DESCRIPTION
Saves any uploaded files on the Request to the File System.
.PARAMETER Key
The name of the key within the $WebEvent's Data HashTable that stores the file names.
.PARAMETER Path
The path to save files. If this is a directory then the file name of the uploaded file will be used, but if this is a file path then that name is used instead.
If the Request has multiple files in, and you specify a file path, then all files will be saved to that one file path - overwriting each other.
.PARAMETER FileName
An optional FileName to save a specific files if multiple files were supplied in the Request. By default, every file is saved.
.EXAMPLE
Save-PodeRequestFile -Key 'avatar'
.EXAMPLE
Save-PodeRequestFile -Key 'avatar' -Path 'F:/Images'
.EXAMPLE
Save-PodeRequestFile -Key 'avatar' -Path 'F:/Images' -FileName 'icon.png'
#>
function Save-PodeRequestFile {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[string]
$Path = '.',
[Parameter()]
[string[]]
$FileName
)
# if path is '.', replace with server root
$Path = Get-PodeRelativePath -Path $Path -JoinRoot
# ensure the parameter name exists in data
if (!(Test-PodeRequestFile -Key $Key)) {
throw "A parameter called '$($Key)' was not supplied in the request, or has no data available"
}
# get the file names
$files = @($WebEvent.Data[$Key])
if (($null -ne $FileName) -and ($FileName.Length -gt 0)) {
$files = @(foreach ($file in $files) {
if ($FileName -icontains $file) {
$file
}
})
}
# ensure the file data exists
foreach ($file in $files) {
if (!$WebEvent.Files.ContainsKey($file)) {
throw "No data for file '$($file)' was uploaded in the request"
}
}
# save the files
foreach ($file in $files) {
# if the path is a directory, add the filename
$filePath = $Path
if (Test-Path -Path $filePath -PathType Container) {
$filePath = [System.IO.Path]::Combine($filePath, $file)
}
# save the file
$WebEvent.Files[$file].Save($filePath)
}
}
<#
.SYNOPSIS
Test to see if the Request contains the key for any uploaded files.
.DESCRIPTION
Test to see if the Request contains the key for any uploaded files.
.PARAMETER Key
The name of the key within the $WebEvent's Data HashTable that stores the file names.
.PARAMETER FileName
An optional FileName to test for a specific file within the list of uploaded files.
.EXAMPLE
Test-PodeRequestFile -Key 'avatar'
.EXAMPLE
Test-PodeRequestFile -Key 'avatar' -FileName 'icon.png'
#>
function Test-PodeRequestFile {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[string]
$FileName
)
# ensure the parameter name exists in data
if (!$WebEvent.Data.ContainsKey($Key)) {
return $false
}
# ensure it has filenames
if ([string]::IsNullOrEmpty($WebEvent.Data[$Key])) {
return $false
}
# do we have any specific files?
if (![string]::IsNullOrEmpty($FileName)) {
return (@($WebEvent.Data[$Key]) -icontains $FileName)
}
# we have files
return $true
}
<#
.SYNOPSIS
Short description
.DESCRIPTION
Long description
.PARAMETER Type
The type name of the view engine (inbuilt types are: Pode and HTML).
.PARAMETER ScriptBlock
A ScriptBlock for specifying custom view engine rendering rules.
.PARAMETER Extension
A custom extension for the engine's files.
.EXAMPLE
Set-PodeViewEngine -Type HTML
.EXAMPLE
Set-PodeViewEngine -Type Markdown
.EXAMPLE
Set-PodeViewEngine -Type PSHTML -Extension PS1 -ScriptBlock { param($path, $data) /* logic */ }
#>
function Set-PodeViewEngine {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Type,
[Parameter()]
[scriptblock]
$ScriptBlock = $null,
[Parameter()]
[string]
$Extension
)
# truncate markdown
if ($Type -ieq 'Markdown') {
$Type = 'md'
}
# override extension with type
if ([string]::IsNullOrWhiteSpace($Extension)) {
$Extension = $Type
}
# check if the scriptblock has any using vars
if ($null -ne $ScriptBlock) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
}
# setup view engine config
$PodeContext.Server.ViewEngine.Type = $Type.ToLowerInvariant()
$PodeContext.Server.ViewEngine.Extension = $Extension.ToLowerInvariant()
$PodeContext.Server.ViewEngine.ScriptBlock = $ScriptBlock
$PodeContext.Server.ViewEngine.UsingVariables = $usingVars
$PodeContext.Server.ViewEngine.IsDynamic = (@('html', 'md') -inotcontains $Type)
}
<#
.SYNOPSIS
Includes the contents of a partial View into another dynamic View.
.DESCRIPTION
Includes the contents of a partial View into another dynamic View. The partial View can be static or dynamic.
.PARAMETER Path
The path to a partial View, relative to the "/views" directory. (Extension is optional).
.PARAMETER Data
Any dynamic data to supply to a dynamic partial View.
.PARAMETER Folder
If supplied, a custom views folder will be used.
.EXAMPLE
Use-PodePartialView -Path 'shared/footer'
#>
function Use-PodePartialView {
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Path,
[Parameter()]
$Data = @{},
[Parameter()]
[string]
$Folder
)
# default data if null
if ($null -eq $Data) {
$Data = @{}
}
# add view engine extension
$ext = Get-PodeFileExtension -Path $Path
if ([string]::IsNullOrWhiteSpace($ext)) {
$Path += ".$($PodeContext.Server.ViewEngine.Extension)"
}
# only look in the view directory
$viewFolder = $PodeContext.Server.InbuiltDrives['views']
if (![string]::IsNullOrWhiteSpace($Folder)) {
$viewFolder = $PodeContext.Server.Views[$Folder]
}
$Path = [System.IO.Path]::Combine($viewFolder, $Path)
# test the file path, and set status accordingly
if (!(Test-PodePath $Path -NoStatus)) {
throw "File not found at path: $($Path)"
}
# run any engine logic
return (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)
}
<#
.SYNOPSIS
Broadcasts a message to connected WebSocket clients.
.DESCRIPTION
Broadcasts a message to all, or some, connected WebSocket clients. You can specify a path to send messages to, or a specific ClientId.
.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.
.PARAMETER Path
The Path of connected clients to send the message.
.PARAMETER ClientId
A specific ClientId of a connected client to send a message. Not currently used.
.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.
.PARAMETER Mode
The Mode to broadcast a message: Auto, Broadcast, Direct. (Default: Auto)
.PARAMETER IgnoreEvent
If supplied, if a SignalEvent is available it's data, such as path/clientId, will be ignored.
.EXAMPLE
Send-PodeSignal -Value @{ Message = 'Hello, world!' }
.EXAMPLE
Send-PodeSignal -Value @{ Data = @(123, 100, 101) } -Path '/response-charts'
#>
function Send-PodeSignal {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
$Value,
[Parameter()]
[string]
$Path,
[Parameter()]
[string]
$ClientId,
[Parameter()]
[int]
$Depth = 10,
[Parameter()]
[ValidateSet('Auto', 'Broadcast', 'Direct')]
[string]
$Mode = 'Auto',
[switch]
$IgnoreEvent
)
# error if not configured
if (!$PodeContext.Server.Signals.Enabled) {
throw 'WebSockets have not been configured to send signal messages'
}
# do nothing if no value
if (($null -eq $Value) -or ([string]::IsNullOrEmpty($Value))) {
return
}
# jsonify the value
if ($Value -isnot [string]) {
if ($Depth -le 0) {
$Value = (ConvertTo-Json -InputObject $Value -Compress)
}
else {
$Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress)
}
}
# check signal event
if (!$IgnoreEvent -and ($null -ne $SignalEvent)) {
if ([string]::IsNullOrWhiteSpace($Path)) {
$Path = $SignalEvent.Data.Path
}
if ([string]::IsNullOrWhiteSpace($ClientId)) {
$ClientId = $SignalEvent.Data.ClientId
}
if (($Mode -ieq 'Auto') -and ($SignalEvent.Data.Direct -or ($SignalEvent.ClientId -ieq $SignalEvent.Data.ClientId))) {
$Mode = 'Direct'
}
}
# broadcast or direct?
if ($Mode -iin @('Auto', 'Broadcast')) {
$PodeContext.Server.Signals.Listener.AddServerSignal($Value, $Path, $ClientId)
}
else {
$SignalEvent.Response.Write($Value)
}
}
<#
.SYNOPSIS
Add a custom path that contains additional views.
.DESCRIPTION
Add a custom path that contains additional views.
.PARAMETER Name
The Name of the views folder.
.PARAMETER Source
The literal, or relative, path to the directory that contains views.
.EXAMPLE
Add-PodeViewFolder -Name 'assets' -Source './assets'
#>
function Add-PodeViewFolder {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Source
)
# ensure the folder doesn't already exist
if ($PodeContext.Server.Views.ContainsKey($Name)) {
throw "The Views folder name already exists: $($Name)"
}
# ensure the path exists at server root
$Source = Get-PodeRelativePath -Path $Source -JoinRoot
if (!(Test-PodePath -Path $Source -NoStatus)) {
throw "The Views path does not exist: $($Source)"
}
# setup a temp drive for the path
$Source = New-PodePSDrive -Path $Source
# add the route(s)
Write-Verbose "Adding View Folder: [$($Name)] $($Source)"
$PodeContext.Server.Views[$Name] = $Source
}
<#
.SYNOPSIS
Pre-emptively send an HTTP response back to the client. This can be dangerous, so only use this function if you know what you're doing.
.DESCRIPTION
Pre-emptively send an HTTP response back to the client. This can be dangerous, so only use this function if you know what you're doing.
.EXAMPLE
Send-PodeResponse
#>
function Send-PodeResponse {
[CmdletBinding()]
param()
if ($null -ne $WebEvent.Response) {
$WebEvent.Response.Send()
}
}
<#
.SYNOPSIS
Adds a Route for a specific HTTP Method(s).
.DESCRIPTION
Adds a Route for a specific HTTP Method(s), with path, that when called with invoke any logic and/or Middleware.
.PARAMETER Method
The HTTP Method of this Route, multiple can be supplied.
.PARAMETER Path
The URI path for the Route.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware.
.PARAMETER ScriptBlock
A ScriptBlock for the Route's main logic.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Route should be bound against.
.PARAMETER ContentType
The content type the Route should use when parsing any payloads.
.PARAMETER TransferEncoding
The transfer encoding the Route should use when parsing any payloads.
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Route's main logic.
.PARAMETER ArgumentList
An array of arguments to supply to the Route's ScriptBlock.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
.PARAMETER AllowAnon
If supplied, the Route will allow anonymous access for non-authenticated users.
.PARAMETER Login
If supplied, the Route will be flagged to Authentication as being a Route that handles user logins.
.PARAMETER Logout
If supplied, the Route will be flagged to Authentication as being a Route that handles users logging out.
.PARAMETER PassThru
If supplied, the route created will be returned so it can be passed through a pipe.
.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER OAResponses
An alternative way to associate OpenApi responses unsing New-PodeOAResponse instead of piping multiple Add-PodeOAResponse
.PARAMETER OAReference
A reference to OpenAPI reusable pathItem component created with Add-PodeOAComponentPathItem
.PARAMETER OADefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeRoute -Method Post -Path '/users/:userId/message' -Middleware (Get-PodeCsrfMiddleware) -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -TransferEncoding gzip -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeRoute -Method Get -Path '/api/cpu' -ErrorContentType 'application/json' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -Role 'Developer', 'QA' -ScriptBlock { /* logic */ }
.EXAMPLE
$Responses = New-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' |
New-PodeOAResponse -StatusCode 404 -Description 'User not found' |
New-PodeOAResponse -StatusCode 405 -Description 'Invalid Input'
Add-PodeRoute -PassThru -Method Put -Path '/user/:username' -OAResponses $Responses -ScriptBlock {
#code is going here
}
#>
function Add-PodeRoute {
[CmdletBinding(DefaultParameterSetName = 'Script')]
[OutputType([System.Object[]])]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string[]]
$Method,
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[object[]]
$Middleware,
[Parameter(ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter( )]
[AllowNull()]
[string[]]
$EndpointName,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[ValidateSet('', 'gzip', 'deflate')]
[string]
$TransferEncoding,
[Parameter()]
[string]
$ErrorContentType,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[object[]]
$ArgumentList,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string]
$Access,
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$IfExists = 'Default',
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[Parameter()]
[string[]]
$User,
[switch]
$AllowAnon,
[switch]
$Login,
[switch]
$Logout,
[hashtable]
$OAResponses,
[string]
$OAReference,
[switch]
$PassThru,
[string[]]
$OADefinitionTag
)
# check if we have any route group info defined
if ($null -ne $RouteGroup) {
if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
$Path = "$($RouteGroup.Path)$($Path)"
}
if ($null -ne $RouteGroup.Middleware) {
$Middleware = $RouteGroup.Middleware + $Middleware
}
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
$EndpointName = $RouteGroup.EndpointName
}
if ([string]::IsNullOrWhiteSpace($ContentType)) {
$ContentType = $RouteGroup.ContentType
}
if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
$TransferEncoding = $RouteGroup.TransferEncoding
}
if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
$ErrorContentType = $RouteGroup.ErrorContentType
}
if ([string]::IsNullOrWhiteSpace($Authentication)) {
$Authentication = $RouteGroup.Authentication
}
if ([string]::IsNullOrWhiteSpace($Access)) {
$Access = $RouteGroup.Access
}
if ($RouteGroup.AllowAnon) {
$AllowAnon = $RouteGroup.AllowAnon
}
if ($RouteGroup.IfExists -ine 'default') {
$IfExists = $RouteGroup.IfExists
}
if ($null -ne $RouteGroup.AccessMeta.Role) {
$Role = $RouteGroup.AccessMeta.Role + $Role
}
if ($null -ne $RouteGroup.AccessMeta.Group) {
$Group = $RouteGroup.AccessMeta.Group + $Group
}
if ($null -ne $RouteGroup.AccessMeta.Scope) {
$Scope = $RouteGroup.AccessMeta.Scope + $Scope
}
if ($null -ne $RouteGroup.AccessMeta.User) {
$User = $RouteGroup.AccessMeta.User + $User
}
if ($null -ne $RouteGroup.AccessMeta.Custom) {
$CustomAccess = $RouteGroup.AccessMeta.Custom
}
if ($null -ne $RouteGroup.OADefinitionTag ) {
$OADefinitionTag = $RouteGroup.OADefinitionTag
}
}
# var for new routes created
$newRoutes = @()
# store the original path
$origPath = $Path
# split route on '?' for query
$Path = Split-PodeRouteQuery -Path $Path
if ([string]::IsNullOrWhiteSpace($Path)) {
throw 'No Path supplied for Route'
}
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlashes -Path $Path
$OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
$Path = Resolve-PodePlaceholders -Path $Path
# get endpoints from name
$endpoints = Find-PodeEndpoints -EndpointName $EndpointName
# get default route IfExists state
if ($IfExists -ieq 'Default') {
$IfExists = Get-PodeRouteIfExistsPreference
}
# if middleware, scriptblock and file path are all null/empty, error
if ((Test-PodeIsEmpty $Middleware) -and (Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath) -and (Test-PodeIsEmpty $Authentication)) {
throw "No logic passed for Route: $($Path)"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# convert any middleware into valid hashtables
$Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)
# if an access name was supplied, setup access as middleware first to it's after auth middleware
if (![string]::IsNullOrWhiteSpace($Access)) {
if ([string]::IsNullOrWhiteSpace($Authentication)) {
throw 'Access requires Authentication to be supplied on Routes'
}
if (!(Test-PodeAccessExists -Name $Access)) {
throw "Access method does not exist: $($Access)"
}
$options = @{
Name = $Access
}
$Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
}
# if an auth name was supplied, setup the auth as the first middleware
if (![string]::IsNullOrWhiteSpace($Authentication)) {
if (!(Test-PodeAuthExists -Name $Authentication)) {
throw "Authentication method does not exist: $($Authentication)"
}
$options = @{
Name = $Authentication
Login = $Login
Logout = $Logout
Anon = $AllowAnon
}
$Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
}
# custom access
if ($null -eq $CustomAccess) {
$CustomAccess = @{}
}
# workout a default content type for the route
$ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType
# workout a default transfer encoding for the route
$TransferEncoding = Find-PodeRouteTransferEncoding -Path $Path -TransferEncoding $TransferEncoding
# loop through each method
foreach ($_method in $Method) {
# ensure the route doesn't already exist for each endpoint
$endpoints = @(foreach ($_endpoint in $endpoints) {
$found = Test-PodeRouteInternal -Method $_method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')
if ($found) {
if ($IfExists -ieq 'Overwrite') {
Remove-PodeRoute -Method $_method -Path $origPath -EndpointName $_endpoint.Name
}
if ($IfExists -ieq 'Skip') {
continue
}
}
$_endpoint
})
if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
continue
}
#add security header method if autoMethods is enabled
if ( $PodeContext.Server.Security.autoMethods ) {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value $_method.ToUpper() -Append
}
$DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag
#add the default OpenApi responses
if ( $PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.defaultResponses) {
$DefaultResponse = @{}
foreach ($tag in $DefinitionTag) {
$DefaultResponse[$tag] = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.defaultResponses.Clone()
}
}
# add the route(s)
Write-Verbose "Adding Route: [$($_method)] $($Path)"
$methodRoutes = @(foreach ($_endpoint in $endpoints) {
@{
Logic = $ScriptBlock
UsingVariables = $usingVars
Middleware = $Middleware
Authentication = $Authentication
Access = $Access
AccessMeta = @{
Role = $Role
Group = $Group
Scope = $Scope
User = $User
Custom = $CustomAccess
}
Endpoint = @{
Protocol = $_endpoint.Protocol
Address = $_endpoint.Address.Trim()
Name = $_endpoint.Name
}
ContentType = $ContentType
TransferEncoding = $TransferEncoding
ErrorType = $ErrorContentType
Arguments = $ArgumentList
Method = $_method
Path = $Path
OpenApi = @{
Path = $OpenApiPath
Responses = $DefaultResponse
Parameters = $null
RequestBody = $null
CallBacks = @{}
Authentication = @()
Servers = @()
DefinitionTag = $DefinitionTag
}
IsStatic = $false
Metrics = @{
Requests = @{
Total = 0
StatusCodes = @{}
}
}
}
})
if (![string]::IsNullOrWhiteSpace($Authentication)) {
Set-PodeOAAuth -Route $methodRoutes -Name $Authentication -AllowAnon:$AllowAnon
}
$PodeContext.Server.Routes[$_method][$Path] += @($methodRoutes)
if ($PassThru) {
$newRoutes += $methodRoutes
}
}
if ($OAReference) {
Test-PodeOAComponentInternal -Field pathItems -DefinitionTag $DefinitionTag -Name $OAReference -PostValidation
foreach ($r in @($newRoutes)) {
$r.OpenApi = @{
'$ref' = "#/components/paths/$OAReference"
DefinitionTag = $DefinitionTag
Path = $OpenApiPath
}
}
}
elseif ($OAResponses) {
foreach ($r in @($newRoutes)) {
$r.OpenApi.Responses = $OAResponses
}
}
# return the routes?
if ($PassThru) {
return $newRoutes
}
}
<#
.SYNOPSIS
Add a static Route for rendering static content.
.DESCRIPTION
Add a static Route for rendering static content. You can also define default pages to display.
.PARAMETER Path
The URI path for the static Route.
.PARAMETER Source
The literal, or relative, path to the directory that contains the static content.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to bind the static Route against.
.PARAMETER ContentType
The content type the static Route should use when parsing any payloads.
.PARAMETER TransferEncoding
The transfer encoding the static Route should use when parsing any payloads.
.PARAMETER Defaults
An array of default pages to display, such as 'index.html'.
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
.PARAMETER AllowAnon
If supplied, the static route will allow anonymous access for non-authenticated users.
.PARAMETER DownloadOnly
When supplied, all static content on this Route will be attached as downloads - rather than rendered.
.PARAMETER PassThru
If supplied, the static route created will be returned so it can be passed through a pipe.
.PARAMETER IfExists
Specifies what action to take when a Static Route already exists. (Default: Default)
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER FileBrowser
If supplied, when the path is a folder, instead of returning 404, will return A browsable content of the directory.
.PARAMETER RedirectToDefault
If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path.
.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets'
.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html')
.EXAMPLE
Add-PodeStaticRoute -Path '/installers' -Source './exes' -DownloadOnly
.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') -RedirectToDefault
#>
function Add-PodeStaticRoute {
[CmdletBinding()]
[OutputType([System.Object[]])]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter(Mandatory = $true)]
[string]
$Source,
[Parameter()]
[object[]]
$Middleware,
[Parameter()]
[string[]]
$EndpointName,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[ValidateSet('', 'gzip', 'deflate')]
[string]
$TransferEncoding,
[Parameter()]
[string[]]
$Defaults,
[Parameter()]
[string]
$ErrorContentType,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string]
$Access,
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$IfExists = 'Default',
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[Parameter()]
[string[]]
$User,
[switch]
$AllowAnon,
[switch]
$DownloadOnly,
[switch]
$FileBrowser,
[switch]
$PassThru,
[switch]
$RedirectToDefault
)
# check if we have any route group info defined
if ($null -ne $RouteGroup) {
if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
$Path = "$($RouteGroup.Path)$($Path)"
}
if (![string]::IsNullOrWhiteSpace($RouteGroup.Source)) {
$Source = [System.IO.Path]::Combine($Source, $RouteGroup.Source.TrimStart('\/'))
}
if ($null -ne $RouteGroup.Middleware) {
$Middleware = $RouteGroup.Middleware + $Middleware
}
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
$EndpointName = $RouteGroup.EndpointName
}
if ([string]::IsNullOrWhiteSpace($ContentType)) {
$ContentType = $RouteGroup.ContentType
}
if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
$TransferEncoding = $RouteGroup.TransferEncoding
}
if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
$ErrorContentType = $RouteGroup.ErrorContentType
}
if ([string]::IsNullOrWhiteSpace($Authentication)) {
$Authentication = $RouteGroup.Authentication
}
if ([string]::IsNullOrWhiteSpace($Access)) {
$Access = $RouteGroup.Access
}
if (Test-PodeIsEmpty $Defaults) {
$Defaults = $RouteGroup.Defaults
}
if ($RouteGroup.AllowAnon) {
$AllowAnon = $RouteGroup.AllowAnon
}
if ($RouteGroup.DownloadOnly) {
$DownloadOnly = $RouteGroup.DownloadOnly
}
if ($RouteGroup.FileBrowser) {
$FileBrowser = $RouteGroup.FileBrowser
}
if ($RouteGroup.RedirectToDefault) {
$RedirectToDefault = $RouteGroup.RedirectToDefault
}
if ($RouteGroup.IfExists -ine 'default') {
$IfExists = $RouteGroup.IfExists
}
if ($null -ne $RouteGroup.AccessMeta.Role) {
$Role = $RouteGroup.AccessMeta.Role + $Role
}
if ($null -ne $RouteGroup.AccessMeta.Group) {
$Group = $RouteGroup.AccessMeta.Group + $Group
}
if ($null -ne $RouteGroup.AccessMeta.Scope) {
$Scope = $RouteGroup.AccessMeta.Scope + $Scope
}
if ($null -ne $RouteGroup.AccessMeta.User) {
$User = $RouteGroup.AccessMeta.User + $User
}
if ($null -ne $RouteGroup.AccessMeta.Custom) {
$CustomAccess = $RouteGroup.AccessMeta.Custom
}
}
# store the route method
$Method = 'Static'
# store the original path
$origPath = $Path
# split route on '?' for query
$Path = Split-PodeRouteQuery -Path $Path
if ([string]::IsNullOrWhiteSpace($Path)) {
throw "[$($Method)]: No Path supplied for Static Route"
}
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlashes -Path $Path -Static
$OpenApiPath = ConvertTo-PodeOpenApiRoutePath -Path $Path
$Path = Resolve-PodePlaceholders -Path $Path
# get endpoints from name
$endpoints = Find-PodeEndpoints -EndpointName $EndpointName
# get default route IfExists state
if ($IfExists -ieq 'Default') {
$IfExists = Get-PodeRouteIfExistsPreference
}
# ensure the route doesn't already exist for each endpoint
$endpoints = @(foreach ($_endpoint in $endpoints) {
$found = Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')
if ($found) {
if ($IfExists -ieq 'Overwrite') {
Remove-PodeStaticRoute -Path $origPath -EndpointName $_endpoint.Name
}
if ($IfExists -ieq 'Skip') {
continue
}
}
$_endpoint
})
if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
return
}
# if static, ensure the path exists at server root
$Source = Get-PodeRelativePath -Path $Source -JoinRoot
if (!(Test-PodePath -Path $Source -NoStatus)) {
throw "[$($Method))] $($Path): The Source path supplied for Static Route does not exist: $($Source)"
}
# setup a temp drive for the path
$Source = New-PodePSDrive -Path $Source
# setup default static files
if ($null -eq $Defaults) {
$Defaults = Get-PodeStaticRouteDefault
}
if (!$RedirectToDefault) {
$RedirectToDefault = $PodeContext.Server.Web.Static.RedirectToDefault
}
# convert any middleware into valid hashtables
$Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState)
# if an access name was supplied, setup access as middleware first to it's after auth middleware
if (![string]::IsNullOrWhiteSpace($Access)) {
if ([string]::IsNullOrWhiteSpace($Authentication)) {
throw 'Access requires Authentication to be supplied on Static Routes'
}
if (!(Test-PodeAccessExists -Name $Access)) {
throw "Access method does not exist: $($Access)"
}
$options = @{
Name = $Access
}
$Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
}
# if an auth name was supplied, setup the auth as the first middleware
if (![string]::IsNullOrWhiteSpace($Authentication)) {
if (!(Test-PodeAuthExists -Name $Authentication)) {
throw "Authentication method does not exist: $($Authentication)"
}
$options = @{
Name = $Authentication
Anon = $AllowAnon
}
$Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware)
}
# workout a default content type for the route
$ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType
# workout a default transfer encoding for the route
$TransferEncoding = Find-PodeRouteTransferEncoding -Path $Path -TransferEncoding $TransferEncoding
#The path use KleeneStar(Asterisk)
$KleeneStar = $OrigPath.Contains('*')
# add the route(s)
Write-Verbose "Adding Route: [$($Method)] $($Path)"
$newRoutes = @(foreach ($_endpoint in $endpoints) {
@{
Source = $Source
Path = $Path
KleeneStar = $KleeneStar
Method = $Method
Defaults = $Defaults
RedirectToDefault = $RedirectToDefault
Middleware = $Middleware
Authentication = $Authentication
Access = $Access
AccessMeta = @{
Role = $Role
Group = $Group
Scope = $Scope
User = $User
Custom = $CustomAccess
}
Endpoint = @{
Protocol = $_endpoint.Protocol
Address = $_endpoint.Address.Trim()
Name = $_endpoint.Name
}
ContentType = $ContentType
TransferEncoding = $TransferEncoding
ErrorType = $ErrorContentType
Download = $DownloadOnly
IsStatic = $true
FileBrowser = $FileBrowser.isPresent
OpenApi = @{
Path = $OpenApiPath
Responses = @{}
Parameters = $null
RequestBody = $null
CallBacks = @{}
Authentication = @()
Servers = @()
DefinitionTag = $DefinitionTag
}
Metrics = @{
Requests = @{
Total = 0
StatusCodes = @{}
}
}
}
})
$PodeContext.Server.Routes[$Method][$Path] += @($newRoutes)
# return the routes?
if ($PassThru) {
return $newRoutes
}
}
<#
.SYNOPSIS
Adds a Signal Route for WebSockets.
.DESCRIPTION
Adds a Signal Route, with path, that when called with invoke any logic.
.PARAMETER Path
The URI path for the Signal Route.
.PARAMETER ScriptBlock
A ScriptBlock for the Signal Route's main logic.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Signal Route should be bound against.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Signal Route's main logic.
.PARAMETER ArgumentList
An array of arguments to supply to the Signal Route's ScriptBlock.
.PARAMETER IfExists
Specifies what action to take when a Signal Route already exists. (Default: Default)
.EXAMPLE
Add-PodeSignalRoute -Path '/message' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeSignalRoute -Path '/message' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeSignalRoute {
[CmdletBinding(DefaultParameterSetName = 'Script')]
[OutputType([System.Object[]])]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter(ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter()]
[string[]]
$EndpointName,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[object[]]
$ArgumentList,
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$IfExists = 'Default'
)
# check if we have any route group info defined
if ($null -ne $RouteGroup) {
if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
$Path = "$($RouteGroup.Path)$($Path)"
}
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
$EndpointName = $RouteGroup.EndpointName
}
if ($RouteGroup.IfExists -ine 'default') {
$IfExists = $RouteGroup.IfExists
}
}
$Method = 'Signal'
# store the original path
$origPath = $Path
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlashes -Path $Path
# get endpoints from name
$endpoints = Find-PodeEndpoints -EndpointName $EndpointName
# get default route IfExists state
if ($IfExists -ieq 'Default') {
$IfExists = Get-PodeRouteIfExistsPreference
}
# ensure the route doesn't already exist for each endpoint
$endpoints = @(foreach ($_endpoint in $endpoints) {
$found = Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $_endpoint.Protocol -Address $_endpoint.Address -ThrowError:($IfExists -ieq 'Error')
if ($found) {
if ($IfExists -ieq 'Overwrite') {
Remove-PodeSignalRoute -Path $origPath -EndpointName $_endpoint.Name
}
if ($IfExists -ieq 'Skip') {
continue
}
}
$_endpoint
})
if (($null -eq $endpoints) -or ($endpoints.Length -eq 0)) {
return
}
# if scriptblock and file path are all null/empty, error
if ((Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath)) {
throw "[$($Method)] $($Path): No logic passed"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add the route(s)
Write-Verbose "Adding Route: [$($Method)] $($Path)"
$newRoutes = @(foreach ($_endpoint in $endpoints) {
@{
Logic = $ScriptBlock
UsingVariables = $usingVars
Endpoint = @{
Protocol = $_endpoint.Protocol
Address = $_endpoint.Address.Trim()
Name = $_endpoint.Name
}
Arguments = $ArgumentList
Method = $Method
Path = $Path
IsStatic = $false
Metrics = @{
Requests = @{
Total = 0
}
}
}
})
$PodeContext.Server.Routes[$Method][$Path] += @($newRoutes)
}
<#
.SYNOPSIS
Add a Route Group for multiple Routes.
.DESCRIPTION
Add a Route Group for sharing values between multiple Routes.
.PARAMETER Path
The URI path to use as a base for the Routes, that should be prepended.
.PARAMETER Routes
A ScriptBlock for adding Routes.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to give each Route.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Routes.
.PARAMETER ContentType
The content type to use for the Routes, when parsing any payloads.
.PARAMETER TransferEncoding
The transfer encoding to use for the Routes, when parsing any payloads.
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on the Routes.
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER AllowAnon
If supplied, the Routes will allow anonymous access for non-authenticated users.
.PARAMETER OADefinitionTag
An Array of strings representing the unique tag for the API specification.
This tag helps in distinguishing between different versions or types of API specifications within the application.
You can use this tag to reference the specific API documentation, schema, or version that your function interacts with.
.EXAMPLE
Add-PodeRouteGroup -Path '/api' -Routes { Add-PodeRoute -Path '/route1' -Etc }
#>
function Add-PodeRouteGroup {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path,
[Parameter(Mandatory = $true)]
[scriptblock]
$Routes,
[Parameter()]
[object[]]
$Middleware,
[Parameter()]
[string[]]
$EndpointName,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[ValidateSet('', 'gzip', 'deflate')]
[string]
$TransferEncoding,
[Parameter()]
[string]
$ErrorContentType,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string]
$Access,
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$IfExists = 'Default',
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[Parameter()]
[string[]]
$User,
[switch]
$AllowAnon,
[string[]]
$OADefinitionTag
)
if (Test-PodeIsEmpty $Routes) {
throw 'No scriptblock for -Routes passed'
}
if ($Path -eq '/') {
$Path = $null
}
# check for scoped vars
$Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState
# group details
if ($null -ne $RouteGroup) {
if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
$Path = "$($RouteGroup.Path)$($Path)"
}
if ($null -ne $RouteGroup.Middleware) {
$Middleware = $RouteGroup.Middleware + $Middleware
}
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
$EndpointName = $RouteGroup.EndpointName
}
if ([string]::IsNullOrWhiteSpace($ContentType)) {
$ContentType = $RouteGroup.ContentType
}
if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
$TransferEncoding = $RouteGroup.TransferEncoding
}
if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
$ErrorContentType = $RouteGroup.ErrorContentType
}
if ([string]::IsNullOrWhiteSpace($Authentication)) {
$Authentication = $RouteGroup.Authentication
}
if ([string]::IsNullOrWhiteSpace($Access)) {
$Access = $RouteGroup.Access
}
if ($RouteGroup.AllowAnon) {
$AllowAnon = $RouteGroup.AllowAnon
}
if ($RouteGroup.IfExists -ine 'default') {
$IfExists = $RouteGroup.IfExists
}
if ($null -ne $RouteGroup.AccessMeta.Role) {
$Role = $RouteGroup.AccessMeta.Role + $Role
}
if ($null -ne $RouteGroup.AccessMeta.Group) {
$Group = $RouteGroup.AccessMeta.Group + $Group
}
if ($null -ne $RouteGroup.AccessMeta.Scope) {
$Scope = $RouteGroup.AccessMeta.Scope + $Scope
}
if ($null -ne $RouteGroup.AccessMeta.User) {
$User = $RouteGroup.AccessMeta.User + $User
}
if ($null -ne $RouteGroup.AccessMeta.Custom) {
$CustomAccess = $RouteGroup.AccessMeta.Custom
}
if ($null -ne $RouteGroup.OADefinitionTag ) {
$OADefinitionTag = $RouteGroup.OADefinitionTag
}
}
$RouteGroup = @{
Path = $Path
Middleware = $Middleware
EndpointName = $EndpointName
ContentType = $ContentType
TransferEncoding = $TransferEncoding
ErrorContentType = $ErrorContentType
Authentication = $Authentication
Access = $Access
AllowAnon = $AllowAnon
IfExists = $IfExists
OADefinitionTag = $OADefinitionTag
AccessMeta = @{
Role = $Role
Group = $Group
Scope = $Scope
User = $User
Custom = $CustomAccess
}
}
# add routes
$null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}
<#
.SYNOPSIS
Add a Static Route Group for multiple Static Routes.
.DESCRIPTION
Add a Static Route Group for sharing values between multiple Static Routes.
.PARAMETER Path
The URI path to use as a base for the Static Routes.
.PARAMETER Source
A literal, or relative, base path to the directory that contains the static content, that should be prepended.
.PARAMETER Routes
A ScriptBlock for adding Static Routes.
.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware to give each Static Route.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Static Routes.
.PARAMETER ContentType
The content type to use for the Static Routes, when parsing any payloads.
.PARAMETER TransferEncoding
The transfer encoding to use for the Static Routes, when parsing any payloads.
.PARAMETER Defaults
An array of default pages to display, such as 'index.html', for each Static Route.
.PARAMETER ErrorContentType
The content type of any error pages that may get returned.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on the Static Routes.
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
.PARAMETER IfExists
Specifies what action to take when a Static Route already exists. (Default: Default)
.PARAMETER AllowAnon
If supplied, the Static Routes will allow anonymous access for non-authenticated users.
.PARAMETER FileBrowser
When supplied, If the path is a folder, instead of returning 404, will return A browsable content of the directory.
.PARAMETER DownloadOnly
When supplied, all static content on the Routes will be attached as downloads - rather than rendered.
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER RedirectToDefault
If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path.
.EXAMPLE
Add-PodeStaticRouteGroup -Path '/static' -Routes { Add-PodeStaticRoute -Path '/images' -Etc }
#>
function Add-PodeStaticRouteGroup {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path,
[Parameter()]
[string]
$Source,
[Parameter(Mandatory = $true)]
[scriptblock]
$Routes,
[Parameter()]
[object[]]
$Middleware,
[Parameter()]
[string[]]
$EndpointName,
[Parameter()]
[string]
$ContentType,
[Parameter()]
[ValidateSet('', 'gzip', 'deflate')]
[string]
$TransferEncoding,
[Parameter()]
[string[]]
$Defaults,
[Parameter()]
[string]
$ErrorContentType,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string]
$Access,
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$IfExists = 'Default',
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[Parameter()]
[string[]]
$User,
[switch]
$AllowAnon,
[switch]
$FileBrowser,
[switch]
$DownloadOnly,
[switch]
$RedirectToDefault
)
if (Test-PodeIsEmpty $Routes) {
throw 'No scriptblock for -Routes passed'
}
if ($Path -eq '/') {
$Path = $null
}
# check for scoped vars
$Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState
# group details
if ($null -ne $RouteGroup) {
if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
$Path = "$($RouteGroup.Path)$($Path)"
}
if (![string]::IsNullOrWhiteSpace($RouteGroup.Source)) {
$Source = [System.IO.Path]::Combine($Source, $RouteGroup.Source.TrimStart('\/'))
}
if ($null -ne $RouteGroup.Middleware) {
$Middleware = $RouteGroup.Middleware + $Middleware
}
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
$EndpointName = $RouteGroup.EndpointName
}
if ([string]::IsNullOrWhiteSpace($ContentType)) {
$ContentType = $RouteGroup.ContentType
}
if ([string]::IsNullOrWhiteSpace($TransferEncoding)) {
$TransferEncoding = $RouteGroup.TransferEncoding
}
if ([string]::IsNullOrWhiteSpace($ErrorContentType)) {
$ErrorContentType = $RouteGroup.ErrorContentType
}
if ([string]::IsNullOrWhiteSpace($Authentication)) {
$Authentication = $RouteGroup.Authentication
}
if ([string]::IsNullOrWhiteSpace($Access)) {
$Access = $RouteGroup.Access
}
if (Test-PodeIsEmpty $Defaults) {
$Defaults = $RouteGroup.Defaults
}
if ($RouteGroup.AllowAnon) {
$AllowAnon = $RouteGroup.AllowAnon
}
if ($RouteGroup.DownloadOnly) {
$DownloadOnly = $RouteGroup.DownloadOnly
}
if ($RouteGroup.FileBrowser) {
$FileBrowser = $RouteGroup.FileBrowser
}
if ($RouteGroup.RedirectToDefault) {
$RedirectToDefault = $RouteGroup.RedirectToDefault
}
if ($RouteGroup.IfExists -ine 'default') {
$IfExists = $RouteGroup.IfExists
}
if ($null -ne $RouteGroup.AccessMeta.Role) {
$Role = $RouteGroup.AccessMeta.Role + $Role
}
if ($null -ne $RouteGroup.AccessMeta.Group) {
$Group = $RouteGroup.AccessMeta.Group + $Group
}
if ($null -ne $RouteGroup.AccessMeta.Scope) {
$Scope = $RouteGroup.AccessMeta.Scope + $Scope
}
if ($null -ne $RouteGroup.AccessMeta.User) {
$User = $RouteGroup.AccessMeta.User + $User
}
if ($null -ne $RouteGroup.AccessMeta.Custom) {
$CustomAccess = $RouteGroup.AccessMeta.Custom
}
}
$RouteGroup = @{
Path = $Path
Source = $Source
Middleware = $Middleware
EndpointName = $EndpointName
ContentType = $ContentType
TransferEncoding = $TransferEncoding
Defaults = $Defaults
RedirectToDefault = $RedirectToDefault
ErrorContentType = $ErrorContentType
Authentication = $Authentication
Access = $Access
AllowAnon = $AllowAnon
DownloadOnly = $DownloadOnly
FileBrowser = $FileBrowser
IfExists = $IfExists
AccessMeta = @{
Role = $Role
Group = $Group
Scope = $Scope
User = $User
Custom = $CustomAccess
}
}
# add routes
$null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}
<#
.SYNOPSIS
Adds a Signal Route Group for multiple WebSockets.
.DESCRIPTION
Adds a Signal Route Group for sharing values between multiple WebSockets.
.PARAMETER Path
The URI path to use as a base for the Signal Routes, that should be prepended.
.PARAMETER Routes
A ScriptBlock for adding Signal Routes.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) to use for the Signal Routes.
.PARAMETER IfExists
Specifies what action to take when a Signal Route already exists. (Default: Default)
.EXAMPLE
Add-PodeSignalRouteGroup -Path '/signals' -Routes { Add-PodeSignalRoute -Path '/signal1' -Etc }
#>
function Add-PodeSignalRouteGroup {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path,
[Parameter(Mandatory = $true)]
[scriptblock]
$Routes,
[Parameter()]
[string[]]
$EndpointName,
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$IfExists = 'Default'
)
if (Test-PodeIsEmpty $Routes) {
throw 'No scriptblock for -Routes passed'
}
if ($Path -eq '/') {
$Path = $null
}
# check for scoped vars
$Routes, $usingVars = Convert-PodeScopedVariables -ScriptBlock $Routes -PSSession $PSCmdlet.SessionState
# group details
if ($null -ne $RouteGroup) {
if (![string]::IsNullOrWhiteSpace($RouteGroup.Path)) {
$Path = "$($RouteGroup.Path)$($Path)"
}
if ([string]::IsNullOrWhiteSpace($EndpointName)) {
$EndpointName = $RouteGroup.EndpointName
}
if ($RouteGroup.IfExists -ine 'default') {
$IfExists = $RouteGroup.IfExists
}
}
$RouteGroup = @{
Path = $Path
EndpointName = $EndpointName
IfExists = $IfExists
}
# add routes
$null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat -NoNewClosure
}
<#
.SYNOPSIS
Remove a specific Route.
.DESCRIPTION
Remove a specific Route.
.PARAMETER Method
The method of the Route to remove.
.PARAMETER Path
The path of the Route to remove.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Route to be removed.
.EXAMPLE
Remove-PodeRoute -Method Get -Route '/about'
.EXAMPLE
Remove-PodeRoute -Method Post -Route '/users/:userId' -EndpointName User
#>
function Remove-PodeRoute {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName
)
# split route on '?' for query
$Path = Split-PodeRouteQuery -Path $Path
if ([string]::IsNullOrWhiteSpace($Path)) {
throw "[$($Method)]: No Route path supplied for removing a Route"
}
# ensure the route has appropriate slashes and replace parameters
$Path = Update-PodeRouteSlashes -Path $Path
$Path = Resolve-PodePlaceholders -Path $Path
# ensure route does exist
if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
return
}
# remove the operationId from the openapi operationId list
if ($PodeContext.Server.Routes[$Method][$Path].OpenAPI) {
foreach ( $tag in $PodeContext.Server.Routes[$Method][$Path].OpenAPI.DefinitionTag) {
if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $PodeContext.Server.Routes[$Method][$Path].OpenAPI.OperationId)) {
$PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $PodeContext.Server.Routes[$Method][$Path].OpenAPI.OperationId }
}
}
}
# remove the route's logic
$PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
$_.Endpoint.Name -ine $EndpointName
})
# if the route has no more logic, just remove it
if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
$null = $PodeContext.Server.Routes[$Method].Remove($Path)
}
}
<#
.SYNOPSIS
Remove a specific static Route.
.DESCRIPTION
Remove a specific static Route.
.PARAMETER Path
The path of the static Route to remove.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the static Route to be removed.
.EXAMPLE
Remove-PodeStaticRoute -Path '/assets'
#>
function Remove-PodeStaticRoute {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName
)
$Method = 'Static'
# ensure the route has appropriate slashes and replace parameters
$Path = Update-PodeRouteSlashes -Path $Path -Static
# ensure route does exist
if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
return
}
# remove the route's logic
$PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
$_.Endpoint.Name -ine $EndpointName
})
# if the route has no more logic, just remove it
if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
$null = $PodeContext.Server.Routes[$Method].Remove($Path)
}
}
<#
.SYNOPSIS
Remove a specific Signal Route.
.DESCRIPTION
Remove a specific Signal Route.
.PARAMETER Path
The path of the Signal Route to remove.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Signal Route to be removed.
.EXAMPLE
Remove-PodeSignalRoute -Route '/message'
#>
function Remove-PodeSignalRoute {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName
)
$Method = 'Signal'
# ensure the route has appropriate slashes and replace parameters
$Path = Update-PodeRouteSlashes -Path $Path
# ensure route does exist
if (!$PodeContext.Server.Routes[$Method].Contains($Path)) {
return
}
# remove the route's logic
$PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
$_.Endpoint.Name -ine $EndpointName
})
# if the route has no more logic, just remove it
if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
$null = $PodeContext.Server.Routes[$Method].Remove($Path)
}
}
<#
.SYNOPSIS
Removes all added Routes, or Routes for a specific Method.
.DESCRIPTION
Removes all added Routes, or Routes for a specific Method.
.PARAMETER Method
The Method to from which to remove all Routes.
.EXAMPLE
Clear-PodeRoutes
.EXAMPLE
Clear-PodeRoutes -Method Get
#>
function Clear-PodeRoutes {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method
)
if (![string]::IsNullOrWhiteSpace($Method)) {
$PodeContext.Server.Routes[$Method].Clear()
}
else {
$PodeContext.Server.Routes.Keys.Clone() | ForEach-Object {
$PodeContext.Server.Routes[$_].Clear()
}
}
}
<#
.SYNOPSIS
Removes all added static Routes.
.DESCRIPTION
Removes all added static Routes.
.EXAMPLE
Clear-PodeStaticRoutes
#>
function Clear-PodeStaticRoutes {
[CmdletBinding()]
param()
$PodeContext.Server.Routes['Static'].Clear()
}
<#
.SYNOPSIS
Removes all added Signal Routes.
.DESCRIPTION
Removes all added Signal Routes.
.EXAMPLE
Clear-PodeSignalRoutes
#>
function Clear-PodeSignalRoutes {
[CmdletBinding()]
param()
$PodeContext.Server.Routes['Signal'].Clear()
}
<#
.SYNOPSIS
Takes an array of Commands, or a Module, and converts them into Routes.
.DESCRIPTION
Takes an array of Commands (Functions/Aliases), or a Module, and generates appropriate Routes for the commands.
.PARAMETER Commands
An array of Commands to convert - if a Module is supplied, these Commands must be present within that Module.
.PARAMETER Module
A Module whose exported commands will be converted.
.PARAMETER Method
An override HTTP method to use when generating the Routes. If not supplied, Pode will make a best guess based on the Command's Verb.
.PARAMETER Path
An optional Path for the Route, to prepend before the Command Name and Module.
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
.PARAMETER AllowAnon
If supplied, the Route will allow anonymous access for non-authenticated users.
.PARAMETER NoVerb
If supplied, the Command's Verb will not be included in the Route's path.
.PARAMETER NoOpenApi
If supplied, no OpenAPI definitions will be generated for the routes created.
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
.EXAMPLE
ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Middleware { ... }
.EXAMPLE
ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Authentication AuthName
.EXAMPLE
ConvertTo-PodeRoute -Module Pester -Path '/api'
.EXAMPLE
ConvertTo-PodeRoute -Commands @('Invoke-Pester') -Module Pester
#>
function ConvertTo-PodeRoute {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[string[]]
$Commands,
[Parameter()]
[string]
$Module,
[Parameter()]
[ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
[string]
$Method,
[Parameter()]
[string]
$Path = '/',
[Parameter()]
[object[]]
$Middleware,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string]
$Access,
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[Parameter()]
[string[]]
$User,
[switch]
$AllowAnon,
[switch]
$NoVerb,
[switch]
$NoOpenApi
)
# if a module was supplied, import it - then validate the commands
if (![string]::IsNullOrWhiteSpace($Module)) {
Import-PodeModule -Name $Module
Write-Verbose 'Getting exported commands from module'
$ModuleCommands = (Get-Module -Name $Module | Sort-Object -Descending | Select-Object -First 1).ExportedCommands.Keys
# if commands were supplied validate them - otherwise use all exported ones
if (Test-PodeIsEmpty $Commands) {
Write-Verbose "Using all commands in $($Module) for converting to routes"
$Commands = $ModuleCommands
}
else {
Write-Verbose "Validating supplied commands against module's exported commands"
foreach ($cmd in $Commands) {
if ($ModuleCommands -inotcontains $cmd) {
throw "Module $($Module) does not contain function $($cmd) to convert to a Route"
}
}
}
}
# if there are no commands, fail
if (Test-PodeIsEmpty $Commands) {
throw 'No commands supplied to convert to Routes'
}
# trim end trailing slashes from the path
$Path = Protect-PodeValue -Value $Path -Default '/'
$Path = $Path.TrimEnd('/')
# create the routes for each of the commands
foreach ($cmd in $Commands) {
# get module verb/noun and comvert verb to HTTP method
$split = ($cmd -split '\-')
if ($split.Length -ge 2) {
$verb = $split[0]
$noun = $split[1..($split.Length - 1)] -join ([string]::Empty)
}
else {
$verb = [string]::Empty
$noun = $split[0]
}
# determine the http method, or use the one passed
$_method = $Method
if ([string]::IsNullOrWhiteSpace($_method)) {
$_method = Convert-PodeFunctionVerbToHttpMethod -Verb $verb
}
# use the full function name, or remove the verb
$name = $cmd
if ($NoVerb) {
$name = $noun
}
# build the route's path
$_path = ("$($Path)/$($Module)/$($name)" -replace '[/]+', '/')
# create the route
$params = @{
Method = $_method
Path = $_path
Middleware = $Middleware
Authentication = $Authentication
Access = $Access
Role = $Role
Group = $Group
Scope = $Scope
User = $User
AllowAnon = $AllowAnon
ArgumentList = $cmd
PassThru = $true
}
$route = Add-PodeRoute @params -ScriptBlock {
param($cmd)
# either get params from the QueryString or Payload
if ($WebEvent.Method -ieq 'get') {
$parameters = $WebEvent.Query
}
else {
$parameters = $WebEvent.Data
}
# invoke the function
$result = (. $cmd @parameters)
# if we have a result, convert it to json
if (!(Test-PodeIsEmpty $result)) {
Write-PodeJsonResponse -Value $result -Depth 1
}
}
# set the openapi metadata of the function, unless told to skip
if ($NoOpenApi) {
continue
}
$help = Get-Help -Name $cmd
$route = ($route | Set-PodeOARouteInfo -Summary $help.Synopsis -Tags $Module -PassThru)
# set the routes parameters (get = query, everything else = payload)
$params = (Get-Command -Name $cmd).Parameters
if (($null -eq $params) -or ($params.Count -eq 0)) {
continue
}
$props = @(foreach ($key in $params.Keys) {
$params[$key] | ConvertTo-PodeOAPropertyFromCmdletParameter
})
if ($_method -ieq 'get') {
$route | Set-PodeOARequest -Parameters @(foreach ($prop in $props) { $prop | ConvertTo-PodeOAParameter -In Query })
}
else {
$route | Set-PodeOARequest -RequestBody (
New-PodeOARequestBody -ContentSchemas @{ 'application/json' = (New-PodeOAObjectProperty -Array -Properties $props) }
)
}
}
}
<#
.SYNOPSIS
Helper function to generate simple GET routes.
.DESCRIPTION
Helper function to generate simple GET routes from ScritpBlocks, Files, and Views.
The output is always rendered as HTML.
.PARAMETER Name
A unique name for the page, that will be used in the Path for the route.
.PARAMETER ScriptBlock
A ScriptBlock to invoke, where any results will be converted to HTML.
.PARAMETER FilePath
A FilePath, literal or relative, to a valid HTML file.
.PARAMETER View
The name of a View to render, this can be HTML or Dynamic.
.PARAMETER Data
A hashtable of Data to supply to a Dynamic File/View, or to be splatted as arguments for the ScriptBlock.
.PARAMETER Path
An optional Path for the Route, to prepend before the Name.
.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.
.PARAMETER Authentication
The name of an Authentication method which should be used as middleware on this Route.
.PARAMETER Access
The name of an Access method which should be used as middleware on this Route.
.PARAMETER AllowAnon
If supplied, the Page will allow anonymous access for non-authenticated users.
.PARAMETER FlashMessages
If supplied, Views will have any flash messages supplied to them for rendering.
.PARAMETER Role
One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Group
One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER Scope
One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method.
.PARAMETER User
One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method.
.EXAMPLE
Add-PodePage -Name Services -ScriptBlock { Get-Service }
.EXAMPLE
Add-PodePage -Name Index -View 'index'
.EXAMPLE
Add-PodePage -Name About -FilePath '.\views\about.pode' -Data @{ Date = [DateTime]::UtcNow }
#>
function Add-PodePage {
[CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter(Mandatory = $true, ParameterSetName = 'View')]
[string]
$View,
[Parameter()]
[hashtable]
$Data,
[Parameter()]
[string]
$Path = '/',
[Parameter()]
[object[]]
$Middleware,
[Parameter()]
[Alias('Auth')]
[string]
$Authentication,
[Parameter()]
[string]
$Access,
[Parameter()]
[string[]]
$Role,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$Scope,
[Parameter()]
[string[]]
$User,
[switch]
$AllowAnon,
[Parameter(ParameterSetName = 'View')]
[switch]
$FlashMessages
)
$logic = $null
$arg = $null
# ensure the name is a valid alphanumeric
if ($Name -inotmatch '^[a-z0-9\-_]+$') {
throw "The Page name should be a valid AlphaNumeric value: $($Name)"
}
# trim end trailing slashes from the path
$Path = Protect-PodeValue -Value $Path -Default '/'
$Path = $Path.TrimEnd('/')
# define the appropriate logic
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'scriptblock' {
if (Test-PodeIsEmpty $ScriptBlock) {
throw 'A non-empty ScriptBlock is required to created a Page Route'
}
$arg = @($ScriptBlock, $Data)
$logic = {
param($script, $data)
# invoke the function (optional splat data)
if (Test-PodeIsEmpty $data) {
$result = Invoke-PodeScriptBlock -ScriptBlock $script -Return
}
else {
$result = Invoke-PodeScriptBlock -ScriptBlock $script -Arguments $data -Return
}
# if we have a result, convert it to html
if (!(Test-PodeIsEmpty $result)) {
Write-PodeHtmlResponse -Value $result
}
}
}
'file' {
$FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -TestPath
$arg = @($FilePath, $Data)
$logic = {
param($file, $data)
Write-PodeFileResponse -Path $file -ContentType 'text/html' -Data $data
}
}
'view' {
$arg = @($View, $Data, $FlashMessages)
$logic = {
param($view, $data, [bool]$flash)
Write-PodeViewResponse -Path $view -Data $data -FlashMessages:$flash
}
}
}
# build the route's path
$_path = ("$($Path)/$($Name)" -replace '[/]+', '/')
# create the route
$params = @{
Method = 'Get'
Path = $_path
Middleware = $Middleware
Authentication = $Authentication
Access = $Access
Role = $Role
Group = $Group
Scope = $Scope
User = $User
AllowAnon = $AllowAnon
ArgumentList = $arg
ScriptBlock = $logic
}
Add-PodeRoute @params
}
<#
.SYNOPSIS
Get a Route(s).
.DESCRIPTION
Get a Route(s).
.PARAMETER Method
A Method to filter the routes.
.PARAMETER Path
A Path to filter the routes.
.PARAMETER EndpointName
The name of an endpoint to filter routes.
.EXAMPLE
Get-PodeRoute -Method Get -Path '/about'
.EXAMPLE
Get-PodeRoute -Method Post -Path '/users/:userId' -EndpointName User
#>
function Get-PodeRoute {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[Parameter()]
[string]
$Path,
[Parameter()]
[string[]]
$EndpointName
)
# start off with every route
$routes = @()
foreach ($route in $PodeContext.Server.Routes.Values.Values) {
$routes += $route
}
# if we have a method, filter
if (![string]::IsNullOrWhiteSpace($Method)) {
$routes = @(foreach ($route in $routes) {
if ($route.Method -ine $Method) {
continue
}
$route
})
}
# if we have a path, filter
if (![string]::IsNullOrWhiteSpace($Path)) {
$Path = Split-PodeRouteQuery -Path $Path
$Path = Update-PodeRouteSlashes -Path $Path
$Path = Resolve-PodePlaceholders -Path $Path
$routes = @(foreach ($route in $routes) {
if ($route.Path -ine $Path) {
continue
}
$route
})
}
# further filter by endpoint names
if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
$routes = @(foreach ($name in $EndpointName) {
foreach ($route in $routes) {
if ($route.Endpoint.Name -ine $name) {
continue
}
$route
}
})
}
# return
return $routes
}
<#
.SYNOPSIS
Get a static Route(s).
.DESCRIPTION
Get a static Route(s).
.PARAMETER Path
A Path to filter the static routes.
.PARAMETER EndpointName
The name of an endpoint to filter static routes.
.EXAMPLE
Get-PodeStaticRoute -Path '/assets'
.EXAMPLE
Get-PodeStaticRoute -Path '/assets' -EndpointName User
#>
function Get-PodeStaticRoute {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path,
[Parameter()]
[string[]]
$EndpointName
)
# start off with every route
$routes = @()
foreach ($route in $PodeContext.Server.Routes['Static'].Values) {
$routes += $route
}
# if we have a path, filter
if (![string]::IsNullOrWhiteSpace($Path)) {
$Path = Update-PodeRouteSlashes -Path $Path -Static
$routes = @(foreach ($route in $routes) {
if ($route.Path -ine $Path) {
continue
}
$route
})
}
# further filter by endpoint names
if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
$routes = @(foreach ($name in $EndpointName) {
foreach ($route in $routes) {
if ($route.Endpoint.Name -ine $name) {
continue
}
$route
}
})
}
# return
return $routes
}
<#
.SYNOPSIS
Get a Signal Route(s).
.DESCRIPTION
Get a Signal Route(s).
.PARAMETER Path
A Path to filter the signal routes.
.PARAMETER EndpointName
The name of an endpoint to filter signal routes.
.EXAMPLE
Get-PodeSignalRoute -Path '/message'
#>
function Get-PodeSignalRoute {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path,
[Parameter()]
[string[]]
$EndpointName
)
# start off with every route
$routes = @()
foreach ($route in $PodeContext.Server.Routes['Signal'].Values) {
$routes += $route
}
# if we have a path, filter
if (![string]::IsNullOrWhiteSpace($Path)) {
$Path = Update-PodeRouteSlashes -Path $Path
$routes = @(foreach ($route in $routes) {
if ($route.Path -ine $Path) {
continue
}
$route
})
}
# further filter by endpoint names
if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
$routes = @(foreach ($name in $EndpointName) {
foreach ($route in $routes) {
if ($route.Endpoint.Name -ine $name) {
continue
}
$route
}
})
}
# return
return $routes
}
<#
.SYNOPSIS
Automatically loads route ps1 files
.DESCRIPTION
Automatically loads route ps1 files from either a /routes folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.PARAMETER IfExists
Specifies what action to take when a Route already exists. (Default: Default)
.EXAMPLE
Use-PodeRoutes
.EXAMPLE
Use-PodeRoutes -Path './my-routes' -IfExists Skip
#>
function Use-PodeRoutes {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path,
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$IfExists = 'Default'
)
if ($IfExists -ieq 'Default') {
$IfExists = Get-PodeRouteIfExistsPreference
}
Use-PodeFolder -Path $Path -DefaultPath 'routes'
}
<#
.SYNOPSIS
Set the default IfExists preference for Routes.
.DESCRIPTION
Set the default IfExists preference for Routes.
.PARAMETER Value
Specifies what action to take when a Route already exists. (Default: Default)
.EXAMPLE
Set-PodeRouteIfExistsPreference -Value Overwrite
#>
function Set-PodeRouteIfExistsPreference {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Default', 'Error', 'Overwrite', 'Skip')]
[string]
$Value = 'Default'
)
$PodeContext.Server.Preferences.Routes.IfExists = $Value
}
<#
.SYNOPSIS
Test if a Route already exists.
.DESCRIPTION
Test if a Route already exists for a given Method and Path.
.PARAMETER Method
The HTTP Method of the Route.
.PARAMETER Path
The URI path of the Route.
.PARAMETER EndpointName
The EndpointName of an Endpoint the Route is bound against.
.PARAMETER CheckWildcard
If supplied, Pode will check for the Route on the Method first, and then check for the Route on the '*' Method.
.EXAMPLE
Test-PodeRoute -Method Post -Path '/example'
.EXAMPLE
Test-PodeRoute -Method Post -Path '/example' -CheckWildcard
.EXAMPLE
Test-PodeRoute -Method Get -Path '/example/:exampleId' -CheckWildcard
#>
function Test-PodeRoute {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string]
$Method,
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName,
[switch]
$CheckWildcard
)
# split route on '?' for query
$Path = Split-PodeRouteQuery -Path $Path
if ([string]::IsNullOrWhiteSpace($Path)) {
throw 'No Path supplied for testing Route'
}
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlashes -Path $Path
$Path = Resolve-PodePlaceholders -Path $Path
# get endpoint from name
$endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0]
# check for routes
$found = (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
if (!$found -and $CheckWildcard) {
$found = (Test-PodeRouteInternal -Method '*' -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
}
return $found
}
<#
.SYNOPSIS
Test if a Static Route already exists.
.DESCRIPTION
Test if a Static Route already exists for a given Path.
.PARAMETER Path
The URI path of the Static Route.
.PARAMETER EndpointName
The EndpointName of an Endpoint the Static Route is bound against.
.EXAMPLE
Test-PodeStaticRoute -Path '/assets'
#>
function Test-PodeStaticRoute {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName
)
# store the route method
$Method = 'Static'
# split route on '?' for query
$Path = Split-PodeRouteQuery -Path $Path
if ([string]::IsNullOrWhiteSpace($Path)) {
throw 'No Path supplied for testing Static Route'
}
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlashes -Path $Path -Static
$Path = Resolve-PodePlaceholders -Path $Path
# get endpoint from name
$endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0]
# check for routes
return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
}
<#
.SYNOPSIS
Test if a Signal Route already exists.
.DESCRIPTION
Test if a Signal Route already exists for a given Path.
.PARAMETER Path
The URI path of the Signal Route.
.PARAMETER EndpointName
The EndpointName of an Endpoint the Signal Route is bound against.
.EXAMPLE
Test-PodeSignalRoute -Path '/message'
#>
function Test-PodeSignalRoute {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string]
$EndpointName
)
$Method = 'Signal'
# ensure the route has appropriate slashes
$Path = Update-PodeRouteSlashes -Path $Path
# get endpoint from name
$endpoint = @(Find-PodeEndpoints -EndpointName $EndpointName)[0]
# check for routes
return (Test-PodeRouteInternal -Method $Method -Path $Path -Protocol $endpoint.Protocol -Address $endpoint.Address)
}
<#
.SYNOPSIS
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.
.DESCRIPTION
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.
.PARAMETER Name
The Name of the Schedule.
.PARAMETER Cron
One, or an Array, of Cron Expressions to define when the Schedule should trigger.
.PARAMETER ScriptBlock
The script defining the Schedule's logic.
.PARAMETER Limit
The number of times the Schedule should trigger before being removed.
.PARAMETER StartTime
A DateTime for when the Schedule should start triggering.
.PARAMETER EndTime
A DateTime for when the Schedule should stop triggering, and be removed.
.PARAMETER ArgumentList
A hashtable of arguments to supply to the Schedule's ScriptBlock.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Schedule's logic.
.PARAMETER OnStart
If supplied, the schedule will trigger when the server starts, regardless if the cron-expression matches the current time.
.EXAMPLE
Add-PodeSchedule -Name 'RunEveryMinute' -Cron '@minutely' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeSchedule -Name 'RunEveryTuesday' -Cron '0 0 * * TUE' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeSchedule -Name 'StartAfter2days' -Cron '@hourly' -StartTime [DateTime]::Now.AddDays(2) -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeSchedule -Name 'Args' -Cron '@minutely' -ScriptBlock { /* logic */ } -ArgumentList @{ Arg1 = 'value' }
#>
function Add-PodeSchedule {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string[]]
$Cron,
[Parameter(Mandatory = $true, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter()]
[int]
$Limit = 0,
[Parameter()]
[DateTime]
$StartTime,
[Parameter()]
[DateTime]
$EndTime,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[hashtable]
$ArgumentList,
[switch]
$OnStart
)
# error if serverless
Test-PodeIsServerless -FunctionName 'Add-PodeSchedule' -ThrowError
# ensure the schedule doesn't already exist
if ($PodeContext.Schedules.Items.ContainsKey($Name)) {
throw "[Schedule] $($Name): Schedule already defined"
}
# ensure the limit is valid
if ($Limit -lt 0) {
throw "[Schedule] $($Name): Cannot have a negative limit"
}
# ensure the start/end dates are valid
if (($null -ne $EndTime) -and ($EndTime -lt [DateTime]::Now)) {
throw "[Schedule] $($Name): The EndTime value must be in the future"
}
if (($null -ne $StartTime) -and ($null -ne $EndTime) -and ($EndTime -le $StartTime)) {
throw "[Schedule] $($Name): Cannot have a StartTime after the EndTime"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add the schedule
$parsedCrons = ConvertFrom-PodeCronExpressions -Expressions @($Cron)
$nextTrigger = Get-PodeCronNextEarliestTrigger -Expressions $parsedCrons -StartTime $StartTime -EndTime $EndTime
$PodeContext.Schedules.Enabled = $true
$PodeContext.Schedules.Items[$Name] = @{
Name = $Name
StartTime = $StartTime
EndTime = $EndTime
Crons = $parsedCrons
CronsRaw = @($Cron)
Limit = $Limit
Count = 0
NextTriggerTime = $nextTrigger
LastTriggerTime = $null
Script = $ScriptBlock
UsingVariables = $usingVars
Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{})
OnStart = $OnStart
Completed = ($null -eq $nextTrigger)
}
}
<#
.SYNOPSIS
Set the maximum number of concurrent schedules.
.DESCRIPTION
Set the maximum number of concurrent schedules.
.PARAMETER Maximum
The Maximum number of schedules to run.
.EXAMPLE
Set-PodeScheduleConcurrency -Maximum 25
#>
function Set-PodeScheduleConcurrency {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[int]
$Maximum
)
# error if <=0
if ($Maximum -le 0) {
throw "Maximum concurrent schedules must be >=1 but got: $($Maximum)"
}
# ensure max > min
$_min = 1
if ($null -ne $PodeContext.RunspacePools.Schedules) {
$_min = $PodeContext.RunspacePools.Schedules.Pool.GetMinRunspaces()
}
if ($_min -gt $Maximum) {
throw "Maximum concurrent schedules cannot be less than the minimum of $($_min) but got: $($Maximum)"
}
# set the max schedules
$PodeContext.Threads.Schedules = $Maximum
if ($null -ne $PodeContext.RunspacePools.Schedules) {
$PodeContext.RunspacePools.Schedules.Pool.SetMaxRunspaces($Maximum)
}
}
<#
.SYNOPSIS
Adhoc invoke a Schedule's logic.
.DESCRIPTION
Adhoc invoke a Schedule's logic outside of its defined cron-expression. This invocation doesn't count towards the Schedule's limit.
.PARAMETER Name
The Name of the Schedule.
.PARAMETER ArgumentList
A hashtable of arguments to supply to the Schedule's ScriptBlock.
.EXAMPLE
Invoke-PodeSchedule -Name 'schedule-name'
#>
function Invoke-PodeSchedule {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name,
[Parameter()]
[hashtable]
$ArgumentList = $null
)
# ensure the schedule exists
if (!$PodeContext.Schedules.Items.ContainsKey($Name)) {
throw "Schedule '$($Name)' does not exist"
}
# run schedule logic
Invoke-PodeInternalScheduleLogic -Schedule $PodeContext.Schedules.Items[$Name] -ArgumentList $ArgumentList
}
<#
.SYNOPSIS
Removes a specific Schedule.
.DESCRIPTION
Removes a specific Schedule.
.PARAMETER Name
The Name of the Schedule to be removed.
.EXAMPLE
Remove-PodeSchedule -Name 'RenewToken'
#>
function Remove-PodeSchedule {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name
)
$null = $PodeContext.Schedules.Items.Remove($Name)
}
<#
.SYNOPSIS
Removes all Schedules.
.DESCRIPTION
Removes all Schedules.
.EXAMPLE
Clear-PodeSchedules
#>
function Clear-PodeSchedules {
[CmdletBinding()]
param()
$PodeContext.Schedules.Items.Clear()
}
<#
.SYNOPSIS
Edits an existing Schedule.
.DESCRIPTION
Edits an existing Schedule's properties, such an cron expressions or scriptblock.
.PARAMETER Name
The Name of the Schedule.
.PARAMETER Cron
Any new Cron Expressions for the Schedule.
.PARAMETER ScriptBlock
The new ScriptBlock for the Schedule.
.PARAMETER ArgumentList
Any new Arguments for the Schedule.
.EXAMPLE
Edit-PodeSchedule -Name 'Hello' -Cron '@minutely'
.EXAMPLE
Edit-PodeSchedule -Name 'Hello' -Cron @('@hourly', '0 0 * * TUE')
#>
function Edit-PodeSchedule {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name,
[Parameter()]
[string[]]
$Cron,
[Parameter()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[hashtable]
$ArgumentList
)
# ensure the schedule exists
if (!$PodeContext.Schedules.Items.ContainsKey($Name)) {
throw "Schedule '$($Name)' does not exist"
}
$_schedule = $PodeContext.Schedules.Items[$Name]
# edit cron if supplied
if (!(Test-PodeIsEmpty $Cron)) {
$_schedule.Crons = (ConvertFrom-PodeCronExpressions -Expressions @($Cron))
$_schedule.CronsRaw = $Cron
$_schedule.NextTriggerTime = Get-PodeCronNextEarliestTrigger -Expressions $_schedule.Crons -StartTime $_schedule.StartTime -EndTime $_schedule.EndTime
}
# edit scriptblock if supplied
if (!(Test-PodeIsEmpty $ScriptBlock)) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
$_schedule.Script = $ScriptBlock
$_schedule.UsingVariables = $usingVars
}
# edit arguments if supplied
if (!(Test-PodeIsEmpty $ArgumentList)) {
$_schedule.Arguments = $ArgumentList
}
}
<#
.SYNOPSIS
Returns any defined schedules.
.DESCRIPTION
Returns any defined schedules, with support for filtering.
.PARAMETER Name
Any schedule Names to filter the schedules.
.PARAMETER StartTime
An optional StartTime to only return Schedules that will trigger after this date.
.PARAMETER EndTime
An optional EndTime to only return Schedules that will trigger before this date.
.EXAMPLE
Get-PodeSchedule
.EXAMPLE
Get-PodeSchedule -Name Name1, Name2
.EXAMPLE
Get-PodeSchedule -Name Name1, Name2 -StartTime [datetime]::new(2020, 3, 1) -EndTime [datetime]::new(2020, 3, 31)
#>
function Get-PodeSchedule {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Name,
[Parameter()]
$StartTime = $null,
[Parameter()]
$EndTime = $null
)
$schedules = $PodeContext.Schedules.Items.Values
# further filter by schedule names
if (($null -ne $Name) -and ($Name.Length -gt 0)) {
$schedules = @(foreach ($_name in $Name) {
foreach ($schedule in $schedules) {
if ($schedule.Name -ine $_name) {
continue
}
$schedule
}
})
}
# filter by some start time
if ($null -ne $StartTime) {
$schedules = @(foreach ($schedule in $schedules) {
if (($null -ne $schedule.StartTime) -and ($StartTime -lt $schedule.StartTime)) {
continue
}
$_end = $EndTime
if ($null -eq $_end) {
$_end = $schedule.EndTime
}
if (($null -ne $schedule.EndTime) -and
(($StartTime -gt $schedule.EndTime) -or
((Get-PodeScheduleNextTrigger -Name $schedule.Name -DateTime $StartTime) -gt $_end))) {
continue
}
$schedule
})
}
# filter by some end time
if ($null -ne $EndTime) {
$schedules = @(foreach ($schedule in $schedules) {
if (($null -ne $schedule.EndTime) -and ($EndTime -gt $schedule.EndTime)) {
continue
}
$_start = $StartTime
if ($null -eq $_start) {
$_start = $schedule.StartTime
}
if (($null -ne $schedule.StartTime) -and
(($EndTime -lt $schedule.StartTime) -or
((Get-PodeScheduleNextTrigger -Name $schedule.Name -DateTime $_start) -gt $EndTime))) {
continue
}
$schedule
})
}
# return
return $schedules
}
<#
.SYNOPSIS
Tests whether the passed Schedule exists.
.DESCRIPTION
Tests whether the passed Schedule exists by its name.
.PARAMETER Name
The Name of the Schedule.
.EXAMPLE
if (Test-PodeSchedule -Name ScheduleName) { }
#>
function Test-PodeSchedule {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return (($null -ne $PodeContext.Schedules.Items) -and $PodeContext.Schedules.Items.ContainsKey($Name))
}
<#
.SYNOPSIS
Get the next trigger time for a Schedule.
.DESCRIPTION
Get the next trigger time for a Schedule, either from the Schedule's StartTime or from a defined DateTime.
.PARAMETER Name
The Name of the Schedule.
.PARAMETER DateTime
An optional specific DateTime to get the next trigger time after. This DateTime must be between the Schedule's StartTime and EndTime.
.EXAMPLE
Get-PodeScheduleNextTrigger -Name Schedule1
.EXAMPLE
Get-PodeScheduleNextTrigger -Name Schedule1 -DateTime [datetime]::new(2020, 3, 10)
#>
function Get-PodeScheduleNextTrigger {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name,
[Parameter()]
$DateTime = $null
)
# ensure the schedule exists
if (!$PodeContext.Schedules.Items.ContainsKey($Name)) {
throw "Schedule '$($Name)' does not exist"
}
$_schedule = $PodeContext.Schedules.Items[$Name]
# ensure date is after start/before end
if (($null -ne $DateTime) -and ($null -ne $_schedule.StartTime) -and ($DateTime -lt $_schedule.StartTime)) {
throw "Supplied date is before the start time of the schedule at $($_schedule.StartTime)"
}
if (($null -ne $DateTime) -and ($null -ne $_schedule.EndTime) -and ($DateTime -gt $_schedule.EndTime)) {
throw "Supplied date is after the end time of the schedule at $($_schedule.EndTime)"
}
# get the next trigger
if ($null -eq $DateTime) {
$DateTime = $_schedule.StartTime
}
return (Get-PodeCronNextEarliestTrigger -Expressions $_schedule.Crons -StartTime $DateTime -EndTime $_schedule.EndTime)
}
<#
.SYNOPSIS
Automatically loads schedule ps1 files
.DESCRIPTION
Automatically loads schedule ps1 files from either a /schedules folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeSchedules
.EXAMPLE
Use-PodeSchedules -Path './my-schedules'
#>
function Use-PodeSchedules {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'schedules'
}
<#
.SYNOPSIS
Converts Scoped Variables within a given ScriptBlock.
.DESCRIPTION
Converts Scoped Variables within a given ScriptBlock, and returns the updated ScriptBlock back, including any
using-variable values that will need to be supplied as parameters to the ScriptBlock first.
.PARAMETER ScriptBlock
The ScriptBlock to be converted.
.PARAMETER PSSession
An optional SessionState object, used to retrieve using-variable values.
If not supplied, using-variable values will not be converted.
.PARAMETER Exclude
An optional array of one or more Scoped Variable Names to Exclude from converting. (ie: Session, Using, or a Name from Add-PodeScopedVariable)
.EXAMPLE
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
.EXAMPLE
$ScriptBlock = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -Exclude Session, Using
#>
function Convert-PodeScopedVariables {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[System.Management.Automation.SessionState]
$PSSession,
[Parameter()]
[string[]]
$Exclude
)
# do nothing if no scriptblock
if ($null -eq $ScriptBlock) {
return $ScriptBlock
}
# using vars
$usingVars = $null
# loop through each defined scoped variable and convert, unless excluded
foreach ($key in $PodeContext.Server.ScopedVariables.Keys) {
# excluded?
if ($Exclude -icontains $key) {
continue
}
# convert scoped var
$ScriptBlock, $otherResults = Convert-PodeScopedVariable -Name $key -ScriptBlock $ScriptBlock -PSSession $PSSession
# using vars?
if (($null -ne $otherResults) -and ($key -ieq 'using')) {
$usingVars = $otherResults
}
}
# return just the scriptblock, or include using vars as well
if ($null -ne $usingVars) {
return $ScriptBlock, $usingVars
}
return $ScriptBlock
}
<#
.SYNOPSIS
Converts a Scoped Variable within a given ScriptBlock.
.DESCRIPTION
Converts a Scoped Variable within a given ScriptBlock, and returns the updated ScriptBlock back, including any
other values that will need to be supplied as parameters to the ScriptBlock first.
.PARAMETER Name
The Name of the Scoped Variable to convert. (ie: Session, Using, or a Name from Add-PodeScopedVariable)
.PARAMETER ScriptBlock
The ScriptBlock to be converted.
.PARAMETER PSSession
An optional SessionState object, used to retrieve using-variable values or other values where scope is required.
.EXAMPLE
$ScriptBlock = Convert-PodeScopedVariable -Name State -ScriptBlock $ScriptBlock
.EXAMPLE
$ScriptBlock, $otherResults = Convert-PodeScopedVariable -Name Using -ScriptBlock $ScriptBlock
#>
function Convert-PodeScopedVariable {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(ValueFromPipeline = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[System.Management.Automation.SessionState]
$PSSession
)
# do nothing if no scriptblock
if ($null -eq $ScriptBlock) {
return $ScriptBlock
}
# check if scoped var defined
if (!(Test-PodeScopedVariable -Name $Name)) {
throw "Scoped Variable not found: $($Name)"
}
# get the scoped var metadata
$scopedVar = $PodeContext.Server.ScopedVariables[$Name]
# invoke the logic for the appropriate conversion type required - internal function map, custom scriptblock, or simple replace
switch ($scopedVar.Type) {
'internal' {
switch ($scopedVar.Name) {
'using' {
return Convert-PodeScopedVariableInbuiltUsing -ScriptBlock $ScriptBlock -PSSession $PSSession
}
}
}
'scriptblock' {
return Invoke-PodeScriptBlock `
-ScriptBlock $scopedVar.ScriptBlock `
-Arguments $ScriptBlock, $PSSession, $scopedVar.Get.Pattern, $scopedVar.Set.Pattern `
-Splat `
-Return `
-NoNewClosure
}
'replace' {
# convert scriptblock to string
$strScriptBlock = "$($ScriptBlock)"
# see if the script contains any form of the scoped variable, and if not just return
$found = $strScriptBlock -imatch "\`$$($Name)\:"
if (!$found) {
return $ScriptBlock
}
# loop and replace "set" syntax if replace template supplied
if (![string]::IsNullOrEmpty($scopedVar.Set.Replace)) {
while ($strScriptBlock -imatch $scopedVar.Set.Pattern) {
$setReplace = $scopedVar.Set.Replace.Replace('{{name}}', $Matches['name'])
$strScriptBlock = $strScriptBlock.Replace($Matches['full'], $setReplace)
}
}
# loop and replace "get" syntax
while ($strScriptBlock -imatch $scopedVar.Get.Pattern) {
$getReplace = $scopedVar.Get.Replace.Replace('{{name}}', $Matches['name'])
$strScriptBlock = $strScriptBlock.Replace($Matches['full'], "($($getReplace))")
}
# convert update scriptblock back
return [scriptblock]::Create($strScriptBlock)
}
}
}
<#
.SYNOPSIS
Adds a new Scoped Variable.
.DESCRIPTION
Adds a new Scoped Variable, to make calling certain functions simpler.
For example "$state:Name" instead of "Get-PodeState" and "Set-PodeState".
.PARAMETER Name
The Name of the Scoped Variable.
.PARAMETER GetReplace
A template to be used when converting "$var = $SV:<name>" to a "Get-SVValue -Name <name>" syntax.
You can use the "{{name}}" placeholder to show where the <name> would be placed in the conversion. The result will also be automatically wrapped in brackets.
For example, "$var = $state:<name>" to "Get-PodeState -Name <name>" would need a GetReplace value of "Get-PodeState -Name '{{name}}'".
.PARAMETER SetReplace
An optional template to be used when converting "$SV:<name> = <value>" to a "Set-SVValue -Name <name> -Value <value>" syntax.
You can use the "{{name}}" placeholder to show where the <name> would be placed in the conversion. The <value> will automatically be appended to the end.
For example, "$state:<name> = <value>" to "Set-PodeState -Name <name> -Value <value>" would need a SetReplace value of "Set-PodeState -Name '{{name}}' -Value ".
.PARAMETER ScriptBlock
For more advanced conversions, that aren't as simple as a simple find/replace, you can supply a ScriptBlock instead.
This ScriptBlock will be supplied ScriptBlock to convert, followed by a SessionState object, and the Get/Set regex patterns, as parameters.
The ScriptBlock should returned a converted ScriptBlock that works, plus an optional array of values that should be supplied to the ScriptBlock when invoked.
.EXAMPLE
Add-PodeScopedVariable -Name 'cache' -SetReplace "Set-PodeCache -Key '{{name}}' -InputObject " -GetReplace "Get-PodeCache -Key '{{name}}'"
.EXAMPLE
Add-PodeScopedVariable -Name 'config' -ScriptBlock {
param($ScriptBlock, $SessionState, $GetPattern, $SetPattern)
$strScriptBlock = "$($ScriptBlock)"
$template = "(Get-PodeConfig).'{{name}}'"
# allows "$port = $config:port" instead of "$port = (Get-PodeConfig).port"
while ($strScriptBlock -imatch $GetPattern) {
$getReplace = $template.Replace('{{name}}', $Matches['name'])
$strScriptBlock = $strScriptBlock.Replace($Matches['full'], "($($getReplace))")
}
return [scriptblock]::Create($strScriptBlock)
}
#>
function Add-PodeScopedVariable {
[CmdletBinding(DefaultParameterSetName = 'Replace')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Replace')]
[string]
$GetReplace,
[Parameter(ParameterSetName = 'Replace')]
[string]
$SetReplace = $null,
[Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')]
[scriptblock]
$ScriptBlock
)
Add-PodeScopedVariableInternal @PSBoundParameters
}
<#
.SYNOPSIS
Removes a Scoped Variable.
.DESCRIPTION
Removes a Scoped Variable.
.PARAMETER Name
The Name of a Scoped Variable to remove.
.EXAMPLE
Remove-PodeScopedVariable -Name State
#>
function Remove-PodeScopedVariable {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$null = $PodeContext.Server.ScopedVariables.Remove($Name)
}
<#
.SYNOPSIS
Tests if a Scoped Variable exists.
.DESCRIPTION
Tests if a Scoped Variable exists.
.PARAMETER Name
The Name of the Scoped Variable to check.
.EXAMPLE
if (Test-PodeScopedVariable -Name $Name) { ... }
#>
function Test-PodeScopedVariable {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.ScopedVariables.Contains($Name)
}
<#
.SYNOPSIS
Removes all Scoped Variables.
.DESCRIPTION
Removes all Scoped Variables.
.EXAMPLE
Clear-PodeScopedVariables
#>
function Clear-PodeScopedVariables {
$null = $PodeContext.Server.ScopedVariables.Clear()
}
<#
.SYNOPSIS
Get a Scoped Variable(s).
.DESCRIPTION
Get a Scoped Variable(s).
.PARAMETER Name
The Name of the Scoped Variable(s) to retrieve.
.EXAMPLE
Get-PodeScopedVariable -Name State
.EXAMPLE
Get-PodeScopedVariable -Name State, Using
#>
function Get-PodeScopedVariable {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Name
)
# return all if no Name
if ([string]::IsNullOrEmpty($Name) -or ($Name.Length -eq 0)) {
return $PodeContext.Server.ScopedVariables.Values
}
# return filtered
return @(foreach ($n in $Name) {
$PodeContext.Server.ScopedVariables[$n]
})
}
<#
.SYNOPSIS
Automatically loads Scoped Variable ps1 files
.DESCRIPTION
Automatically loads Scoped Variable ps1 files from either a /scoped-vars folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeScopedVariables
.EXAMPLE
Use-PodeScopedVariables -Path './my-vars'
#>
function Use-PodeScopedVariables {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'scoped-vars'
}
<#
.SYNOPSIS
Register a Secret Vault.
.DESCRIPTION
Register a Secret Vault, which is defined by either custom logic or using the SecretManagement module.
.PARAMETER Name
The unique friendly Name of the Secret Vault within Pode.
.PARAMETER VaultParameters
A hashtable of extra parameters that should be supplied to either the SecretManagement module, or custom scriptblocks.
.PARAMETER UnlockSecret
An optional Secret to be used to unlock the Secret Vault if need.
.PARAMETER UnlockSecureSecret
An optional Secret, as a SecureString, to be used to unlock the Secret Vault if need.
.PARAMETER UnlockInterval
An optional number of minutes that Pode will periodically check/unlock the Secret Vault. (Default: 0)
.PARAMETER NoUnlock
If supplied, the Secret Vault will not be unlocked after registration. To unlock you'll need to call Unlock-PodeSecretVault.
.PARAMETER CacheTtl
An optional number of minutes that Secrets should be cached for. (Default: 0)
.PARAMETER InitScriptBlock
An optional scriptblock to run before the Secret Vault is registered, letting you initialise any connection, contexts, etc.
.PARAMETER VaultName
For SecretManagement module Secret Vaults, you can use thie parameter to specify the actual Vault name, and use the above Name parameter as a more friendly name if required.
.PARAMETER ModuleName
For SecretManagement module Secret Vaults, this is the name/path of the extension module to be used.
.PARAMETER ScriptBlock
For custom Secret Vaults, this is a scriptblock used to read the Secret from the Vault.
.PARAMETER UnlockScriptBlock
For custom Secret Vaults, this is an optional scriptblock used to unlock the Secret Vault.
.PARAMETER RemoveScriptBlock
For custom Secret Vaults, this is an optional scriptblock used to remove a Secret from the Vault.
.PARAMETER SetScriptBlock
For custom Secret Vaults, this is an optional scriptblock used to create/update a Secret in the Vault.
.PARAMETER UnregisterScriptBlock
For custom Secret Vaults, this is an optional scriptblock used unregister the Secret Vault with any custom clean-up logic.
.EXAMPLE
Register-PodeSecretVault -Name 'VaultName' -ModuleName 'Az.KeyVault' -VaultParameters @{ AZKVaultName = $name; SubscriptionId = $subId }
.EXAMPLE
Register-PodeSecretVault -Name 'VaultName' -VaultParameters @{ Address = 'http://127.0.0.1:8200' } -ScriptBlock { ... }
#>
function Register-PodeSecretVault {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[hashtable]
$VaultParameters,
[Parameter()]
[string]
$UnlockSecret,
[Parameter()]
[securestring]
$UnlockSecureSecret,
[Parameter()]
[int]
$UnlockInterval = 0,
[switch]
$NoUnlock,
[Parameter()]
[int]
$CacheTtl = 0, # in minutes
[Parameter()]
[scriptblock]
$InitScriptBlock,
[Parameter(ParameterSetName = 'SecretManagement')]
[string]
$VaultName,
[Parameter(Mandatory = $true, ParameterSetName = 'SecretManagement')]
[Alias('Module')]
[string]
$ModuleName,
[Parameter(Mandatory = $true, ParameterSetName = 'Custom')]
[scriptblock]
$ScriptBlock, # Read a secret
[Parameter(ParameterSetName = 'Custom')]
[Alias('Unlock')]
[scriptblock]
$UnlockScriptBlock,
[Parameter(ParameterSetName = 'Custom')]
[Alias('Remove')]
[scriptblock]
$RemoveScriptBlock,
[Parameter(ParameterSetName = 'Custom')]
[Alias('Set')]
[scriptblock]
$SetScriptBlock,
[Parameter(ParameterSetName = 'Custom')]
[Alias('Unregister')]
[scriptblock]
$UnregisterScriptBlock
)
# has the vault already been registered?
if (Test-PodeSecretVault -Name $Name) {
$autoImported = [string]::Empty
if ($PodeContext.Server.Secrets.Vaults[$Name].AutoImported) {
$autoImported = ' from auto-importing'
}
throw "A Secret Vault with the name '$($Name)' has already been registered$($autoImported)"
}
# base vault config
if (![string]::IsNullOrEmpty($UnlockSecret)) {
$UnlockSecureSecret = $UnlockSecret | ConvertTo-SecureString -AsPlainText -Force
}
$vault = @{
Name = $Name
Type = $PSCmdlet.ParameterSetName.ToLowerInvariant()
Parameters = $VaultParameters
AutoImported = $false
LockableName = "__Pode_SecretVault_$($Name)__"
Unlock = @{
Secret = $UnlockSecureSecret
Expiry = $null
Interval = $UnlockInterval
Enabled = (!(Test-PodeIsEmpty $UnlockSecureSecret))
}
Cache = @{
Ttl = $CacheTtl
Enabled = ($CacheTtl -gt 0)
}
}
# initialise the secret vault
if ($null -ne $InitScriptBlock) {
$vault | Initialize-PodeSecretVault -ScriptBlock $InitScriptBlock
}
# set vault config depending on vault type
switch ($vault.Type) {
'custom' {
$vault | Register-PodeSecretCustomVault `
-ScriptBlock $ScriptBlock `
-UnlockScriptBlock $UnlockScriptBlock `
-RemoveScriptBlock $RemoveScriptBlock `
-SetScriptBlock $SetScriptBlock `
-UnregisterScriptBlock $UnregisterScriptBlock
}
'secretmanagement' {
$vault | Register-PodeSecretManagementVault `
-VaultName $VaultName `
-ModuleName $ModuleName
}
}
# create timer to clear cached secrets every minute
Start-PodeSecretCacheHousekeeper
# create a lockable so secrets are thread safe
New-PodeLockable -Name $vault.LockableName
# add vault config to context
$PodeContext.Server.Secrets.Vaults[$Name] = $vault
# unlock the vault?
if (!$NoUnlock -and $vault.Unlock.Enabled) {
Unlock-PodeSecretVault -Name $Name
}
}
<#
.SYNOPSIS
Unregister a Secret Vault.
.DESCRIPTION
Unregister a Secret Vault. If the Vault was via the SecretManagement module it will also be unregistered there as well.
.PARAMETER Name
The Name of the Secret Vault in Pode to unregister.
.EXAMPLE
Unregister-PodeSecretVault -Name 'VaultName'
#>
function Unregister-PodeSecretVault {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# has the vault been registered?
if (!(Test-PodeSecretVault -Name $Name)) {
return
}
# get vault
$vault = $PodeContext.Server.Secrets.Vaults[$Name]
# unlock depending on vault type, and set expiry
switch ($vault.Type) {
'custom' {
$vault | Unregister-PodeSecretCustomVault
}
'secretmanagement' {
$vault | Unregister-PodeSecretManagementVault
}
}
# unregister from Pode
$null = $PodeContext.Server.Secrets.Vaults.Remove($Name)
}
<#
.SYNOPSIS
Unlock the Secret Vault.
.DESCRIPTION
Unlock the Secret Vault.
.PARAMETER Name
The Name of the Secret Vault in Pode to be unlocked.
.EXAMPLE
Unlock-PodeSecretVault -Name 'VaultName'
#>
function Unlock-PodeSecretVault {
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# has the vault been registered?
if (!(Test-PodeSecretVault -Name $Name)) {
throw "No Secret Vault with the name '$($Name)' has been registered"
}
# get vault
$vault = $PodeContext.Server.Secrets.Vaults[$Name]
$expiry = $null
# is unlocking even enabled?
if (!$vault.Unlock.Enabled) {
return
}
# unlock depending on vault type, and set expiry
$expiry = Lock-PodeObject -Name $vault.LockableName -Return -ScriptBlock {
switch ($vault.Type) {
'custom' {
return ($vault | Unlock-PodeSecretCustomVault)
}
'secretmanagement' {
return ($vault | Unlock-PodeSecretManagementVault)
}
}
}
# if we have an expiry returned, set to UTC and configure unlock schedule
if ($null -ne $expiry) {
$expiry = ([datetime]$expiry).ToUniversalTime()
if ($expiry -le [datetime]::UtcNow) {
throw "Secret Vault unlock expiry date is in the past (UTC): $($expiry)"
}
$vault.Unlock.Expiry = $expiry
Start-PodeSecretVaultUnlocker
}
}
<#
.SYNOPSIS
Fetches and returns information of a Secret Vault.
.DESCRIPTION
Fetches and returns information of a Secret Vault.
.PARAMETER Name
The Name(s) of a Secret Vault to retrieve.
.EXAMPLE
$vault = Get-PodeSecretVault -Name 'VaultName'
.EXAMPLE
$vaults = Get-PodeSecretVault -Name 'VaultName1', 'VaultName2'
#>
function Get-PodeSecretVault {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]
$Name
)
$vaults = $PodeContext.Server.Secrets.Vaults.Values
# further filter by vault names
if (($null -ne $Name) -and ($Name.Length -gt 0)) {
$vaults = @(foreach ($_name in $Name) {
foreach ($vault in $vaults) {
if ($vault.Name -ine $_name) {
continue
}
$vault
}
})
}
# return
return $vaults
}
<#
.SYNOPSIS
Tests if a Secret Vault has been registered.
.DESCRIPTION
Tests if a Secret Vault has been registered.
.PARAMETER Name
The Name of the Secret Vault to test.
.EXAMPLE
if (Test-PodeSecretVault -Name 'VaultName') { ... }
#>
function Test-PodeSecretVault {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return (($null -ne $PodeContext.Server.Secrets.Vaults) -and $PodeContext.Server.Secrets.Vaults.ContainsKey($Name))
}
<#
.SYNOPSIS
Mount a Secret from a Secret Vault.
.DESCRIPTION
Mount a Secret from a Secret Vault, so it can be more easily referenced and support caching.
.PARAMETER Name
A unique friendly Name for the Secret.
.PARAMETER Vault
The friendly name of the Secret Vault this Secret can be found in.
.PARAMETER Property
An optional array of Properties to be returned if the Secret contains multiple properties.
.PARAMETER ExpandProperty
An optional Property to be expanded from the Secret and return if it contains multiple properties.
.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.
.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.
.PARAMETER CacheTtl
An optional number of minutes to Cache the Secret's value for. You can use this parameter to override the Secret Vault's value. (Default: -1)
If the value is -1 it uses the Secret Vault's CacheTtl. A value of 0 is to disable caching for this Secret. A value >0 overrides the Secret Vault.
.EXAMPLE
Mount-PodeSecret -Name 'SecretName' -Vault 'VaultName' -Key 'path/to/secret' -ExpandProperty 'foo'
.EXAMPLE
Mount-PodeSecret -Name 'SecretName' -Vault 'VaultName' -Key 'key_of_secret' -CacheTtl 5
#>
function Mount-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter()]
[string[]]
$Property,
[Parameter()]
[string]
$ExpandProperty,
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter()]
[object[]]
$ArgumentList,
# in minutes (-1 means use the vault default, 0 is off, anything higher than 0 is an override)
[Parameter()]
[int]
$CacheTtl = -1
)
# has the secret been mounted already?
if (Test-PodeSecret -Name $Name) {
throw "A Secret with the name '$($Name)' has already been mounted"
}
# does the vault exist?
if (!(Test-PodeSecretVault -Name $Vault)) {
throw "No Secret Vault with the name '$($Vault)' has been registered"
}
# check properties
if (!(Test-PodeIsEmpty $Property) -and !(Test-PodeIsEmpty $ExpandProperty)) {
throw 'You can only provide one of either Property or ExpandPropery, but not both'
}
# which cache value?
if ($CacheTtl -lt 0) {
$CacheTtl = [int]$PodeContext.Server.Secrets.Vaults[$Vault].Cache.Ttl
}
# mount secret reference
$props = $Property
if (![string]::IsNullOrWhiteSpace($ExpandProperty)) {
$props = $ExpandProperty
}
$PodeContext.Server.Secrets.Keys[$Name] = @{
Key = $Key
Properties = @{
Fields = $props
Expand = (![string]::IsNullOrWhiteSpace($ExpandProperty))
Enabled = (!(Test-PodeIsEmpty $props))
}
Vault = $Vault
Arguments = $ArgumentList
Cache = @{
Ttl = $CacheTtl
Enabled = ($CacheTtl -gt 0)
}
}
}
<#
.SYNOPSIS
Dismount a previously mounted Secret.
.DESCRIPTION
Dismount a previously mounted Secret.
.PARAMETER Name
The friendly Name of the Secret.
.PARAMETER Remove
If supplied, the Secret will also be removed from the Secret Vault as well.
.EXAMPLE
Dismount-PodeSecret -Name 'SecretName'
.EXAMPLE
Dismount-PodeSecret -Name 'SecretName' -Remove
#>
function Dismount-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[switch]
$Remove
)
# do nothing if the secret hasn't been mounted, unless Remove is specified
if (!(Test-PodeSecret -Name $Name)) {
if ($Remove) {
throw "No Secret with the name '$($Name)' has been mounted to be removed from a Secret Vault"
}
return
}
# if "remove" switch passed, remove the secret from the vault as well
if ($Remove) {
$secret = $PodeContext.Server.Secrets.Keys[$Name]
Remove-PodeSecret -Key $secret.Key -Vault $secret.Vault -ArgumentList $secret.Arguments
}
# remove reference
$null = $PodeContext.Server.Secrets.Keys.Remove($Name)
}
<#
.SYNOPSIS
Retrieve the value of a mounted Secret.
.DESCRIPTION
Retrieve the value of a mounted Secret from a Secret Vault. You can also use "$value = $secret:<NAME>" syntax in certain places.
.PARAMETER Name
The friendly Name of a Secret.
.EXAMPLE
$value = Get-PodeSecret -Name 'SecretName'
.EXAMPLE
$value = $secret:SecretName
#>
function Get-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# has the secret been mounted?
if (!(Test-PodeSecret -Name $Name)) {
throw "No Secret with the name '$($Name)' has been mounted"
}
# get the secret and vault
$secret = $PodeContext.Server.Secrets.Keys[$Name]
# is the value cached?
if ($secret.Cache.Enabled -and ($null -ne $secret.Cache.Expiry) -and ($secret.Cache.Expiry -gt [datetime]::UtcNow)) {
return $secret.Cache.Value
}
# fetch the secret depending on vault type
$vault = $PodeContext.Server.Secrets.Vaults[$secret.Vault]
$value = Lock-PodeObject -Name $vault.LockableName -Return -ScriptBlock {
switch ($vault.Type) {
'custom' {
return Get-PodeSecretCustomKey -Vault $secret.Vault -Key $secret.Key -ArgumentList $secret.Arguments
}
'secretmanagement' {
return Get-PodeSecretManagementKey -Vault $secret.Vault -Key $secret.Key
}
}
}
# filter the value by any properties
if ($secret.Properties.Enabled) {
if ($secret.Properties.Expand) {
$value = Select-Object -InputObject $value -ExpandProperty $secret.Properties.Fields
}
else {
$value = Select-Object -InputObject $value -Property $secret.Properties.Fields
}
}
# cache the value if needed
if ($secret.Cache.Enabled) {
$secret.Cache.Value = $value
$secret.Cache.Expiry = [datetime]::UtcNow.AddMinutes($secret.Cache.Ttl)
}
# return value
return $value
}
<#
.SYNOPSIS
Test if a Secret has been mounted.
.DESCRIPTION
Test if a Secret has been mounted.
.PARAMETER Name
The friendly Name of a Secret.
.EXAMPLE
if (Test-PodeSecret -Name 'SecretName') { ... }
#>
function Test-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return (($null -ne $PodeContext.Server.Secrets.Keys) -and $PodeContext.Server.Secrets.Keys.ContainsKey($Name))
}
<#
.SYNOPSIS
Update the value of a mounted Secret.
.DESCRIPTION
Update the value of a mounted Secret in a Secret Vault. You can also use "$secret:<NAME> = $value" syntax in certain places.
.PARAMETER Name
The friendly Name of a Secret.
.PARAMETER InputObject
The value to use when updating the Secret.
Only the following object types are supported: byte[], string, securestring, pscredential, hashtable.
.PARAMETER Metadata
An optional Metadata hashtable.
.EXAMPLE
Update-PodeSecret -Name 'SecretName' -InputObject @{ key = value }
.EXAMPLE
Update-PodeSecret -Name 'SecretName' -InputObject 'value'
.EXAMPLE
$secret:SecretName = 'value'
#>
function Update-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
#> byte[], string, securestring, pscredential, hashtable
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]
$InputObject,
[Parameter()]
[hashtable]
$Metadata
)
# has the secret been mounted?
if (!(Test-PodeSecret -Name $Name)) {
throw "No Secret with the name '$($Name)' has been mounted"
}
# make sure the value type is correct
$InputObject = Protect-PodeSecretValueType -Value $InputObject
# get the secret and vault
$secret = $PodeContext.Server.Secrets.Keys[$Name]
# reset the cache if enabled
if ($secret.Cache.Enabled) {
$secret.Cache.Value = $InputObject
$secret.Cache.Expiry = [datetime]::UtcNow.AddMinutes($secret.Cache.Ttl)
}
# if we're expanding a property, convert this to a hashtable
if ($secret.Properties.Enabled -and $secret.Properties.Expand) {
$InputObject = @{
"$($secret.Properties.Fields)" = $InputObject
}
}
# set the secret depending on vault type
$vault = $PodeContext.Server.Secrets.Vaults[$secret.Vault]
Lock-PodeObject -Name $vault.LockableName -ScriptBlock {
switch ($vault.Type) {
'custom' {
Set-PodeSecretCustomKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata -ArgumentList $secret.Arguments
}
'secretmanagement' {
Set-PodeSecretManagementKey -Vault $secret.Vault -Key $secret.Key -Value $InputObject -Metadata $Metadata
}
}
}
}
<#
.SYNOPSIS
Remove a Secret from a Secret Vault.
.DESCRIPTION
Remove a Secret from a Secret Vault. To remove a mounted Secret, you can pass the Remove switch to Dismount-PodeSecret.
.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.
.PARAMETER Vault
The friendly name of the Secret Vault this Secret can be found in.
.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.
.EXAMPLE
Remove-PodeSecret -Key 'path/to/secret' -Vault 'VaultName'
#>
function Remove-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter()]
[object[]]
$ArgumentList
)
# has the vault been registered?
if (!(Test-PodeSecretVault -Name $Vault)) {
throw "No Secret Vault with the name '$($Vault)' has been registered"
}
# remove the secret depending on vault type
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
Lock-PodeObject -Name $_vault.LockableName -ScriptBlock {
switch ($_vault.Type) {
'custom' {
Remove-PodeSecretCustomKey -Vault $Vault -Key $Key -ArgumentList $ArgumentList
}
'secretmanagement' {
Remove-PodeSecretManagementKey -Vault $Vault -Key $Key
}
}
}
}
<#
.SYNOPSIS
Read a Secret from a Secret Vault.
.DESCRIPTION
Read a Secret from a Secret Vault.
.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.
.PARAMETER Vault
The friendly name of the Secret Vault this Secret can be found in.
.PARAMETER Property
An optional array of Properties to be returned if the Secret contains multiple properties.
.PARAMETER ExpandProperty
An optional Property to be expanded from the Secret and return if it contains multiple properties.
.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.
.EXAMPLE
$value = Read-PodeSecret -Key 'path/to/secret' -Vault 'VaultName'
.EXAMPLE
$value = Read-PodeSecret -Key 'key_of_secret' -Vault 'VaultName' -Property prop1, prop2
#>
function Read-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter(Mandatory = $true)]
[string]
$Vault,
[Parameter()]
[string[]]
$Property,
[Parameter()]
[string]
$ExpandProperty,
[Parameter()]
[object[]]
$ArgumentList
)
# has the vault been registered?
if (!(Test-PodeSecretVault -Name $Vault)) {
throw "No Secret Vault with the name '$($Vault)' has been registered"
}
# fetch the secret depending on vault type
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
$value = Lock-PodeObject -Name $_vault.LockableName -Return -ScriptBlock {
switch ($_vault.Type) {
'custom' {
return Get-PodeSecretCustomKey -Vault $Vault -Key $Key -ArgumentList $ArgumentList
}
'secretmanagement' {
return Get-PodeSecretManagementKey -Vault $Vault -Key $Key
}
}
}
# filter the value by any properties
if (![string]::IsNullOrWhiteSpace($ExpandProperty)) {
$value = Select-Object -InputObject $value -ExpandProperty $ExpandProperty
}
elseif (![string]::IsNullOrEmpty($Property)) {
$value = Select-Object -InputObject $value -Property $Property
}
# return value
return $value
}
<#
.SYNOPSIS
Create/update a Secret in a Secret Vault.
.DESCRIPTION
Create/update a Secret in a Secret Vault.
.PARAMETER Key
The Key/Path of the Secret within the Secret Vault.
.PARAMETER Vault
The friendly name of the Secret Vault this Secret should be created in.
.PARAMETER InputObject
The value to use when updating the Secret.
Only the following object types are supported: byte[], string, securestring, pscredential, hashtable.
.PARAMETER Metadata
An optional Metadata hashtable.
.PARAMETER ArgumentList
An optional array of Arguments to be supplied to a custom Secret Vault's scriptblocks.
.EXAMPLE
Set-PodeSecret -Key 'path/to/secret' -Vault 'VaultName' -InputObject 'value'
.EXAMPLE
Set-PodeSecret -Key 'key_of_secret' -Vault 'VaultName' -InputObject @{ key = value }
#>
function Set-PodeSecret {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Key,
[Parameter(Mandatory = $true)]
[string]
$Vault,
#> byte[], string, securestring, pscredential, hashtable
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]
$InputObject,
[Parameter()]
[hashtable]
$Metadata,
[Parameter()]
[object[]]
$ArgumentList
)
# has the vault been registered?
if (!(Test-PodeSecretVault -Name $Vault)) {
throw "No Secret Vault with the name '$($Vault)' has been registered"
}
# make sure the value type is correct
$InputObject = Protect-PodeSecretValueType -Value $InputObject
# set the secret depending on vault type
$_vault = $PodeContext.Server.Secrets.Vaults[$Vault]
Lock-PodeObject -Name $_vault.LockableName -ScriptBlock {
switch ($_vault.Type) {
'custom' {
Set-PodeSecretCustomKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata -ArgumentList $ArgumentList
}
'secretmanagement' {
Set-PodeSecretManagementKey -Vault $Vault -Key $Key -Value $InputObject -Metadata $Metadata
}
}
}
}
<#
.SYNOPSIS
Sets inbuilt definitions for security headers.
.DESCRIPTION
Sets inbuilt definitions for security headers, in either Simple or Strict types.
.PARAMETER Type
The Type of security to use.
.PARAMETER UseHsts
If supplied, the Strict-Transport-Security header will be set.
.PARAMETER XssBlock
If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off)
.EXAMPLE
Set-PodeSecurity -Type Simple
.EXAMPLE
Set-PodeSecurity -Type Strict -UseHsts
#>
function Set-PodeSecurity {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Simple', 'Strict')]
[string]
$Type,
[switch]
$UseHsts,
[switch]
$XssBlock
)
# general headers
Set-PodeSecurityContentTypeOptions
Set-PodeSecurityPermissionsPolicy `
-SyncXhr 'none' `
-Fullscreen 'self' `
-Camera 'none' `
-Geolocation 'self' `
-PictureInPicture 'self' `
-Accelerometer 'none' `
-Microphone 'none' `
-Usb 'none' `
-Autoplay 'self' `
-Payment 'none' `
-Magnetometer 'self' `
-Gyroscope 'self' `
-DisplayCapture 'self'
Set-PodeSecurityCrossOrigin -Embed Require-Corp -Open Same-Origin -Resource Same-Origin
Set-PodeSecurityAccessControl -Origin '*' -Methods '*' -Headers '*' -Duration 7200
Set-PodeSecurityContentSecurityPolicy -Default 'self' -XssBlock:$XssBlock
# only add hsts if specifiec
if ($UseHsts) {
Set-PodeSecurityStrictTransportSecurity -Duration 31536000 -IncludeSubDomains
}
# type specific headers
switch ($Type.ToLowerInvariant()) {
'simple' {
Set-PodeSecurityFrameOptions -Type SameOrigin
Set-PodeSecurityReferrerPolicy -Type Strict-Origin
}
'strict' {
Set-PodeSecurityFrameOptions -Type Deny
Set-PodeSecurityReferrerPolicy -Type No-Referrer
}
}
# hide server info
Hide-PodeSecurityServer
}
<#
.SYNOPSIS
Removes definitions for all security headers.
.DESCRIPTION
Removes definitions for all security headers.
.EXAMPLE
Remove-PodeSecurity
#>
function Remove-PodeSecurity {
[CmdletBinding()]
param()
$PodeContext.Server.Security.Headers.Clear()
Show-PodeSecurityServer
}
<#
.SYNOPSIS
Add definition for specified security header.
.DESCRIPTION
Add definition for specified security header.
.PARAMETER Name
The Name of the security header.
.PARAMETER Value
The Value of the security header.
.PARAMETER Append
Append the value to the header instead of replacing it
.EXAMPLE
Add-PodeSecurityHeader -Name 'X-Header-Name' -Value 'SomeValue'
#>
function Add-PodeSecurityHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string]
$Value,
[Parameter()]
[switch]
$Append
)
if ([string]::IsNullOrWhiteSpace($Value)) {
return
}
if ($Append -and $PodeContext.Server.Security.Headers.ContainsKey($Name)) {
$Headers = @(($PodeContext.Server.Security.Headers[$Name].split(',')).trim())
if ($Headers -inotcontains $Value) {
$Headers += $Value
$PodeContext.Server.Security.Headers[$Name] = (($Headers.trim() | Select-Object -Unique) -join ', ')
}
else {
return
}
}
else {
$PodeContext.Server.Security.Headers[$Name] = $Value
}
}
<#
.SYNOPSIS
Removes definition for specified security header.
.DESCRIPTION
Removes definition for specified security header.
.PARAMETER Name
The Name of the security header.
.EXAMPLE
Remove-PodeSecurityHeader -Name 'X-Header-Name'
#>
function Remove-PodeSecurityHeader {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$PodeContext.Server.Security.Headers.Remove($Name)
}
<#
.SYNOPSIS
Hide the Server HTTP Header from Responses
.DESCRIPTION
Hide the Server HTTP Header from Responses
.EXAMPLE
Hide-PodeSecurityServer
#>
function Hide-PodeSecurityServer {
[CmdletBinding()]
param()
$PodeContext.Server.Security.ServerDetails = $false
}
<#
.SYNOPSIS
Show the Server HTTP Header on Responses
.DESCRIPTION
Show the Server HTTP Header on Responses
.EXAMPLE
Show-PodeSecurityServer
#>
function Show-PodeSecurityServer {
[CmdletBinding()]
param()
$PodeContext.Server.Security.ServerDetails = $true
}
<#
.SYNOPSIS
Set a value for the X-Frame-Options header.
.DESCRIPTION
Set a value for the X-Frame-Options header.
.PARAMETER Type
The Type to use.
.EXAMPLE
Set-PodeSecurityFrameOptions -Type SameOrigin
#>
function Set-PodeSecurityFrameOptions {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Deny', 'SameOrigin')]
[string]
$Type
)
Add-PodeSecurityHeader -Name 'X-Frame-Options' -Value $Type.ToUpperInvariant()
}
<#
.SYNOPSIS
Removes definition for the X-Frame-Options header.
.DESCRIPTION
Removes definition for the X-Frame-Options header.
.EXAMPLE
Remove-PodeSecurityFrameOptions
#>
function Remove-PodeSecurityFrameOptions {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'X-Frame-Options'
}
<#
.SYNOPSIS
Set the value to use for the Content-Security-Policy and X-XSS-Protection headers.
.DESCRIPTION
Set the value to use for the Content-Security-Policy and X-XSS-Protection headers.
.PARAMETER Default
The values to use for the Default portion of the header.
.PARAMETER Child
The values to use for the Child portion of the header.
.PARAMETER Connect
The values to use for the Connect portion of the header.
.PARAMETER Font
The values to use for the Font portion of the header.
.PARAMETER Frame
The values to use for the Frame portion of the header.
.PARAMETER Image
The values to use for the Image portion of the header.
.PARAMETER Manifest
The values to use for the Manifest portion of the header.
.PARAMETER Media
The values to use for the Media portion of the header.
.PARAMETER Object
The values to use for the Object portion of the header.
.PARAMETER Scripts
The values to use for the Scripts portion of the header.
.PARAMETER Style
The values to use for the Style portion of the header.
.PARAMETER BaseUri
The values to use for the BaseUri portion of the header.
.PARAMETER FormAction
The values to use for the FormAction portion of the header.
.PARAMETER FrameAncestor
The values to use for the FrameAncestor portion of the header.
.PARAMETER Sandbox
The value to use for the Sandbox portion of the header.
.PARAMETER UpgradeInsecureRequests
If supplied, the header will have the upgrade-insecure-requests value added.
.PARAMETER XssBlock
If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off)
.EXAMPLE
Set-PodeSecurityContentSecurityPolicy -Default 'self'
#>
function Set-PodeSecurityContentSecurityPolicy {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Default,
[Parameter()]
[string[]]
$Child,
[Parameter()]
[string[]]
$Connect,
[Parameter()]
[string[]]
$Font,
[Parameter()]
[string[]]
$Frame,
[Parameter()]
[string[]]
$Image,
[Parameter()]
[string[]]
$Manifest,
[Parameter()]
[string[]]
$Media,
[Parameter()]
[string[]]
$Object,
[Parameter()]
[string[]]
$Scripts,
[Parameter()]
[string[]]
$Style,
[Parameter()]
[string[]]
$BaseUri,
[Parameter()]
[string[]]
$FormAction,
[Parameter()]
[string[]]
$FrameAncestor,
[Parameter()]
[ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock',
'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts',
'Allow-Storage-Access-By-User-Activation', 'Allow-Top-Navigation', 'Allow-Top-Navigation-By-User-Activation', 'None')]
[string]
$Sandbox = 'None',
[switch]
$UpgradeInsecureRequests,
[switch]
$XssBlock
)
# build the header's value
$values = @(
Protect-PodeContentSecurityKeyword -Name 'default-src' -Value $Default
Protect-PodeContentSecurityKeyword -Name 'child-src' -Value $Child
Protect-PodeContentSecurityKeyword -Name 'connect-src' -Value $Connect
Protect-PodeContentSecurityKeyword -Name 'font-src' -Value $Font
Protect-PodeContentSecurityKeyword -Name 'frame-src' -Value $Frame
Protect-PodeContentSecurityKeyword -Name 'img-src' -Value $Image
Protect-PodeContentSecurityKeyword -Name 'manifest-src' -Value $Manifest
Protect-PodeContentSecurityKeyword -Name 'media-src' -Value $Media
Protect-PodeContentSecurityKeyword -Name 'object-src' -Value $Object
Protect-PodeContentSecurityKeyword -Name 'script-src' -Value $Scripts
Protect-PodeContentSecurityKeyword -Name 'style-src' -Value $Style
Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $BaseUri
Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $FormAction
Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $FrameAncestor
)
if ($Sandbox -ine 'None') {
$values += "sandbox $($Sandbox.ToLowerInvariant())".Trim()
}
if ($UpgradeInsecureRequests) {
$values += 'upgrade-insecure-requests'
}
$values = ($values -ne $null)
$value = ($values -join '; ')
# add the header
Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value
# this is done to explicitly disable XSS auditors in modern browsers
# as having it enabled has now been found to cause more vulnerabilities
if ($XssBlock) {
Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '1; mode=block'
}
else {
Add-PodeSecurityHeader -Name 'X-XSS-Protection' -Value '0'
}
}
<#
.SYNOPSIS
Adds additional values to already defined values for the Content-Security-Policy header.
.DESCRIPTION
Adds additional values to already defined values for the Content-Security-Policy header, instead of overriding them.
.PARAMETER Default
The values to add for the Default portion of the header.
.PARAMETER Child
The values to add for the Child portion of the header.
.PARAMETER Connect
The values to add for the Connect portion of the header.
.PARAMETER Font
The values to add for the Font portion of the header.
.PARAMETER Frame
The values to add for the Frame portion of the header.
.PARAMETER Image
The values to add for the Image portion of the header.
.PARAMETER Manifest
The values to add for the Manifest portion of the header.
.PARAMETER Media
The values to add for the Media portion of the header.
.PARAMETER Object
The values to add for the Object portion of the header.
.PARAMETER Scripts
The values to add for the Scripts portion of the header.
.PARAMETER Style
The values to add for the Style portion of the header.
.PARAMETER BaseUri
The values to add for the BaseUri portion of the header.
.PARAMETER FormAction
The values to add for the FormAction portion of the header.
.PARAMETER FrameAncestor
The values to add for the FrameAncestor portion of the header.
.PARAMETER Sandbox
The value to use for the Sandbox portion of the header.
.PARAMETER UpgradeInsecureRequests
If supplied, the header will have the upgrade-insecure-requests value added.
.EXAMPLE
Add-PodeSecurityContentSecurityPolicy -Default '*.twitter.com' -Image 'data'
#>
function Add-PodeSecurityContentSecurityPolicy {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Default,
[Parameter()]
[string[]]
$Child,
[Parameter()]
[string[]]
$Connect,
[Parameter()]
[string[]]
$Font,
[Parameter()]
[string[]]
$Frame,
[Parameter()]
[string[]]
$Image,
[Parameter()]
[string[]]
$Manifest,
[Parameter()]
[string[]]
$Media,
[Parameter()]
[string[]]
$Object,
[Parameter()]
[string[]]
$Scripts,
[Parameter()]
[string[]]
$Style,
[Parameter()]
[string[]]
$BaseUri,
[Parameter()]
[string[]]
$FormAction,
[Parameter()]
[string[]]
$FrameAncestor,
[Parameter()]
[ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock',
'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts',
'Allow-Storage-Access-By-User-Activation', 'Allow-Top-Navigation', 'Allow-Top-Navigation-By-User-Activation', 'None')]
[string]
$Sandbox = 'None',
[switch]
$UpgradeInsecureRequests
)
# build the header's value
$values = @(
Protect-PodeContentSecurityKeyword -Name 'default-src' -Value $Default -Append
Protect-PodeContentSecurityKeyword -Name 'child-src' -Value $Child -Append
Protect-PodeContentSecurityKeyword -Name 'connect-src' -Value $Connect -Append
Protect-PodeContentSecurityKeyword -Name 'font-src' -Value $Font -Append
Protect-PodeContentSecurityKeyword -Name 'frame-src' -Value $Frame -Append
Protect-PodeContentSecurityKeyword -Name 'img-src' -Value $Image -Append
Protect-PodeContentSecurityKeyword -Name 'manifest-src' -Value $Manifest -Append
Protect-PodeContentSecurityKeyword -Name 'media-src' -Value $Media -Append
Protect-PodeContentSecurityKeyword -Name 'object-src' -Value $Object -Append
Protect-PodeContentSecurityKeyword -Name 'script-src' -Value $Scripts -Append
Protect-PodeContentSecurityKeyword -Name 'style-src' -Value $Style -Append
Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $BaseUri -Append
Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $FormAction -Append
Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $FrameAncestor -Append
)
if ($Sandbox -ine 'None') {
$values += "sandbox $($Sandbox.ToLowerInvariant())".Trim()
}
if ($UpgradeInsecureRequests) {
$values += 'upgrade-insecure-requests'
}
$values = ($values -ne $null)
$value = ($values -join '; ')
# add the header
Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value
}
<#
.SYNOPSIS
Removes definition for the Content-Security-Policy and X-XSS-Protection headers.
.DESCRIPTION
Removes definition for the Content-Security-Policy and X-XSS-Protection headers.
.EXAMPLE
Remove-PodeSecurityContentSecurityPolicy
#>
function Remove-PodeSecurityContentSecurityPolicy {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'Content-Security-Policy'
Remove-PodeSecurityHeader -Name 'X-XSS-Protection'
}
<#
.SYNOPSIS
Set the value to use for the Permissions-Policy header.
.DESCRIPTION
Set the value to use for the Permissions-Policy header.
.PARAMETER Accelerometer
The values to use for the Accelerometer portion of the header.
.PARAMETER AmbientLightSensor
The values to use for the AmbientLightSensor portion of the header.
.PARAMETER Autoplay
The values to use for the Autoplay portion of the header.
.PARAMETER Battery
The values to use for the Battery portion of the header.
.PARAMETER Camera
The values to use for the Camera portion of the header.
.PARAMETER DisplayCapture
The values to use for the DisplayCapture portion of the header.
.PARAMETER DocumentDomain
The values to use for the DocumentDomain portion of the header.
.PARAMETER EncryptedMedia
The values to use for the EncryptedMedia portion of the header.
.PARAMETER Fullscreen
The values to use for the Fullscreen portion of the header.
.PARAMETER Gamepad
The values to use for the Gamepad portion of the header.
.PARAMETER Geolocation
The values to use for the Geolocation portion of the header.
.PARAMETER Gyroscope
The values to use for the Gyroscope portion of the header.
.PARAMETER InterestCohort
The values to use for the InterestCohort portal of the header.
.PARAMETER LayoutAnimations
The values to use for the LayoutAnimations portion of the header.
.PARAMETER LegacyImageFormats
The values to use for the LegacyImageFormats portion of the header.
.PARAMETER Magnetometer
The values to use for the Magnetometer portion of the header.
.PARAMETER Microphone
The values to use for the Microphone portion of the header.
.PARAMETER Midi
The values to use for the Midi portion of the header.
.PARAMETER OversizedImages
The values to use for the OversizedImages portion of the header.
.PARAMETER Payment
The values to use for the Payment portion of the header.
.PARAMETER PictureInPicture
The values to use for the PictureInPicture portion of the header.
.PARAMETER PublicKeyCredentials
The values to use for the PublicKeyCredentials portion of the header.
.PARAMETER Speakers
The values to use for the Speakers portion of the header.
.PARAMETER SyncXhr
The values to use for the SyncXhr portion of the header.
.PARAMETER UnoptimisedImages
The values to use for the UnoptimisedImages portion of the header.
.PARAMETER UnsizedMedia
The values to use for the UnsizedMedia portion of the header.
.PARAMETER Usb
The values to use for the Usb portion of the header.
.PARAMETER ScreenWakeLake
The values to use for the ScreenWakeLake portion of the header.
.PARAMETER WebShare
The values to use for the WebShare portion of the header.
.PARAMETER XrSpatialTracking
The values to use for the XrSpatialTracking portion of the header.
.EXAMPLE
Set-PodeSecurityPermissionsPolicy -LayoutAnimations 'none' -UnoptimisedImages 'none' -OversizedImages 'none' -SyncXhr 'none' -UnsizedMedia 'none'
#>
function Set-PodeSecurityPermissionsPolicy {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Accelerometer,
[Parameter()]
[string[]]
$AmbientLightSensor,
[Parameter()]
[string[]]
$Autoplay,
[Parameter()]
[string[]]
$Battery,
[Parameter()]
[string[]]
$Camera,
[Parameter()]
[string[]]
$DisplayCapture,
[Parameter()]
[string[]]
$DocumentDomain,
[Parameter()]
[string[]]
$EncryptedMedia,
[Parameter()]
[string[]]
$Fullscreen,
[Parameter()]
[string[]]
$Gamepad,
[Parameter()]
[string[]]
$Geolocation,
[Parameter()]
[string[]]
$Gyroscope,
[Parameter()]
[string[]]
$InterestCohort,
[Parameter()]
[string[]]
$LayoutAnimations,
[Parameter()]
[string[]]
$LegacyImageFormats,
[Parameter()]
[string[]]
$Magnetometer,
[Parameter()]
[string[]]
$Microphone,
[Parameter()]
[string[]]
$Midi,
[Parameter()]
[string[]]
$OversizedImages,
[Parameter()]
[string[]]
$Payment,
[Parameter()]
[string[]]
$PictureInPicture,
[Parameter()]
[string[]]
$PublicKeyCredentials,
[Parameter()]
[string[]]
$Speakers,
[Parameter()]
[string[]]
$SyncXhr,
[Parameter()]
[string[]]
$UnoptimisedImages,
[Parameter()]
[string[]]
$UnsizedMedia,
[Parameter()]
[string[]]
$Usb,
[Parameter()]
[string[]]
$ScreenWakeLake,
[Parameter()]
[string[]]
$WebShare,
[Parameter()]
[string[]]
$XrSpatialTracking
)
# build the header's value
$values = @(
Protect-PodePermissionsPolicyKeyword -Name 'accelerometer' -Value $Accelerometer
Protect-PodePermissionsPolicyKeyword -Name 'ambient-light-sensor' -Value $AmbientLightSensor
Protect-PodePermissionsPolicyKeyword -Name 'autoplay' -Value $Autoplay
Protect-PodePermissionsPolicyKeyword -Name 'battery' -Value $Battery
Protect-PodePermissionsPolicyKeyword -Name 'camera' -Value $Camera
Protect-PodePermissionsPolicyKeyword -Name 'display-capture' -Value $DisplayCapture
Protect-PodePermissionsPolicyKeyword -Name 'document-domain' -Value $DocumentDomain
Protect-PodePermissionsPolicyKeyword -Name 'encrypted-media' -Value $EncryptedMedia
Protect-PodePermissionsPolicyKeyword -Name 'fullscreen' -Value $Fullscreen
Protect-PodePermissionsPolicyKeyword -Name 'gamepad' -Value $Gamepad
Protect-PodePermissionsPolicyKeyword -Name 'geolocation' -Value $Geolocation
Protect-PodePermissionsPolicyKeyword -Name 'gyroscope' -Value $Gyroscope
Protect-PodePermissionsPolicyKeyword -Name 'interest-cohort' -Value $InterestCohort
Protect-PodePermissionsPolicyKeyword -Name 'layout-animations' -Value $LayoutAnimations
Protect-PodePermissionsPolicyKeyword -Name 'legacy-image-formats' -Value $LegacyImageFormats
Protect-PodePermissionsPolicyKeyword -Name 'magnetometer' -Value $Magnetometer
Protect-PodePermissionsPolicyKeyword -Name 'microphone' -Value $Microphone
Protect-PodePermissionsPolicyKeyword -Name 'midi' -Value $Midi
Protect-PodePermissionsPolicyKeyword -Name 'oversized-images' -Value $OversizedImages
Protect-PodePermissionsPolicyKeyword -Name 'payment' -Value $Payment
Protect-PodePermissionsPolicyKeyword -Name 'picture-in-picture' -Value $PictureInPicture
Protect-PodePermissionsPolicyKeyword -Name 'publickey-credentials-get' -Value $PublicKeyCredentials
Protect-PodePermissionsPolicyKeyword -Name 'speaker-selection' -Value $Speakers
Protect-PodePermissionsPolicyKeyword -Name 'sync-xhr' -Value $SyncXhr
Protect-PodePermissionsPolicyKeyword -Name 'unoptimized-images' -Value $UnoptimisedImages
Protect-PodePermissionsPolicyKeyword -Name 'unsized-media' -Value $UnsizedMedia
Protect-PodePermissionsPolicyKeyword -Name 'usb' -Value $Usb
Protect-PodePermissionsPolicyKeyword -Name 'screen-wake-lock' -Value $ScreenWakeLake
Protect-PodePermissionsPolicyKeyword -Name 'web-share' -Value $WebShare
Protect-PodePermissionsPolicyKeyword -Name 'xr-spatial-tracking' -Value $XrSpatialTracking
)
$values = ($values -ne $null)
$value = ($values -join ', ')
# add the header
Add-PodeSecurityHeader -Name 'Permissions-Policy' -Value $value
}
<#
.SYNOPSIS
Adds additional values to already defined values for the Permissions-Policy header.
.DESCRIPTION
Adds additional values to already defined values for the Permissions-Policy header, instead of overriding them.
.PARAMETER Accelerometer
The values to add for the Accelerometer portion of the header.
.PARAMETER AmbientLightSensor
The values to add for the AmbientLightSensor portion of the header.
.PARAMETER Autoplay
The values to add for the Autoplay portion of the header.
.PARAMETER Battery
The values to add for the Battery portion of the header.
.PARAMETER Camera
The values to add for the Camera portion of the header.
.PARAMETER DisplayCapture
The values to add for the DisplayCapture portion of the header.
.PARAMETER DocumentDomain
The values to add for the DocumentDomain portion of the header.
.PARAMETER EncryptedMedia
The values to add for the EncryptedMedia portion of the header.
.PARAMETER Fullscreen
The values to add for the Fullscreen portion of the header.
.PARAMETER Gamepad
The values to add for the Gamepad portion of the header.
.PARAMETER Geolocation
The values to add for the Geolocation portion of the header.
.PARAMETER Gyroscope
The values to add for the Gyroscope portion of the header.
.PARAMETER InterestCohort
The values to use for the InterestCohort portal of the header.
.PARAMETER LayoutAnimations
The values to add for the LayoutAnimations portion of the header.
.PARAMETER LegacyImageFormats
The values to add for the LegacyImageFormats portion of the header.
.PARAMETER Magnetometer
The values to add for the Magnetometer portion of the header.
.PARAMETER Microphone
The values to add for the Microphone portion of the header.
.PARAMETER Midi
The values to add for the Midi portion of the header.
.PARAMETER OversizedImages
The values to add for the OversizedImages portion of the header.
.PARAMETER Payment
The values to add for the Payment portion of the header.
.PARAMETER PictureInPicture
The values to add for the PictureInPicture portion of the header.
.PARAMETER PublicKeyCredentials
The values to add for the PublicKeyCredentials portion of the header.
.PARAMETER Speakers
The values to add for the Speakers portion of the header.
.PARAMETER SyncXhr
The values to add for the SyncXhr portion of the header.
.PARAMETER UnoptimisedImages
The values to add for the UnoptimisedImages portion of the header.
.PARAMETER UnsizedMedia
The values to add for the UnsizedMedia portion of the header.
.PARAMETER Usb
The values to add for the Usb portion of the header.
.PARAMETER ScreenWakeLake
The values to add for the ScreenWakeLake portion of the header.
.PARAMETER WebShare
The values to add for the WebShare portion of the header.
.PARAMETER XrSpatialTracking
The values to add for the XrSpatialTracking portion of the header.
.EXAMPLE
Add-PodeSecurityPermissionsPolicy -AmbientLightSensor 'none'
#>
function Add-PodeSecurityPermissionsPolicy {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Accelerometer,
[Parameter()]
[string[]]
$AmbientLightSensor,
[Parameter()]
[string[]]
$Autoplay,
[Parameter()]
[string[]]
$Battery,
[Parameter()]
[string[]]
$Camera,
[Parameter()]
[string[]]
$DisplayCapture,
[Parameter()]
[string[]]
$DocumentDomain,
[Parameter()]
[string[]]
$EncryptedMedia,
[Parameter()]
[string[]]
$Fullscreen,
[Parameter()]
[string[]]
$Gamepad,
[Parameter()]
[string[]]
$Geolocation,
[Parameter()]
[string[]]
$Gyroscope,
[Parameter()]
[string[]]
$InterestCohort,
[Parameter()]
[string[]]
$LayoutAnimations,
[Parameter()]
[string[]]
$LegacyImageFormats,
[Parameter()]
[string[]]
$Magnetometer,
[Parameter()]
[string[]]
$Microphone,
[Parameter()]
[string[]]
$Midi,
[Parameter()]
[string[]]
$OversizedImages,
[Parameter()]
[string[]]
$Payment,
[Parameter()]
[string[]]
$PictureInPicture,
[Parameter()]
[string[]]
$PublicKeyCredentials,
[Parameter()]
[string[]]
$Speakers,
[Parameter()]
[string[]]
$SyncXhr,
[Parameter()]
[string[]]
$UnoptimisedImages,
[Parameter()]
[string[]]
$UnsizedMedia,
[Parameter()]
[string[]]
$Usb,
[Parameter()]
[string[]]
$ScreenWakeLake,
[Parameter()]
[string[]]
$WebShare,
[Parameter()]
[string[]]
$XrSpatialTracking
)
# build the header's value
$values = @(
Protect-PodePermissionsPolicyKeyword -Name 'accelerometer' -Value $Accelerometer -Append
Protect-PodePermissionsPolicyKeyword -Name 'ambient-light-sensor' -Value $AmbientLightSensor -Append
Protect-PodePermissionsPolicyKeyword -Name 'autoplay' -Value $Autoplay -Append
Protect-PodePermissionsPolicyKeyword -Name 'battery' -Value $Battery -Append
Protect-PodePermissionsPolicyKeyword -Name 'camera' -Value $Camera -Append
Protect-PodePermissionsPolicyKeyword -Name 'display-capture' -Value $DisplayCapture -Append
Protect-PodePermissionsPolicyKeyword -Name 'document-domain' -Value $DocumentDomain -Append
Protect-PodePermissionsPolicyKeyword -Name 'encrypted-media' -Value $EncryptedMedia -Append
Protect-PodePermissionsPolicyKeyword -Name 'fullscreen' -Value $Fullscreen -Append
Protect-PodePermissionsPolicyKeyword -Name 'gamepad' -Value $Gamepad -Append
Protect-PodePermissionsPolicyKeyword -Name 'geolocation' -Value $Geolocation -Append
Protect-PodePermissionsPolicyKeyword -Name 'gyroscope' -Value $Gyroscope -Append
Protect-PodePermissionsPolicyKeyword -Name 'interest-cohort' -Value $InterestCohort -Append
Protect-PodePermissionsPolicyKeyword -Name 'layout-animations' -Value $LayoutAnimations -Append
Protect-PodePermissionsPolicyKeyword -Name 'legacy-image-formats' -Value $LegacyImageFormats -Append
Protect-PodePermissionsPolicyKeyword -Name 'magnetometer' -Value $Magnetometer -Append
Protect-PodePermissionsPolicyKeyword -Name 'microphone' -Value $Microphone -Append
Protect-PodePermissionsPolicyKeyword -Name 'midi' -Value $Midi -Append
Protect-PodePermissionsPolicyKeyword -Name 'oversized-images' -Value $OversizedImages -Append
Protect-PodePermissionsPolicyKeyword -Name 'payment' -Value $Payment -Append
Protect-PodePermissionsPolicyKeyword -Name 'picture-in-picture' -Value $PictureInPicture -Append
Protect-PodePermissionsPolicyKeyword -Name 'publickey-credentials-get' -Value $PublicKeyCredentials -Append
Protect-PodePermissionsPolicyKeyword -Name 'speaker-selection' -Value $Speakers -Append
Protect-PodePermissionsPolicyKeyword -Name 'sync-xhr' -Value $SyncXhr -Append
Protect-PodePermissionsPolicyKeyword -Name 'unoptimized-images' -Value $UnoptimisedImages -Append
Protect-PodePermissionsPolicyKeyword -Name 'unsized-media' -Value $UnsizedMedia -Append
Protect-PodePermissionsPolicyKeyword -Name 'usb' -Value $Usb -Append
Protect-PodePermissionsPolicyKeyword -Name 'screen-wake-lock' -Value $ScreenWakeLake -Append
Protect-PodePermissionsPolicyKeyword -Name 'web-share' -Value $WebShare -Append
Protect-PodePermissionsPolicyKeyword -Name 'xr-spatial-tracking' -Value $XrSpatialTracking -Append
)
$values = ($values -ne $null)
$value = ($values -join ', ')
# add the header
Add-PodeSecurityHeader -Name 'Permissions-Policy' -Value $value
}
<#
.SYNOPSIS
Removes definition for the Permissions-Policy header.
.DESCRIPTION
Removes definitions for the Permissions-Policy header.
.EXAMPLE
Remove-PodeSecurityPermissionsPolicy
#>
function Remove-PodeSecurityPermissionsPolicy {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'Permissions-Policy'
}
<#
.SYNOPSIS
Set a value for the Referrer-Policy header.
.DESCRIPTION
Set a value for the Referrer-Policy header.
.PARAMETER Type
The Type to use.
.EXAMPLE
Set-PodeSecurityReferrerPolicy -Type No-Referrer
#>
function Set-PodeSecurityReferrerPolicy {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('No-Referrer', 'No-Referrer-When-Downgrade', 'Same-Origin', 'Origin', 'Strict-Origin',
'Origin-When-Cross-Origin', 'Strict-Origin-When-Cross-Origin', 'Unsafe-Url')]
[string]
$Type
)
Add-PodeSecurityHeader -Name 'Referrer-Policy' -Value $Type.ToLowerInvariant()
}
<#
.SYNOPSIS
Removes definition for the Referrer-Policy header.
.DESCRIPTION
Removes definitions for the Referrer-Policy header.
.EXAMPLE
Remove-PodeSecurityReferrerPolicy
#>
function Remove-PodeSecurityReferrerPolicy {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'Referrer-Policy'
}
<#
.SYNOPSIS
Set a value for the X-Content-Type-Options header.
.DESCRIPTION
Set a value for the X-Content-Type-Options header to "nosniff".
.EXAMPLE
Set-PodeSecurityContentTypeOptions
#>
function Set-PodeSecurityContentTypeOptions {
[CmdletBinding()]
param()
Add-PodeSecurityHeader -Name 'X-Content-Type-Options' -Value 'nosniff'
}
<#
.SYNOPSIS
Removes definition for the X-Content-Type-Options header.
.DESCRIPTION
Removes definitions for the X-Content-Type-Options header.
.EXAMPLE
Remove-PodeSecurityContentTypeOptions
#>
function Remove-PodeSecurityContentTypeOptions {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'X-Content-Type-Options'
}
<#
.SYNOPSIS
Set a value for the Strict-Transport-Security header.
.DESCRIPTION
Set a value for the Strict-Transport-Security header.
.PARAMETER Duration
The Duration the browser to respect the header in seconds. (Default: 1 year)
.PARAMETER IncludeSubDomains
If supplied, the header will have includeSubDomains.
.EXAMPLE
Set-PodeSecurityStrictTransportSecurity -Duration 86400 -IncludeSubDomains
#>
function Set-PodeSecurityStrictTransportSecurity {
[CmdletBinding()]
param(
[Parameter()]
[int]
$Duration = 31536000,
[switch]
$IncludeSubDomains
)
if ($Duration -le 0) {
throw "Invalid Strict-Transport-Security duration supplied: $($Duration). Should be greater than 0"
}
$value = "max-age=$($Duration)"
if ($IncludeSubDomains) {
$value += '; includeSubDomains'
}
Add-PodeSecurityHeader -Name 'Strict-Transport-Security' -Value $value
}
<#
.SYNOPSIS
Removes definition for the Strict-Transport-Security header.
.DESCRIPTION
Removes definitions for the Strict-Transport-Security header.
.EXAMPLE
Remove-PodeSecurityStrictTransportSecurity
#>
function Remove-PodeSecurityStrictTransportSecurity {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'Strict-Transport-Security'
}
<#
.SYNOPSIS
Removes definitions for the Cross-Origin headers.
.DESCRIPTION
Removes definitions for the Cross-Origin headers: Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy
.PARAMETER Embed
Specifies a value for Cross-Origin-Embedder-Policy.
.PARAMETER Open
Specifies a value for Cross-Origin-Opener-Policy.
.PARAMETER Resource
Specifies a value for Cross-Origin-Resource-Policy.
.EXAMPLE
Set-PodeSecurityCrossOrigin -Embed Require-Corp -Open Same-Origin -Resource Same-Origin
#>
function Set-PodeSecurityCrossOrigin {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('', 'Unsafe-None', 'Require-Corp')]
[string]
$Embed = '',
[Parameter()]
[ValidateSet('', 'Unsafe-None', 'Same-Origin-Allow-Popups', 'Same-Origin')]
[string]
$Open = '',
[Parameter()]
[ValidateSet('', 'Same-Site', 'Same-Origin', 'Cross-Origin')]
[string]
$Resource = ''
)
Add-PodeSecurityHeader -Name 'Cross-Origin-Embedder-Policy' -Value $Embed.ToLowerInvariant()
Add-PodeSecurityHeader -Name 'Cross-Origin-Opener-Policy' -Value $Open.ToLowerInvariant()
Add-PodeSecurityHeader -Name 'Cross-Origin-Resource-Policy' -Value $Resource.ToLowerInvariant()
}
<#
.SYNOPSIS
Removes definitions for the Cross-Origin headers.
.DESCRIPTION
Removes definitions for the Cross-Origin headers: Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy
.EXAMPLE
Remove-PodeSecurityCrossOrigin
#>
function Remove-PodeSecurityCrossOrigin {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'Cross-Origin-Embedder-Policy'
Remove-PodeSecurityHeader -Name 'Cross-Origin-Opener-Policy'
Remove-PodeSecurityHeader -Name 'Cross-Origin-Resource-Policy'
}
<#
.SYNOPSIS
Set definitions for Access-Control headers.
.DESCRIPTION
Removes definitions for the Access-Control headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age, Access-Control-Allow-Credentials
.PARAMETER Origin
Specifies a value for Access-Control-Allow-Origin.
.PARAMETER Methods
Specifies a value for Access-Control-Allow-Methods.
.PARAMETER Headers
Specifies a value for Access-Control-Allow-Headers.
.PARAMETER Duration
Specifies a value for Access-Control-Max-Age in seconds. (Default: 7200)
Use a value of one for debugging any CORS related issues
.PARAMETER Credentials
Specifies a value for Access-Control-Allow-Credentials
.PARAMETER WithOptions
If supplied, a global Options Route will be created.
.PARAMETER AuthorizationHeader
Add 'Authorization' to the headers list
.PARAMETER AutoHeaders
Automatically populate the list of allowed Headers based on the OpenApi definition.
This parameter can works in conjuntion with CrossDomainXhrRequests,AuthorizationHeader and Headers (Headers cannot be '*').
By default add 'content-type' to the headers
.PARAMETER AutoMethods
Automatically populate the list of allowed Methods based on the defined Routes.
This parameter can works in conjuntion with the parameter Methods, if Methods is not including '*'
.PARAMETER CrossDomainXhrRequests
Add 'x-requested-with' to the list of allowed headers
More info available here:
https://fetch.spec.whatwg.org/
https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-7.0#credentials-in-cross-origin-requests
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
.EXAMPLE
Set-PodeSecurityAccessControl -Origin '*' -Methods '*' -Headers '*' -Duration 7200
#>
function Set-PodeSecurityAccessControl {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Origin,
[Parameter()]
[ValidateSet('', 'Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
[string[]]
$Methods = '',
[Parameter()]
[string[]]
$Headers,
[Parameter()]
[int]
$Duration = 7200,
[switch]
$Credentials,
[switch]
$WithOptions,
[switch]
$AuthorizationHeader,
[switch]
$AutoHeaders,
[switch]
$AutoMethods,
[switch]
$CrossDomainXhrRequests
)
# origin
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Origin' -Value $Origin
# methods
if (![string]::IsNullOrWhiteSpace($Methods)) {
if ($Methods -icontains '*') {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value '*'
}
else {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value ($Methods -join ', ')
}
}
# headers
if (![string]::IsNullOrWhiteSpace($Headers) -or $AuthorizationHeader -or $CrossDomainXhrRequests) {
if ($Headers -icontains '*') {
if ($Credentials) {
throw 'The * wildcard for Headers, when Credentials is passed, will be taken as a literal string and not a wildcard'
}
$Headers = @('*')
}
if ($AuthorizationHeader) {
if ([string]::IsNullOrWhiteSpace($Headers)) {
$Headers = @()
}
$Headers += 'Authorization'
}
if ($CrossDomainXhrRequests) {
if ([string]::IsNullOrWhiteSpace($Headers)) {
$Headers = @()
}
$Headers += 'x-requested-with'
}
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value (($Headers | Select-Object -Unique) -join ', ')
}
if ($AutoHeaders) {
if ($Headers -icontains '*') {
throw 'The * wildcard for Headers, is not comptatibile with the AutoHeaders switch'
}
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Headers' -Value 'content-type' -Append
$PodeContext.Server.Security.autoHeaders = $true
}
if ($AutoMethods) {
if ($Methods -icontains '*') {
throw 'The * wildcard for Methods, is not comptatibile with the AutoMethods switch'
}
if ($WithOptions) {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Methods' -Value 'Options' -Append
}
$PodeContext.Server.Security.autoMethods = $true
}
# duration
if ($Duration -le 0) {
throw "Invalid Access-Control-Max-Age duration supplied: $($Duration). Should be greater than 0"
}
Add-PodeSecurityHeader -Name 'Access-Control-Max-Age' -Value $Duration
# creds
if ($Credentials) {
Add-PodeSecurityHeader -Name 'Access-Control-Allow-Credentials' -Value 'true'
}
# opts route
if ($WithOptions) {
Add-PodeRoute -Method Options -Path * -ScriptBlock {
Set-PodeResponseStatus -Code 200
}
}
}
<#
.SYNOPSIS
Removes definitions for the Access-Control headers.
.DESCRIPTION
Removes definitions for the Access-Control headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age, Access-Control-Allow-Credentials
.EXAMPLE
Remove-PodeSecurityAccessControl
#>
function Remove-PodeSecurityAccessControl {
[CmdletBinding()]
param()
Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Origin'
Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Methods'
Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Headers'
Remove-PodeSecurityHeader -Name 'Access-Control-Max-Age'
Remove-PodeSecurityHeader -Name 'Access-Control-Allow-Credentials'
}
<#
.SYNOPSIS
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.
If you're storing sessions outside of Pode, you must supply a Secret value so sessions aren't corrupted.
.PARAMETER Secret
An optional Secret to use when signing Sessions (Default: random GUID).
.PARAMETER Name
The name of the cookie/header used for the Session.
.PARAMETER Duration
The duration a Session should last for, before being expired.
.PARAMETER Generator
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 Secret is required.
.PARAMETER Scope
The Scope that the Session applies to, possible values are Browser and Tab (Default: Browser).
The Browser scope is the default logic, where authentication and general data for the sessions are shared across all tabs.
The Tab scope keep the authentication data shared across all tabs, but general data is separated across different tabs.
For the Tab scope, the "Tab ID" required will be sourced from the "X-PODE-SESSION-TAB-ID" header.
.PARAMETER Extend
If supplied, the Sessions will have their durations extended on each successful Request.
.PARAMETER HttpOnly
If supplied, the Session cookie will only be accessible to browsers.
.PARAMETER Secure
If supplied, the Session cookie will only be accessible over HTTPS Requests.
.PARAMETER Strict
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 -Duration 120
.EXAMPLE
Enable-PodeSessionMiddleware -Duration 120 -Extend -Generator { return [System.IO.Path]::GetRandomFileName() }
.EXAMPLE
Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -UseHeaders -Strict
#>
function Enable-PodeSessionMiddleware {
[CmdletBinding(DefaultParameterSetName = 'Cookies')]
param(
[Parameter()]
[string]
$Secret,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]
$Name = 'pode.sid',
[Parameter()]
[ValidateScript({
if ($_ -lt 0) {
throw "Duration must be 0 or greater, but got: $($_)s"
}
return $true
})]
[int]
$Duration = 0,
[Parameter()]
[scriptblock]
$Generator,
[Parameter()]
[psobject]
$Storage = $null,
[Parameter()]
[ValidateSet('Browser', 'Tab')]
[string]
$Scope = 'Browser',
[switch]
$Extend,
[Parameter(ParameterSetName = 'Cookies')]
[switch]
$HttpOnly,
[Parameter(ParameterSetName = 'Cookies')]
[switch]
$Secure,
[switch]
$Strict,
[Parameter(ParameterSetName = 'Headers')]
[switch]
$UseHeaders
)
# check that session logic hasn't already been initialised
if (Test-PodeSessionsEnabled) {
throw 'Session Middleware has already been intialised'
}
# ensure the override store has the required methods
if (!(Test-PodeIsEmpty $Storage)) {
$members = @($Storage | Get-Member | Select-Object -ExpandProperty Name)
@('delete', 'get', 'set') | ForEach-Object {
if ($members -inotcontains $_) {
throw "Custom session storage does not implement the required '$($_)()' method"
}
}
}
# 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 = Get-PodeServerDefaultSecret
}
# if no custom storage, use the inmem one
if (Test-PodeIsEmpty $Storage) {
$Storage = (Get-PodeSessionInMemStore)
Set-PodeSessionInMemClearDown
}
# set options against server context
$PodeContext.Server.Sessions = @{
Name = $Name
Secret = $Secret
GenerateId = (Protect-PodeValue -Value $Generator -Default { return (New-PodeGuid) })
Store = $Storage
Info = @{
Duration = $Duration
Extend = $Extend.IsPresent
Secure = $Secure.IsPresent
Strict = $Strict.IsPresent
HttpOnly = $HttpOnly.IsPresent
UseHeaders = $UseHeaders.IsPresent
Scope = @{
Type = $Scope.ToLowerInvariant()
IsBrowser = ($Scope -ieq 'Browser')
}
}
}
# return scriptblock for the session middleware
Get-PodeSessionMiddleware |
New-PodeMiddleware |
Add-PodeMiddleware -Name '__pode_mw_sessions__'
}
<#
.SYNOPSIS
Remove the current Session, logging it out.
.DESCRIPTION
Remove the current Session, logging it out. This will remove the session from Storage, and Cookies.
.EXAMPLE
Remove-PodeSession
#>
function Remove-PodeSession {
[CmdletBinding()]
param()
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions have not been configured'
}
# do nothing if session is null
if ($null -eq $WebEvent.Session) {
return
}
# remove the session, and from auth and cookies
Remove-PodeAuthSession
}
<#
.SYNOPSIS
Saves the current Session's data.
.DESCRIPTION
Saves the current Session's data.
.PARAMETER Force
If supplied, the data will be saved even if nothing has changed.
.EXAMPLE
Save-PodeSession -Force
#>
function Save-PodeSession {
[CmdletBinding()]
param(
[switch]
$Force
)
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions have not been configured'
}
# error if session is null
if ($null -eq $WebEvent.Session) {
throw 'There is no session available to save'
}
# if auth is in use, then assign to session store
if (!(Test-PodeIsEmpty $WebEvent.Auth) -and $WebEvent.Auth.Store) {
$WebEvent.Session.Data.Auth = $WebEvent.Auth
}
# save the session
Save-PodeSessionInternal -Force:$Force
}
<#
.SYNOPSIS
Returns the currently authenticated SessionId.
.DESCRIPTION
Returns the currently authenticated SessionId. If there's no session, or it's not authenticated, then null is returned instead.
You can also have the SessionId returned as signed as well.
.PARAMETER Signed
If supplied, the returned SessionId will also be signed.
.PARAMETER Force
If supplied, the sessionId will be returned regardless of authentication.
.EXAMPLE
$sessionId = Get-PodeSessionId
#>
function Get-PodeSessionId {
[CmdletBinding()]
param(
[switch]
$Signed,
[switch]
$Force
)
$sessionId = $null
# do nothing if not authenticated, or force passed
if (!$Force -and ((Test-PodeIsEmpty $WebEvent.Session.Data.Auth.User) -or !$WebEvent.Session.Data.Auth.IsAuthenticated)) {
return $sessionId
}
# get the sessionId
$sessionId = $WebEvent.Session.FullId
# do they want the session signed?
if ($Signed) {
$strict = $PodeContext.Server.Sessions.Info.Strict
$secret = $PodeContext.Server.Sessions.Secret
# sign the value if we have a secret
$sessionId = (Invoke-PodeValueSign -Value $sessionId -Secret $secret -Strict:$strict)
}
# return the ID
return $sessionId
}
function Get-PodeSessionTabId {
[CmdletBinding()]
param()
if ($PodeContext.Server.Sessions.Info.Scope.IsBrowser) {
return $null
}
return Get-PodeHeader -Name 'X-PODE-SESSION-TAB-ID'
}
<#
.SYNOPSIS
Resets the current Session's expiry date.
.DESCRIPTION
Resets the current Session's expiry date, to be from the current time plus the defined Session duration.
.EXAMPLE
Reset-PodeSessionExpiry
#>
function Reset-PodeSessionExpiry {
[CmdletBinding()]
param()
# if sessions haven't been setup, error
if (!(Test-PodeSessionsEnabled)) {
throw 'Sessions have not been configured'
}
# error if session is null
if ($null -eq $WebEvent.Session) {
throw 'There is no session available to save'
}
# temporarily set this session to auto-extend
$WebEvent.Session.Extend = $true
# reset on response
Set-PodeSession
}
<#
.SYNOPSIS
Returns the defined Session duration.
.DESCRIPTION
Returns the defined Session duration that all Session are created using.
.EXAMPLE
$duration = Get-PodeSessionDuration
#>
function Get-PodeSessionDuration {
[CmdletBinding()]
param()
return [int]$PodeContext.Server.Sessions.Info.Duration
}
<#
.SYNOPSIS
Returns the datetime on which the current Session's will expire.
.DESCRIPTION
Returns the datetime on which the current Session's will expire.
.EXAMPLE
$expiry = Get-PodeSessionExpiry
#>
function Get-PodeSessionExpiry {
[CmdletBinding()]
param()
# error if session is null
if ($null -eq $WebEvent.Session) {
throw 'There is no session available to save'
}
# default min date
if ($null -eq $WebEvent.Session.TimeStamp) {
return [datetime]::MinValue
}
# use datetime.now or existing timestamp?
$expiry = [DateTime]::UtcNow
if (!$WebEvent.Session.Extend -and ($null -ne $WebEvent.Session.TimeStamp)) {
$expiry = $WebEvent.Session.TimeStamp
}
# add session duration on
$expiry = $expiry.AddSeconds($PodeContext.Server.Sessions.Info.Duration)
# return expiry
return $expiry
}
function Test-PodeSessionsEnabled {
return (($null -ne $PodeContext.Server.Sessions) -and ($PodeContext.Server.Sessions.Count -gt 0))
}
function Get-PodeSessionInfo {
return $PodeContext.Server.Sessions.Info
}
function Test-PodeSessionScopeIsBrowser {
return [bool]$PodeContext.Server.Sessions.Info.Scope.IsBrowser
}
<#
.SYNOPSIS
Converts the current HTTP request to a Route to be an SSE connection.
.DESCRIPTION
Converts the current HTTP request to a Route to be an SSE connection, by sending the required headers back to the client.
The connection can only be configured if the request's Accept header is "text/event-stream", unless Forced.
.PARAMETER Name
The Name of the SSE connection, which ClientIds will be stored under.
.PARAMETER Group
An optional Group for this SSE connection, to enable broadcasting events to all connections for an SSE connection name in a Group.
.PARAMETER Scope
The Scope of the SSE connection, either Default, Local or Global (Default: Default).
- If the Scope is Default, then it will be Global unless the default has been updated via Set-PodeSseDefaultScope.
- If the Scope is Local, then the SSE connection will only be opened for the duration of the request to a Route that configured it.
- If the Scope is Global, then the SSE connection will be cached internally so events can be sent to the connection from Tasks, Timers, and other Routes, etc.
.PARAMETER RetryDuration
An optional RetryDuration, in milliseconds, for the period of time a browser should wait before reattempting a connection if lost (Default: 0).
.PARAMETER ClientId
An optional ClientId to use for the SSE connection, this value will be signed if signing is enabled (Default: GUID).
.PARAMETER AllowAllOrigins
If supplied, then Access-Control-Allow-Origin will be set to * on the response.
.PARAMETER Force
If supplied, the Accept header of the request will be ignored; attempting to configure an SSE connection even if the header isn't "text/event-stream".
.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions'
.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -Scope Local
.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -Group 'admins'
.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -AllowAllOrigins
.EXAMPLE
ConvertTo-PodeSseConnection -Name 'Actions' -ClientId 'my-client-id'
#>
function ConvertTo-PodeSseConnection {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string]
$Group,
[Parameter()]
[ValidateSet('Default', 'Local', 'Global')]
[string]
$Scope = 'Default',
[Parameter()]
[int]
$RetryDuration = 0,
[Parameter()]
[string]
$ClientId,
[switch]
$AllowAllOrigins,
[switch]
$Force
)
# check Accept header - unless forcing
if (!$Force -and ((Get-PodeHeader -Name 'Accept') -ine 'text/event-stream')) {
throw 'SSE can only be configured on requests with an Accept header value of text/event-stream'
}
# check for default scope, and set
if ($Scope -ieq 'default') {
$Scope = $PodeContext.Server.Sse.DefaultScope
}
# generate clientId
$ClientId = New-PodeSseClientId -ClientId $ClientId
# set and send SSE headers
$ClientId = $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $Group, $RetryDuration, $AllowAllOrigins.IsPresent)
# create SSE property on WebEvent
$WebEvent.Sse = @{
Name = $Name
Group = $Group
ClientId = $ClientId
LastEventId = Get-PodeHeader -Name 'Last-Event-ID'
IsLocal = ($Scope -ieq 'local')
}
}
<#
.SYNOPSIS
Sets the default scope for new SSE connections.
.DESCRIPTION
Sets the default scope for new SSE connections.
.PARAMETER Scope
The default Scope for new SSE connections, either Local or Global.
- If the Scope is Local, then new SSE connections will only be opened for the duration of the request to a Route that configured it.
- If the Scope is Global, then new SSE connections will be cached internally so events can be sent to the connection from Tasks, Timers, and other Routes, etc.
.EXAMPLE
Set-PodeSseDefaultScope -Scope Local
.EXAMPLE
Set-PodeSseDefaultScope -Scope Global
#>
function Set-PodeSseDefaultScope {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Local', 'Global')]
[string]
$Scope
)
$PodeContext.Server.Sse.DefaultScope = $Scope
}
<#
.SYNOPSIS
Retrieves the default SSE connection scope for new SSE connections.
.DESCRIPTION
Retrieves the default SSE connection scope for new SSE connections.
.EXAMPLE
$scope = Get-PodeSseDefaultScope
#>
function Get-PodeSseDefaultScope {
[CmdletBinding()]
param()
return $PodeContext.Server.Sse.DefaultScope
}
<#
.SYNOPSIS
Send an Event to one or more SSE connections.
.DESCRIPTION
Send an Event to one or more SSE connections. This can either be:
- Every client for an SSE connection Name
- Specific ClientIds for an SSE connection Name
- The current SSE connection being referenced within $WebEvent.Sse
.PARAMETER Name
An SSE connection Name.
.PARAMETER Group
An optional array of 1 or more SSE connection Groups to send Events to, for the specified SSE connection Name.
.PARAMETER ClientId
An optional array of 1 or more SSE connection ClientIds to send Events to, for the specified SSE connection Name.
.PARAMETER Id
An optional ID for the Event being sent.
.PARAMETER EventType
An optional EventType for the Event being sent.
.PARAMETER Data
The Data for the Event being sent, either as a String or a Hashtable/PSObject. If the latter, it will be converted into JSON.
.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.
.PARAMETER FromEvent
If supplied, the SSE connection Name and ClientId will atttempt to be retrived from $WebEvent.Sse.
These details will be set if ConvertTo-PodeSseConnection has just been called. Or if X-PODE-SSE-CLIENT-ID and X-PODE-SSE-NAME are set on an HTTP request.
.EXAMPLE
Send-PodeSseEvent -FromEvent -Data 'This is an event'
.EXAMPLE
Send-PodeSseEvent -FromEvent -Data @{ Message = 'A message' }
.EXAMPLE
Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' }
.EXAMPLE
Send-PodeSseEvent -Name 'Actions' -Group 'admins' -Data @{ Message = 'A message' }
.EXAMPLE
Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } -ID 123 -EventType 'action'
#>
function Send-PodeSseEvent {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Name')]
[string]
$Name,
[Parameter(ParameterSetName = 'Name')]
[string[]]
$Group = $null,
[Parameter(ParameterSetName = 'Name')]
[string[]]
$ClientId = $null,
[Parameter()]
[string]
$Id,
[Parameter()]
[string]
$EventType,
[Parameter(Mandatory = $true)]
$Data,
[Parameter()]
[int]
$Depth = 10,
[Parameter(ParameterSetName = 'WebEvent')]
[switch]
$FromEvent
)
# do nothing if no value
if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) {
return
}
# jsonify the value
if ($Data -isnot [string]) {
if ($Depth -le 0) {
$Data = (ConvertTo-Json -InputObject $Data -Compress)
}
else {
$Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress)
}
}
# send directly back to current connection
if ($FromEvent -and $WebEvent.Sse.IsLocal) {
$WebEvent.Response.SendSseEvent($EventType, $Data, $Id)
return
}
# from event and global?
if ($FromEvent) {
$Name = $WebEvent.Sse.Name
$Group = $WebEvent.Sse.Group
$ClientId = $WebEvent.Sse.ClientId
}
# error if no name
if ([string]::IsNullOrEmpty($Name)) {
throw 'An SSE connection Name is required, either from -Name or $WebEvent.Sse.Name'
}
# check if broadcast level
if (!(Test-PodeSseBroadcastLevel -Name $Name -Group $Group -ClientId $ClientId)) {
throw "SSE failed to broadcast due to defined SSE broadcast level for $($Name): $(Get-PodeSseBroadcastLevel -Name $Name)"
}
# send event
$PodeContext.Server.Http.Listener.SendSseEvent($Name, $Group, $ClientId, $EventType, $Data, $Id)
}
<#
.SYNOPSIS
Close one or more SSE connections.
.DESCRIPTION
Close one or more SSE connections. Either all connections for an SSE connection Name, or specific ClientIds for a Name.
.PARAMETER Name
The Name of the SSE connection which has the ClientIds for the connections to close. If supplied on its own, all connections will be closed.
.PARAMETER Group
An optional array of 1 or more SSE connection Groups, that are for the SSE connection Name. If supplied without any ClientIds, then all connections for the Group(s) will be closed.
.PARAMETER ClientId
An optional array of 1 or more SSE connection ClientIds, that are for the SSE connection Name.
If not supplied, every SSE connection for the supplied Name will be closed.
.EXAMPLE
Close-PodeSseConnection -Name 'Actions'
.EXAMPLE
Close-PodeSseConnection -Name 'Actions' -Group 'admins'
.EXAMPLE
Close-PodeSseConnection -Name 'Actions' -ClientId @('my-client-id', 'my-other'id')
#>
function Close-PodeSseConnection {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string[]]
$Group = $null,
[Parameter()]
[string[]]
$ClientId = $null
)
$PodeContext.Server.Http.Listener.CloseSseConnection($Name, $Group, $ClientId)
}
<#
.SYNOPSIS
Test if an SSE connection ClientId is validly signed.
.DESCRIPTION
Test if an SSE connection ClientId is validly signed.
.PARAMETER ClientId
An optional SSE connection ClientId, if not supplied it will be retrieved from $WebEvent.
.EXAMPLE
if (Test-PodeSseClientIdValid) { ... }
.EXAMPLE
if (Test-PodeSseClientIdValid -ClientId 's:my-already-signed-client-id.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ=') { ... }
#>
function Test-PodeSseClientIdSigned {
[CmdletBinding()]
param(
[Parameter()]
[string]
$ClientId
)
# get clientId from WebEvent if not passed
if ([string]::IsNullOrEmpty($ClientId)) {
$ClientId = $WebEvent.Request.SseClientId
}
# test if clientId is validly signed
return Test-PodeValueSigned -Value $ClientId -Secret $PodeContext.Server.Sse.Secret -Strict:($PodeContext.Server.Sse.Strict)
}
<#
.SYNOPSIS
Test if an SSE connection ClientId is valid.
.DESCRIPTION
Test if an SSE connection ClientId, passed or from $WebEvent, is valid. A ClientId is valid if it's not signed and we're not signing ClientIds,
or if we are signing ClientIds and the ClientId is validly signed.
.PARAMETER ClientId
An optional SSE connection ClientId, if not supplied it will be retrieved from $WebEvent.
.EXAMPLE
if (Test-PodeSseClientIdValid) { ... }
.EXAMPLE
if (Test-PodeSseClientIdValid -ClientId 'my-client-id') { ... }
#>
function Test-PodeSseClientIdValid {
[CmdletBinding()]
param(
[Parameter()]
[string]
$ClientId
)
# get clientId from WebEvent if not passed
if ([string]::IsNullOrEmpty($ClientId)) {
$ClientId = $WebEvent.Request.SseClientId
}
# if no clientId, then it's not valid
if ([string]::IsNullOrEmpty($ClientId)) {
return $false
}
# if we're not signing, then valid if not signed, but invalid if signed
if (!$PodeContext.Server.Sse.Signed) {
return !$ClientId.StartsWith('s:')
}
# test if clientId is validly signed
return Test-PodeSseClientIdSigned -ClientId $ClientId
}
<#
.SYNOPSIS
Test if the name of an SSE connection exists or not.
.DESCRIPTION
Test if the name of an SSE connection exists or not.
.PARAMETER Name
The Name of an SSE connection to test.
.EXAMPLE
if (Test-PodeSseName -Name 'Example') { ... }
#>
function Test-PodeSseName {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Server.Http.Listener.TestSseConnectionExists($Name)
}
<#
.SYNOPSIS
Test if an SSE connection ClientId exists or not.
.DESCRIPTION
Test if an SSE connection ClientId exists or not.
.PARAMETER Name
The Name of an SSE connection.
.PARAMETER ClientId
The SSE connection ClientId to test.
.EXAMPLE
if (Test-PodeSseClientId -Name 'Example' -ClientId 'my-client-id') { ... }
#>
function Test-PodeSseClientId {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$ClientId
)
return $PodeContext.Server.Http.Listener.TestSseConnectionExists($Name, $ClientId)
}
<#
.SYNOPSIS
Generate a new SSE connection ClientId.
.DESCRIPTION
Generate a new SSE connection ClientId, which will be signed if signing enabled.
.PARAMETER ClientId
An optional SSE connection ClientId to use, if a custom ClientId is needed and required to be signed.
.EXAMPLE
$clientId = New-PodeSseClientId
.EXAMPLE
$clientId = New-PodeSseClientId -ClientId 'my-client-id'
.EXAMPLE
$clientId = New-PodeSseClientId -ClientId 's:my-already-signed-client-id.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ='
#>
function New-PodeSseClientId {
[CmdletBinding()]
param(
[Parameter()]
[string]
$ClientId
)
# if no clientId passed, generate a random guid
if ([string]::IsNullOrEmpty($ClientId)) {
$ClientId = New-PodeGuid -Secure
}
# if we're signing the clientId, and it's not already signed, then sign it
if ($PodeContext.Server.Sse.Signed -and !$ClientId.StartsWith('s:')) {
$ClientId = Invoke-PodeValueSign -Value $ClientId -Secret $PodeContext.Server.Sse.Secret -Strict:($PodeContext.Server.Sse.Strict)
}
# return the clientId
return $ClientId
}
<#
.SYNOPSIS
Enable the signing of SSE connection ClientIds.
.DESCRIPTION
Enable the signing of SSE connection ClientIds.
.PARAMETER Secret
A Secret to sign ClientIds, Get-PodeServerDefaultSecret can be used.
.PARAMETER Strict
If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress.
.EXAMPLE
Enable-PodeSseSigning
.EXAMPLE
Enable-PodeSseSigning -Strict
.EXAMPLE
Enable-PodeSseSigning -Secret 'Sup3rS3cr37!' -Strict
.EXAMPLE
Enable-PodeSseSigning -Secret 'Sup3rS3cr37!'
#>
function Enable-PodeSseSigning {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Secret,
[switch]
$Strict
)
# flag that we're signing SSE connections
$PodeContext.Server.Sse.Signed = $true
$PodeContext.Server.Sse.Secret = $Secret
$PodeContext.Server.Sse.Strict = $Strict.IsPresent
}
<#
.SYNOPSIS
Disable the signing of SSE connection ClientIds.
.DESCRIPTION
Disable the signing of SSE connection ClientIds.
.EXAMPLE
Disable-PodeSseSigning
#>
function Disable-PodeSseSigning {
[CmdletBinding()]
param()
# flag that we're not signing SSE connections
$PodeContext.Server.Sse.Signed = $false
$PodeContext.Server.Sse.Secret = $null
$PodeContext.Server.Sse.Strict = $false
}
<#
.SYNOPSIS
Set an allowed broadcast level for SSE connections.
.DESCRIPTION
Set an allowed broadcast level for SSE connections, either for all SSE connection names or specific ones.
.PARAMETER Name
An optional Name for an SSE connection (default: *).
.PARAMETER Type
The broadcast level Type for the SSE connection.
Name = Allow broadcasting at all levels, including broadcasting to all Groups and/or ClientIds for an SSE connection Name.
Group = Allow broadcasting to only Groups or specific ClientIds. If neither Groups nor ClientIds are supplied, sending an event will fail.
ClientId = Allow broadcasting to only ClientIds. If no ClientIds are supplied, sending an event will fail.
.EXAMPLE
Set-PodeSseBroadcastLevel -Type Name
.EXAMPLE
Set-PodeSseBroadcastLevel -Type Group
.EXAMPLE
Set-PodeSseBroadcastLevel -Name 'Actions' -Type ClientId
#>
function Set-PodeSseBroadcastLevel {
[CmdletBinding()]
param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]
$Name = '*',
[Parameter()]
[ValidateSet('Name', 'Group', 'ClientId')]
[string]
$Type
)
$PodeContext.Server.Sse.BroadcastLevel[$Name] = $Type.ToLowerInvariant()
}
<#
.SYNOPSIS
Retrieve the broadcast level for an SSE connection Name.
.DESCRIPTION
Retrieve the broadcast level for an SSE connection Name. If one hasn't been set explicitly then the base level will be checked.
If no broadcasting level have been set at all, then the "Name" level will be returned.
.PARAMETER Name
The Name of an SSE connection.
.EXAMPLE
$level = Get-PodeSseBroadcastLevel -Name 'Actions'
#>
function Get-PodeSseBroadcastLevel {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# if no levels, return null
if ($PodeContext.Server.Sse.BroadcastLevel.Count -eq 0) {
return 'name'
}
# get level or default level
$level = $PodeContext.Server.Sse.BroadcastLevel[$Name]
if ([string]::IsNullOrEmpty($level)) {
$level = $PodeContext.Server.Sse.BroadcastLevel['*']
}
if ([string]::IsNullOrEmpty($level)) {
$level = 'name'
}
# return level
return $level
}
<#
.SYNOPSIS
Test if an SSE connection can be broadcasted to, given the Name, Group, and ClientIds.
.DESCRIPTION
Test if an SSE connection can be broadcasted to, given the Name, Group, and ClientIds.
.PARAMETER Name
The Name of the SSE connection.
.PARAMETER Group
An array of 1 or more Groups.
.PARAMETER ClientId
An array of 1 or more ClientIds.
.EXAMPLE
if (Test-PodeSseBroadcastLevel -Name 'Actions') { ... }
.EXAMPLE
if (Test-PodeSseBroadcastLevel -Name 'Actions' -Group 'admins') { ... }
.EXAMPLE
if (Test-PodeSseBroadcastLevel -Name 'Actions' -ClientId 'my-client-id') { ... }
#>
function Test-PodeSseBroadcastLevel {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[string[]]
$Group,
[Parameter()]
[string[]]
$ClientId
)
# get level, and if no level or level=name, return true
$level = Get-PodeSseBroadcastLevel -Name $Name
if ([string]::IsNullOrEmpty($level) -or ($level -ieq 'name')) {
return $true
}
# if level=group, return false if no groups or clientIds
# if level=clientId, return false if no clientIds
switch ($level) {
'group' {
if ((($null -eq $Group) -or ($Group.Length -eq 0)) -and (($null -eq $ClientId) -or ($ClientId.Length -eq 0))) {
return $false
}
}
'clientid' {
if (($null -eq $ClientId) -or ($ClientId.Length -eq 0)) {
return $false
}
}
}
# valid, return true
return $true
}
<#
.SYNOPSIS
Sets an object within the shared state.
.DESCRIPTION
Sets an object within the shared state.
.PARAMETER Name
The name of the state object.
.PARAMETER Value
The value to set in the state.
.PARAMETER Scope
An optional Scope for the state object, used when saving the state.
.EXAMPLE
Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' }
.EXAMPLE
Set-PodeState -Name 'Users' -Value @('user1', 'user2') -Scope General, Users
#>
function Set-PodeState {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(ValueFromPipeline = $true)]
[object]
$Value,
[Parameter()]
[string[]]
$Scope
)
if ($null -eq $PodeContext.Server.State) {
throw 'Pode has not been initialised'
}
if ($null -eq $Scope) {
$Scope = @()
}
$PodeContext.Server.State[$Name] = @{
Value = $Value
Scope = $Scope
}
return $Value
}
<#
.SYNOPSIS
Retrieves some state object from the shared state.
.DESCRIPTION
Retrieves some state object from the shared state.
.PARAMETER Name
The name of the state object.
.PARAMETER WithScope
If supplied, the state's value and scope will be returned as a hashtable.
.EXAMPLE
Get-PodeState -Name 'Data'
#>
function Get-PodeState {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[switch]
$WithScope
)
if ($null -eq $PodeContext.Server.State) {
throw 'Pode has not been initialised'
}
if ($WithScope) {
return $PodeContext.Server.State[$Name]
}
else {
return $PodeContext.Server.State[$Name].Value
}
}
<#
.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 '<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.
.DESCRIPTION
Removes some state object from the shared state. After removal, the original object being stored is returned.
.PARAMETER Name
The name of the state object.
.EXAMPLE
Remove-PodeState -Name 'Data'
#>
function Remove-PodeState {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
if ($null -eq $PodeContext.Server.State) {
throw 'Pode has not been initialised'
}
$value = $PodeContext.Server.State[$Name].Value
$null = $PodeContext.Server.State.Remove($Name)
return $value
}
<#
.SYNOPSIS
Saves the current shared state to a supplied JSON file.
.DESCRIPTION
Saves the current shared state to a supplied JSON file. When using this function, it's recommended to wrap it in a Lock-PodeObject block.
.PARAMETER Path
The path to a JSON file which the current state will be saved to.
.PARAMETER Scope
An optional array of scopes for state objects that should be saved. (This has a lower precedence than Exclude/Include)
.PARAMETER Exclude
An optional array of state object names to exclude from being saved. (This has a higher precedence than Include)
.PARAMETER Include
An optional array of state object names to only include when being saved.
.PARAMETER Depth
Saved JSON maximum depth. Will be passed to ConvertTo-JSON's -Depth parameter. Default is 10.
.PARAMETER Compress
If supplied, the saved JSON will be compressed.
.EXAMPLE
Save-PodeState -Path './state.json'
.EXAMPLE
Save-PodeState -Path './state.json' -Exclude Name1, Name2
.EXAMPLE
Save-PodeState -Path './state.json' -Scope Users
#>
function Save-PodeState {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[Parameter()]
[string[]]
$Scope,
[Parameter()]
[string[]]
$Exclude,
[Parameter()]
[string[]]
$Include,
[Parameter()]
[int16]
$Depth = 10,
[switch]
$Compress
)
# error if attempting to use outside of the pode server
if ($null -eq $PodeContext.Server.State) {
throw 'Pode has not been initialised'
}
# get the full path to save the state
$Path = Get-PodeRelativePath -Path $Path -JoinRoot
# contruct the state to save (excludes, etc)
$state = $PodeContext.Server.State.Clone()
# scopes
if (($null -ne $Scope) -and ($Scope.Length -gt 0)) {
foreach ($_key in $state.Clone().Keys) {
# remove if no scope
if (($null -eq $state[$_key].Scope) -or ($state[$_key].Scope.Length -eq 0)) {
$null = $state.Remove($_key)
continue
}
# check scopes (only remove if none match)
$found = $false
foreach ($_scope in $state[$_key].Scope) {
if ($Scope -icontains $_scope) {
$found = $true
break
}
}
if ($found) {
continue
}
# none matched, remove
$null = $state.Remove($_key)
}
}
# include keys
if (($null -ne $Include) -and ($Include.Length -gt 0)) {
foreach ($_key in $state.Clone().Keys) {
if ($Include -inotcontains $_key) {
$null = $state.Remove($_key)
}
}
}
# exclude keys
if (($null -ne $Exclude) -and ($Exclude.Length -gt 0)) {
foreach ($_key in $state.Clone().Keys) {
if ($Exclude -icontains $_key) {
$null = $state.Remove($_key)
}
}
}
# save the state
$null = ConvertTo-Json -InputObject $state -Depth $Depth -Compress:$Compress | Out-File -FilePath $Path -Force
}
<#
.SYNOPSIS
Restores the shared state from some JSON file.
.DESCRIPTION
Restores the shared state from some JSON file.
.PARAMETER Path
The path to a JSON file that contains the state information.
.PARAMETER Merge
If supplied, the state loaded from the JSON file will be merged with the current state, instead of overwriting it.
.PARAMETER Depth
Saved JSON maximum depth. Will be passed to ConvertFrom-JSON's -Depth parameter (Powershell >=6). Default is 10.
.EXAMPLE
Restore-PodeState -Path './state.json'
#>
function Restore-PodeState {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Path,
[switch]
$Merge,
[int16]
$Depth = 10
)
# error if attempting to use outside of the pode server
if ($null -eq $PodeContext.Server.State) {
throw 'Pode has not been initialised'
}
# get the full path to the state
$Path = Get-PodeRelativePath -Path $Path -JoinRoot
if (!(Test-Path $Path)) {
return
}
# restore the state from file
$state = @{}
if (Test-PodeIsPSCore) {
$state = (Get-Content $Path -Force | ConvertFrom-Json -AsHashtable -Depth $Depth)
}
else {
$props = (Get-Content $Path -Force | ConvertFrom-Json).psobject.properties
foreach ($prop in $props) {
$state[$prop.Name] = $prop.Value
}
}
# check for no scopes, and add for backwards compat
$convert = $false
foreach ($_key in $state.Clone().Keys) {
if ($null -eq $state[$_key].Scope) {
$convert = $true
break
}
}
if ($convert) {
foreach ($_key in $state.Clone().Keys) {
$state[$_key] = @{
Value = $state[$_key]
Scope = @()
}
}
}
# set the scope to the main context
if ($Merge) {
foreach ($_key in $state.Clone().Keys) {
$PodeContext.Server.State[$_key] = $state[$_key]
}
}
else {
$PodeContext.Server.State = $state.Clone()
}
}
<#
.SYNOPSIS
Tests if the shared state contains some state object.
.DESCRIPTION
Tests if the shared state contains some state object.
.PARAMETER Name
The name of the state object.
.EXAMPLE
Test-PodeState -Name 'Data'
#>
function Test-PodeState {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
if ($null -eq $PodeContext.Server.State) {
throw 'Pode has not been initialised'
}
return $PodeContext.Server.State.ContainsKey($Name)
}
<#
.SYNOPSIS
Adds a new Task.
.DESCRIPTION
Adds a new Task, which can be asynchronously or synchronously invoked.
.PARAMETER Name
The Name of the Task.
.PARAMETER ScriptBlock
The script for the Task.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Task's logic.
.PARAMETER ArgumentList
A hashtable of arguments to supply to the Task's ScriptBlock.
.EXAMPLE
Add-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeLogic }
.EXAMPLE
Add-PodeTask -Name 'Example1' -ScriptBlock { return Get-SomeObject }
#>
function Add-PodeTask {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[hashtable]
$ArgumentList
)
# ensure the task doesn't already exist
if ($PodeContext.Tasks.Items.ContainsKey($Name)) {
throw "[Task] $($Name): Task already defined"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add the task
$PodeContext.Tasks.Enabled = $true
$PodeContext.Tasks.Items[$Name] = @{
Name = $Name
Script = $ScriptBlock
UsingVariables = $usingVars
Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{})
}
}
<#
.SYNOPSIS
Set the maximum number of concurrent Tasks.
.DESCRIPTION
Set the maximum number of concurrent Tasks.
.PARAMETER Maximum
The Maximum number of Tasks to run.
.EXAMPLE
Set-PodeTaskConcurrency -Maximum 10
#>
function Set-PodeTaskConcurrency {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[int]
$Maximum
)
# error if <=0
if ($Maximum -le 0) {
throw "Maximum concurrent tasks must be >=1 but got: $($Maximum)"
}
# ensure max > min
$_min = 1
if ($null -ne $PodeContext.RunspacePools.Tasks) {
$_min = $PodeContext.RunspacePools.Tasks.Pool.GetMinRunspaces()
}
if ($_min -gt $Maximum) {
throw "Maximum concurrent tasks cannot be less than the minimum of $($_min) but got: $($Maximum)"
}
# set the max tasks
$PodeContext.Threads.Tasks = $Maximum
if ($null -ne $PodeContext.RunspacePools.Tasks) {
$PodeContext.RunspacePools.Tasks.Pool.SetMaxRunspaces($Maximum)
}
}
<#
.SYNOPSIS
Invoke a Task.
.DESCRIPTION
Invoke a Task either asynchronously or synchronously, with support for returning values.
.PARAMETER Name
The Name of the Task.
.PARAMETER ArgumentList
A hashtable of arguments to supply to the Task's ScriptBlock.
.PARAMETER Timeout
A Timeout, in seconds, to abort running the task. (Default: -1 [never timeout])
.PARAMETER Wait
If supplied, Pode will wait until the Task has finished executing, and then return any values.
.EXAMPLE
Invoke-PodeTask -Name 'Example1' -Wait -Timeout 5
.EXAMPLE
$task = Invoke-PodeTask -Name 'Example1'
.EXAMPLE
Invoke-PodeTask -Name 'Example1' | Wait-PodeTask -Timeout 3
#>
function Invoke-PodeTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name,
[Parameter()]
[hashtable]
$ArgumentList = $null,
[Parameter()]
[int]
$Timeout = -1,
[switch]
$Wait
)
# ensure the task exists
if (!$PodeContext.Tasks.Items.ContainsKey($Name)) {
throw "Task '$($Name)' does not exist"
}
# run task logic
$task = Invoke-PodeInternalTask -Task $PodeContext.Tasks.Items[$Name] -ArgumentList $ArgumentList -Timeout $Timeout
# wait, and return result?
if ($Wait) {
return (Wait-PodeTask -Task $task -Timeout $Timeout)
}
# return task
return $task
}
<#
.SYNOPSIS
Removes a specific Task.
.DESCRIPTION
Removes a specific Task.
.PARAMETER Name
The Name of Task to be removed.
.EXAMPLE
Remove-PodeTask -Name 'Example1'
#>
function Remove-PodeTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name
)
$null = $PodeContext.Tasks.Items.Remove($Name)
}
<#
.SYNOPSIS
Removes all Tasks.
.DESCRIPTION
Removes all Tasks.
.EXAMPLE
Clear-PodeTasks
#>
function Clear-PodeTasks {
[CmdletBinding()]
param()
$PodeContext.Tasks.Items.Clear()
}
<#
.SYNOPSIS
Edits an existing Task.
.DESCRIPTION
Edits an existing Task's properties, such as scriptblock.
.PARAMETER Name
The Name of the Task.
.PARAMETER ScriptBlock
The new ScriptBlock for the Task.
.PARAMETER ArgumentList
Any new Arguments for the Task.
.EXAMPLE
Edit-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeNewLogic }
#>
function Edit-PodeTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name,
[Parameter()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[hashtable]
$ArgumentList
)
# ensure the task exists
if (!$PodeContext.Tasks.Items.ContainsKey($Name)) {
throw "Task '$($Name)' does not exist"
}
$_task = $PodeContext.Tasks.Items[$Name]
# edit scriptblock if supplied
if (!(Test-PodeIsEmpty $ScriptBlock)) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
$_task.Script = $ScriptBlock
$_task.UsingVariables = $usingVars
}
# edit arguments if supplied
if (!(Test-PodeIsEmpty $ArgumentList)) {
$_task.Arguments = $ArgumentList
}
}
<#
.SYNOPSIS
Returns any defined Tasks.
.DESCRIPTION
Returns any defined Tasks, with support for filtering.
.PARAMETER Name
Any Task Names to filter the Tasks.
.EXAMPLE
Get-PodeTask
.EXAMPLE
Get-PodeTask -Name Example1, Example2
#>
function Get-PodeTask {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Name
)
$tasks = $PodeContext.Tasks.Items.Values
# further filter by task names
if (($null -ne $Name) -and ($Name.Length -gt 0)) {
$tasks = @(foreach ($_name in $Name) {
foreach ($task in $tasks) {
if ($task.Name -ine $_name) {
continue
}
$task
}
})
}
# return
return $tasks
}
<#
.SYNOPSIS
Automatically loads task ps1 files
.DESCRIPTION
Automatically loads task ps1 files from either a /tasks folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeTasks
.EXAMPLE
Use-PodeTasks -Path './my-tasks'
#>
function Use-PodeTasks {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'tasks'
}
<#
.SYNOPSIS
Close and dispose of a Task.
.DESCRIPTION
Close and dispose of a Task, even if still running.
.PARAMETER Task
The Task to be closed.
.EXAMPLE
Invoke-PodeTask -Name 'Example1' | Close-PodeTask
#>
function Close-PodeTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Task
)
Close-PodeTaskInternal -Result $Task
}
<#
.SYNOPSIS
Test if a running Task has completed.
.DESCRIPTION
Test if a running Task has completed.
.PARAMETER Task
The Task to be check.
.EXAMPLE
Invoke-PodeTask -Name 'Example1' | Test-PodeTaskCompleted
#>
function Test-PodeTaskCompleted {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$Task
)
return [bool]$Task.Runspace.Handler.IsCompleted
}
<#
.SYNOPSIS
Waits for a task to finish, and returns a result if there is one.
.DESCRIPTION
Waits for a task to finish, and returns a result if there is one.
.PARAMETER Task
The task to wait on.
.PARAMETER Timeout
An optional Timeout in milliseconds.
.EXAMPLE
$context = Wait-PodeTask -Task $listener.GetContextAsync()
.EXAMPLE
$result = Invoke-PodeTask -Name 'Example1' | Wait-PodeTask
#>
function Wait-PodeTask {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
$Task,
[Parameter()]
[int]
$Timeout = -1
)
if ($Task -is [System.Threading.Tasks.Task]) {
return (Wait-PodeNetTaskInternal -Task $Task -Timeout $Timeout)
}
if ($Task -is [hashtable]) {
return (Wait-PodeTaskInternal -Task $Task -Timeout $Timeout)
}
throw 'Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable]'
}
<#
.SYNOPSIS
Places a temporary lock on an object, or Lockable, while a ScriptBlock is invoked.
.DESCRIPTION
Places a temporary lock on an object, or Lockable, while a ScriptBlock is invoked.
.PARAMETER Object
The Object, or Lockable, to lock. If no Object is supplied then the global lockable is used by default.
.PARAMETER Name
The Name of a Lockable object in Pode to lock, if no Name is supplied then the global lockable is used by default.
.PARAMETER ScriptBlock
The ScriptBlock to invoke.
.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a lock cannot be acquired. (Default: Infinite)
.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 -ScriptBlock { /* logic */ }
.EXAMPLE
Lock-PodeObject -Object $SomeArray -ScriptBlock { /* logic */ }
.EXAMPLE
Lock-PodeObject -Name 'LockName' -Timeout 5000 -ScriptBlock { /* logic */ }
.EXAMPLE
$result = (Lock-PodeObject -Return -Object $SomeArray -ScriptBlock { /* logic */ })
#>
function Lock-PodeObject {
[CmdletBinding(DefaultParameterSetName = 'Object')]
[OutputType([object])]
param(
[Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')]
[object]
$Object,
[Parameter(Mandatory = $true, ParameterSetName = 'Name')]
[string]
$Name,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[int]
$Timeout = [System.Threading.Timeout]::Infinite,
[switch]
$Return,
[switch]
$CheckGlobal
)
try {
if ([string]::IsNullOrEmpty($Name)) {
Enter-PodeLockable -Object $Object -Timeout $Timeout -CheckGlobal:$CheckGlobal
}
else {
Enter-PodeLockable -Name $Name -Timeout $Timeout -CheckGlobal:$CheckGlobal
}
if ($null -ne $ScriptBlock) {
Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return
}
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
finally {
if ([string]::IsNullOrEmpty($Name)) {
Exit-PodeLockable -Object $Object
}
else {
Exit-PodeLockable -Name $Name
}
}
}
<#
.SYNOPSIS
Creates a new custom Lockable object.
.DESCRIPTION
Creates a new custom Lockable object for use with Lock-PodeObject, and Enter/Exit-PodeLockable.
.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.Threading.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.Threading.Lockables.Custom.Remove($Name)
}
}
<#
.SYNOPSIS
Get a custom Lockable object.
.DESCRIPTION
Get a custom Lockable object for use with Lock-PodeObject, and Enter/Exit-PodeLockable.
.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.Threading.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.Threading.Lockables.Custom.ContainsKey($Name)
}
<#
.SYNOPSIS
Place a lock on an object or Lockable.
.DESCRIPTION
Place a lock on an object or Lockable. This should eventually be followed by a call to Exit-PodeLockable.
.PARAMETER Object
The Object, or Lockable, to lock. If no Object is supplied then the global lockable is used by default.
.PARAMETER Name
The Name of a Lockable object in Pode to lock, if no Name is supplied then the global lockable is used by default.
.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a lock cannot be acquired. (Default: Infinite)
.PARAMETER CheckGlobal
If supplied, will check the global Lockable object and wait until it's freed-up before locking the passed object.
.EXAMPLE
Enter-PodeLockable -Object $SomeArray
.EXAMPLE
Enter-PodeLockable -Name 'LockName' -Timeout 5000
#>
function Enter-PodeLockable {
[CmdletBinding(DefaultParameterSetName = 'Object')]
param(
[Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')]
[object]
$Object,
[Parameter(Mandatory = $true, ParameterSetName = 'Name')]
[string]
$Name,
[Parameter()]
[int]
$Timeout = [System.Threading.Timeout]::Infinite,
[switch]
$CheckGlobal
)
# get object by name if set
if (![string]::IsNullOrEmpty($Name)) {
$Object = Get-PodeLockable -Name $Name
}
# if object is null, default to global
if ($null -eq $Object) {
$Object = $PodeContext.Threading.Lockables.Global
}
# check if value type and throw
if ($Object -is [valuetype]) {
throw 'Cannot lock value types'
}
# check if null and throw
if ($null -eq $Object) {
throw 'Cannot lock a null object'
}
# check if the global lockable is locked
if ($CheckGlobal) {
Lock-PodeObject -Object $PodeContext.Threading.Lockables.Global -ScriptBlock {} -Timeout $Timeout
}
# attempt to acquire lock
$locked = $false
[System.Threading.Monitor]::TryEnter($Object.SyncRoot, $Timeout, [ref]$locked)
if (!$locked) {
throw 'Failed to acquire lock on object'
}
}
<#
.SYNOPSIS
Remove a lock from an object or Lockable.
.DESCRIPTION
Remove a lock from an object or Lockable, that was originally locked via Enter-PodeLockable.
.PARAMETER Object
The Object, or Lockable, to unlock. If no Object is supplied then the global lockable is used by default.
.PARAMETER Name
The Name of a Lockable object in Pode to unlock, if no Name is supplied then the global lockable is used by default.
.EXAMPLE
Exit-PodeLockable -Object $SomeArray
.EXAMPLE
Exit-PodeLockable -Name 'LockName'
#>
function Exit-PodeLockable {
[CmdletBinding(DefaultParameterSetName = 'Object')]
param(
[Parameter(ValueFromPipeline = $true, ParameterSetName = 'Object')]
[object]
$Object,
[Parameter(Mandatory = $true, ParameterSetName = 'Name')]
[string]
$Name
)
# get object by name if set
if (![string]::IsNullOrEmpty($Name)) {
$Object = Get-PodeLockable -Name $Name
}
# if object is null, default to global
if ($null -eq $Object) {
$Object = $PodeContext.Threading.Lockables.Global
}
# check if value type and throw
if ($Object -is [valuetype]) {
throw 'Cannot unlock value types'
}
# check if null and throw
if ($null -eq $Object) {
throw 'Cannot unlock a null object'
}
if ([System.Threading.Monitor]::IsEntered($Object.SyncRoot)) {
[System.Threading.Monitor]::Pulse($Object.SyncRoot)
[System.Threading.Monitor]::Exit($Object.SyncRoot)
}
}
<#
.SYNOPSIS
Remove all Lockables.
.DESCRIPTION
Remove all Lockables.
.EXAMPLE
Clear-PodeLockables
#>
function Clear-PodeLockables {
[CmdletBinding()]
param()
if (Test-PodeIsEmpty $PodeContext.Threading.Lockables.Custom) {
return
}
foreach ($name in $PodeContext.Threading.Lockables.Custom.Keys.Clone()) {
Remove-PodeLockable -Name $name
}
}
<#
.SYNOPSIS
Create a new Mutex.
.DESCRIPTION
Create a new Mutex.
.PARAMETER Name
The Name of the Mutex.
.PARAMETER Scope
The Scope of the Mutex, can be either Self, Local, or Global. (Default: Self)
Self: The current process, or child processes.
Local: All processes for the current login session on Windows, or the the same as Self on Unix.
Global: All processes on the system, across every session.
.EXAMPLE
New-PodeMutex -Name 'SelfMutex'
.EXAMPLE
New-PodeMutex -Name 'LocalMutex' -Scope Local
.EXAMPLE
New-PodeMutex -Name 'GlobalMutex' -Scope Global
#>
function New-PodeMutex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[ValidateSet('Self', 'Local', 'Global')]
[string]
$Scope = 'Self'
)
if (Test-PodeMutex -Name $Name) {
throw "A mutex with the following name already exists: $($Name)"
}
$mutex = $null
switch ($Scope.ToLowerInvariant()) {
'self' {
$mutex = [System.Threading.Mutex]::new($false)
}
'local' {
$mutex = [System.Threading.Mutex]::new($false, "Local\$($Name)")
}
'global' {
$mutex = [System.Threading.Mutex]::new($false, "Global\$($Name)")
}
}
$PodeContext.Threading.Mutexes[$Name] = $mutex
}
<#
.SYNOPSIS
Test if a Mutex exists.
.DESCRIPTION
Test if a Mutex exists.
.PARAMETER Name
The Name of the Mutex.
.EXAMPLE
Test-PodeMutex -Name 'LocalMutex'
#>
function Test-PodeMutex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Threading.Mutexes.ContainsKey($Name)
}
<#
.SYNOPSIS
Get a Mutex.
.DESCRIPTION
Get a Mutex.
.PARAMETER Name
The Name of the Mutex.
.EXAMPLE
$mutex = Get-PodeMutex -Name 'SelfMutex'
#>
function Get-PodeMutex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Threading.Mutexes[$Name]
}
<#
.SYNOPSIS
Remove a Mutex.
.DESCRIPTION
Remove a Mutex.
.PARAMETER Name
The Name of the Mutex.
.EXAMPLE
Remove-PodeMutex -Name 'GlobalMutex'
#>
function Remove-PodeMutex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
if (Test-PodeMutex -Name $Name) {
$PodeContext.Threading.Mutexes[$Name].Dispose()
$PodeContext.Threading.Mutexes.Remove($Name)
}
}
<#
.SYNOPSIS
Places a temporary hold on a Mutex, invokes a ScriptBlock, then releases the Mutex.
.DESCRIPTION
Places a temporary hold on a Mutex, invokes a ScriptBlock, then releases the Mutex.
.PARAMETER Name
The Name of the Mutex.
.PARAMETER ScriptBlock
The ScriptBlock to invoke.
.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Mutex. (Default: Infinite)
.PARAMETER Return
If supplied, any values from the ScriptBlock will be returned.
.EXAMPLE
Use-PodeMutex -Name 'SelfMutex' -Timeout 5000 -ScriptBlock {}
.EXAMPLE
$result = Use-PodeMutex -Name 'LocalMutex' -Return -ScriptBlock {}
#>
function Use-PodeMutex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[int]
$Timeout = [System.Threading.Timeout]::Infinite,
[switch]
$Return
)
try {
$acquired = $false
Enter-PodeMutex -Name $Name -Timeout $Timeout
$acquired = $true
Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
finally {
if ($acquired) {
Exit-PodeMutex -Name $Name
}
}
}
<#
.SYNOPSIS
Acquires a hold on a Mutex.
.DESCRIPTION
Acquires a hold on a Mutex. This should eventually by followed by a call to Exit-PodeMutex.
.PARAMETER Name
The Name of the Mutex.
.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Mutex. (Default: Infinite)
.EXAMPLE
Enter-PodeMutex -Name 'SelfMutex' -Timeout 5000
#>
function Enter-PodeMutex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[int]
$Timeout = [System.Threading.Timeout]::Infinite
)
$mutex = Get-PodeMutex -Name $Name
if ($null -eq $mutex) {
throw "No mutex found called '$($Name)'"
}
if (!$mutex.WaitOne($Timeout)) {
throw "Failed to acquire mutex ownership. Mutex name: $($Name)"
}
}
<#
.SYNOPSIS
Release the hold on a Mutex.
.DESCRIPTION
Release the hold on a Mutex, that was originally acquired by Enter-PodeMutex.
.PARAMETER Name
The Name of the Mutex.
.EXAMPLE
Exit-PodeMutex -Name 'SelfMutex'
#>
function Exit-PodeMutex {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$mutex = Get-PodeMutex -Name $Name
if ($null -eq $mutex) {
throw "No mutex found called '$($Name)'"
}
$mutex.ReleaseMutex()
}
<#
.SYNOPSIS
Removes all Mutexes.
.DESCRIPTION
Removes all Mutexes.
.EXAMPLE
Clear-PodeMutexes
#>
function Clear-PodeMutexes {
[CmdletBinding()]
param()
if (Test-PodeIsEmpty $PodeContext.Threading.Mutexes) {
return
}
foreach ($name in $PodeContext.Threading.Mutexes.Keys.Clone()) {
Remove-PodeMutex -Name $name
}
}
<#
.SYNOPSIS
Create a new Semaphore.
.DESCRIPTION
Create a new Semaphore.
.PARAMETER Name
The Name of the Semaphore.
.PARAMETER Count
The number of threads to allow a hold on the Semaphore. (Default: 1)
.PARAMETER Scope
The Scope of the Semaphore, can be either Self, Local, or Global. (Default: Self)
Self: The current process, or child processes.
Local: All processes for the current login session on Windows, or the the same as Self on Unix.
Global: All processes on the system, across every session.
.EXAMPLE
New-PodeSemaphore -Name 'SelfSemaphore'
.EXAMPLE
New-PodeSemaphore -Name 'LocalSemaphore' -Scope Local
.EXAMPLE
New-PodeSemaphore -Name 'GlobalSemaphore' -Count 3 -Scope Global
#>
function New-PodeSemaphore {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[int]
$Count = 1,
[Parameter()]
[ValidateSet('Self', 'Local', 'Global')]
[string]
$Scope = 'Self'
)
if (Test-PodeSemaphore -Name $Name) {
throw "A semaphore with the following name already exists: $($Name)"
}
if ($Count -le 0) {
$Count = 1
}
$semaphore = $null
switch ($Scope.ToLowerInvariant()) {
'self' {
$semaphore = [System.Threading.Semaphore]::new($Count, $Count)
}
'local' {
$semaphore = [System.Threading.Semaphore]::new($Count, $Count, "Local\$($Name)")
}
'global' {
$semaphore = [System.Threading.Semaphore]::new($Count, $Count, "Global\$($Name)")
}
}
$PodeContext.Threading.Semaphores[$Name] = $semaphore
}
<#
.SYNOPSIS
Test if a Semaphore exists.
.DESCRIPTION
Test if a Semaphore exists.
.PARAMETER Name
The Name of the Semaphore.
.EXAMPLE
Test-PodeSemaphore -Name 'LocalSemaphore'
#>
function Test-PodeSemaphore {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Threading.Semaphores.ContainsKey($Name)
}
<#
.SYNOPSIS
Get a Semaphore.
.DESCRIPTION
Get a Semaphore.
.PARAMETER Name
The Name of the Semaphore.
.EXAMPLE
$semaphore = Get-PodeSemaphore -Name 'SelfSemaphore'
#>
function Get-PodeSemaphore {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return $PodeContext.Threading.Semaphores[$Name]
}
<#
.SYNOPSIS
Remove a Semaphore.
.DESCRIPTION
Remove a Semaphore.
.PARAMETER Name
The Name of the Semaphore.
.EXAMPLE
Remove-PodeSemaphore -Name 'GlobalSemaphore'
#>
function Remove-PodeSemaphore {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
if (Test-PodeSemaphore -Name $Name) {
$PodeContext.Threading.Semaphores[$Name].Dispose()
$PodeContext.Threading.Semaphores.Remove($Name)
}
}
<#
.SYNOPSIS
Places a temporary hold on a Semaphore, invokes a ScriptBlock, then releases the Semaphore.
.DESCRIPTION
Places a temporary hold on a Semaphore, invokes a ScriptBlock, then releases the Semaphore.
.PARAMETER Name
The Name of the Semaphore.
.PARAMETER ScriptBlock
The ScriptBlock to invoke.
.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Semaphore. (Default: Infinite)
.PARAMETER Return
If supplied, any values from the ScriptBlock will be returned.
.EXAMPLE
Use-PodeSemaphore -Name 'SelfSemaphore' -Timeout 5000 -ScriptBlock {}
.EXAMPLE
$result = Use-PodeSemaphore -Name 'LocalSemaphore' -Return -ScriptBlock {}
#>
function Use-PodeSemaphore {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[int]
$Timeout = [System.Threading.Timeout]::Infinite,
[switch]
$Return
)
try {
$acquired = $false
Enter-PodeSemaphore -Name $Name -Timeout $Timeout
$acquired = $true
Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
finally {
if ($acquired) {
Exit-PodeSemaphore -Name $Name
}
}
}
<#
.SYNOPSIS
Acquires a hold on a Semaphore.
.DESCRIPTION
Acquires a hold on a Semaphore. This should eventually by followed by a call to Exit-PodeSemaphore.
.PARAMETER Name
The Name of the Semaphore.
.PARAMETER Timeout
If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Semaphore. (Default: Infinite)
.EXAMPLE
Enter-PodeSemaphore -Name 'SelfSemaphore' -Timeout 5000
#>
function Enter-PodeSemaphore {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[int]
$Timeout = [System.Threading.Timeout]::Infinite
)
$semaphore = Get-PodeSemaphore -Name $Name
if ($null -eq $semaphore) {
throw "No semaphore found called '$($Name)'"
}
if (!$semaphore.WaitOne($Timeout)) {
throw "Failed to acquire semaphore ownership. Semaphore name: $($Name)"
}
}
<#
.SYNOPSIS
Release the hold on a Semaphore.
.DESCRIPTION
Release the hold on a Semaphore, that was originally acquired by Enter-PodeSemaphore.
.PARAMETER Name
The Name of the Semaphore.
.PARAMETER ReleaseCount
The number of releases to release in one go. (Default: 1)
.EXAMPLE
Exit-PodeSemaphore -Name 'SelfSemaphore'
#>
function Exit-PodeSemaphore {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter()]
[int]
$ReleaseCount = 1
)
$semaphore = Get-PodeSemaphore -Name $Name
if ($null -eq $semaphore) {
throw "No semaphore found called '$($Name)'"
}
if ($ReleaseCount -lt 1) {
$ReleaseCount = 1
}
$semaphore.Release($ReleaseCount)
}
<#
.SYNOPSIS
Removes all Semaphores.
.DESCRIPTION
Removes all Semaphores.
.EXAMPLE
Clear-PodeSemaphores
#>
function Clear-PodeSemaphores {
[CmdletBinding()]
param()
if (Test-PodeIsEmpty $PodeContext.Threading.Semaphores) {
return
}
foreach ($name in $PodeContext.Threading.Semaphores.Keys.Clone()) {
Remove-PodeSemaphore -Name $name
}
}
<#
.SYNOPSIS
Adds a new Timer with logic to periodically invoke.
.DESCRIPTION
Adds a new Timer with logic to periodically invoke, with options to only run a specific number of times.
.PARAMETER Name
The Name of the Timer.
.PARAMETER Interval
The number of seconds to periodically invoke the Timer's ScriptBlock.
.PARAMETER ScriptBlock
The script for the Timer.
.PARAMETER Limit
The number of times the Timer should be invoked before being removed. (If 0, it will run indefinitely)
.PARAMETER Skip
The number of "invokes" to skip before the Timer actually runs.
.PARAMETER ArgumentList
An array of arguments to supply to the Timer's ScriptBlock.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Timer's logic.
.PARAMETER OnStart
If supplied, the timer will trigger when the server starts.
.EXAMPLE
Add-PodeTimer -Name 'Hello' -Interval 10 -ScriptBlock { 'Hello, world!' | Out-Default }
.EXAMPLE
Add-PodeTimer -Name 'RunOnce' -Interval 1 -Limit 1 -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeTimer -Name 'RunAfter60secs' -Interval 10 -Skip 6 -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeTimer -Name 'Args' -Interval 2 -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeTimer {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[int]
$Interval,
[Parameter(Mandatory = $true, ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter()]
[int]
$Limit = 0,
[Parameter()]
[int]
$Skip = 0,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[object[]]
$ArgumentList,
[switch]
$OnStart
)
# error if serverless
Test-PodeIsServerless -FunctionName 'Add-PodeTimer' -ThrowError
# ensure the timer doesn't already exist
if ($PodeContext.Timers.Items.ContainsKey($Name)) {
throw "[Timer] $($Name): Timer already defined"
}
# is the interval valid?
if ($Interval -le 0) {
throw "[Timer] $($Name): Interval must be greater than 0"
}
# is the limit valid?
if ($Limit -lt 0) {
throw "[Timer] $($Name): Cannot have a negative limit"
}
# is the skip valid?
if ($Skip -lt 0) {
throw "[Timer] $($Name): Cannot have a negative skip value"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# calculate the next tick time (based on Skip)
$NextTriggerTime = [DateTime]::Now.AddSeconds($Interval)
if ($Skip -gt 1) {
$NextTriggerTime = $NextTriggerTime.AddSeconds($Interval * $Skip)
}
# add the timer
$PodeContext.Timers.Enabled = $true
$PodeContext.Timers.Items[$Name] = @{
Name = $Name
Interval = $Interval
Limit = $Limit
Count = 0
Skip = $Skip
NextTriggerTime = $NextTriggerTime
LastTriggerTime = $null
Script = $ScriptBlock
UsingVariables = $usingVars
Arguments = $ArgumentList
OnStart = $OnStart
Completed = $false
}
}
<#
.SYNOPSIS
Adhoc invoke a Timer's logic.
.DESCRIPTION
Adhoc invoke a Timer's logic outside of its defined interval. This invocation doesn't count towards the Timer's limit.
.PARAMETER Name
The Name of the Timer.
.PARAMETER ArgumentList
An array of arguments to supply to the Timer's ScriptBlock.
.EXAMPLE
Invoke-PodeTimer -Name 'timer-name'
#>
function Invoke-PodeTimer {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name,
[Parameter()]
[object[]]
$ArgumentList = $null
)
# ensure the timer exists
if (!$PodeContext.Timers.Items.ContainsKey($Name)) {
throw "Timer '$($Name)' does not exist"
}
# run timer logic
Invoke-PodeInternalTimer -Timer $PodeContext.Timers.Items[$Name] -ArgumentList $ArgumentList
}
<#
.SYNOPSIS
Removes a specific Timer.
.DESCRIPTION
Removes a specific Timer.
.PARAMETER Name
The Name of Timer to be removed.
.EXAMPLE
Remove-PodeTimer -Name 'SaveState'
#>
function Remove-PodeTimer {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name
)
$null = $PodeContext.Timers.Items.Remove($Name)
}
<#
.SYNOPSIS
Removes all Timers.
.DESCRIPTION
Removes all Timers.
.EXAMPLE
Clear-PodeTimers
#>
function Clear-PodeTimers {
[CmdletBinding()]
param()
$PodeContext.Timers.Items.Clear()
}
<#
.SYNOPSIS
Edits an existing Timer.
.DESCRIPTION
Edits an existing Timer's properties, such as interval or scriptblock.
.PARAMETER Name
The Name of the Timer.
.PARAMETER Interval
The new Interval for the Timer in seconds.
.PARAMETER ScriptBlock
The new ScriptBlock for the Timer.
.PARAMETER ArgumentList
Any new Arguments for the Timer.
.EXAMPLE
Edit-PodeTimer -Name 'Hello' -Interval 10
#>
function Edit-PodeTimer {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]
$Name,
[Parameter()]
[int]
$Interval = 0,
[Parameter()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[object[]]
$ArgumentList
)
# ensure the timer exists
if (!$PodeContext.Timers.Items.ContainsKey($Name)) {
throw "Timer '$($Name)' does not exist"
}
$_timer = $PodeContext.Timers.Items[$Name]
# edit interval if supplied
if ($Interval -gt 0) {
$_timer.Interval = $Interval
}
# edit scriptblock if supplied
if (!(Test-PodeIsEmpty $ScriptBlock)) {
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
$_timer.Script = $ScriptBlock
$_timer.UsingVariables = $usingVars
}
# edit arguments if supplied
if (!(Test-PodeIsEmpty $ArgumentList)) {
$_timer.Arguments = $ArgumentList
}
}
<#
.SYNOPSIS
Returns any defined timers.
.DESCRIPTION
Returns any defined timers, with support for filtering.
.PARAMETER Name
Any timer Names to filter the timers.
.EXAMPLE
Get-PodeTimer
.EXAMPLE
Get-PodeTimer -Name Name1, Name2
#>
function Get-PodeTimer {
[CmdletBinding()]
param(
[Parameter()]
[string[]]
$Name
)
$timers = $PodeContext.Timers.Items.Values
# further filter by timer names
if (($null -ne $Name) -and ($Name.Length -gt 0)) {
$timers = @(foreach ($_name in $Name) {
foreach ($timer in $timers) {
if ($timer.Name -ine $_name) {
continue
}
$timer
}
})
}
# return
return $timers
}
<#
.SYNOPSIS
Tests whether the passed Timer exists.
.DESCRIPTION
Tests whether the passed Timer exists by its name.
.PARAMETER Name
The Name of the Timer.
.EXAMPLE
if (Test-PodeTimer -Name TimerName) { }
#>
function Test-PodeTimer {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
return (($null -ne $PodeContext.Timers.Items) -and $PodeContext.Timers.Items.ContainsKey($Name))
}
<#
.SYNOPSIS
Automatically loads timer ps1 files
.DESCRIPTION
Automatically loads timer ps1 files from either a /timers folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeTimers
.EXAMPLE
Use-PodeTimers -Path './my-timers'
#>
function Use-PodeTimers {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'timers'
}
<#
.SYNOPSIS
Dispose and close streams, tokens, and other Disposables.
.DESCRIPTION
Dispose and close streams, tokens, and other Disposables.
.PARAMETER Disposable
The Disposable object to dispose and close.
.PARAMETER Close
Should the Disposable also be closed, as well as disposed?
.PARAMETER CheckNetwork
If an error is thrown, check the reason - if it's network related ignore the error.
.EXAMPLE
Close-PodeDisposable -Disposable $stream -Close
#>
function Close-PodeDisposable {
[CmdletBinding()]
param(
[Parameter()]
[System.IDisposable]
$Disposable,
[switch]
$Close,
[switch]
$CheckNetwork
)
if ($null -eq $Disposable) {
return
}
try {
if ($Close) {
$Disposable.Close()
}
}
catch [exception] {
if ($CheckNetwork -and (Test-PodeValidNetworkFailure $_.Exception)) {
return
}
$_ | Write-PodeErrorLog
throw $_.Exception
}
finally {
$Disposable.Dispose()
}
}
<#
.SYNOPSIS
Returns the literal path of the server.
.DESCRIPTION
Returns the literal path of the server.
.EXAMPLE
$path = Get-PodeServerPath
#>
function Get-PodeServerPath {
[CmdletBinding()]
[OutputType([string])]
param()
return $PodeContext.Server.Root
}
<#
.SYNOPSIS
Starts a Stopwatch on some ScriptBlock, and outputs the duration at the end.
.DESCRIPTION
Starts a Stopwatch on some ScriptBlock, and outputs the duration at the end.
.PARAMETER Name
The name of the Stopwatch.
.PARAMETER ScriptBlock
The ScriptBlock to time.
.EXAMPLE
Start-PodeStopwatch -Name 'ReadFile' -ScriptBlock { $content = Get-Content './file.txt' }
#>
function Start-PodeStopwatch {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[scriptblock]
$ScriptBlock
)
try {
$watch = [System.Diagnostics.Stopwatch]::StartNew()
. $ScriptBlock
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
finally {
$watch.Stop()
"[Stopwatch]: $($watch.Elapsed) [$($Name)]" | Out-PodeHost
}
}
<#
.SYNOPSIS
Like the "using" keyword in .NET. Allows you to use a Stream and then disposes of it.
.DESCRIPTION
Like the "using" keyword in .NET. Allows you to use a Stream and then disposes of it.
.PARAMETER Stream
The Stream to use and then dispose.
.PARAMETER ScriptBlock
The ScriptBlock to invoke. It will be supplied the Stream.
.EXAMPLE
$content = (Use-PodeStream -Stream $stream -ScriptBlock { return $args[0].ReadToEnd() })
#>
function Use-PodeStream {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[System.IDisposable]
$Stream,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock
)
try {
return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $Stream -Return -NoNewClosure)
}
catch {
$_ | Write-PodeErrorLog
throw $_.Exception
}
finally {
$Stream.Dispose()
}
}
<#
.SYNOPSIS
Loads a script, by dot-sourcing, at the supplied path.
.DESCRIPTION
Loads a script, by dot-sourcing, at the supplied path. If the path is relative, the server's path is prepended.
.PARAMETER Path
The path, literal or relative to the server, to some script.
.EXAMPLE
Use-PodeScript -Path './scripts/tools.ps1'
#>
function Use-PodeScript {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Path
)
# if path is '.', replace with server root
$_path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve
# we have a path, if it's a directory/wildcard then loop over all files
if (![string]::IsNullOrWhiteSpace($_path)) {
$_paths = Get-PodeWildcardFiles -Path $Path -Wildcard '*.ps1'
if (!(Test-PodeIsEmpty $_paths)) {
foreach ($_path in $_paths) {
Use-PodeScript -Path $_path
}
return
}
}
# check if the path exists
if (!(Test-PodePath $_path -NoStatus)) {
throw "The script path does not exist: $(Protect-PodeValue -Value $_path -Default $Path)"
}
# dot-source the script
. $_path
# load any functions from the file into pode's runspaces
Import-PodeFunctionsIntoRunspaceState -FilePath $_path
}
<#
.SYNOPSIS
Returns the loaded configuration of the server.
.DESCRIPTION
Returns the loaded configuration of the server.
.EXAMPLE
$s = Get-PodeConfig
#>
function Get-PodeConfig {
[CmdletBinding()]
[OutputType([hashtable])]
param()
return $PodeContext.Server.Configuration
}
<#
.SYNOPSIS
Adds a ScriptBlock as Endware to run at the end of each web Request.
.DESCRIPTION
Adds a ScriptBlock as Endware to run at the end of each web Request.
.PARAMETER ScriptBlock
The ScriptBlock to add. It will be supplied the current web event.
.PARAMETER ArgumentList
An array of arguments to supply to the Endware's ScriptBlock.
.EXAMPLE
Add-PodeEndware -ScriptBlock { /* logic */ }
#>
function Add-PodeEndware {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[object[]]
$ArgumentList
)
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add the scriptblock to array of endware that needs to be run
$PodeContext.Server.Endware += @{
Logic = $ScriptBlock
UsingVariables = $usingVars
Arguments = $ArgumentList
}
}
<#
.SYNOPSIS
Automatically loads endware ps1 files
.DESCRIPTION
Automatically loads endware ps1 files from either a /endware folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeEndware
.EXAMPLE
Use-PodeEndware -Path './endware'
#>
function Use-PodeEndware {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'endware'
}
<#
.SYNOPSIS
Imports a Module into the current, and all runspaces that Pode uses.
.DESCRIPTION
Imports a Module into the current, and all runspaces that Pode uses. Modules can also be imported from the ps_modules directory.
.PARAMETER Name
The name of a globally installed Module, or one within the ps_modules directory, to import.
.PARAMETER Path
The path, literal or relative, to a Module to import.
.EXAMPLE
Import-PodeModule -Name IISManager
.EXAMPLE
Import-PodeModule -Path './modules/utilities.psm1'
#>
function Import-PodeModule {
[CmdletBinding(DefaultParameterSetName = 'Name')]
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Name')]
[string]
$Name,
[Parameter(Mandatory = $true, ParameterSetName = 'Path')]
[string]
$Path
)
# script root path
$rootPath = $null
if ($null -eq $PodeContext) {
$rootPath = (Protect-PodeValue -Value $MyInvocation.PSScriptRoot -Default $pwd.Path)
}
# get the path of a module, or import modules on mass
switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
'name' {
$modulePath = Join-PodeServerRoot -Folder ([System.IO.Path]::Combine('ps_modules', $Name)) -Root $rootPath
if (Test-PodePath -Path $modulePath -NoStatus) {
$Path = (Get-ChildItem ([System.IO.Path]::Combine($modulePath, '*', "$($Name).ps*1")) -Recurse -Force | Select-Object -First 1).FullName
}
else {
$Path = Find-PodeModuleFile -Name $Name -ListAvailable
}
}
'path' {
$Path = Get-PodeRelativePath -Path $Path -RootPath $rootPath -JoinRoot -Resolve
$paths = Get-PodeWildcardFiles -Path $Path -RootPath $rootPath -Wildcard '*.ps*1'
if (!(Test-PodeIsEmpty $paths)) {
foreach ($_path in $paths) {
Import-PodeModule -Path $_path
}
return
}
}
}
# if it's still empty, error
if ([string]::IsNullOrWhiteSpace($Path)) {
throw "Failed to import module: $(Protect-PodeValue -Value $Path -Default $Name)"
}
# check if the path exists
if (!(Test-PodePath $Path -NoStatus)) {
throw "The module path does not exist: $(Protect-PodeValue -Value $Path -Default $Name)"
}
$null = Import-Module $Path -Force -DisableNameChecking -Scope Global -ErrorAction Stop
}
<#
.SYNOPSIS
Imports a Snapin into the current, and all runspaces that Pode uses.
.DESCRIPTION
Imports a Snapin into the current, and all runspaces that Pode uses.
.PARAMETER Name
The name of a Snapin to import.
.EXAMPLE
Import-PodeSnapin -Name 'WDeploySnapin3.0'
#>
function Import-PodeSnapin {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
# if non-windows or core, fail
if ((Test-PodeIsPSCore) -or (Test-PodeIsUnix)) {
throw 'Snapins are only supported on Windows PowerShell'
}
# import the snap-in
$null = Add-PSSnapin -Name $Name
}
<#
.SYNOPSIS
Protects a value, by returning a default value is the main one is null/empty.
.DESCRIPTION
Protects a value, by returning a default value is the main one is null/empty.
.PARAMETER Value
The main value to use.
.PARAMETER Default
A default value to return should the main value be null/empty.
.EXAMPLE
$Name = Protect-PodeValue -Value $Name -Default 'Rick'
#>
function Protect-PodeValue {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter()]
$Value,
[Parameter()]
$Default
)
return (Resolve-PodeValue -Check (Test-PodeIsEmpty $Value) -TrueValue $Default -FalseValue $Value)
}
<#
.SYNOPSIS
Resolves a query, and returns a value based on the response.
.DESCRIPTION
Resolves a query, and returns a value based on the response.
.PARAMETER Check
The query, or variable, to evalulate.
.PARAMETER TrueValue
The value to use if evaluated to True.
.PARAMETER FalseValue
The value to use if evaluated to False.
.EXAMPLE
$Port = Resolve-PodeValue -Check $AllowSsl -TrueValue 443 -FalseValue -80
#>
function Resolve-PodeValue {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[bool]
$Check,
[Parameter()]
$TrueValue,
[Parameter()]
$FalseValue
)
if ($Check) {
return $TrueValue
}
return $FalseValue
}
<#
.SYNOPSIS
Invokes a ScriptBlock.
.DESCRIPTION
Invokes a ScriptBlock, supplying optional arguments, splatting, and returning any optional values.
.PARAMETER ScriptBlock
The ScriptBlock to invoke.
.PARAMETER Arguments
Any arguments that should be supplied to the ScriptBlock.
.PARAMETER UsingVariables
Optional array of "using-variable" values, which will be automatically prepended to any supplied Arguments when supplied to the ScriptBlock.
.PARAMETER Scoped
Run the ScriptBlock in a scoped context.
.PARAMETER Return
Return any values that the ScriptBlock may return.
.PARAMETER Splat
Spat the argument onto the ScriptBlock.
.PARAMETER NoNewClosure
Don't create a new closure before invoking the ScriptBlock.
.EXAMPLE
Invoke-PodeScriptBlock -ScriptBlock { Write-Host 'Hello!' }
.EXAMPLE
Invoke-PodeScriptBlock -Arguments 'Morty' -ScriptBlock { /* logic */ }
#>
function Invoke-PodeScriptBlock {
[CmdletBinding()]
[OutputType([object])]
param(
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[Parameter()]
$Arguments = $null,
[Parameter()]
[object[]]
$UsingVariables = $null,
[switch]
$Scoped,
[switch]
$Return,
[switch]
$Splat,
[switch]
$NoNewClosure
)
# force no new closure if running serverless
if ($PodeContext.Server.IsServerless) {
$NoNewClosure = $true
}
# if new closure needed, create it
if (!$NoNewClosure) {
$ScriptBlock = ($ScriptBlock).GetNewClosure()
}
# merge arguments together, if we have using vars supplied
if (($null -ne $UsingVariables) -and ($UsingVariables.Length -gt 0)) {
$Arguments = @(Merge-PodeScriptblockArguments -ArgumentList $Arguments -UsingVariables $UsingVariables)
}
# invoke the scriptblock
if ($Scoped) {
if ($Splat) {
$result = (& $ScriptBlock @Arguments)
}
else {
$result = (& $ScriptBlock $Arguments)
}
}
else {
if ($Splat) {
$result = (. $ScriptBlock @Arguments)
}
else {
$result = (. $ScriptBlock $Arguments)
}
}
# if needed, return the result
if ($Return) {
return $result
}
}
<#
.SYNOPSIS
Merges Arguments and Using Variables together.
.DESCRIPTION
Merges Arguments and Using Variables together to be supplied to a ScriptBlock.
The Using Variables will be prepended so then are supplied first to a ScriptBlock.
.PARAMETER ArgumentList
And optional array of Arguments.
.PARAMETER UsingVariables
And optional array of "using-variable" values to be prepended.
.EXAMPLE
$Arguments = @(Merge-PodeScriptblockArguments -ArgumentList $Arguments -UsingVariables $UsingVariables)
.EXAMPLE
$Arguments = @(Merge-PodeScriptblockArguments -UsingVariables $UsingVariables)
#>
function Merge-PodeScriptblockArguments {
param(
[Parameter()]
[object[]]
$ArgumentList = $null,
[Parameter()]
[object[]]
$UsingVariables = $null
)
if ($null -eq $ArgumentList) {
$ArgumentList = @()
}
if (($null -eq $UsingVariables) -or ($UsingVariables.Length -le 0)) {
return $ArgumentList
}
$_vars = @()
foreach ($_var in $UsingVariables) {
$_vars += , $_var.Value
}
return ($_vars + $ArgumentList)
}
<#
.SYNOPSIS
Tests if a value is empty - the value can be of any type.
.DESCRIPTION
Tests if a value is empty - the value can be of any type.
.PARAMETER Value
The value to test.
.EXAMPLE
if (Test-PodeIsEmpty @{}) { /* logic */ }
#>
function Test-PodeIsEmpty {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter()]
$Value
)
if ($null -eq $Value) {
return $true
}
if ($Value -is [string]) {
return [string]::IsNullOrWhiteSpace($Value)
}
if ($Value -is [array]) {
return ($Value.Length -eq 0)
}
if (($Value -is [hashtable]) -or ($Value -is [System.Collections.Specialized.OrderedDictionary])) {
return ($Value.Count -eq 0)
}
if ($Value -is [scriptblock]) {
return ([string]::IsNullOrWhiteSpace($Value.ToString()))
}
if ($Value -is [valuetype]) {
return $false
}
return ([string]::IsNullOrWhiteSpace($Value) -or ((Get-PodeCount $Value) -eq 0))
}
<#
.SYNOPSIS
Tests if the the current session is running in PowerShell Core.
.DESCRIPTION
Tests if the the current session is running in PowerShell Core.
.EXAMPLE
if (Test-PodeIsPSCore) { /* logic */ }
#>
function Test-PodeIsPSCore {
[CmdletBinding()]
[OutputType([bool])]
param()
return (Get-PodePSVersionTable).PSEdition -ieq 'core'
}
<#
.SYNOPSIS
Tests if the current OS is Unix.
.DESCRIPTION
Tests if the current OS is Unix.
.EXAMPLE
if (Test-PodeIsUnix) { /* logic */ }
#>
function Test-PodeIsUnix {
[CmdletBinding()]
[OutputType([bool])]
param()
return (Get-PodePSVersionTable).Platform -ieq 'unix'
}
<#
.SYNOPSIS
Tests if the current OS is Windows.
.DESCRIPTION
Tests if the current OS is Windows.
.EXAMPLE
if (Test-PodeIsWindows) { /* logic */ }
#>
function Test-PodeIsWindows {
[CmdletBinding()]
[OutputType([bool])]
param()
$v = Get-PodePSVersionTable
return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop'))
}
<#
.SYNOPSIS
Tests if the current OS is MacOS.
.DESCRIPTION
Tests if the current OS is MacOS.
.EXAMPLE
if (Test-PodeIsMacOS) { /* logic */ }
#>
function Test-PodeIsMacOS {
[CmdletBinding()]
[OutputType([bool])]
param()
return ([bool]$IsMacOS)
}
<#
.SYNOPSIS
Tests if the scope you're in is currently within a Pode runspace.
.DESCRIPTION
Tests if the scope you're in is currently within a Pode runspace.
.EXAMPLE
If (Test-PodeInRunspace) { ... }
#>
function Test-PodeInRunspace {
[CmdletBinding()]
param()
return ([bool]$PODE_SCOPE_RUNSPACE)
}
<#
.SYNOPSIS
Outputs an object to the main Host.
.DESCRIPTION
Due to Pode's use of runspaces, this will output a given object back to the main Host.
It's advised to use this function, so that any output respects the -Quiet flag of the server.
.PARAMETER InputObject
The object to output.
.EXAMPLE
'Hello, world!' | Out-PodeHost
.EXAMPLE
@{ Name = 'Rick' } | Out-PodeHost
#>
function Out-PodeHost {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]
$InputObject
)
if (!$PodeContext.Server.Quiet) {
$InputObject | Out-Default
}
}
<#
.SYNOPSIS
Writes an object to the Host.
.DESCRIPTION
Writes an object to the Host.
It's advised to use this function, so that any output respects the -Quiet flag of the server.
.PARAMETER Object
The object to write.
.PARAMETER ForegroundColor
An optional foreground colour.
.PARAMETER NoNewLine
Whether or not to write a new line.
.PARAMETER Explode
Show the object content
.PARAMETER ShowType
Show the Object Type
.EXAMPLE
'Some output' | Write-PodeHost -ForegroundColor Cyan
#>
function Write-PodeHost {
[CmdletBinding(DefaultParameterSetName = 'inbuilt')]
param(
[Parameter(Position = 0, ValueFromPipeline = $true)]
[object]
$Object,
[Parameter()]
[System.ConsoleColor]
$ForegroundColor,
[switch]
$NoNewLine,
[Parameter( Mandatory = $true, ParameterSetName = 'object')]
[switch]
$Explode,
[Parameter( Mandatory = $false, ParameterSetName = 'object')]
[switch]
$ShowType
)
if ($PodeContext.Server.Quiet) {
return
}
if ($Explode.IsPresent ) {
if ($null -eq $Object) {
if ($ShowType) {
$Object = "`tNull Value"
}
}
else {
$type = $Object.gettype().FullName
$Object = $Object | Out-String
if ($ShowType) {
$Object = "`tTypeName: $type`n$Object"
}
}
}
if ($ForegroundColor) {
Write-Host -Object $Object -ForegroundColor $ForegroundColor -NoNewline:$NoNewLine
}
else {
Write-Host -Object $Object -NoNewline:$NoNewLine
}
}
<#
.SYNOPSIS
Returns whether or not the server is running via IIS.
.DESCRIPTION
Returns whether or not the server is running via IIS.
.EXAMPLE
if (Test-PodeIsIIS) { }
#>
function Test-PodeIsIIS {
[CmdletBinding()]
param()
return $PodeContext.Server.IsIIS
}
<#
.SYNOPSIS
Returns the IIS application path.
.DESCRIPTION
Returns the IIS application path, or null if not using IIS.
.EXAMPLE
$path = Get-PodeIISApplicationPath
#>
function Get-PodeIISApplicationPath {
[CmdletBinding()]
param()
if (!$PodeContext.Server.IsIIS) {
return $null
}
return $PodeContext.Server.IIS.Path.Raw
}
<#
.SYNOPSIS
Returns whether or not the server is running via Heroku.
.DESCRIPTION
Returns whether or not the server is running via Heroku.
.EXAMPLE
if (Test-PodeIsHeroku) { }
#>
function Test-PodeIsHeroku {
[CmdletBinding()]
param()
return $PodeContext.Server.IsHeroku
}
<#
.SYNOPSIS
Returns whether or not the server is being hosted behind another application.
.DESCRIPTION
Returns whether or not the server is being hosted behind another application, such as Heroku or IIS.
.EXAMPLE
if (Test-PodeIsHosted) { }
#>
function Test-PodeIsHosted {
[CmdletBinding()]
param()
return ((Test-PodeIsIIS) -or (Test-PodeIsHeroku))
}
<#
.SYNOPSIS
Defines variables to be created when the Pode server stops.
.DESCRIPTION
Allows you to define a variable, with a value, that should be created on the in the main scope after the Pode server is stopped.
.PARAMETER Name
The Name of the variable to be set
.PARAMETER Value
The Value of the variable to be set
.EXAMPLE
Out-PodeVariable -Name ExampleVar -Value @{ Name = 'Bob' }
#>
function Out-PodeVariable {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(ValueFromPipeline = $true)]
[object]
$Value
)
$PodeContext.Server.Output.Variables[$Name] = $Value
}
<#
.SYNOPSIS
A helper function to generate cron expressions.
.DESCRIPTION
A helper function to generate cron expressions, which can be used for Schedules and other functions that use cron expressions.
This helper function only covers simple cron use-cases, with some advanced use-cases. If you need further advanced cron
expressions it would be best to write the expression by hand.
.PARAMETER Minute
This is an array of Minutes that the expression should use between 0-59.
.PARAMETER Hour
This is an array of Hours that the expression should use between 0-23.
.PARAMETER Date
This is an array of Dates in the monnth that the expression should use between 1-31.
.PARAMETER Month
This is an array of Months that the expression should use between January-December.
.PARAMETER Day
This is an array of Days in the week that the expression should use between Monday-Sunday.
.PARAMETER Every
This can be used to more easily specify "Every Hour" than writing out all the hours.
.PARAMETER Interval
This can only be used when using the Every parameter, and will setup an interval on the "every" used.
If you want "every 2 hours" then Every should be set to Hour and Interval to 2.
.EXAMPLE
New-PodeCron -Every Day # every 00:00
.EXAMPLE
New-PodeCron -Every Day -Day Tuesday, Friday -Hour 1 # every tuesday and friday at 01:00
.EXAMPLE
New-PodeCron -Every Month -Date 15 # every 15th of the month at 00:00
.EXAMPLE
New-PodeCron -Every Date -Interval 2 -Date 2 # every month, every other day from 2nd, at 00:00
.EXAMPLE
New-PodeCron -Every Year -Month June # every 1st june, at 00:00
.EXAMPLE
New-PodeCron -Every Hour -Hour 1 -Interval 1 # every hour, starting at 01:00
.EXAMPLE
New-PodeCron -Every Minute -Hour 1, 2, 3, 4, 5 -Interval 15 # every 15mins, starting at 01:00 until 05:00
.EXAMPLE
New-PodeCron -Every Hour -Day Monday # every hour of every monday
.EXAMPLE
New-PodeCron -Every Quarter # every 1st jan, apr, jul, oct, at 00:00
#>
function New-PodeCron {
[CmdletBinding()]
[OutputType([String])]
param(
[Parameter()]
[ValidateRange(0, 59)]
[int[]]
$Minute = $null,
[Parameter()]
[ValidateRange(0, 23)]
[int[]]
$Hour = $null,
[Parameter()]
[ValidateRange(1, 31)]
[int[]]
$Date = $null,
[Parameter()]
[ValidateSet('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December')]
[string[]]
$Month = $null,
[Parameter()]
[ValidateSet('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')]
[string[]]
$Day = $null,
[Parameter()]
[ValidateSet('Minute', 'Hour', 'Day', 'Date', 'Month', 'Quarter', 'Year', 'None')]
[string]
$Every = 'None',
[Parameter()]
[int]
$Interval = 0
)
# cant have None and Interval
if (($Every -ieq 'none') -and ($Interval -gt 0)) {
throw 'Cannot supply an interval when -Every is set to None'
}
# base cron
$cron = @{
Minute = '*'
Hour = '*'
Date = '*'
Month = '*'
Day = '*'
}
# convert month/day to numbers
if ($Month.Length -gt 0) {
$MonthInts = @(foreach ($item in $Month) {
(@{
January = 1
February = 2
March = 3
April = 4
May = 5
June = 6
July = 7
August = 8
September = 9
October = 10
November = 11
December = 12
})[$item]
})
}
if ($Day.Length -gt 0) {
$DayInts = @(foreach ($item in $Day) {
(@{
Sunday = 0
Monday = 1
Tuesday = 2
Wednesday = 3
Thursday = 4
Friday = 5
Saturday = 6
})[$item]
})
}
# set "every" defaults
switch ($Every.ToUpperInvariant()) {
'MINUTE' {
if (Set-PodeCronInterval -Cron $cron -Type 'Minute' -Value $Minute -Interval $Interval) {
$Minute = @()
}
}
'HOUR' {
$cron.Minute = '0'
if (Set-PodeCronInterval -Cron $cron -Type 'Hour' -Value $Hour -Interval $Interval) {
$Hour = @()
}
}
'DAY' {
$cron.Minute = '0'
$cron.Hour = '0'
if (Set-PodeCronInterval -Cron $cron -Type 'Day' -Value $DayInts -Interval $Interval) {
$DayInts = @()
}
}
'DATE' {
$cron.Minute = '0'
$cron.Hour = '0'
if (Set-PodeCronInterval -Cron $cron -Type 'Date' -Value $Date -Interval $Interval) {
$Date = @()
}
}
'MONTH' {
$cron.Minute = '0'
$cron.Hour = '0'
if ($DayInts.Length -eq 0) {
$cron.Date = '1'
}
if (Set-PodeCronInterval -Cron $cron -Type 'Month' -Value $MonthInts -Interval $Interval) {
$MonthInts = @()
}
}
'QUARTER' {
$cron.Minute = '0'
$cron.Hour = '0'
$cron.Date = '1'
$cron.Month = '1,4,7,10'
if ($Interval -gt 0) {
throw 'Cannot supply interval value for every quarter'
}
}
'YEAR' {
$cron.Minute = '0'
$cron.Hour = '0'
$cron.Date = '1'
$cron.Month = '1'
if ($Interval -gt 0) {
throw 'Cannot supply interval value for every year'
}
}
}
# set any custom overrides
if ($Minute.Length -gt 0) {
$cron.Minute = $Minute -join ','
}
if ($Hour.Length -gt 0) {
$cron.Hour = $Hour -join ','
}
if ($DayInts.Length -gt 0) {
$cron.Day = $DayInts -join ','
}
if ($Date.Length -gt 0) {
$cron.Date = $Date -join ','
}
if ($MonthInts.Length -gt 0) {
$cron.Month = $MonthInts -join ','
}
# build and return
return "$($cron.Minute) $($cron.Hour) $($cron.Date) $($cron.Month) $($cron.Day)"
}
<#
.SYNOPSIS
Gets the version of the Pode module.
.DESCRIPTION
The Get-PodeVersion function checks the version of the Pode module specified in the module manifest. If the module version is not a placeholder value ('$version$'), it returns the actual version prefixed with 'v.'. If the module version is the placeholder value, indicating the development branch, it returns '[develop branch]'.
.PARAMETER None
This function does not accept any parameters.
.OUTPUTS
System.String
Returns a string indicating the version of the Pode module or '[dev]' if on a development version.
.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '1.2.3' }
PS> Get-PodeVersion
Returns 'v1.2.3'.
.EXAMPLE
PS> $moduleManifest = @{ ModuleVersion = '$version$' }
PS> Get-PodeVersion
Returns '[dev]'.
.NOTES
This function assumes that $moduleManifest is a hashtable representing the loaded module manifest, with a key of ModuleVersion.
#>
function Get-PodeVersion {
$moduleManifest = Get-PodeModuleManifest
if ($moduleManifest.ModuleVersion -ne '$version$') {
return "v$($moduleManifest.ModuleVersion)"
}
else {
return '[dev]'
}
}
<#
.SYNOPSIS
Converts an XML node to a PowerShell hashtable.
.DESCRIPTION
The ConvertFrom-PodeXml function converts an XML node, including all its child nodes and attributes, into an ordered hashtable. This is useful for manipulating XML data in a more PowerShell-centric way.
.PARAMETER node
The XML node to convert. This parameter takes an XML node and processes it, along with its child nodes and attributes.
.PARAMETER Prefix
A string prefix used to indicate an attribute. Default is an empty string.
.PARAMETER ShowDocElement
Indicates whether to show the document element. Default is false.
.PARAMETER KeepAttributes
If set, the function keeps the attributes of the XML nodes in the resulting hashtable.
.EXAMPLE
$node = [xml](Get-Content 'path\to\file.xml').DocumentElement
ConvertFrom-PodeXml -node $node
Converts the XML document's root node to a hashtable.
.INPUTS
System.Xml.XmlNode
You can pipe a XmlNode to ConvertFrom-PodeXml.
.OUTPUTS
System.Collections.Hashtable
Outputs an ordered hashtable representing the XML node structure.
.NOTES
This cmdlet is useful for transforming XML data into a structure that's easier to manipulate in PowerShell scripts.
.LINK
https://badgerati.github.io/Pode/Functions/Utility/ConvertFrom-PodeXml
#>
function ConvertFrom-PodeXml {
[CmdletBinding()]
[OutputType([System.Collections.Specialized.OrderedDictionary])]
param
(
[Parameter(Mandatory = $true, ValueFromPipeline)]
[System.Xml.XmlNode]$node, #we are working through the nodes
[string]$Prefix = '', #do we indicate an attribute with a prefix?
$ShowDocElement = $false, #Do we show the document element?,
[switch]
$KeepAttributes
)
#if option set, we skip the Document element
if ($node.DocumentElement -and !($ShowDocElement))
{ $node = $node.DocumentElement }
$oHash = [ordered] @{ } # start with an ordered hashtable.
#The order of elements is always significant regardless of what they are
if ($null -ne $node.Attributes ) {
#if there are elements
# record all the attributes first in the ordered hash
$node.Attributes | ForEach-Object {
$oHash.$("$Prefix$($_.FirstChild.parentNode.LocalName)") = $_.FirstChild.value
}
}
# check to see if there is a pseudo-array. (more than one
# child-node with the same name that must be handled as an array)
$node.ChildNodes | #we just group the names and create an empty
#array for each
Group-Object -Property LocalName | Where-Object { $_.count -gt 1 } | Select-Object Name |
ForEach-Object {
$oHash.($_.Name) = @() <# create an empty array for each one#>
}
foreach ($child in $node.ChildNodes) {
#now we look at each node in turn.
$childName = $child.LocalName
if ($child -is [system.xml.xmltext]) {
# if it is simple XML text
$oHash.$childname += $child.InnerText
}
# if it has a #text child we may need to cope with attributes
elseif ($child.FirstChild.Name -eq '#text' -and $child.ChildNodes.Count -eq 1) {
if ($null -ne $child.Attributes -and $KeepAttributes ) {
#hah, an attribute
<#we need to record the text with the #text label and preserve all
the attributes #>
$aHash = [ordered]@{ }
$child.Attributes | ForEach-Object {
$aHash.$($_.FirstChild.parentNode.LocalName) = $_.FirstChild.value
}
#now we add the text with an explicit name
$aHash.'#text' += $child.'#text'
$oHash.$childname += $aHash
}
else {
#phew, just a simple text attribute.
$oHash.$childname += $child.FirstChild.InnerText
}
}
elseif ($null -ne $child.'#cdata-section' ) {
# if it is a data section, a block of text that isnt parsed by the parser,
# but is otherwise recognized as markup
$oHash.$childname = $child.'#cdata-section'
}
elseif ($child.ChildNodes.Count -gt 1 -and
($child | Get-Member -MemberType Property).Count -eq 1) {
$oHash.$childname = @()
foreach ($grandchild in $child.ChildNodes) {
$oHash.$childname += (ConvertFrom-PodeXml $grandchild)
}
}
else {
# create an array as a value to the hashtable element
$oHash.$childname += (ConvertFrom-PodeXml $child)
}
}
return $oHash
}
<#
.SYNOPSIS
Adds a Verb for a TCP data.
.DESCRIPTION
Adds a Verb for a TCP data.
.PARAMETER Verb
The Verb for the Verb.
.PARAMETER ScriptBlock
A ScriptBlock for the Verb's main logic.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) this Verb should be bound against.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Verb's main logic.
.PARAMETER ArgumentList
An array of arguments to supply to the Verb's ScriptBlock.
.PARAMETER UpgradeToSsl
If supplied, the Verb will auto-upgrade the connection to use SSL.
.PARAMETER Close
If supplied, the Verb will auto-close the connection.
.EXAMPLE
Add-PodeVerb -Verb 'Hello' -ScriptBlock { /* logic */ }
.EXAMPLE
Add-PodeVerb -Verb 'Hello' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
.EXAMPLE
Add-PodeVerb -Verb 'Quit' -Close
.EXAMPLE
Add-PodeVerb -Verb 'StartTls' -UpgradeToSsl
#>
function Add-PodeVerb {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$Verb,
[Parameter(ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[object[]]
$ArgumentList,
[Parameter()]
[string[]]
$EndpointName,
[switch]
$UpgradeToSsl,
[switch]
$Close
)
# find placeholder parameters in verb (ie: COMMAND :parameter)
$Verb = Resolve-PodePlaceholders -Path $Verb
# get endpoints from name
$endpoints = Find-PodeEndpoints -EndpointName $EndpointName
# ensure the verb doesn't already exist for each endpoint
foreach ($_endpoint in $endpoints) {
Test-PodeVerbAndError -Verb $Verb -Protocol $_endpoint.Protocol -Address $_endpoint.Address
}
# if scriptblock and file path are all null/empty, error
if ((Test-PodeIsEmpty $ScriptBlock) -and (Test-PodeIsEmpty $FilePath) -and !$Close -and !$UpgradeToSsl) {
throw "[Verb] $($Verb): No logic passed"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# add the verb(s)
Write-Verbose "Adding Verb: $($Verb)"
$PodeContext.Server.Verbs[$Verb] += @(foreach ($_endpoint in $endpoints) {
@{
Logic = $ScriptBlock
UsingVariables = $usingVars
Endpoint = @{
Protocol = $_endpoint.Protocol
Address = $_endpoint.Address.Trim()
Name = $_endpoint.Name
}
Arguments = $ArgumentList
Verb = $Verb
Connection = @{
UpgradeToSsl = $UpgradeToSsl
Close = $Close
}
}
})
}
<#
.SYNOPSIS
Remove a specific Verb.
.DESCRIPTION
Remove a specific Verb.
.PARAMETER Verb
The Verb of the Verb to remove.
.PARAMETER EndpointName
The EndpointName of an Endpoint(s) bound to the Verb to be removed.
.EXAMPLE
Remove-PodeVerb -Verb 'Hello'
.EXAMPLE
Remove-PodeVerb -Verb 'Hello :username' -EndpointName User
#>
function Remove-PodeVerb {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Verb,
[Parameter()]
[string]
$EndpointName
)
# ensure the verb placeholders are replaced
$Verb = Resolve-PodePlaceholders -Path $Verb
# ensure verb does exist
if (!$PodeContext.Server.Verbs.Contains($Verb)) {
return
}
# remove the verb's logic
$PodeContext.Server.Verbs[$Verb] = @($PodeContext.Server.Verbs[$Verb] | Where-Object {
$_.Endpoint.Name -ine $EndpointName
})
# if the verb has no more logic, just remove it
if ((Get-PodeCount $PodeContext.Server.Verbs[$Verb]) -eq 0) {
$null = $PodeContext.Server.Verbs.Remove($Verb)
}
}
<#
.SYNOPSIS
Removes all added Verbs.
.DESCRIPTION
Removes all added Verbs.
.EXAMPLE
Clear-PodeVerbs
#>
function Clear-PodeVerbs {
[CmdletBinding()]
param()
$PodeContext.Server.Verbs.Clear()
}
<#
.SYNOPSIS
Get a Verb(s).
.DESCRIPTION
Get a Verb(s).
.PARAMETER Verb
A Verb to filter the verbs.
.PARAMETER EndpointName
The name of an endpoint to filter verbs.
.EXAMPLE
Get-PodeVerb -Verb 'Hello'
.EXAMPLE
Get-PodeVerb -Verb 'Hello :username' -EndpointName User
#>
function Get-PodeVerb {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Verb,
[Parameter()]
[string[]]
$EndpointName
)
# start off with every verb
$verbs = @()
# if we have a verb, filter
if (![string]::IsNullOrWhiteSpace($Verb)) {
$Verb = Resolve-PodePlaceholders -Path $Verb
$verbs = $PodeContext.Server.Verbs[$Verb]
}
else {
foreach ($v in $PodeContext.Server.Verbs.Values) {
$verbs += $v
}
}
# further filter by endpoint names
if (($null -ne $EndpointName) -and ($EndpointName.Length -gt 0)) {
$verbs = @(foreach ($name in $EndpointName) {
foreach ($v in $verbs) {
if ($v.Endpoint.Name -ine $name) {
continue
}
$v
}
})
}
# return
return $verbs
}
<#
.SYNOPSIS
Automatically loads verb ps1 files
.DESCRIPTION
Automatically loads verb ps1 files from either a /verbs folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
.EXAMPLE
Use-PodeVerbs
.EXAMPLE
Use-PodeVerbs -Path './my-verbs'
#>
function Use-PodeVerbs {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Path
)
Use-PodeFolder -Path $Path -DefaultPath 'verbs'
}
using namespace Pode
<#
.SYNOPSIS
Set the maximum number of concurrent WebSocket connection threads.
.DESCRIPTION
Set the maximum number of concurrent WebSocket connection threads.
.PARAMETER Maximum
The Maximum number of threads available to process WebSocket connection messages received.
.EXAMPLE
Set-PodeWebSocketConcurrency -Maximum 5
#>
function Set-PodeWebSocketConcurrency {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[int]
$Maximum
)
# error if <=0
if ($Maximum -le 0) {
throw "Maximum concurrent WebSocket threads must be >=1 but got: $($Maximum)"
}
# add 1, for the waiting script
$Maximum++
# ensure max > min
$_min = 1
if ($null -ne $PodeContext.RunspacePools.WebSockets) {
$_min = $PodeContext.RunspacePools.WebSockets.Pool.GetMinRunspaces()
}
if ($_min -gt $Maximum) {
throw "Maximum concurrent WebSocket threads cannot be less than the minimum of $($_min) but got: $($Maximum)"
}
# set the max tasks
$PodeContext.Threads.WebSockets = $Maximum
if ($null -ne $PodeContext.RunspacePools.WebSockets) {
$PodeContext.RunspacePools.WebSockets.Pool.SetMaxRunspaces($Maximum)
}
}
<#
.SYNOPSIS
Connect to an external WebSocket.
.DESCRIPTION
Connect to an external WebSocket.
.PARAMETER Name
The Name of the WebSocket connection.
.PARAMETER Url
The URL of the WebSocket. Should start with either ws:// or wss://.
.PARAMETER ScriptBlock
The ScriptBlock to invoke for processing received messages from the WebSocket. The ScriptBlock will have access to a $WsEvent variable with details of the received message.
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the WebSocket's logic.
.PARAMETER ContentType
An optional ContentType for parsing/converting received/sent messages. (default: application/json)
.PARAMETER ArgumentList
AN optional array of extra arguments, that will be passed to the ScriptBlock.
.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -ScriptBlock { ... }
.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -ScriptBlock { param($arg1, $arg2) ... } -ArgumentList 'arg1', 'arg2'
.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -FilePath './some/path/file.ps1'
.EXAMPLE
Connect-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket' -ScriptBlock { ... } -ContentType 'text/xml'
#>
function Connect-PodeWebSocket {
[CmdletBinding(DefaultParameterSetName = 'Script')]
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[Parameter(Mandatory = $true)]
[string]
$Url,
[Parameter(ParameterSetName = 'Script')]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory = $true, ParameterSetName = 'File')]
[string]
$FilePath,
[Parameter()]
[string]
$ContentType = 'application/json',
[Parameter()]
[object[]]
$ArgumentList
)
# ensure we have a receiver
New-PodeWebSocketReceiver
# fail if already exists
if (Test-PodeWebSocket -Name $Name) {
throw "Already connected to websocket with name '$($Name)'"
}
# if we have a file path supplied, load that path as a scriptblock
if ($PSCmdlet.ParameterSetName -ieq 'file') {
$ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
}
# check for scoped vars
$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
# connect
try {
$PodeContext.Server.WebSockets.Receiver.ConnectWebSocket($Name, $Url, $ContentType)
}
catch {
throw "Failed to connect to websocket: $($_.Exception.Message)"
}
$PodeContext.Server.WebSockets.Connections[$Name] = @{
Name = $Name
Url = $Url
Logic = $ScriptBlock
UsingVariables = $usingVars
Arguments = $ArgumentList
}
}
<#
.SYNOPSIS
Disconnect from a WebSocket connection.
.DESCRIPTION
Disconnect from a WebSocket connection. These connections can be reconnected later using Reset-PodeWebSocket
.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).
.EXAMPLE
Disconnect-PodeWebSocket -Name 'Example'
#>
function Disconnect-PodeWebSocket {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Name
)
if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
$Name = $WsEvent.Request.WebSocket.Name
}
if ([string]::IsNullOrWhiteSpace($Name)) {
throw 'No Name for a WebSocket to disconnect from supplied'
}
if (Test-PodeWebSocket -Name $Name) {
$PodeContext.Server.WebSockets.Receiver.DisconnectWebSocket($Name)
}
}
<#
.SYNOPSIS
Remove a WebSocket connection.
.DESCRIPTION
Disconnects and then removes a WebSocket connection.
.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).
.EXAMPLE
Remove-PodeWebSocket -Name 'Example'
#>
function Remove-PodeWebSocket {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Name
)
if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
$Name = $WsEvent.Request.WebSocket.Name
}
if ([string]::IsNullOrWhiteSpace($Name)) {
throw 'No Name for a WebSocket to remove supplied'
}
$PodeContext.Server.WebSockets.Receiver.RemoveWebSocket($Name)
$PodeContext.Server.WebSockets.Connections.Remove($Name)
}
<#
.SYNOPSIS
Send a message back to a WebSocket connection.
.DESCRIPTION
Send a message back to a WebSocket connection.
.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).
.PARAMETER Message
The Message to send. Can either be a raw string, hashtable, or psobject. Non-strings will be parsed to JSON, or the WebSocket's ContentType.
.PARAMETER Depth
An optional Depth to parse any JSON or XML messages. (default: 10)
.PARAMETER Type
An optional message Type. (default: Text)
.EXAMPLE
Send-PodeWebSocket -Name 'Example' -Message @{ message = 'Hello, there' }
#>
function Send-PodeWebSocket {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Name,
[Parameter()]
$Message,
[Parameter()]
[int]
$Depth = 10,
[Parameter()]
[ValidateSet('Text', 'Binary')]
[string]
$Type = 'Text'
)
# get ws name
if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
$Name = $WsEvent.Request.WebSocket.Name
}
# do we have a name?
if ([string]::IsNullOrWhiteSpace($Name)) {
throw 'No Name for a WebSocket to send message to supplied'
}
# do the socket exist?
if (!(Test-PodeWebSocket -Name $Name)) {
return
}
# get the websocket
$ws = $PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name)
# parse message
$Message = ConvertTo-PodeResponseContent -InputObject $Message -ContentType $ws.ContentType -Depth $Depth
# send message
$ws.Send($Message, $Type)
}
<#
.SYNOPSIS
Reset an existing WebSocket connection.
.DESCRIPTION
Reset an existing WebSocket connection, either using it's current URL or a new one.
.PARAMETER Name
The Name of the WebSocket connection (optional if in the scope where $WsEvent is available).
.PARAMETER Url
An optional new URL to reset the connection to. If not supplied, the connection's original URL will be used.
.EXAMPLE
Reset-PodeWebSocket -Name 'Example'
.EXAMPLE
Reset-PodeWebSocket -Name 'Example' -Url 'ws://example.com/some/socket'
#>
function Reset-PodeWebSocket {
[CmdletBinding()]
param(
[Parameter()]
[string]
$Name,
[Parameter()]
[string]
$Url
)
if ([string]::IsNullOrWhiteSpace($Name) -and ($null -ne $WsEvent)) {
$WsEvent.Request.WebSocket.Reconnect($Url)
return
}
if ([string]::IsNullOrWhiteSpace($Name)) {
throw 'No Name for a WebSocket to reset supplied'
}
if (Test-PodeWebSocket -Name $Name) {
$PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name).Reconnect($Url)
}
}
<#
.SYNOPSIS
Test whether an WebSocket connection exists.
.DESCRIPTION
Test whether an WebSocket connection exists for the given Name.
.PARAMETER Name
The Name of the WebSocket connection.
.EXAMPLE
Test-PodeWebSocket -Name 'Example'
#>
function Test-PodeWebSocket {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$Name
)
$found = ($null -ne $PodeContext.Server.WebSockets.Receiver.GetWebSocket($Name))
if ($found) {
return $true
}
if ($PodeContext.Server.WebSockets.Connections.ContainsKey($Name)) {
Remove-PodeWebSocket -Name $Name
}
return $false
}
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy.
This embedded PowerShell module is packaged and distributed by the author.
The contents of which can be found on the releases pages at <https://github.com/Badgerati/Pode/releases>.
To verify contents, either:
1. Compare the Checksum on the release notes against the Module's source.
2. Download the zip from the release, run 'checksum -t sha256' on it, and compare.
function Remove-PodeModule($path)
{
$path = Join-Path $path 'Pode'
if (Test-Path $path)
{
Write-Host "Deleting module directory: $($path)"
Remove-Item -Path $path -Recurse -Force | Out-Null
if (!$?) {
throw "Failed to delete: $path"
}
}
}
# Determine which Program Files path to use
$progFiles = [string]$env:ProgramFiles
# Remove PS Module
# Set the module path
$modulePath = Join-Path $progFiles (Join-Path 'WindowsPowerShell' 'Modules')
# Delete module
Remove-PodeModule $modulePath
# Remove PS-Core Module
$def = (Get-Command pwsh -ErrorAction SilentlyContinue).Definition
if (![string]::IsNullOrWhiteSpace($def))
{
# Set the module path
$modulePath = Join-Path $progFiles (Join-Path 'PowerShell' 'Modules')
# Delete module
Remove-PodeModule $modulePath
}
Log in or click on link to see number of positives.
- pode.2.10.1.nupkg (da28e1624416) - ## / 68
- Pode.dll (d204933feeca) - ## / 72
- Pode.dll (906ebf752df0) - ## / 74
- Pode.dll (a3a8a13b21f3) - ## / 65
In cases where actual malware is found, the packages are subject to removal. Software sometimes has false positives. Moderators do not necessarily validate the safety of the underlying software, only that a package retrieves software from the official distribution point and/or validate embedded software against official distribution point (where distribution rights allow redistribution).
Chocolatey Pro provides runtime protection from possible malware.
Add to Builder | Version | Downloads | Last Updated | Status |
---|---|---|---|---|
Pode 2.10.1 | 95 | Monday, May 27, 2024 | Approved | |
Pode 2.10.0 | 71 | Monday, April 15, 2024 | Approved | |
Pode 2.9.0 | 196 | Monday, October 30, 2023 | Approved | |
Pode 2.8.0 | 247 | Friday, February 3, 2023 | Approved | |
Pode 2.7.2 | 143 | Tuesday, October 25, 2022 | Approved | |
Pode 2.7.1 | 120 | Thursday, July 21, 2022 | Approved | |
Pode 2.7.0 | 130 | Wednesday, June 22, 2022 | Approved | |
Pode 2.6.2 | 175 | Wednesday, March 2, 2022 | Approved | |
Pode 2.6.1 | 95 | Monday, February 21, 2022 | Approved | |
Pode 2.6.0 | 97 | Thursday, February 10, 2022 | Approved | |
Pode 2.5.2 | 110 | Tuesday, January 4, 2022 | Approved | |
Pode 2.5.1 | 108 | Tuesday, December 21, 2021 | Approved | |
Pode 2.5.0 | 119 | Saturday, November 13, 2021 | Approved | |
Pode 2.4.2 | 137 | Monday, September 13, 2021 | Approved | |
Pode 2.4.1 | 122 | Monday, August 9, 2021 | Approved | |
Pode 2.4.0 | 102 | Wednesday, July 21, 2021 | Approved | |
Pode 2.3.0 | 127 | Tuesday, June 1, 2021 | Approved | |
Pode 2.2.3 | 143 | Saturday, April 10, 2021 | Approved | |
Pode 2.2.2 | 97 | Friday, April 9, 2021 | Approved | |
Pode 2.2.1 | 98 | Saturday, March 27, 2021 | Approved | |
Pode 2.2.0 | 121 | Sunday, March 21, 2021 | Approved | |
Pode 2.1.1 | 126 | Friday, February 19, 2021 | Approved | |
Pode 2.1.0 | 1170 | Wednesday, February 3, 2021 | Approved | |
Pode 2.0.3 | 159 | Monday, December 21, 2020 | Approved | |
Pode 2.0.2 | 127 | Saturday, December 5, 2020 | Approved | |
Pode 2.0.1 | 118 | Sunday, November 29, 2020 | Approved | |
Pode 2.0.0 | 186 | Saturday, November 14, 2020 | Approved | |
Pode 1.8.4 | 180 | Friday, October 16, 2020 | Approved | |
Pode 1.8.3 | 165 | Sunday, September 20, 2020 | Approved | |
Pode 1.8.2 | 201 | Friday, July 31, 2020 | Approved | |
Pode 1.8.1 | 180 | Friday, June 26, 2020 | Approved | |
Pode 1.8.0 | 194 | Sunday, May 24, 2020 | Approved | |
Pode 1.7.3 | 193 | Sunday, May 10, 2020 | Approved | |
Pode 1.7.2 | 169 | Monday, April 27, 2020 | Approved | |
Pode 1.7.1 | 170 | Friday, April 17, 2020 | Approved | |
Pode 1.7.0 | 178 | Friday, April 10, 2020 | Approved | |
Pode 1.6.1 | 233 | Saturday, March 7, 2020 | Approved | |
Pode 1.6.0 | 194 | Tuesday, March 3, 2020 | Approved | |
Pode 1.5.0 | 230 | Sunday, February 2, 2020 | Approved | |
Pode 1.4.0 | 207 | Friday, January 10, 2020 | Approved | |
Pode 1.3.0 | 191 | Friday, December 27, 2019 | Approved | |
Pode 1.2.1 | 209 | Monday, December 2, 2019 | Approved | |
Pode 1.2.0 | 195 | Wednesday, November 13, 2019 | Approved | |
Pode 1.1.0 | 212 | Saturday, September 28, 2019 | Approved | |
Pode 1.0.1 | 217 | Wednesday, September 4, 2019 | Approved | |
Pode 1.0.0 | 200 | Monday, September 2, 2019 | Approved | |
Pode 0.32.0 | 240 | Friday, June 28, 2019 | Approved | |
Pode 0.31.0 | 208 | Tuesday, June 11, 2019 | Approved | |
Pode 0.30.0 | 204 | Sunday, May 26, 2019 | Approved | |
Pode 0.29.0 | 200 | Friday, May 10, 2019 | Approved | |
Pode 0.28.1 | 240 | Tuesday, April 16, 2019 | Approved | |
Pode 0.28.0 | 189 | Saturday, April 13, 2019 | Approved | |
Pode 0.27.3 | 208 | Thursday, April 4, 2019 | Approved | |
Pode 0.27.2 | 227 | Wednesday, March 27, 2019 | Approved | |
Pode 0.27.1 | 219 | Saturday, March 16, 2019 | Approved | |
Pode 0.27.0 | 218 | Thursday, March 14, 2019 | Approved | |
Pode 0.26.0 | 241 | Sunday, February 17, 2019 | Approved | |
Pode 0.25.0 | 236 | Tuesday, February 5, 2019 | Approved | |
Pode 0.24.0 | 264 | Friday, January 18, 2019 | Approved | |
Pode 0.23.0 | 253 | Monday, December 24, 2018 | Approved | |
Pode 0.22.0 | 247 | Friday, December 7, 2018 | Approved | |
Pode 0.21.0 | 268 | Friday, November 2, 2018 | Approved | |
Pode 0.20.0 | 273 | Saturday, October 20, 2018 | Approved | |
Pode 0.19.1 | 235 | Tuesday, October 9, 2018 | Approved | |
Pode 0.19.0 | 253 | Friday, September 14, 2018 | Approved | |
Pode 0.18.0 | 245 | Saturday, August 25, 2018 | Approved | |
Pode 0.17.0 | 214 | Sunday, August 19, 2018 | Approved | |
Pode 0.16.0 | 261 | Wednesday, August 8, 2018 | Approved | |
Pode 0.15.0 | 282 | Friday, July 13, 2018 | Approved | |
Pode 0.14.0 | 263 | Friday, July 6, 2018 | Approved | |
Pode 0.13.0 | 258 | Saturday, June 23, 2018 | Approved | |
Pode 0.12.0 | 251 | Friday, June 15, 2018 | Approved | |
Pode 0.11.3 | 288 | Sunday, June 10, 2018 | Approved | |
Pode 0.11.2 | 280 | Friday, June 8, 2018 | Approved | |
Pode 0.11.1 | 305 | Friday, June 1, 2018 | Approved | |
Pode 0.11.0 | 268 | Wednesday, May 30, 2018 | Approved | |
Pode 0.10.1 | 323 | Wednesday, May 16, 2018 | Approved | |
Pode 0.9.0 | 346 | Thursday, January 11, 2018 | Approved |
Copyright 2017-2023
This package has no dependencies.
Ground Rules:
- This discussion is only about Pode and the Pode package. If you have feedback for Chocolatey, please contact the Google Group.
- This discussion will carry over multiple versions. If you have a comment about a particular version, please note that in your comments.
- The maintainers of this Chocolatey Package will be notified about new comments that are posted to this Disqus thread, however, it is NOT a guarantee that you will get a response. If you do not hear back from the maintainers after posting a message below, please follow up by using the link on the left side of this page or follow this link to contact maintainers. If you still hear nothing back, please follow the package triage process.
- Tell us what you love about the package or Pode, or tell us what needs improvement.
- Share your experiences with the package, or extra configuration or gotchas that you've found.
- If you use a url, the comment will be flagged for moderation until you've been whitelisted. Disqus moderated comments are approved on a weekly schedule if not sooner. It could take between 1-5 days for your comment to show up.