Downloads:
15,800
Downloads of v 0.25.0:
232
Last Update:
05 Feb 2019
Package Maintainer(s):
Software Author(s):
- Badgerati
Tags:
pode powershell web server rest api http tcp smtp listener webpages json xml html unix cross-platform access-control file-monitoring multithreaded rate-limiting cron schedule middleware session authentication active-directory- 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
This is not the latest version of Pode available.
- 1
- 2
- 3
0.25.0 | Updated: 05 Feb 2019
- 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:
15,800
Downloads of v 0.25.0:
232
Maintainer(s):
Software Author(s):
- Badgerati
Pode 0.25.0
This is not the latest version of Pode available.
- 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 --version=0.25.0 --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'" --version="'0.25.0'" [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'" --version="'0.25.0'"
$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: '0.25.0'
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 '0.25.0'
end
See docs at https://docs.chef.io/resource_chocolatey_package.html.
cChocoPackageInstaller pode
{
Name = "pode"
Version = "0.25.0"
Source = "INTERNAL REPO URL"
}
Requires cChoco DSC Resource. See docs at https://github.com/chocolatey/cChoco.
package { 'pode':
ensure => '0.25.0',
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 05 Feb 2019.
Pode is a Cross-Platform PowerShell framework for creating web servers to host REST APIs, Web Sites, and TCP/SMTP Servers
Features
- Cross-platform using PowerShell Core (with support for PS4.0+)
- Listen on a single or multiple IP address/hostnames
- Support for HTTP, HTTPS, TCP and SMTP
- Host REST APIs, Web Pages, and Static Content (with caching)
- 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 request logging to CLI, Files, and custom loggers to other services like LogStash
- Cross-state variable access across multiple runspaces
- Optional file monitoring to trigger internal server restart on file changes
- 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
- Authentication on requests, such as Basic and Windows Active Directory
- (Windows) Generate/bind self-signed certificates, and signed certificates
- (Windows) Open the hosted server as a desktop application
The MIT License (MIT)
Copyright (c) [2017-2018] [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.
#
# 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 = '0.25.0'
# 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-2018 Matthew Kelly (Badgerati), licensed under the MIT License.'
# Description of the functionality provided by this module
Description = 'Pode is a Cross-Platform PowerShell framework for creating web servers to host REST APIs, Web Sites, and TCP/SMTP Servers'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '3.0'
# Functions to export from this Module
FunctionsToExport = @(
'Route',
'Get-PodeRoute',
'Handler',
'Get-PodeTcpHandler',
'Get-SmtpEmail',
'Tcp',
'Server',
'Engine',
'Start-SmtpServer',
'Start-TcpServer',
'Start-WebServer',
'Html',
'Json',
'Write-ToResponse',
'Write-ToResponseFromFile',
'View',
'Xml',
'Pode',
'Timer',
'Logger',
'Csv',
'Test-IsUnix',
'Test-IsWindows',
'Test-IsPSCore',
'Status',
'Redirect',
'Include',
'Lock',
'State',
'Listen',
'Access',
'Limit',
'Stopwatch',
'Dispose',
'Stream',
'Schedule',
'Middleware',
'Endware',
'Session',
'Invoke-ScriptBlock',
'Auth',
'Attach',
'Script',
'Import',
'Coalesce',
'Save',
'Get-PodeConfiguration',
'Root'
)
# 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', 'listener', 'rest', 'api', 'tcp', 'smtp', 'websites',
'powershell-core', 'windows', 'unix', 'linux', 'pode', 'PSEdition_Core', 'cross-platform', 'access-control',
'file-monitoring', 'multithreaded', 'rate-limiting', 'cron', 'schedule', 'middleware', 'session',
'authentication', 'active-directory', 'caching')
# 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://cdn.rawgit.com/Badgerati/Pode/master/images/icon.png'
}
}
}
# get existing functions from memory for later comparison
$sysfuncs = Get-ChildItem Function:
# load pode functions
$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
Get-ChildItem "$($root)\Tools\*.ps1" | Resolve-Path | ForEach-Object { . $_ }
# check if there are any extensions and load them
$ext = 'C:/Pode/Extensions'
if ($PSVersionTable.Platform -ieq 'unix') {
$ext = '/usr/src/pode/extensions'
}
if (Test-Path $ext) {
Get-ChildItem "$($ext)/*.ps1" | Resolve-Path | ForEach-Object { . $_ }
}
# get functions from memory and compare to existing to find new functions added
$podefuncs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }
# export the module
Export-ModuleMember -Function ($podefuncs.Name)
function Auth
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('use', 'check')]
[Alias('a')]
[string]
$Action,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('n')]
[string]
$Name,
[Parameter()]
[Alias('v')]
[object]
$Validator,
[Parameter()]
[Alias('p')]
[scriptblock]
$Parser,
[Parameter()]
[Alias('o')]
[hashtable]
$Options,
[Parameter()]
[Alias('t')]
[string]
$Type,
[switch]
[Alias('c')]
$Custom
)
# for the 'use' action, ensure we have a validator. and a parser for custom types
if ($Action -ieq 'use') {
# was a validator passed
if (Test-Empty $Validator) {
throw "Authentication method '$($Name)' is missing required Validator script"
}
# is the validator a string/scriptblock?
$vTypes = @('string', 'scriptblock')
if ($vTypes -inotcontains (Get-Type $Validator).Name) {
throw "Authentication method '$($Name)' has an invalid validator supplied, should be one of: $($vTypes -join ', ')"
}
# don't fail if custom and type supplied, and it's already defined
if ($Custom)
{
$typeDefined = (![string]::IsNullOrWhiteSpace($Type) -and $PodeContext.Server.Authentications.ContainsKey($Type))
if (!$typeDefined -and (Test-Empty $Parser)) {
throw "Custom authentication method '$($Name)' is missing required Parser script"
}
}
}
# invoke the appropriate auth logic for the action
switch ($Action.ToLowerInvariant())
{
'use' {
Invoke-AuthUse -Name $Name -Type $Type -Validator $Validator -Parser $Parser -Options $Options -Custom:$Custom
}
'check' {
return (Invoke-AuthCheck -Name $Name -Options $Options)
}
}
}
function Invoke-AuthUse
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory=$true)]
[object]
$Validator,
[Parameter()]
[string]
$Type,
[Parameter()]
[scriptblock]
$Parser,
[Parameter()]
[hashtable]
$Options,
[switch]
$Custom
)
# get the auth data
$AuthData = (Get-PodeAuthMethod -Name $Name -Type $Type -Validator $Validator -Parser $Parser -Custom:$Custom)
# ensure the name doesn't already exist
if ($PodeContext.Server.Authentications.ContainsKey($AuthData.Name)) {
throw "Authentication method '$($AuthData.Name)' already defined"
}
# ensure the parser/validators aren't just empty scriptblocks
if (Test-Empty $AuthData.Parser) {
throw "Authentication method '$($AuthData.Name)' is has no Parser ScriptBlock logic defined"
}
if (Test-Empty $AuthData.Validator) {
throw "Authentication method '$($AuthData.Name)' is has no Validator ScriptBlock logic defined"
}
# setup object for auth method
$obj = @{
'Type' = $AuthData.Type;
'Options' = $Options;
'Parser' = $AuthData.Parser;
'Validator' = $AuthData.Validator;
'Custom' = $AuthData.Custom;
}
# apply auth method to session
$PodeContext.Server.Authentications[$AuthData.Name] = $obj
}
function Invoke-AuthCheck
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter()]
[hashtable]
$Options
)
# ensure the auth type exists
if (!$PodeContext.Server.Authentications.ContainsKey($Name)) {
throw "Authentication method '$($Name)' is not defined"
}
# coalesce the options, and set auth type for middleware
$Options = (coalesce $Options @{})
$Options.AuthType = $Name
# setup the middleware logic
$logic = {
param($s)
# Route options for using sessions
$storeInSession = ($s.Middleware.Options.Session -ne $false)
$usingSessions = (!(Test-Empty $s.Session))
# check for logout command
if ($s.Middleware.Options.Logout -eq $true) {
Remove-PodeAuth -Session $s
return (Set-PodeAuthStatus -StatusCode 302 -Options $s.Middleware.Options)
}
# if the session already has a user/isAuth'd, then setup method and return
if ($usingSessions -and !(Test-Empty $s.Session.Data.Auth.User) -and $s.Session.Data.Auth.IsAuthenticated) {
$s.Auth = $s.Session.Data.Auth
return (Set-PodeAuthStatus -Options $s.Middleware.Options)
}
# check if the login flag is set, in which case just return
if ($s.Middleware.Options.Login -eq $true) {
Remove-PodeSessionCookie -Response $s.Response -Session $s.Session
return $true
}
# get the auth type
$auth = $PodeContext.Server.Authentications[$s.Middleware.Options.AuthType]
# validate the request and get a user
try {
# if it's a custom type the parser will return the data for use to pass to the validator
if ($auth.Custom) {
$data = (Invoke-ScriptBlock -ScriptBlock $auth.Parser -Arguments @($s, $auth.Options) -Return -Splat)
$data += $auth.Options
$result = (Invoke-ScriptBlock -ScriptBlock $auth.Validator -Arguments $data -Return -Splat)
}
else {
$result = (Invoke-ScriptBlock -ScriptBlock $auth.Parser -Arguments @($s, $auth) -Return -Splat)
}
}
catch {
$_.Exception | Out-Default
return (Set-PodeAuthStatus -StatusCode 500 -Options $s.Middleware.Options)
}
# if there is no result return false (failed auth)
if ((Test-Empty $result) -or (Test-Empty $result.User)) {
return (Set-PodeAuthStatus -StatusCode (coalesce $result.Code 401) `
-Description $result.Message -Options $s.Middleware.Options)
}
# assign the user to the session, and wire up a quick method
$s.Auth = @{}
$s.Auth.User = $result.User
$s.Auth.IsAuthenticated = $true
$s.Auth.Store = $storeInSession
# continue
return (Set-PodeAuthStatus -Options $s.Middleware.Options)
}
# return the middleware
return @{
'Logic' = $logic;
'Options' = $Options;
}
}
function Get-PodeAuthMethod
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory=$true)]
[object]
$Validator,
[Parameter()]
[string]
$Type,
[Parameter()]
[scriptblock]
$Parser,
[switch]
$Custom
)
# set type as name, if no type passed
if ([string]::IsNullOrWhiteSpace($Type)) {
$Type = $Name
}
# if the validator is a string - check and get an inbuilt validator
if ((Get-Type $Validator).Name -ieq 'string') {
$Validator = (Get-PodeAuthValidator -Validator $Validator)
}
# first, is it just a custom type?
if ($Custom) {
# if type supplied, re-use an already defined custom type's parser
if ($PodeContext.Server.Authentications.ContainsKey($Type))
{
$Parser = $PodeContext.Server.Authentications[$Type].Parser
}
return @{
'Name' = $Name;
'Type' = $Type;
'Custom' = $true;
'Parser' = $Parser;
'Validator' = $Validator;
}
}
# otherwise, check the inbuilt ones
switch ($Type.ToLowerInvariant())
{
'basic' {
return (Get-PodeAuthBasic -Name $Name -ScriptBlock $Validator)
}
'form' {
return (Get-PodeAuthForm -Name $Name -ScriptBlock $Validator)
}
}
# if we get here, check if a parser was passed for custom type
if (Test-Empty $Parser) {
throw "Authentication method '$($Type)' does not exist as an inbuilt type, nor has a Parser been passed for a custom type"
}
# a parser was passed, it is a custom type
return @{
'Name' = $Name;
'Type' = $Type;
'Custom' = $true;
'Parser' = $Parser;
'Validator' = $Validator;
}
}
function Remove-PodeAuth
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Session
)
# blank out the auth
$Session.Auth = @{}
# if a session auth is found, blank it
if (!(Test-Empty $Session.Session.Data.Auth)) {
$Session.Session.Data.Remove('Auth')
}
# redirect to a failure url, or onto the current path?
if (Test-Empty $Session.Middleware.Options.FailureUrl) {
$Session.Middleware.Options.FailureUrl = $Session.Request.Url.AbsolutePath
}
# Delete the session (remove from store, blank it, and remove from Response)
Remove-PodeSessionCookie -Response $Session.Response -Session $Session.Session
}
function Set-PodeAuthStatus
{
param (
[Parameter()]
[int]
$StatusCode = 0,
[Parameter()]
[string]
$Description,
[Parameter()]
[hashtable]
$Options
)
# if a statuscode supplied, assume failure
if ($StatusCode -gt 0)
{
# check if we have a failure url redirect
if (!(Test-Empty $Options.FailureUrl)) {
redirect $Options.FailureUrl
}
else {
status $StatusCode $Description
}
return $false
}
# if no statuscode, success
else
{
# check if we have a success url redirect
if (!(Test-Empty $Options.SuccessUrl)) {
redirect $Options.SuccessUrl
return $false
}
return $true
}
}
function Get-PodeAuthValidator
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Validator
)
# source the script for the validator
switch ($Validator.ToLowerInvariant()) {
'windows-ad' {
# Check PowerShell/OS version
$version = $PSVersionTable.PSVersion
if ((Test-IsUnix) -or ($version.Major -eq 6 -and $version.Minor -eq 0)) {
throw 'Windows AD authentication is currently only supported on Windows PowerShell, and Windows PowerShell Core v6.1+'
}
# setup the AD vaidator
return {
param($username, $password, $options)
$fqdn = $options.Fqdn
if (Test-Empty $fqdn) {
$fqdn = $env:USERDNSDOMAIN
}
$ad = (New-Object System.DirectoryServices.DirectoryEntry "LDAP://$($fqdn)", "$($username)", "$($password)")
if (Test-Empty $ad.distinguishedName) {
return $null
}
return @{ 'user' = @{
'Username' = $ad.psbase.username;
'FQDN' = $fqdn;
} }
}
}
default {
throw "An inbuilt validator does not exist for '$($Validator)'"
}
}
}
function Get-PodeAuthBasic
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
$parser = {
param($s, $auth)
# get the auth header
$header = $s.Request.Headers['Authorization']
if ($null -eq $header) {
return @{
'User' = $null;
'Message' = 'No Authorization header found';
'Code' = 401;
}
}
# ensure the first atom is basic (or opt override)
$atoms = $header -isplit '\s+'
$authType = (coalesce $auth.Options.Name 'Basic')
if ($atoms[0] -ine $authType) {
return @{
'User' = $null;
'Message' = "Header is not $($authType) Authorization";
}
}
# decode the aut header
$encType = (coalesce $auth.Options.Encoding 'ISO-8859-1')
try {
$enc = [System.Text.Encoding]::GetEncoding($encType)
}
catch {
return @{
'User' = $null;
'Message' = 'Invalid encoding specified for Authorization';
'Code' = 400;
}
}
try {
$decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1]))
}
catch {
return @{
'User' = $null;
'Message' = 'Invalid Base64 string found in Authorization header';
'Code' = 400;
}
}
# validate and return user/result
$index = $decoded.IndexOf(':')
$u = $decoded.Substring(0, $index)
$p = $decoded.Substring($index + 1)
return (Invoke-ScriptBlock -ScriptBlock $auth.Validator -Arguments @($u, $p, $auth.Options) -Return -Splat)
}
return @{
'Name' = $Name;
'Type' = 'Basic';
'Custom' = $false;
'Parser' = $parser;
'Validator' = $ScriptBlock;
}
}
function Get-PodeAuthForm
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
$parser = {
param($s, $auth)
# get user/pass keys to get from payload
$userField = (coalesce $auth.Options.UsernameField 'username')
$passField = (coalesce $auth.Options.PasswordField 'password')
# get the user/pass
$username = $s.Data.$userField
$password = $s.Data.$passField
# if either are empty, deny
if ((Test-Empty $username) -or (Test-Empty $password)) {
return @{
'User' = $null;
'Message' = 'Username or Password not supplied';
'Code' = 401;
}
}
# validate and return
return (Invoke-ScriptBlock -ScriptBlock $auth.Validator -Arguments @($username, $password, $auth.Options) -Return -Splat)
}
return @{
'Name' = $Name;
'Type' = 'Form';
'Custom' = $false;
'Parser' = $parser;
'Validator' = $ScriptBlock;
}
}
function Get-PodeContentType
{
param (
[Parameter()]
[string]
$Extension,
[switch]
$DefaultIsNull
)
if ($Extension -eq $null) {
$Extension = '.'
}
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' }
'.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' }
'.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' }
'.master' { return 'application/xml' }
'.mbox' { return 'application/mbox' }
'.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' }
'.z' { return 'application/x-compress' }
'.zip' { return 'application/zip'}
default { return (iftet $DefaultIsNull $null 'text/plain') }
}
}
function New-PodeContext
{
param (
[scriptblock]
$ScriptBlock,
[int]
$Threads = 1,
[int]
$Interval = 0,
[string]
$ServerRoot,
[string]
$Name = $null,
[string[]]
$FileMonitorExclude = $null,
[string[]]
$FileMonitorInclude = $null,
[switch]
$DisableLogging,
[switch]
$FileMonitor
)
# set a random server name if one not supplied
if (Test-Empty $Name) {
$Name = Get-RandomName
}
# ensure threads are always >0
if ($Threads -le 0) {
$Threads = 1
}
# basic context object
$ctx = New-Object -TypeName psobject |
Add-Member -MemberType NoteProperty -Name Threads -Value $Threads -PassThru |
Add-Member -MemberType NoteProperty -Name Timers -Value @{} -PassThru |
Add-Member -MemberType NoteProperty -Name Schedules -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 Tokens -Value @{} -PassThru |
Add-Member -MemberType NoteProperty -Name RequestsToLog -Value $null -PassThru |
Add-Member -MemberType NoteProperty -Name Lockable -Value $null -PassThru |
Add-Member -MemberType NoteProperty -Name Server -Value @{} -PassThru
# set the server name, logic and root
$ctx.Server.Name = $Name
$ctx.Server.Root = $ServerRoot
$ctx.Server.Logic = $ScriptBlock
$ctx.Server.Interval = $Interval
# check if there is any global configuration
$ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx
# setup file monitoring details (code has priority over config)
if (!(Test-Empty $ctx.Server.Configuration)) {
if (!$FileMonitor) {
$FileMonitor = [bool]$ctx.Server.Configuration.server.fileMonitor.enable
}
if (Test-Empty $FileMonitorExclude) {
$FileMonitorExclude = @($ctx.Server.Configuration.server.fileMonitor.exclude)
}
if (Test-Empty $FileMonitorInclude) {
$FileMonitorInclude = @($ctx.Server.Configuration.server.fileMonitor.include)
}
}
$ctx.Server.FileMonitor = @{
'Enabled' = $FileMonitor;
'Exclude' = (Convert-PathPatternsToRegex -Paths $FileMonitorExclude);
'Include' = (Convert-PathPatternsToRegex -Paths $FileMonitorInclude);
}
# set the server default type
$ctx.Server.Type = ([string]::Empty)
if ($Interval -gt 0) {
$ctx.Server.Type = 'SERVICE'
}
# set the IP address details
$ctx.Server.Endpoints = @()
# setup gui details
$ctx.Server.Gui = @{
'Enabled' = $false;
'Name' = $null;
'Icon' = $null;
'State' = 'Normal';
'ShowInTaskbar' = $true;
'WindowStyle' = 'SingleBorderWindow';
}
# shared temp drives
$ctx.Server.Drives = @{}
$ctx.Server.InbuiltDrives = @{}
# shared state between runspaces
$ctx.Server.State = @{}
# view engine for rendering pages
$ctx.Server.ViewEngine = @{
'Engine' = 'html';
'Extension' = 'html';
'Script' = $null;
}
# routes for pages and api
$ctx.Server.Routes = @{
'delete' = @{};
'get' = @{};
'head' = @{};
'merge' = @{};
'options' = @{};
'patch' = @{};
'post' = @{};
'put' = @{};
'trace' = @{};
'static' = @{};
'*' = @{};
}
# handlers for tcp
$ctx.Server.Handlers = @{
'tcp' = $null;
'smtp' = $null;
'service' = $null;
}
# 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 = @{
'Session' = @{};
}
# authnetication methods
$ctx.Server.Authentications = @{}
# logging methods
$ctx.Server.Logging = @{
'Methods' = @{};
'Disabled' = $DisableLogging;
}
# create new cancellation tokens
$ctx.Tokens = @{
'Cancellation' = New-Object System.Threading.CancellationTokenSource;
'Restart' = New-Object System.Threading.CancellationTokenSource;
}
# requests that should be logged
$ctx.RequestsToLog = New-Object System.Collections.ArrayList
# middleware that needs to run
$ctx.Server.Middleware = @()
# endware that needs to run
$ctx.Server.Endware = @()
# runspace pools
$ctx.RunspacePools = @{
'Main' = $null;
'Schedules' = $null;
'Gui' = $null;
}
# session state
$ctx.Lockable = [hashtable]::Synchronized(@{})
$state = [initialsessionstate]::CreateDefault()
$state.ImportPSModule((Get-Module -Name Pode).Path)
$_session = New-PodeStateContext $ctx
$variables = @(
(New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PodeContext', $_session, $null),
(New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'Console', $Host, $null)
)
$variables | ForEach-Object {
$state.Variables.Add($_)
}
# setup runspaces
$ctx.Runspaces = @()
# setup main runspace pool
$threadsCounts = @{
'Default' = 1;
'Timer' = 1;
'Log' = 1;
'Schedule' = 1;
'Misc' = 1;
}
$totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum + $Threads
$ctx.RunspacePools.Main = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $state, $Host)
$ctx.RunspacePools.Main.Open()
# setup schedule runspace pool
$ctx.RunspacePools.Schedules = [runspacefactory]::CreateRunspacePool(1, 2, $state, $Host)
$ctx.RunspacePools.Schedules.Open()
# setup gui runspace pool (only for non-ps-core)
if (!(Test-IsPSCore)) {
$ctx.RunspacePools.Gui = [runspacefactory]::CreateRunspacePool(1, 1, $state, $Host)
$ctx.RunspacePools.Gui.ApartmentState = 'STA'
$ctx.RunspacePools.Gui.Open()
}
# return the new context
return $ctx
}
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 RunspacePools -Value $Context.RunspacePools -PassThru |
Add-Member -MemberType NoteProperty -Name Tokens -Value $Context.Tokens -PassThru |
Add-Member -MemberType NoteProperty -Name RequestsToLog -Value $Context.RequestsToLog -PassThru |
Add-Member -MemberType NoteProperty -Name Lockable -Value $Context.Lockable -PassThru |
Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru)
}
function Get-PodeConfiguration
{
return $PodeContext.Server.Configuration
}
function Open-PodeConfiguration
{
param (
[Parameter()]
[string]
$ServerRoot = $null,
[Parameter()]
$Context
)
$config = @{}
# set the path to the root config file
$configPath = (Join-ServerRoot -Folder '.' -FilePath 'pode.json' -Root $ServerRoot)
# check to see if an environmental config exists (if the env var is set)
if (!(Test-Empty $env:PODE_ENVIRONMENT)) {
$_path = (Join-ServerRoot -Folder '.' -FilePath "pode.$($env:PODE_ENVIRONMENT).json" -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 = (Get-Content $configPath -Raw | ConvertFrom-Json)
Set-PodeWebConfiguration -Configuration $config -Context $Context
}
return $config
}
function Set-PodeWebConfiguration
{
param (
[Parameter()]
$Configuration,
[Parameter()]
$Context
)
$Context.Server.Web = @{
'Static' = @{
'Defaults' = $Configuration.web.static.defaults;
'Cache' = @{
'Enabled' = [bool]$Configuration.web.static.cache.enable;
'MaxAge' = [int](coalesce $Configuration.web.static.cache.maxAge 3600);
'Include' = (Convert-PathPatternsToRegex -Paths @($Configuration.web.static.cache.include) -NotSlashes);
'Exclude' = (Convert-PathPatternsToRegex -Paths @($Configuration.web.static.cache.exclude) -NotSlashes);
}
}
}
}
function State
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('set', 'get', 'remove')]
[Alias('a')]
[string]
$Action,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('n')]
[string]
$Name,
[Parameter()]
[Alias('o')]
[object]
$Object
)
try {
if ($null -eq $PodeContext -or $null -eq $PodeContext.Server.State) {
return $null
}
switch ($Action.ToLowerInvariant())
{
'set' {
$PodeContext.Server.State[$Name] = $Object
}
'get' {
$Object = $PodeContext.Server.State[$Name]
}
'remove' {
$Object = $PodeContext.Server.State[$Name]
$PodeContext.Server.State.Remove($Name) | Out-Null
}
}
return $Object
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
function Listen
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('ipp', 'e', 'endpoint')]
[string]
$IPPort,
[Parameter()]
[ValidateSet('HTTP', 'HTTPS', 'SMTP', 'TCP')]
[Alias('t')]
[string]
$Type,
[Parameter()]
[Alias('cert')]
[string]
$Certificate = $null,
[Parameter()]
[Alias('n', 'id')]
[string]
$Name = $null,
[switch]
[Alias('f')]
$Force
)
# parse the endpoint for host/port info
$_endpoint = Get-PodeEndpointInfo -Endpoint $IPPort
# if a name was supplied, check it is unique
if (![string]::IsNullOrWhiteSpace($Name) -and
(Get-Count ($PodeContext.Server.Endpoints | Where-Object { $_.Name -eq $Name })) -ne 0)
{
throw "An endpoint with the name '$($Name)' has already been defined"
}
# new endpoint object
$obj = @{
'Name' = $Name;
'Address' = $null;
'RawAddress' = $IPPort;
'Port' = $null;
'HostName' = 'localhost';
'Ssl' = $false;
'Protocol' = $Type;
'Certificate' = @{
'Name' = $null;
};
}
# set the ip for the context
$obj.Address = (Get-IPAddress $_endpoint.Host)
if (!(Test-IPAddressLocalOrAny -IP $obj.Address)) {
$obj.HostName = "$($obj.Address)"
}
# set the port for the context
$obj.Port = $_endpoint.Port
# if the server type is https, set cert details
if ($Type -ieq 'https') {
$obj.Ssl = $true
$obj.Certificate.Name = $Certificate
}
# if the address is non-local, then check admin privileges
if (!$Force -and !(Test-IPAddressLocal -IP $obj.Address) -and !(Test-IsAdminUser)) {
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 | Where-Object {
($_.Address -eq $obj.Address) -and ($_.Port -eq $obj.Port) -and ($_.Ssl -eq $obj.Ssl)
} | Measure-Object).Count
# has an endpoint already been defined for smtp/tcp?
if (@('smtp', 'tcp') -icontains $Type -and $Type -ieq $PodeContext.Server.Type) {
throw "An endpoint for $($Type.ToUpperInvariant()) has already been defined"
}
if (!$exists) {
# set server type, ensure we aren't trying to change the server's type
$_type = (iftet ($Type -ieq 'https') 'http' $Type)
if ([string]::IsNullOrWhiteSpace($PodeContext.Server.Type)) {
$PodeContext.Server.Type = $_type
}
elseif ($PodeContext.Server.Type -ine $_type) {
throw "Cannot add $($Type.ToUpperInvariant()) endpoint when already listening to $($PodeContext.Server.Type.ToUpperInvariant()) endpoints"
}
# add the new endpoint
$PodeContext.Server.Endpoints += $obj
}
}
function Script
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Path
)
Import -Path $Path
}
function Import
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('p')]
[string]
$Path
)
# ensure the path exists, or it exists as a module
$_path = Resolve-Path -Path $Path -ErrorAction Ignore
if ([string]::IsNullOrWhiteSpace($_path)) {
$_path = (Get-Module -Name $Path -ListAvailable | Select-Object -First 1).Path
}
# if it's still empty, error
if ([string]::IsNullOrWhiteSpace($_path)) {
throw "Failed to import module '$($Path)'"
}
# import the module into each runspace
$PodeContext.RunspacePools.Values | ForEach-Object {
$_.InitialSessionState.ImportPSModule($_path)
}
}
function Get-CronFields
{
return @(
'Minute',
'Hour',
'DayOfMonth',
'Month',
'DayOfWeek'
)
}
function Get-CronFieldConstraints
{
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-CronPredefined
{
return @{
# normal
'@minutely' = '* * * * *';
'@hourly' = '0 * * * *';
'@daily' = '0 0 * * *';
'@weekly' = '0 0 * * 0';
'@monthly' = '0 0 1 * *';
'@quaterly' = '0 0 1 1,4,8,7,10';
'@yearly' = '0 0 1 1 *';
'@annually' = '0 0 1 1 *';
# twice
'@twice-hourly' = '0,30 * * * *';
'@twice-daily' = '0,12 0 * * *';
'@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-CronFieldAliases
{
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-CronExpression
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Expression
)
$Expression = $Expression.Trim()
# check predefineds
$predef = Get-CronPredefined
if (!(Test-Empty $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-CronFields
$constraints = Get-CronFieldConstraints
$aliases = Get-CronFieldAliases
$cron = @{}
for ($i = 0; $i -lt $atoms.Length; $i++)
{
$_cronExp = @{
'Range' = $null;
'Values' = $null;
'Constraints' = $null;
'Random' = $false;
}
$_atom = $atoms[$i]
$_field = $fields[$i]
$_constraint = $constraints.MinMax[$i]
$_aliases = $aliases[$_field]
# replace day of week and months with numbers
switch ($_field)
{
{ $_field -ieq 'month' -or $_field -ieq 'dayofweek' }
{
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
$_atom -imatch $aliasRgx | Out-Null
}
}
}
# ensure atom is a valid value
if (!($_atom -imatch '^[\d|/|*|\-|,r]+$')) {
throw "Invalid atom character: $($_atom)"
}
# replace * with min/max constraint
$_atom = $_atom -ireplace '\*', ($_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-RandomCronExpression
{
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-CronExpression
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Expression,
[Parameter()]
$DateTime = $null
)
function Test-RangeAndValue($AtomContraint, $NowValue) {
if ($null -ne $AtomContraint.Range) {
if ($NowValue -lt $AtomContraint.Range.Min -or $NowValue -gt $AtomContraint.Range.Max) {
return $false
}
}
elseif ($AtomContraint.Values -inotcontains $NowValue) {
return $false
}
return $true
}
# current time
if ($null -eq $DateTime) {
$DateTime = [datetime]::Now
}
# check day of week and day of month (both must fail)
if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfWeek -NowValue ([int]$DateTime.DayOfWeek)) -and
!(Test-RangeAndValue -AtomContraint $Expression.DayOfMonth -NowValue $DateTime.Day)) {
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 Invoke-HMACSHA256Hash
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Value,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Secret
)
$crypto = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($Secret))
return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}
function Invoke-SHA256Hash
{
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-CookieSign
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Value,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Secret
)
return "s:$($Value).$(Invoke-HMACSHA256Hash -Value $Value -Secret $Secret)"
}
function Invoke-CookieUnsign
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Signature,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Secret
)
if (!$Signature.StartsWith('s:')) {
return $null
}
$Signature = $Signature.Substring(2)
$periodIndex = $Signature.LastIndexOf('.')
$value = $Signature.Substring(0, $periodIndex)
$sig = $Signature.Substring($periodIndex + 1)
if ((Invoke-HMACSHA256Hash -Value $value -Secret $Secret) -ne $sig) {
return $null
}
return $value
}
function Invoke-PodeEndware
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$WebEvent,
[Parameter()]
$Endware
)
# if there's no endware, do nothing
if (Test-Empty $Endware) {
return
}
# loop through each of the endware, invoking the next if it returns true
foreach ($eware in @($Endware))
{
try {
Invoke-ScriptBlock -ScriptBlock $eware -Arguments $WebEvent -Scoped | Out-Null
}
catch {
$Error[0] | Out-Default
}
}
}
function Endware
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
# add the scriptblock to array of endware that needs to be run
$PodeContext.Server.Endware += $ScriptBlock
}
function Start-PodeFileMonitor
{
if (!$PodeContext.Server.FileMonitor.Enabled) {
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;
'Exclude' = $PodeContext.Server.FileMonitor.Exclude;
'Include' = $PodeContext.Server.FileMonitor.Include;
}
# setup the events script logic
$action = {
# if there are exclusions, and one matches, return
if (($null -ne $Event.MessageData.Exclude) -and ($Event.SourceEventArgs.Name -imatch $Event.MessageData.Exclude)) {
return
}
# if there are inclusions, and none match, return
if (($null -ne $Event.MessageData.Include) -and ($Event.SourceEventArgs.Name -inotmatch $Event.MessageData.Include)) {
return
}
# 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 {
$Event.MessageData.Context.Tokens.Restart.Cancel()
$Event.Sender.Stop()
} -MessageData @{ 'Context' = $PodeContext; } -SupportEvent
}
function Stop-PodeFileMonitor
{
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 (
[ValidateSet('Create', 'Delete', 'Update')]
[string]
$Type
)
return "PodeFileMonitor$($Type)"
}
function Get-PodeFileMonitorTimerName
{
return 'PodeFileMonitorTimer'
}
function Start-GuiRunspace
{
# do nothing if gui not enabled
if (!$PodeContext.Server.Gui.Enabled) {
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 | Measure-Object).Count -gt 1) {
Write-Host "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
$endpoint = $PodeContext.Server.Gui.Endpoint
if ($null -eq $endpoint) {
$endpoint = $PodeContext.Server.Endpoints[0]
}
$protocol = (iftet $endpoint.Ssl 'https' 'http')
# grab the port
$port = $endpoint.Port
if ($port -eq 0) {
$port = (iftet $endpoint.Ssl 8443 8080)
}
$endpoint = "$($protocol)://$($endpoint.HostName):$($port)"
# poll the server for a response
$count = 0
while ($true) {
try {
Invoke-WebRequest -Method Get -Uri $endpoint -UseBasicParsing -ErrorAction Stop | Out-Null
if (!$?) {
throw
}
break
}
catch {
$count++
if ($count -le 50) {
Start-Sleep -Milliseconds 200
}
else {
throw "Failed to connect to URL: $($endpoint)"
}
}
}
# import the WPF assembly
[System.Reflection.Assembly]::LoadWithPartialName('PresentationFramework') | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName('PresentationCore') | Out-Null
# setup the WPF XAML for the server
$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.Name)`"
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-Empty $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-Empty $PodeContext.Server.Gui.State)) {
$form.WindowState = $PodeContext.Server.Gui.State
}
# get the browser object from XAML and navigate to base page
$form.FindName("WebBrowser").Navigate($endpoint)
# display the form
$form.ShowDialog() | Out-Null
Start-Sleep -Seconds 1
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
finally {
# invoke the cancellation token to close the server
$PodeContext.Tokens.Cancellation.Cancel()
}
}
Add-PodeRunspace -Type 'Gui' -ScriptBlock $script
}
function Gui
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('n')]
[string]
$Name,
[Parameter()]
[Alias('o')]
[hashtable]
$Options
)
# only valid for Windows PowerShell
if (Test-IsPSCore) {
throw 'The gui function is currently unavailable for PS Core, and only works for Windows PowerShell'
}
# enable the gui
$PodeContext.Server.Gui.Enabled = $true
$PodeContext.Server.Gui.Name = $Name
# if we have options, set them up
if (!(Test-Empty $Options)) {
if (!(Test-Empty $Options.Icon)) {
$PodeContext.Server.Gui['Icon'] = (Resolve-Path $Options.Icon).Path
}
if (!(Test-Empty $Options.ShowInTaskbar)) {
$PodeContext.Server.Gui['ShowInTaskbar'] = $Options.ShowInTaskbar
}
if (!(Test-Empty $Options.State)) {
$PodeContext.Server.Gui['State'] = $Options.State
}
if (!(Test-Empty $Options.WindowStyle)) {
$PodeContext.Server.Gui['WindowStyle'] = $Options.WindowStyle
}
if (!(Test-Empty $Options.ListenName)) {
$PodeContext.Server.Gui['ListenName'] = $Options.ListenName
}
}
# validate the settings
$icon = $PodeContext.Server.Gui.Icon
if (!(Test-Empty $icon) -and !(Test-Path $icon)) {
throw "Path to icon for GUI does not exist: $($icon)"
}
$state = $PodeContext.Server.Gui.State
$states = @('Normal', 'Maximized', 'Minimized')
if (!(Test-Empty $state) -and ($states -inotcontains $state)) {
throw "Invalid GUI window state supplied, should be blank or one of $($states -join ' / ')"
}
$style = $PodeContext.Server.Gui.WindowStyle
$styles = @('None', 'SingleBorderWindow', 'ThreeDBorderWindow', 'ToolWindow')
if (!(Test-Empty $style) -and ($styles -inotcontains $style)) {
throw "Invalid GUI window style supplied, should be blank or one of $($styles -join ' / ')"
}
# ensure a listen endpoint with name exists - if one has been passed
if (!(Test-Empty $PodeContext.Server.Gui.ListenName)) {
$found = ($PodeContext.Server.Endpoints | Where-Object {
$_.Name -eq $PodeContext.Server.Gui.ListenName
} | Select-Object -First 1)
if ($null -eq $found) {
throw "Listen endpoint with name '$($Name)' does not exist"
}
$PodeContext.Server.Gui['Endpoint'] = $found
}
}
function Get-PodeTcpHandler
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('SMTP', 'TCP', 'Service')]
[string]
$Type
)
return $PodeContext.Server.Handlers[$Type]
}
function Handler
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('SMTP', 'TCP', 'Service')]
[string]
$Type,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
# lower the type
$Type = $Type.ToLowerInvariant()
# ensure handler isn't already set
if ($null -ne $PodeContext.Server.Handlers[$Type]) {
throw "Handler for $($Type) already defined"
}
# add the handler
$PodeContext.Server.Handlers[$Type] = $ScriptBlock
}
# 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 (!(Test-Empty $Data)) {
$Content = "param(`$data)`nreturn `"$($Content -replace '"', '``"')`""
}
else {
$Content = "return `"$($Content -replace '"', '``"')`""
}
# invoke the content as a script to generate the dynamic content
return (Invoke-ScriptBlock -ScriptBlock ([scriptblock]::Create($Content)) -Arguments $Data -Return)
}
function Get-Type
{
param (
[Parameter()]
$Value
)
if ($null -eq $Value) {
return $null
}
$type = $Value.GetType()
return @{
'Name' = $type.Name.ToLowerInvariant();
'BaseName' = $type.BaseType.Name.ToLowerInvariant();
}
}
function Test-Empty
{
param (
[Parameter()]
$Value
)
$type = Get-Type $Value
if ($null -eq $type) {
return $true
}
switch ($type.Name) {
'string' {
return [string]::IsNullOrWhiteSpace($Value)
}
'hashtable' {
return ($Value.Count -eq 0)
}
'scriptblock' {
return ($null -eq $Value -or [string]::IsNullOrWhiteSpace($Value.ToString()))
}
}
switch ($type.BaseName) {
'valuetype' {
return $false
}
'array' {
return ((Get-Count $Value) -eq 0 -or $Value.Count -eq 0)
}
}
return ([string]::IsNullOrWhiteSpace($Value) -or (Get-Count $Value) -eq 0 -or $Value.Count -eq 0)
}
function Get-PSVersionTable
{
return $PSVersionTable
}
function Test-IsUnix
{
return (Get-PSVersionTable).Platform -ieq 'unix'
}
function Test-IsWindows
{
$v = Get-PSVersionTable
return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop'))
}
function Test-IsPSCore
{
return (Get-PSVersionTable).PSEdition -ieq 'core'
}
function Test-IsAdminUser
{
# check the current platform, if it's unix then return true
if (Test-IsUnix) {
return $true
}
try {
$principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
if ($principal -eq $null) {
return $false
}
return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
catch [exception] {
Write-Host 'Error checking user administrator priviledges' -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
return $false
}
}
function New-PodeSelfSignedCertificate
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$IP,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Port,
[Parameter()]
[string]
$Certificate
)
# only bind if windows at the moment
if (!(Test-IsWindows)) {
Write-Host "Certificates are currently only supported on Windows" -ForegroundColor Yellow
return
}
# check if this ip/port is already bound
$sslPortInUse = (netsh http show sslcert) | Where-Object { $_ -ilike "*IP:port*" -and $_ -ilike "*$($IP):$($Port)" }
if ($sslPortInUse)
{
Write-Host "$($IP):$($Port) already has a certificate bound" -ForegroundColor Green
return
}
# ensure a cert has been supplied
if (Test-Empty $Certificate) {
throw "A certificate is required for ssl connections, either 'self' or '*.example.com' can be supplied to the 'listen' function"
}
# generate a self-signed cert
if (@('self', 'self-signed') -icontains $Certificate)
{
Write-Host "Generating self-signed certificate for $($IP):$($Port)..." -NoNewline -ForegroundColor Cyan
# generate the cert -- has to call "powershell.exe" for ps-core on windows
$cert = (PowerShell.exe -NoProfile -Command {
$expire = (Get-Date).AddYears(1)
$c = New-SelfSignedCertificate -DnsName 'localhost' -CertStoreLocation 'Cert:\LocalMachine\My' -NotAfter $expire `
-KeyAlgorithm RSA -HashAlgorithm SHA256 -KeyLength 4096 -Subject 'CN=localhost';
if ($null -eq $c.Thumbprint) {
return $c
}
return $c.Thumbprint
})
if ($LASTEXITCODE -ne 0 -or !$?) {
throw "Failed to generate self-signed certificte:`n$($cert)"
}
}
# ensure a given cert exists for binding
else
{
Write-Host "Binding $($Certificate) to $($IP):$($Port)..." -NoNewline -ForegroundColor Cyan
# ensure the certificate exists, and get it's thumbprint
$cert = (Get-ChildItem 'Cert:\LocalMachine\My' | Where-Object { $_.Subject -imatch [regex]::Escape($Certificate) })
if (Test-Empty $cert) {
throw "Failed to find the $($Certificate) certificate at LocalMachine\My"
}
$cert = ($cert)[0].Thumbprint
}
# bind the cert to the ip:port
$ipport = "$($IP):$($Port)"
$result = netsh http add sslcert ipport=$ipport certhash=$cert appid=`{e3ea217c-fc3d-406b-95d5-4304ab06c6af`}
if ($LASTEXITCODE -ne 0 -or !$?) {
throw "Failed to attach certificate:`n$($result)"
}
Write-Host " Done" -ForegroundColor Green
}
function Get-HostIPRegex
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('Both', 'Hostname', 'IP')]
[string]
$Type
)
$ip_rgx = '\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\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-PortRegex
{
return '(?<port>\d+)'
}
function Get-PodeEndpointInfo
{
param (
[Parameter()]
[string]
$Endpoint,
[switch]
$AnyPortOnZero
)
if ([string]::IsNullOrWhiteSpace($Endpoint)) {
return $null
}
$hostRgx = Get-HostIPRegex -Type Both
$portRgx = Get-PortRegex
$cmbdRgx = "$($hostRgx)\:$($portRgx)"
# validate that we have a valid ip/host:port address
if (!(($Endpoint -imatch "^$($cmbdRgx)$") -or ($Endpoint -imatch "^$($hostRgx)[\:]{0,1}") -or ($Endpoint -imatch "[\:]{0,1}$($portRgx)$"))) {
throw "Failed to parse '$($Endpoint)' as a valid IP/Host:Port address"
}
# grab the ip address/hostname
$_host = $Matches['host']
if (Test-Empty $_host) {
$_host = '*'
}
# ensure we have a valid ip address/hostname
if (!(Test-IPAddress -IP $_host)) {
throw "The IP address supplied is invalid: $($_host)"
}
# grab the port
$_port = $Matches['port']
if (Test-Empty $_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' = (iftet ($AnyPortOnZero -and $_port -eq 0) '*' $_port);
}
}
function Test-IPAddress
{
param (
[Parameter()]
[string]
$IP
)
if ((Test-Empty $IP) -or ($IP -ieq '*') -or ($IP -ieq 'all') -or ($IP -imatch "^$(Get-HostIPRegex -Type Hostname)$")) {
return $true
}
try {
[System.Net.IPAddress]::Parse($IP) | Out-Null
return $true
}
catch [exception] {
return $false
}
}
function Test-Hostname
{
param (
[Parameter()]
[string]
$Hostname
)
return ($Hostname -imatch "^$(Get-HostIPRegex -Type Hostname)$")
}
function ConvertTo-IPAddress
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Endpoint
)
return [System.Net.IPAddress]::Parse(([System.Net.IPEndPoint]$Endpoint).Address.ToString())
}
function Get-IPAddressesForHostname
{
param (
[Parameter(Mandatory=$true)]
[string]
$Hostname,
[Parameter(Mandatory=$true)]
[ValidateSet('All', 'IPv4', 'IPv6')]
[string]
$Type
)
# get the ip addresses for the hostname
$ips = @([System.Net.Dns]::GetHostAddresses($Hostname))
# return ips based on type
switch ($Type.ToLowerInvariant())
{
'ipv4' {
$ips = @(($ips | Where-Object { $_.AddressFamily -ieq 'InterNetwork' }))
}
'ipv6' {
$ips = @(($ips | Where-Object { $_.AddressFamily -ieq 'InterNetworkV6' }))
}
}
return @(($ips | Select-Object -ExpandProperty IPAddressToString))
}
function Test-IPAddressLocal
{
param (
[Parameter(Mandatory=$true)]
[string]
$IP
)
return (@('127.0.0.1', '::1', '[::1]', 'localhost') -icontains $IP)
}
function Test-IPAddressAny
{
param (
[Parameter(Mandatory=$true)]
[string]
$IP
)
return (@('0.0.0.0', '*', 'all', '::', '[::]') -icontains $IP)
}
function Test-IPAddressLocalOrAny
{
param (
[Parameter(Mandatory=$true)]
[string]
$IP
)
return ((Test-IPAddressLocal -IP $IP) -or (Test-IPAddressAny -IP $IP))
}
function Get-IPAddress
{
param (
[Parameter()]
[string]
$IP
)
if ((Test-Empty $IP) -or ($IP -ieq '*') -or ($IP -ieq 'all')) {
return [System.Net.IPAddress]::Any
}
if (($IP -ieq '::') -or ($IP -ieq '[::]')) {
return [System.Net.IPAddress]::IPv6Any
}
if ($IP -imatch "^$(Get-HostIPRegex -Type Hostname)$") {
return $IP
}
return [System.Net.IPAddress]::Parse($IP)
}
function Test-IPAddressInRange
{
param (
[Parameter(Mandatory=$true)]
$IP,
[Parameter(Mandatory=$true)]
$LowerIP,
[Parameter(Mandatory=$true)]
$UpperIP
)
if ($IP.Family -ine $LowerIP.Family) {
return $false
}
$valid = $true
0..3 | ForEach-Object {
if ($valid -and (($IP.Bytes[$_] -lt $LowerIP.Bytes[$_]) -or ($IP.Bytes[$_] -gt $UpperIP.Bytes[$_]))) {
$valid = $false
}
}
return $valid
}
function Test-IPAddressIsSubnetMask
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$IP
)
return (($IP -split '/').Length -gt 1)
}
function Get-SubnetRange
{
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
0..3 | ForEach-Object {
$network[$_] = [Convert]::ToByte($network[$_], 2)
}
# calculate the bottom range
$bottom = @(0..3 | ForEach-Object { [byte]([byte]$network[$_] -band [byte]$ip_parts[$_]) })
# calculate the range
$range = @(0..3 | ForEach-Object { 256 + (-bnot [byte]$network[$_]) })
# calculate the top range
$top = @(0..3 | ForEach-Object { [byte]([byte]$ip_parts[$_] + [byte]$range[$_]) })
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', 'Schedules', 'Gui')]
[string]
$Type,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock,
[Parameter()]
$Parameters,
[switch]
$Forget
)
try
{
$ps = [powershell]::Create()
$ps.RunspacePool = $PodeContext.RunspacePools[$Type]
$ps.AddScript({ Add-PodePSDrives }) | Out-Null
$ps.AddScript($ScriptBlock) | Out-Null
if (!(Test-Empty $Parameters)) {
$Parameters.Keys | ForEach-Object {
$ps.AddParameter($_, $Parameters[$_]) | Out-Null
}
}
if ($Forget) {
$ps.BeginInvoke() | Out-Null
}
else {
$PodeContext.Runspaces += @{
'Pool' = $Type;
'Runspace' = $ps;
'Status' = $ps.BeginInvoke();
'Stopped' = $false;
}
}
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
function Close-PodeRunspaces
{
param (
[switch]
$ClosePool
)
try {
if (!(Test-Empty $PodeContext.Runspaces)) {
# sleep for 1s before doing this, to let listeners dispose
Start-Sleep -Seconds 1
# now dispose runspaces
$PodeContext.Runspaces | Where-Object { !$_.Stopped } | ForEach-Object {
dispose $_.Runspace
$_.Stopped = $true
}
$PodeContext.Runspaces = @()
}
# dispose the runspace pools
if ($ClosePool -and $null -ne $PodeContext.RunspacePools) {
$PodeContext.RunspacePools.Values | Where-Object { $null -ne $_ -and !$_.IsDisposed } | ForEach-Object {
dispose $_ -Close
}
}
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
function Get-ConsoleKey
{
if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) {
return $null
}
return [Console]::ReadKey($true)
}
function Test-TerminationPressed
{
param (
[Parameter()]
$Key = $null
)
if ($PodeContext.DisableTermination) {
return $false
}
if ($null -eq $Key) {
$Key = Get-ConsoleKey
}
return ($null -ne $Key -and $Key.Key -ieq 'c' -and $Key.Modifiers -band [ConsoleModifiers]::Control)
}
function Test-RestartPressed
{
param (
[Parameter()]
$Key = $null
)
if ($null -eq $Key) {
$Key = Get-ConsoleKey
}
return ($null -ne $Key -and $Key.Key -ieq 'r' -and $Key.Modifiers -band [ConsoleModifiers]::Control)
}
function Start-TerminationListener
{
Add-PodeRunspace -Type 'Main' {
# default variables
$options = "AllowCtrlC,IncludeKeyUp,NoEcho"
$ctrlState = "LeftCtrlPressed"
$char = 'c'
$cancel = $false
# are we on ps-core?
$onCore = ($PSVersionTable.PSEdition -ieq 'core')
while ($true) {
if ($Console.UI.RawUI.KeyAvailable) {
$key = $Console.UI.RawUI.ReadKey($options)
if ([char]$key.VirtualKeyCode -ieq $char) {
if ($onCore) {
$cancel = ($key.Character -ine $char)
}
else {
$cancel = (($key.ControlKeyState -band $ctrlState) -ieq $ctrlState)
}
}
if ($cancel) {
Write-Host 'Terminating...' -NoNewline
$PodeContext.Tokens.Cancellation.Cancel()
break
}
}
Start-Sleep -Milliseconds 10
}
}
}
function Close-Pode
{
param (
[switch]
$Exit
)
# stpo all current runspaces
Close-PodeRunspaces -ClosePool
# stop the file monitor if it's running
Stop-PodeFileMonitor
try {
# remove all the cancellation tokens
dispose $PodeContext.Tokens.Cancellation
dispose $PodeContext.Tokens.Restart
} catch {
$Error[0] | Out-Default
}
# remove all of the pode temp drives
Remove-PodePSDrives
if ($Exit -and ![string]::IsNullOrWhiteSpace($PodeContext.Server.Type)) {
Write-Host " Done" -ForegroundColor Green
}
}
function New-PodePSDrive
{
param (
[Parameter(Mandatory=$true)]
[string]
$Path,
[Parameter()]
[string]
$Name
)
# if no name is passed, used a randomly generated one
if ([string]::IsNullOrWhiteSpace($Name)) {
$Name = "PodeDir$(Get-NewGuid)"
}
# if the path supplied doesn't exist, error
if (!(Test-Path $Path)) {
throw "Path does not exist: $($Path)"
}
# create the temp drive
$drive = (New-PSDrive -Name $Name -PSProvider FileSystem -Root $Path -Scope Global)
# store internally, and return the drive's name
if (!$PodeContext.Server.Drives.ContainsKey($drive.Name)) {
$PodeContext.Server.Drives[$drive.Name] = $Path
}
return "$($drive.Name):"
}
function Add-PodePSDrives
{
$PodeContext.Server.Drives.Keys | ForEach-Object {
New-PodePSDrive -Path $PodeContext.Server.Drives[$_] -Name $_ | Out-Null
}
}
function Add-PodePSInbuiltDrives
{
# create drive for views, if path exists
$path = (Join-ServerRoot 'views')
if (Test-Path $path) {
$PodeContext.Server.InbuiltDrives['views'] = (New-PodePSDrive -Path $path)
}
# create drive for public content, if path exists
$path = (Join-ServerRoot 'public')
if (Test-Path $path) {
$PodeContext.Server.InbuiltDrives['public'] = (New-PodePSDrive -Path $path)
}
}
function Remove-PodePSDrives
{
Get-PSDrive PodeDir* | Remove-PSDrive | Out-Null
}
<#
# Sourced and editted from https://davewyatt.wordpress.com/2014/04/06/thread-synchronization-in-powershell/
#>
function Lock
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[object]
$InputObject,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
if ($null -eq $InputObject) {
return
}
if ($InputObject.GetType().IsValueType) {
throw 'Cannot lock value types'
}
$locked = $false
try {
[System.Threading.Monitor]::Enter($InputObject.SyncRoot)
$locked = $true
if ($ScriptBlock -ne $null) {
Invoke-ScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure
}
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
finally {
if ($locked) {
[System.Threading.Monitor]::Pulse($InputObject.SyncRoot)
[System.Threading.Monitor]::Exit($InputObject.SyncRoot)
}
}
}
function Root
{
return $PodeContext.Server.Root
}
function Join-ServerRoot
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Folder,
[Parameter()]
[string]
$FilePath,
[Parameter()]
[string]
$Root
)
# use the root path of the server
if (Test-Empty $Root) {
$Root = $PodeContext.Server.Root
}
# join the folder/file to the root path
if ([string]::IsNullOrWhiteSpace($FilePath)) {
return (Join-Path $Root $Folder)
}
else {
return (Join-Path $Root (Join-Path $Folder $FilePath))
}
}
function Invoke-ScriptBlock
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[Alias('s')]
[scriptblock]
$ScriptBlock,
[Parameter()]
[Alias('a')]
$Arguments = $null,
[switch]
$Scoped,
[switch]
$Return,
[switch]
$Splat,
[switch]
$NoNewClosure
)
if (!$NoNewClosure) {
$ScriptBlock = ($ScriptBlock).GetNewClosure()
}
if ($Scoped) {
if ($Splat) {
$result = (& $ScriptBlock @Arguments)
}
else {
$result = (& $ScriptBlock $Arguments)
}
}
else {
if ($Splat) {
$result = (. $ScriptBlock @Arguments)
}
else {
$result = (. $ScriptBlock $Arguments)
}
}
if ($Return) {
return $result
}
}
<#
If-This-Else-That. If Check is true return Value1, else return Value2
#>
function Iftet
{
param (
[Parameter()]
[bool]
$Check,
[Parameter()]
$Value1,
[Parameter()]
$Value2
)
if ($Check) {
return $Value1
}
return $Value2
}
function Coalesce
{
param (
[Parameter()]
$Value1,
[Parameter()]
$Value2
)
return (iftet (Test-Empty $Value1) $Value2 $Value1)
}
function Get-FileExtension
{
param (
[Parameter()]
[string]
$Path,
[switch]
$TrimPeriod
)
$ext = [System.IO.Path]::GetExtension($Path)
if ($TrimPeriod) {
$ext = $ext.Trim('.')
}
return $ext
}
function Get-FileName
{
param (
[Parameter()]
[string]
$Path,
[switch]
$WithoutExtension
)
if ($WithoutExtension) {
return [System.IO.Path]::GetFileNameWithoutExtension($Path)
}
return [System.IO.Path]::GetFileName($Path)
}
function Stopwatch
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
try {
$watch = [System.Diagnostics.Stopwatch]::StartNew()
. $ScriptBlock
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
finally {
$watch.Stop()
Out-Default -InputObject "[Stopwatch]: $($watch.Elapsed) [$($Name)]"
}
}
function Test-ValidNetworkFailure
{
param (
[Parameter()]
$Exception
)
$msgs = @(
'*network name is no longer available*',
'*nonexistent network connection*',
'*broken pipe*'
)
return (($msgs | Where-Object { $Exception.Message -ilike $_ } | Measure-Object).Count -gt 0)
}
function ConvertFrom-RequestContent
{
param (
[Parameter()]
$Request
)
# get the requests content type and boundary
$MetaData = Get-ContentTypeAndBoundary -ContentType $Request.ContentType
$Encoding = $Request.ContentEncoding
# result object for data/files
$Result = @{
'Data' = @{};
'Files' = @{};
}
# if there is no content-type then do nothing
if (Test-Empty $MetaData.ContentType) {
return $Result
}
# if the content-type is not multipart/form-data, get the string data
if ($MetaData.ContentType -ine 'multipart/form-data') {
$Content = Read-StreamToEnd -Stream $Request.InputStream -Encoding $Encoding
# if there is no content then do nothing
if (Test-Empty $Content) {
return $Result
}
}
# run action for the content type
switch ($MetaData.ContentType) {
{ $_ -ilike '*/json' } {
$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-NameValueToHashTable -Collection ([System.Web.HttpUtility]::ParseQueryString($Content)))
}
{ $_ -ieq 'multipart/form-data' } {
# convert the stream to bytes
$Content = ConvertFrom-StreamToBytes -Stream $Request.InputStream
$Lines = Get-ByteLinesFromByteArray -Bytes $Content -Encoding $Encoding -IncludeNewLine
# get the indexes for boundary lines (start and end)
$boundaryIndexes = @()
for ($i = 0; $i -lt $Lines.Length; $i++) {
if ((Test-ByteArrayIsBoundary -Bytes $Lines[$i] -Boundary $MetaData.Boundary.Start -Encoding $Encoding) -or
(Test-ByteArrayIsBoundary -Bytes $Lines[$i] -Boundary $MetaData.Boundary.End -Encoding $Encoding)) {
$boundaryIndexes += $i
}
}
# loop through the boundary indexes (exclude last, as it's the end boundary)
for ($i = 0; $i -lt ($boundaryIndexes.Length - 1); $i++)
{
$bIndex = $boundaryIndexes[$i]
# the next line contains the key-value field names (content-disposition)
$fields = @{}
$disp = ConvertFrom-BytesToString -Bytes $Lines[$bIndex+1] -Encoding $Encoding -RemoveNewLine
@($disp -isplit ';') | ForEach-Object {
$atoms = @($_ -isplit '=')
if ($atoms.Length -eq 2) {
$fields.Add($atoms[0].Trim(), $atoms[1].Trim(' "'))
}
}
# use the next line to work out field values
if (!$fields.ContainsKey('filename')) {
$value = ConvertFrom-BytesToString -Bytes $Lines[$bIndex+3] -Encoding $Encoding -RemoveNewLine
$Result.Data.Add($fields.name, $value)
}
# if we have a file, work out file and content type
if ($fields.ContainsKey('filename')) {
$Result.Data.Add($fields.name, $fields.filename)
if (!(Test-Empty $fields.filename)) {
$type = ConvertFrom-BytesToString -Bytes $Lines[$bIndex+2] -Encoding $Encoding -RemoveNewLine
$Result.Files.Add($fields.filename, @{
'ContentType' = (@($type -isplit ':')[1].Trim());
'Bytes' = $null;
})
$bytes = @()
$Lines[($bIndex+4)..($boundaryIndexes[$i+1]-1)] | ForEach-Object {
$bytes += $_
}
$Result.Files[$fields.filename].Bytes = (Remove-NewLineBytesFromArray $bytes $Encoding)
}
}
}
}
default {
$Result.Data = $Content
}
}
return $Result
}
function Test-ByteArrayIsBoundary
{
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-BytesToString $Bytes $Encoding).StartsWith($Boundary)
}
function Get-ContentTypeAndBoundary
{
param (
[Parameter()]
[string]
$ContentType
)
$obj = @{
'ContentType' = [string]::Empty;
'Boundary' = @{
'Start' = [string]::Empty;
'End' = [string]::Empty;
}
}
if (Test-Empty $ContentType) {
return $obj
}
$split = @($ContentType -isplit ';')
$obj.ContentType = $split[0].Trim()
if ($split.Length -gt 1) {
$obj.Boundary.Start = "--$(($split[1] -isplit '=')[1].Trim())"
$obj.Boundary.End = "$($obj.Boundary.Start)--"
}
return $obj
}
function ConvertFrom-NameValueToHashTable
{
param (
[Parameter()]
$Collection
)
if ($null -eq $Collection) {
return $null
}
$ht = @{}
$Collection.Keys | ForEach-Object {
$ht[$_] = $Collection[$_]
}
return $ht
}
function Get-NewGuid
{
return ([guid]::NewGuid()).ToString()
}
function Get-Count
{
param (
[Parameter()]
$Object
)
return ($Object | Measure-Object).Count
}
function Get-ContentAsBytes
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Path
)
if (Test-IsPSCore) {
return (Get-Content -Path $Path -Raw -AsByteStream)
}
return (Get-Content -Path $Path -Raw -Encoding byte)
}
function Test-PathAccess
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Path
)
try {
Get-Item $Path | Out-Null
}
catch [System.UnauthorizedAccessException] {
return $false
}
return $true
}
function Test-PodePath
{
param (
[Parameter()]
$Path,
[switch]
$NoStatus,
[switch]
$FailOnDirectory
)
# if the file doesnt exist then fail on 404
if ((Test-Empty $Path) -or !(Test-Path $Path)) {
if (!$NoStatus) {
status 404
}
return $false
}
# if the file isn't accessible then fail 401
if (!(Test-PathAccess $Path)) {
if (!$NoStatus) {
status 401
}
return $false
}
# if we're failing on a directory then fail on 404
if ($FailOnDirectory -and (Test-PathIsDirectory $Path)) {
if (!$NoStatus) {
status 404
}
return $false
}
return $true
}
function Test-PathIsFile
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Path
)
return (![string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}
function Test-PathIsDirectory
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Path
)
return ([string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}
function Convert-PathSeparators
{
param (
[Parameter()]
$Paths
)
return @($Paths | ForEach-Object {
if (!(Test-Empty $_)) {
$_ -ireplace '[\\/]', [System.IO.Path]::DirectorySeparatorChar
}
})
}
function Convert-PathPatternToRegex
{
param (
[Parameter()]
[string]
$Path
)
$Path = $Path -ireplace '\.', '\.'
$Path = $Path -ireplace '[\\/]', '[\\/]'
$Path = $Path -ireplace '\*', '.*?'
return "^$($Path)$"
}
function Convert-PathPatternsToRegex
{
param (
[Parameter()]
[string[]]
$Paths,
[switch]
$NotSlashes
)
# remove any empty entries
$Paths = @($Paths | Where-Object {
!(Test-Empty $_)
})
# if no paths, return null
if (Test-Empty $Paths) {
return $null
}
# replace certain chars
$Paths = @($Paths | ForEach-Object {
if (!(Test-Empty $_)) {
$tmp = $_ -ireplace '\.', '\.'
if (!$NotSlashes) {
$tmp = $tmp -ireplace '[\\/]', '[\\/]'
}
$tmp -ireplace '\*', '.*?'
}
})
# join them all together
return "^($($Paths -join '|'))$"
}
function Get-PodeLogger
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name
)
return $PodeContext.Server.Logging.Methods[$Name]
}
function Add-PodeLogEndware
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$WebEvent
)
$WebEvent.OnEnd += {
param($s)
$obj = New-PodeLogObject -Request $s.Request -Path $s.Path
Add-PodeLogObject -LogObject $obj -Response $s.Response
}
}
function New-PodeLogObject
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Request,
[Parameter()]
[string]
$Path
)
return @{
'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' = '-';
'StatusDescription' = '-';
'Size' = '-';
};
}
}
function Add-PodeLogObject
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$LogObject,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Response
)
if ($PodeContext.Server.Logging.Disabled -or (Get-Count $PodeContext.Server.Logging.Methods) -eq 0) {
return
}
$LogObject.Response.StatusCode = $Response.StatusCode
$LogObject.Response.StatusDescription = $Response.StatusDescription
if ($Response.ContentLength64 -gt 0) {
$LogObject.Response.Size = $Response.ContentLength64
}
$PodeContext.RequestsToLog.Add($LogObject) | Out-Null
}
function Start-LoggerRunspace
{
if ((Get-Count $PodeContext.Server.Logging.Methods) -eq 0) {
return
}
$script = {
# simple safegaurd function to set blank field to a dash(-)
function sg($value) {
if (Test-Empty $value) {
return '-'
}
return $value
}
# convert a log request into a Combined Log Format string
function Get-RequestString($req) {
$url = "$(sg $req.Request.Method) $(sg $req.Request.Resource) $(sg $req.Request.Protocol)"
return "$(sg $req.Host) $(sg $req.RfcUserIdentity) $(sg $req.User) [$(sg $req.Date)] `"$($url)`" $(sg $req.Response.StatusCode) $(sg $req.Response.Size) `"$(sg $req.Request.Referrer)`" `"$(sg $req.Request.Agent)`""
}
# helper variables for files
$_files_next_run = [DateTime]::Now.Date
# main logic loop
while ($true)
{
# if there are no requests to log, just sleep
if ((Get-Count $PodeContext.RequestsToLog) -eq 0) {
Start-Sleep -Seconds 1
continue
}
# safetly pop off the first log request from the array
$r = $null
lock $PodeContext.RequestsToLog {
$r = $PodeContext.RequestsToLog[0]
$PodeContext.RequestsToLog.RemoveAt(0) | Out-Null
}
# convert the request into a log string
$str = (Get-RequestString $r)
# apply log request to supplied loggers
$PodeContext.Server.Logging.Methods.Keys | ForEach-Object {
switch ($_.ToLowerInvariant())
{
'terminal' {
$str | Out-Default
}
'file' {
$details = $PodeContext.Server.Logging.Methods[$_]
$date = [DateTime]::Now.ToString('yyyy-MM-dd')
# generate path to log path and date file
if ($null -eq $details -or (Test-Empty $details.Path)) {
$path = (Join-ServerRoot 'logs' "$($date).log" )
}
else {
$path = (Join-Path $details.Path "$($date).log")
}
# append log to file
$str | 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 ($null -ne $details -and [int]$details.MaxDays -gt 0 -and $_files_next_run -lt [DateTime]::Now) {
$date = [DateTime]::Now.AddDays(-$details.MaxDays)
Get-ChildItem -Path $path -Filter '*.log' -Force |
Where-Object { $_.CreationTime -lt $date } |
Remove-Item $_ -Force | Out-Null
$_files_next_run = [DateTime]::Now.Date.AddDays(1)
}
}
{ $_ -ilike 'custom_*' } {
Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.Logging.Methods[$_] -Arguments @{
'Log' = $r;
'Lockable' = $PodeContext.Lockable;
}
}
}
}
# small sleep to lower cpu usage
Start-Sleep -Milliseconds 100
}
}
Add-PodeRunspace -Type 'Main' -ScriptBlock $script
}
function Logger
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('n')]
[string]
$Name,
[Parameter()]
[Alias('d')]
[object]
$Details = $null,
[switch]
[Alias('c')]
$Custom
)
# is logging disabled?
if ($PodeContext.Server.Logging.Disabled) {
Write-Host "Logging has been disabled for $($Name)" -ForegroundColor DarkCyan
return
}
# set the logger as custom if flag is passed
if ($Name -inotlike 'custom_*' -and $Custom) {
$Name = "custom_$($Name)"
}
# lowercase the name
$Name = $Name.ToLowerInvariant()
# ensure the logger doesn't already exist
if ($PodeContext.Server.Logging.Methods.ContainsKey($Name)) {
throw "Logger called $($Name) already exists"
}
# ensure the details are of a correct type (inbuilt=hashtable, custom=scriptblock)
$type = (Get-Type $Details)
if ($Name -ilike 'custom_*') {
if ($null -eq $Details) {
throw 'For custom loggers, a ScriptBlock is required'
}
if ($type.Name -ine 'scriptblock') {
throw "Custom logger details should be a ScriptBlock, but got: $($type.Name)"
}
}
else {
if ($null -ne $Details -and $type.Name -ine 'hashtable') {
throw "Inbuilt logger details should be a HashTable, but got: $($type.Name)"
}
}
# add the logger, along with any given details (hashtable/scriptblock)
$PodeContext.Server.Logging.Methods[$Name] = $Details
# if a file logger, create base directory (file is a dummy file, and won't be created)
if ($Name -ieq 'file') {
# has a specific logging path been supplied?
if ($null -eq $Details -or (Test-Empty $Details.Path)) {
$path = (Split-Path -Parent -Path (Join-ServerRoot 'logs' 'tmp.txt'))
}
else {
$path = $Details.Path
}
Write-Host "Log Path: $($path)" -ForegroundColor DarkCyan
New-Item -Path $path -ItemType Directory -Force | Out-Null
}
# if this is the first logger, start the logging runspace
if ($PodeContext.Server.Logging.Methods.Count -eq 1) {
Start-LoggerRunspace
}
}
function Invoke-PodeMiddleware
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$WebEvent,
[Parameter()]
$Middleware,
[Parameter()]
[string]
$Route
)
# if there's no middleware, do nothing
if (Test-Empty $Middleware) {
return $true
}
# filter the middleware down by route (retaining order)
if (!(Test-Empty $Route))
{
$Middleware = @($Middleware | Where-Object {
(Test-Empty $_.Route) -or
($_.Route -ieq '/') -or
($_.Route -ieq $Route) -or
($Route -imatch "^$($_.Route)$")
})
}
# continue or halt?
$continue = $true
# loop through each of the middleware, invoking the next if it returns true
foreach ($midware in @($Middleware))
{
try {
# set any custom middleware options
$WebEvent.Middleware = @{ 'Options' = $midware.Options }
# invoke the middleware logic
$continue = Invoke-ScriptBlock -ScriptBlock $midware.Logic -Arguments $WebEvent -Scoped -Return
# remove any custom middleware options
$WebEvent.Middleware.Clear()
}
catch {
status 500
$continue = $false
$_.Exception | Out-Default
}
if (!$continue) {
break
}
}
return $continue
}
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 '@access' -ScriptBlock {
param($s)
# ensure the request IP address is allowed
if (!(Test-IPAccess -IP $s.Request.RemoteEndPoint.Address)) {
status 403
return $false
}
# IP address is allowed
return $true
})
}
function Get-PodeLimitMiddleware
{
return (Get-PodeInbuiltMiddleware -Name '@limit' -ScriptBlock {
param($s)
# ensure the request IP address has not hit a rate limit
if (!(Test-IPLimit -IP $s.Request.RemoteEndPoint.Address)) {
status 429
return $false
}
# IP address is allowed
return $true
})
}
function Get-PodePublicMiddleware
{
return (Get-PodeInbuiltMiddleware -Name '@public' -ScriptBlock {
param($e)
# get the static file path
$path = Get-PodeStaticRoutePath -Route $e.Path -Protocol $e.Protocol -Endpoint $e.Endpoint
if ($null -eq $path) {
return $true
}
# 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 ($e.Path -imatch $config.Exclude)) {
$caching = $false
}
if (($null -ne $config.Include) -and ($e.Path -inotmatch $config.Include)) {
$caching = $false
}
}
# write the file to the response
Write-ToResponseFromFile -Path $path -Cache:$caching
# static content found, stop
return $false
})
}
function Get-PodeRouteValidateMiddleware
{
return @{
'Name' = '@route-valid';
'Logic' = {
param($s)
# ensure the path has a route
$route = Get-PodeRoute -HttpMethod $s.Method -Route $s.Path -Protocol $s.Protocol -Endpoint $s.Endpoint -CheckWildMethod
# if there's no route defined, it's a 404
if ($null -eq $route -or $null -eq $route.Logic) {
status 404
return $false
}
# set the route parameters
$WebEvent.Parameters = $route.Parameters
# route exists
return $true
}
}
}
function Get-PodeBodyMiddleware
{
return (Get-PodeInbuiltMiddleware -Name '@body' -ScriptBlock {
param($e)
try
{
# attempt to parse that data
$result = ConvertFrom-RequestContent -Request $e.Request
# set session data
$e.Data = $result.Data
$e.Files = $result.Files
# payload parsed
return $true
}
catch [exception]
{
status 400
return $false
}
})
}
function Get-PodeQueryMiddleware
{
return (Get-PodeInbuiltMiddleware -Name '@query' -ScriptBlock {
param($s)
try
{
# set the query string from the request
$s.Query = (ConvertFrom-NameValueToHashTable -Collection $s.Request.QueryString)
return $true
}
catch [exception]
{
status 400
return $false
}
})
}
function Middleware
{
param (
[Parameter(Mandatory=$true, Position=0, ParameterSetName='Script')]
[Parameter(Mandatory=$true, Position=1, ParameterSetName='ScriptRoute')]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock,
[Parameter(Mandatory=$true, Position=0, ParameterSetName='ScriptRoute')]
[Parameter(Mandatory=$true, Position=0, ParameterSetName='HashRoute')]
[Alias('r')]
[string]
$Route,
[Parameter(Mandatory=$true, Position=0, ParameterSetName='Hash')]
[Parameter(Mandatory=$true, Position=1, ParameterSetName='HashRoute')]
[Alias('h')]
[hashtable]
$HashTable,
[Parameter()]
[Alias('n')]
[string]
$Name,
[switch]
$Return
)
# if a name was supplied, ensure it doesn't already exist
if (!(Test-Empty $Name)) {
if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) {
throw "Middleware with defined name of $($Name) already exists"
}
}
# if route is empty, set it to root
$Route = Coalesce $Route '/'
$Route = Split-PodeRouteQuery -Route $Route
$Route = Coalesce $Route '/'
$Route = Update-PodeRouteSlashes -Route $Route
$Route = Update-PodeRoutePlaceholders -Route $Route
# create the middleware hash, or re-use a passed one
if (Test-Empty $HashTable)
{
$HashTable = @{
'Name' = $Name;
'Route' = $Route;
'Logic' = $ScriptBlock;
}
}
else
{
if (Test-Empty $HashTable.Logic) {
throw 'Middleware supplied has no Logic'
}
if (Test-Empty $HashTable.Route) {
$HashTable.Route = $Route
}
if (Test-Empty $HashTable.Name) {
$HashTable.Name = $Name
}
}
# add the scriptblock to array of middleware that needs to be run
if ($Return) {
return $HashTable
}
else {
$PodeContext.Server.Middleware += $HashTable
}
}
function Get-RandomName
{
$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])"
}
# write data to main http response
function Write-ToResponse
{
param (
[Parameter()]
$Value,
[Parameter()]
[string]
$ContentType = $null,
[switch]
$Cache
)
# if there's nothing to write, return
if (Test-Empty $Value) {
return
}
$res = $WebEvent.Response
# if the response stream isn't writable, return
if (($null -eq $res) -or ($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite) {
return
}
# set a cache value
if ($Cache) {
$age = $PodeContext.Server.Web.Static.Cache.MaxAge
$res.AddHeader('Cache-Control', "max-age=$($age), must-revalidate")
$res.AddHeader('Expires', [datetime]::UtcNow.AddSeconds($age).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'"))
}
# specify the content-type if supplied
if (!(Test-Empty $ContentType)) {
$res.ContentType = $ContentType
}
# write the content to the response
$Value = ConvertFrom-ValueToBytes -Value $Value
$res.ContentLength64 = $Value.Length
Write-BytesToStream -Bytes $Value -Stream $res.OutputStream -CheckNetwork
}
function Write-ToResponseFromFile
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Path,
[switch]
$Cache
)
# test the file path, and set status accordingly
if (!(Test-PodePath $Path -FailOnDirectory)) {
return
}
# are we dealing with a dynamic file for the view engine?
$ext = Get-FileExtension -Path $Path -TrimPeriod
# this is a static file
if ((Test-Empty $ext) -or $ext -ine $PodeContext.Server.ViewEngine.Extension) {
$content = Get-ContentAsBytes -Path $Path
Write-ToResponse -Value $content -ContentType (Get-PodeContentType -Extension $ext) -Cache:$Cache
return
}
# generate dynamic content
$content = [string]::Empty
switch ($PodeContext.Server.ViewEngine.Engine)
{
'pode' {
$content = Get-Content -Path $Path -Raw -Encoding utf8
$content = ConvertFrom-PodeFile -Content $content
}
default {
if ($null -ne $PodeContext.Server.ViewEngine.Script) {
$content = (Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.Script -Arguments $Path -Return)
}
}
}
$ext = Get-FileExtension -Path (Get-FileName -Path $Path -WithoutExtension) -TrimPeriod
Write-ToResponse -Value $content -ContentType (Get-PodeContentType -Extension $ext)
}
function Attach
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('p')]
[string]
$Path
)
# only attach files from public/static-route directories
$Path = Get-PodeStaticRoutePath -Route $Path
# test the file path, and set status accordingly
if (!(Test-PodePath $Path)) {
return
}
$filename = Get-FileName -Path $Path
$ext = Get-FileExtension -Path $Path -TrimPeriod
# open up the file as a stream
$fs = (Get-Item $Path).OpenRead()
# setup the response details and headers
$WebEvent.Response.ContentLength64 = $fs.Length
$WebEvent.Response.SendChunked = $false
$WebEvent.Response.ContentType = (Get-PodeContentType -Extension $ext)
$WebEvent.Response.AddHeader('Content-Disposition', "attachment; filename=$($filename)")
# set file as an attachment on the response
$buffer = [byte[]]::new(64 * 1024)
$read = 0
while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) {
$WebEvent.Response.OutputStream.Write($buffer, 0, $read)
}
dispose $fs
}
function Save
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('n')]
[string]
$Name,
[Parameter()]
[Alias('p')]
[string]
$Path = '.'
)
# if path is '.', replace with server root
if ($Path -match '^\.[\\/]{0,1}') {
$Path = $Path -replace '^\.[\\/]{0,1}', ''
$Path = Join-Path $PodeContext.Server.Root $Path
}
# ensure the parameter name exists in data
$fileName = $WebEvent.Data[$Name]
if (Test-Empty $fileName) {
throw "A parameter called '$($Name)' was not supplied in the request"
}
# ensure the file data exists
if (!$WebEvent.Files.ContainsKey($fileName)) {
throw "No data for file '$($fileName)' was uploaded in the request"
}
# if the path is a directory, add the filename
if (Test-PathIsDirectory -Path $Path) {
$Path = Join-Path $Path $fileName
}
# save the file
[System.IO.File]::WriteAllBytes($Path, $WebEvent.Files[$fileName].Bytes)
}
function Status
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[Alias('c')]
[int]
$Code,
[Parameter()]
[Alias('d')]
[string]
$Description
)
$WebEvent.Response.StatusCode = $Code
if (!(Test-Empty $Description)) {
$WebEvent.Response.StatusDescription = $Description
}
}
function Redirect
{
param (
[Parameter()]
[Alias('u')]
[string]
$Url,
[Parameter()]
[Alias('p')]
[int]
$Port = 0,
[Parameter()]
[ValidateSet('', 'HTTP', 'HTTPS')]
[Alias('pr')]
[string]
$Protocol,
[Parameter()]
[Alias('e')]
[string]
$Endpoint,
[switch]
[Alias('m')]
$Moved
)
if (Test-Empty $Url) {
$uri = $WebEvent.Request.Url
# set the protocol
$Protocol = $Protocol.ToLowerInvariant()
if (Test-Empty $Protocol) {
$Protocol = $uri.Scheme
}
# set the endpoint
if (Test-Empty $Endpoint) {
$Endpoint = $uri.Host
}
# set the port
if ($Port -le 0) {
$Port = $uri.Port
}
$PortStr = [string]::Empty
if ($Port -ne 80 -and $Port -ne 443) {
$PortStr = ":$($Port)"
}
# combine to form the url
$Url = "$($Protocol)://$($Endpoint)$($PortStr)$($uri.PathAndQuery)"
}
$WebEvent.Response.RedirectLocation = $Url
if ($Moved) {
status 301 'Moved'
}
else {
status 302 'Redirect'
}
}
function Json
{
param (
[Parameter()]
$Value,
[switch]
$File
)
if ($File) {
# test the file path, and set status accordingly
if (!(Test-PodePath $Path)) {
return
}
else {
$Value = Get-Content -Path $Value -Raw -Encoding utf8
}
}
elseif (Test-Empty $Value) {
$Value = '{}'
}
elseif ((Get-Type $Value).Name -ine 'string') {
$Value = ($Value | ConvertTo-Json -Depth 10 -Compress)
}
Write-ToResponse -Value $Value -ContentType 'application/json; charset=utf-8'
}
function Csv
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Value,
[switch]
$File
)
if ($File) {
# test the file path, and set status accordingly
if (!(Test-PodePath $Path)) {
return
}
else {
$Value = Get-Content -Path $Value -Raw -Encoding utf8
}
}
elseif (Test-Empty $Value) {
$Value = [string]::Empty
}
elseif ((Get-Type $Value).Name -ine 'string') {
$Value = ($Value | ForEach-Object {
New-Object psobject -Property $_
})
if (Test-IsPSCore) {
$Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false)
}
else {
$Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation)
}
}
Write-ToResponse -Value $Value -ContentType 'text/csv; charset=utf-8'
}
function Xml
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Value,
[switch]
$File
)
if ($File) {
# test the file path, and set status accordingly
if (!(Test-PodePath $Path)) {
return
}
else {
$Value = Get-Content -Path $Value -Raw -Encoding utf8
}
}
elseif (Test-Empty $value) {
$Value = [string]::Empty
}
elseif ((Get-Type $Value).Name -ine 'string') {
$Value = ($value | ForEach-Object {
New-Object psobject -Property $_
})
$Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation)
}
Write-ToResponse -Value $Value -ContentType 'application/xml; charset=utf-8'
}
function Html
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Value,
[switch]
$File
)
if ($File) {
# test the file path, and set status accordingly
if (!(Test-PodePath $Path)) {
return
}
else {
$Value = Get-Content -Path $Value -Raw -Encoding utf8
}
}
elseif (Test-Empty $value) {
$Value = [string]::Empty
}
elseif ((Get-Type $Value).Name -ine 'string') {
$Value = ($Value | ConvertTo-Html)
}
Write-ToResponse -Value $Value -ContentType 'text/html; charset=utf-8'
}
# include helper to import the content of a view into another view
function Include
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[Alias('p')]
[string]
$Path,
[Parameter()]
[Alias('d')]
$Data = @{}
)
# default data if null
if ($null -eq $Data) {
$Data = @{}
}
# add view engine extension
$ext = Get-FileExtension -Path $Path
$hasExt = ![string]::IsNullOrWhiteSpace($ext)
if (!$hasExt) {
$Path += ".$($PodeContext.Server.ViewEngine.Extension)"
}
# only look in the view directory
$Path = (Join-Path $PodeContext.Server.InbuiltDrives['views'] $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
$engine = $PodeContext.Server.ViewEngine.Engine
if ($hasExt) {
$engine = $ext.Trim('.')
}
$content = [string]::Empty
switch ($engine.ToLowerInvariant())
{
'html' {
$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.Script) {
$content = (Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.Script -Arguments @($Path, $Data) -Return -Splat)
}
}
}
return $content
}
function View
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[Alias('p')]
$Path,
[Parameter()]
[Alias('d')]
$Data = @{}
)
# 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
}
# add view engine extension
$ext = Get-FileExtension -Path $Path
$hasExt = ![string]::IsNullOrWhiteSpace($ext)
if (!$hasExt) {
$Path += ".$($PodeContext.Server.ViewEngine.Extension)"
}
# only look in the view directory
$Path = (Join-Path $PodeContext.Server.InbuiltDrives['views'] $Path)
# test the file path, and set status accordingly
if (!(Test-PodePath $Path)) {
return
}
# run any engine logic
$engine = $PodeContext.Server.ViewEngine.Engine
if ($hasExt) {
$engine = $ext.Trim('.')
}
$content = [string]::Empty
switch ($engine.ToLowerInvariant())
{
'html' {
$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.Script) {
$content = (Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.Script -Arguments @($Path, $Data) -Return -Splat)
}
}
}
html -Value $content
}
function Tcp
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('write', 'read')]
[Alias('a')]
[string]
$Action,
[Parameter()]
[Alias('m')]
[string]
$Message,
[Parameter()]
[Alias('c')]
$Client
)
if ($null -eq $Client) {
$Client = $TcpEvent.Client
}
switch ($Action.ToLowerInvariant())
{
'write' {
$stream = $Client.GetStream()
$encoder = New-Object System.Text.ASCIIEncoding
$buffer = $encoder.GetBytes("$($Message)`r`n")
$stream.Write($buffer, 0, $buffer.Length)
$stream.Flush()
}
'read' {
$bytes = New-Object byte[] 8192
$stream = $Client.GetStream()
$encoder = New-Object System.Text.ASCIIEncoding
$bytesRead = $stream.Read($bytes, 0, 8192)
$message = $encoder.GetString($bytes, 0, $bytesRead)
return $message
}
}
}
function Get-PodeRoute
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', '*')]
[string]
$HttpMethod,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Route,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Endpoint,
[switch]
$CheckWildMethod
)
# first, if supplied, check the wildcard method
if ($CheckWildMethod -and $PodeContext.Server.Routes['*'].Count -ne 0) {
$found = Get-PodeRoute -HttpMethod '*' -Route $Route -Protocol $Protocol -Endpoint $Endpoint
if ($null -ne $found) {
return $found
}
}
# is this a static route?
$isStatic = ($HttpMethod -ieq 'static')
# first ensure we have the method
$method = $PodeContext.Server.Routes[$HttpMethod]
if ($null -eq $method) {
return $null
}
# if we have a perfect match for the route, return it if the protocol is right
$found = Get-PodeRouteByUrl -Routes $method[$Route] -Protocol $Protocol -Endpoint $Endpoint
if (!$isStatic -and $null -ne $found) {
return @{
'Logic' = $found.Logic;
'Middleware' = $found.Middleware;
'Protocol' = $found.Protocol;
'Endpoint' = $found.Endpoint;
'Parameters' = $null;
}
}
# otherwise, attempt to match on regex parameters
else {
$valid = ($method.Keys | Where-Object {
$Route -imatch "^$($_)$"
} | Select-Object -First 1)
if ($null -eq $valid) {
return $null
}
$found = Get-PodeRouteByUrl -Routes $method[$valid] -Protocol $Protocol -Endpoint $Endpoint
if ($null -eq $found) {
return $null
}
$Route -imatch "$($valid)$" | Out-Null
if ($isStatic) {
return @{
'Path' = $found.Path;
'Defaults' = $found.Defaults;
'Protocol' = $found.Protocol;
'Endpoint' = $found.Endpoint;
'File' = $Matches['file'];
}
}
else {
return @{
'Logic' = $found.Logic;
'Middleware' = $found.Middleware;
'Protocol' = $found.Protocol;
'Endpoint' = $found.Endpoint;
'Parameters' = $Matches;
}
}
}
}
function Get-PodeStaticRoutePath
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Route,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Endpoint
)
# attempt to get a static route for the path
$found = Get-PodeRoute -HttpMethod 'static' -Route $Route -Protocol $Protocol -Endpoint $Endpoint
# if we have a defined static route, use that
if ($null -ne $found) {
# if there's no file, we need to check defaults
if ([string]::IsNullOrWhiteSpace($found.File) -and (Get-Count @($found.Defaults)) -gt 0)
{
if ((Get-Count @($found.Defaults)) -eq 1) {
$found.File = @($found.Defaults)[0]
}
else {
foreach ($def in $found.Defaults) {
if (Test-PodePath (Join-Path $found.Path $def) -NoStatus) {
$found.File = $def
break
}
}
}
}
return (Join-Path $found.Path $found.File)
}
# else, use the public static directory (but only if path is a file)
if (Test-PathIsFile $Route) {
return (Join-Path $PodeContext.Server.InbuiltDrives['public'] $Route)
}
# otherwise, just return null
return $null
}
function Get-PodeRouteByUrl
{
param (
[Parameter()]
[object[]]
$Routes,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Endpoint
)
return (@($Routes) |
Where-Object {
($_.Protocol -ieq $Protocol -or [string]::IsNullOrEmpty($_.Protocol)) -and
([string]::IsNullOrEmpty($_.Endpoint) -or $Endpoint -ilike $_.Endpoint)
} |
Sort-Object -Property { $_.Protocol }, { $_.Endpoint } -Descending |
Select-Object -First 1)
}
function Route
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', '*')]
[Alias('hm')]
[string]
$HttpMethod,
[Parameter(Mandatory=$true)]
[Alias('r')]
[string]
$Route,
[Parameter()]
[Alias('m')]
[object[]]
$Middleware,
[Parameter()]
[Alias('s')]
[scriptblock]
$ScriptBlock,
[Parameter()]
[Alias('d')]
[string[]]
$Defaults,
[Parameter()]
[ValidateSet('', 'HTTP', 'HTTPS')]
[Alias('p')]
[string]
$Protocol,
[Parameter()]
[Alias('e')]
[string]
$Endpoint,
[Parameter()]
[Alias('ln', 'lid')]
[string]
$ListenName,
[switch]
[Alias('rm')]
$Remove
)
# uppercase the method
$HttpMethod = $HttpMethod.ToUpperInvariant()
# if a ListenName was supplied, find it and use it
if (!(Test-Empty $ListenName)) {
# ensure it exists
$found = ($PodeContext.Server.Endpoints | Where-Object { $_.Name -eq $ListenName } | Select-Object -First 1)
if ($null -eq $found) {
throw "Listen endpoint with name '$($Name)' does not exist"
}
# override and set the protocol and endpoint
$Protocol = $found.Protocol
$Endpoint = $found.RawAddress
}
# if an endpoint was supplied (or used from a listen name), set any appropriate wildcards
if (!(Test-Empty $Endpoint)) {
$_endpoint = Get-PodeEndpointInfo -Endpoint $Endpoint -AnyPortOnZero
$Endpoint = "$($_endpoint.Host):$($_endpoint.Port)"
}
# are we removing the route's logic?
if ($Remove) {
Remove-PodeRoute -HttpMethod $HttpMethod -Route $Route -Protocol $Protocol -Endpoint $Endpoint
return
}
# add a new dynamic or static route
if ($HttpMethod -ieq 'static') {
Add-PodeStaticRoute -Route $Route -Path ([string](@($Middleware))[0]) -Protocol $Protocol -Endpoint $Endpoint -Defaults $Defaults
}
else {
if ((Get-Count $Defaults) -gt 0) {
throw "[$($HttpMethod)] $($Route) has default static files defined, which is only for [STATIC] routes"
}
Add-PodeRoute -HttpMethod $HttpMethod -Route $Route -Middleware $Middleware -ScriptBlock $ScriptBlock -Protocol $Protocol -Endpoint $Endpoint
}
}
function Remove-PodeRoute
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', '*')]
[string]
$HttpMethod,
[Parameter(Mandatory=$true)]
[string]
$Route,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Endpoint
)
# split route on '?' for query
$Route = Split-PodeRouteQuery -Route $Route
# ensure route isn't empty
if (Test-Empty $Route) {
throw "No route supplied for removing the $($HttpMethod) definition"
}
# ensure the route has appropriate slashes and replace parameters
$Route = Update-PodeRouteSlashes -Route $Route
$Route = Update-PodeRoutePlaceholders -Route $Route
# ensure route does exist
if (!$PodeContext.Server.Routes[$HttpMethod].ContainsKey($Route)) {
return
}
# remove the route's logic
$PodeContext.Server.Routes[$HttpMethod][$Route] = @($PodeContext.Server.Routes[$HttpMethod][$Route] | Where-Object {
!($_.Protocol -ieq $Protocol -and $_.Endpoint -ieq $Endpoint)
})
# if the route has no more logic, just remove it
if ((Get-Count $PodeContext.Server.Routes[$HttpMethod][$Route]) -eq 0) {
$PodeContext.Server.Routes[$HttpMethod].Remove($Route) | Out-Null
}
}
function Add-PodeRoute
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', '*')]
[string]
$HttpMethod,
[Parameter(Mandatory=$true)]
[string]
$Route,
[Parameter()]
[object[]]
$Middleware,
[Parameter()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Endpoint
)
# if middleware and scriptblock are null, error
if ((Test-Empty $Middleware) -and (Test-Empty $ScriptBlock)) {
throw "[$($HttpMethod)] $($Route) has no logic defined"
}
# ensure middleware is either a scriptblock, or a valid hashtable
if (!(Test-Empty $Middleware)) {
@($Middleware) | ForEach-Object {
$_type = (Get-Type $_).Name
# is the type valid
if ($_type -ine 'scriptblock' -and $_type -ine 'hashtable') {
throw "A middleware supplied for the '[$($HttpMethod)] $($Route)' route is of an invalid type. Expected either ScriptBlock or Hashtable, but got: $($_type)"
}
# is the hashtable valid
if ($_type -ieq 'hashtable') {
if ($null -eq $_.Logic) {
throw "A Hashtable middleware supplied for the '[$($HttpMethod)] $($Route)' route has no Logic defined"
}
$_ltype = (Get-Type $_.Logic).Name
if ($_ltype -ine 'scriptblock') {
throw "A Hashtable middleware supplied for the '[$($HttpMethod)] $($Route)' route has has an invalid Logic type. Expected ScriptBlock, but got: $($_ltype)"
}
}
}
}
# if middleware set, but not scriptblock, set middle and script
if (!(Test-Empty $Middleware) -and ($null -eq $ScriptBlock)) {
# if multiple middleware, error
if ((Get-Type $Middleware).BaseName -ieq 'array' -and (Get-Count $Middleware) -ne 1) {
throw "[$($HttpMethod)] $($Route) has no logic defined"
}
$ScriptBlock = {}
if ((Get-Type $Middleware[0]).Name -ieq 'scriptblock') {
$ScriptBlock = $Middleware[0]
$Middleware = $null
}
}
# split route on '?' for query
$Route = Split-PodeRouteQuery -Route $Route
# ensure route isn't empty
if (Test-Empty $Route) {
throw "No route path supplied for $($HttpMethod) definition"
}
# ensure the route has appropriate slashes
$Route = Update-PodeRouteSlashes -Route $Route
$Route = Update-PodeRoutePlaceholders -Route $Route
# ensure route doesn't already exist
Test-PodeRouteAndError -HttpMethod $HttpMethod -Route $Route -Protocol $Protocol -Endpoint $Endpoint
# if we have middleware, convert scriptblocks to hashtables
if (!(Test-Empty $Middleware))
{
$Middleware = @($Middleware)
for ($i = 0; $i -lt $Middleware.Length; $i++) {
if ((Get-Type $Middleware[$i]).Name -ieq 'scriptblock')
{
$Middleware[$i] = @{
'Logic' = $Middleware[$i]
}
}
}
}
# add the route logic
$PodeContext.Server.Routes[$HttpMethod][$Route] += @(@{
'Logic' = $ScriptBlock;
'Middleware' = $Middleware;
'Protocol' = $Protocol;
'Endpoint' = $Endpoint.Trim();
})
}
function Add-PodeStaticRoute
{
param (
[Parameter(Mandatory=$true)]
[string]
$Route,
[Parameter(Mandatory=$true)]
[string]
$Path,
[Parameter()]
[string[]]
$Defaults,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Endpoint
)
# store the route method
$HttpMethod = 'static'
# split route on '?' for query
$Route = Split-PodeRouteQuery -Route $Route
# ensure route isn't empty
if (Test-Empty $Route) {
throw "No route supplied for $($HttpMethod) definition"
}
# if static, ensure the path exists at server root
if (Test-Empty $Path) {
throw "No path supplied for $($HttpMethod) definition"
}
$Path = (Join-ServerRoot $Path)
if (!(Test-Path $Path)) {
throw "Folder supplied for $($HttpMethod) route does not exist: $($Path)"
}
# setup a temp drive for the path
$Path = New-PodePSDrive -Path $Path
# ensure the route has appropriate slashes
$Route = Update-PodeRouteSlashes -Route $Route -Static
# ensure route doesn't already exist
Test-PodeRouteAndError -HttpMethod $HttpMethod -Route $Route -Protocol $Protocol -Endpoint $Endpoint
# setup default static files
if ($null -eq $Defaults) {
$Defaults = Get-PodeStaticRouteDefaults
}
# add the route path
$PodeContext.Server.Routes[$HttpMethod][$Route] += @(@{
'Path' = $Path;
'Defaults' = $Defaults;
'Protocol' = $Protocol;
'Endpoint' = $Endpoint.Trim();
})
}
function Update-PodeRoutePlaceholders
{
param (
[Parameter(Mandatory=$true)]
[string]
$Route
)
# replace placeholder parameters with regex
$placeholder = '\:(?<tag>[\w]+)'
if ($Route -imatch $placeholder) {
$Route = [regex]::Escape($Route)
}
while ($Route -imatch $placeholder) {
$Route = ($Route -ireplace $Matches[0], "(?<$($Matches['tag'])>[\w-_]+?)")
}
return $Route
}
function Update-PodeRouteSlashes
{
param (
[Parameter(Mandatory=$true)]
[string]
$Route,
[switch]
$Static
)
# ensure route starts with a '/'
if (!$Route.StartsWith('/')) {
$Route = "/$($Route)"
}
if ($Static)
{
# ensure the route ends with a '/*'
$Route = $Route.TrimEnd('*')
if (!$Route.EndsWith('/')) {
$Route = "$($Route)/"
}
$Route = "$($Route)(?<file>*)"
}
# replace * with .*
$Route = ($Route -ireplace '\*', '.*')
return $Route
}
function Split-PodeRouteQuery
{
param (
[Parameter(Mandatory=$true)]
[string]
$Route
)
return ($Route -isplit "\?")[0]
}
function Get-PodeStaticRouteDefaults
{
if (!(Test-Empty $PodeContext.Server.Web.Static.Defaults)) {
return @($PodeContext.Server.Web.Static.Defaults)
}
return @(
'index.html',
'index.htm',
'default.html',
'default.htm'
)
}
function Test-PodeRouteAndError
{
param (
[Parameter(Mandatory=$true)]
[string]
$HttpMethod,
[Parameter(Mandatory=$true)]
[string]
$Route,
[Parameter()]
[string]
$Protocol,
[Parameter()]
[string]
$Endpoint
)
$found = @($PodeContext.Server.Routes[$HttpMethod][$Route])
if (($found | Where-Object { $_.Protocol -ieq $Protocol -and $_.Endpoint -ieq $Endpoint } | Measure-Object).Count -eq 0) {
return
}
$_url = $Protocol
if (![string]::IsNullOrEmpty($_url) -and ![string]::IsNullOrWhiteSpace($Endpoint)) {
$_url = "$($_url)://$($Endpoint)"
}
elseif (![string]::IsNullOrWhiteSpace($Endpoint)) {
$_url = $Endpoint
}
if ([string]::IsNullOrEmpty($_url)) {
throw "[$($HttpMethod)] $($Route) is already defined"
}
else {
throw "[$($HttpMethod)] $($Route) is already defined for $($_url)"
}
}
function Get-PodeSchedule
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name
)
return $PodeContext.Schedules[$Name]
}
function Start-ScheduleRunspace
{
if ((Get-Count $PodeContext.Schedules) -eq 0) {
return
}
$script = {
# first, sleep for a period of time to get to 00 seconds (start of minute)
Start-Sleep -Seconds (60 - [DateTime]::Now.Second)
while ($true)
{
$_remove = @()
$_now = [DateTime]::Now
# select the schedules that need triggering
$PodeContext.Schedules.Values |
Where-Object {
($null -eq $_.StartTime -or $_.StartTime -le $_now) -and
($null -eq $_.EndTime -or $_.EndTime -ge $_now) -and
(Test-CronExpression -Expression $_.Cron -DateTime $_now)
} | ForEach-Object {
# increment total number of triggers for the schedule
if ($_.Countable) {
$_.Count++
$_.Countable = ($_.Count -lt $_.Limit)
}
# check if we have hit the limit, and remove
if ($_.Limit -ne 0 -and $_.Count -ge $_.Limit) {
$_remove += $_.Name
}
try {
# trigger the schedules logic
Add-PodeRunspace -Type 'Schedules' -ScriptBlock (($_.Script).GetNewClosure()) `
-Parameters @{ 'Lockable' = $PodeContext.Lockable } -Forget
}
catch {
$Error[0]
}
# reset the cron if it's random
$_.Cron = Reset-RandomCronExpression -Expression $_.Cron
}
# add any schedules to remove that have exceeded their end time
$_remove += @($PodeContext.Schedules.Values |
Where-Object { ($null -ne $_.EndTime -and $_.EndTime -lt $_now) }).Name
# remove any schedules
$_remove | ForEach-Object {
if ($PodeContext.Schedules.ContainsKey($_)) {
$PodeContext.Schedules.Remove($_)
}
}
# 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
}
function Schedule
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Cron,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[Alias('l')]
[int]
$Limit = 0,
[Parameter()]
[Alias('Start')]
$StartTime = $null,
[Parameter()]
[Alias('End')]
$EndTime = $null
)
# lower the name
$Name = $Name.ToLowerInvariant()
# ensure the schedule doesn't already exist
if ($PodeContext.Schedules.ContainsKey($Name)) {
throw "Schedule called $($Name) already exists"
}
# 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) must have an EndTime in the future"
}
if ($null -ne $StartTime -and $null -ne $EndTime -and $EndTime -lt $StartTime) {
throw "Schedule $($Name) cannot have a StartTime after the EndTime"
}
# parse the cron expression
$exp = ConvertFrom-CronExpression -Expression $Cron
# add the schedule
$PodeContext.Schedules[$Name] = @{
'Name' = $Name;
'StartTime' = $StartTime;
'EndTime' = $EndTime;
'Cron' = $exp;
'Limit' = $Limit;
'Count' = 0;
'Countable' = ($Limit -gt 0);
'Script' = $ScriptBlock;
}
}
function Test-IPLimit
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$IP
)
$type = 'IP'
# get the ip address in bytes
$IP = @{
'String' = $IP.IPAddressToString;
'Family' = $IP.AddressFamily;
'Bytes' = $IP.GetAddressBytes();
}
# get the limit rules and active list
$rules = $PodeContext.Server.Limits.Rules[$type]
$active = $PodeContext.Server.Limits.Active[$type]
$now = [DateTime]::UtcNow
# if there are no rules, it's valid
if (Test-Empty $rules) {
return $true
}
# is the ip active? (get a direct match, then try grouped subnets)
$_active_ip = $active[$IP.String]
if ($null -eq $_active_ip) {
$_groups = ($active.Keys | Where-Object { $active[$_].Rule.Grouped } | ForEach-Object { $active[$_] })
$_active_ip = ($_groups | Where-Object { Test-IPAddressInRange -IP $IP -LowerIP $_.Rule.Lower -UpperIP $_.Rule.Upper } | Select-Object -First 1)
}
# 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 = ($rules.Values | Where-Object { Test-IPAddressInRange -IP $IP -LowerIP $_.Lower -UpperIP $_.Upper } | Select-Object -First 1)
# 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.Add($IP.String, @{
'Rule' = @{
'Limit' = -1
}
})
return $true
}
# add ip to active list (ip if not grouped, else the subnet if it's grouped)
$_ip = (iftet $_rule_ip.Grouped $_rule_ip.IP $IP.String)
$active.Add($_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-IPAccess
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$IP
)
$type = 'IP'
# get the ip address in bytes
$IP = @{
'Family' = $IP.AddressFamily;
'Bytes' = $IP.GetAddressBytes();
}
# get permission lists for ip
$allow = $PodeContext.Server.Access.Allow[$type]
$deny = $PodeContext.Server.Access.Deny[$type]
# are they empty?
$alEmpty = (Test-Empty $allow)
$dnEmpty = (Test-Empty $deny)
# if both are empty, value is valid
if ($alEmpty -and $dnEmpty) {
return $true
}
# if value in allow, it's allowed
if (!$alEmpty -and ($allow.Values | Where-Object { Test-IPAddressInRange -IP $IP -LowerIP $_.Lower -UpperIP $_.Upper } | Measure-Object).Count -gt 0) {
return $true
}
# if value in deny, it's disallowed
if (!$dnEmpty -and ($deny.Values | Where-Object { Test-IPAddressInRange -IP $IP -LowerIP $_.Lower -UpperIP $_.Upper } | Measure-Object).Count -gt 0) {
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 Limit
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('IP')]
[Alias('t')]
[string]
$Type,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[Alias('v')]
[object]
$Value,
[Parameter(Mandatory=$true)]
[Alias('l')]
[int]
$Limit,
[Parameter(Mandatory=$true)]
[Alias('s')]
[int]
$Seconds,
[switch]
$Group
)
# if it's array add them all
if ((Get-Type $Value).BaseName -ieq 'array') {
$Value | ForEach-Object {
limit -Type $Type -Value $_ -Limit $Limit -Seconds $Seconds -Group:$Group
}
return
}
# call the appropriate limit method
switch ($Type.ToLowerInvariant())
{
'ip' {
Add-IPLimit -IP $Value -Limit $Limit -Seconds $Seconds -Group:$Group
}
}
}
function Add-IPLimit
{
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-IPAddressIsSubnetMask -IP $IP) {
$_tmp = Get-SubnetRange -SubnetMask $IP
$_tmpLo = Get-IPAddress -IP $_tmp.Lower
$_tmpHi = Get-IPAddress -IP $_tmp.Upper
}
elseif (Test-IPAddressAny -IP $IP) {
$_tmpLo = Get-IPAddress -IP '0.0.0.0'
$_tmpHi = Get-IPAddress -IP '255.255.255.255'
}
else {
$_tmpLo = Get-IPAddress -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 Access
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('Allow', 'Deny')]
[Alias('p')]
[string]
$Permission,
[Parameter(Mandatory=$true)]
[ValidateSet('IP')]
[Alias('t')]
[string]
$Type,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[Alias('v')]
[object]
$Value
)
# if it's array add them all
if ((Get-Type $Value).BaseName -ieq 'array') {
$Value | ForEach-Object {
access -Permission $Permission -Type $Type -Value $_
}
return
}
# call the appropriate access method
switch ($Type.ToLowerInvariant())
{
'ip' {
Add-IPAccess -Permission $Permission -IP $Value
}
}
}
function Add-IPAccess
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('Allow', 'Deny')]
[string]
$Permission,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[string]
$IP
)
# current access type
$type = 'IP'
# get opposite permission
$opp = "$(if ($Permission -ieq 'allow') { 'Deny' } else { 'Allow' })"
# get permission lists for type
$permType = $PodeContext.Server.Access[$Permission][$type]
$oppType = $PodeContext.Server.Access[$opp][$type]
# setup up perm type
if ($null -eq $permType) {
$PodeContext.Server.Access[$Permission][$type] = @{}
$permType = $PodeContext.Server.Access[$Permission][$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-IPAddressIsSubnetMask -IP $IP) {
$_tmp = Get-SubnetRange -SubnetMask $IP
$_tmpLo = Get-IPAddress -IP $_tmp.Lower
$_tmpHi = Get-IPAddress -IP $_tmp.Upper
}
elseif (Test-IPAddressAny -IP $IP) {
$_tmpLo = Get-IPAddress -IP '0.0.0.0'
$_tmpHi = Get-IPAddress -IP '255.255.255.255'
}
else {
$_tmpLo = Get-IPAddress -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 Server
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[ValidateNotNull()]
[Alias('p')]
[int]
$Port = 0,
[Parameter()]
[ValidateNotNull()]
[Alias('i')]
[int]
$Interval = 0,
[Parameter()]
[string]
$IP,
[Parameter()]
[Alias('n')]
[string]
$Name,
[Parameter()]
[Alias('t')]
[int]
$Threads = 1,
[Parameter()]
[Alias('fme')]
[string[]]
$FileMonitorExclude,
[Parameter()]
[Alias('fmi')]
[string[]]
$FileMonitorInclude,
[switch]
$Smtp,
[switch]
$Tcp,
[switch]
$Http,
[switch]
$Https,
[switch]
[Alias('dt')]
$DisableTermination,
[switch]
[Alias('dl')]
$DisableLogging,
[switch]
[Alias('fm')]
$FileMonitor
)
# ensure the session is clean
$PodeContext = $null
# validate port passed
if ($Port -lt 0) {
throw "Port cannot be negative: $($Port)"
}
# if an ip address was passed, ensure it's valid
if (!(Test-Empty $IP) -and !(Test-IPAddress $IP)) {
throw "Invalid IP address has been supplied: $($IP)"
}
try {
# get the current server type
$serverType = Get-PodeServerType -Port $Port -Interval $Interval -Smtp:$Smtp -Tcp:$Tcp -Https:$Https
# create session object
$PodeContext = New-PodeContext -ScriptBlock $ScriptBlock `
-Threads $Threads `
-Interval $Interval `
-ServerRoot $MyInvocation.PSScriptRoot `
-FileMonitorExclude $FileMonitorExclude `
-FileMonitorInclude $FileMonitorInclude `
-DisableLogging:$DisableLogging `
-FileMonitor:$FileMonitor
# for legacy support, create initial listener from Server parameters
if (@('http', 'https', 'smtp', 'tcp') -icontains $serverType) {
listen "$($IP):$($Port)" $serverType
}
# set it so ctrl-c can terminate
[Console]::TreatControlCAsInput = $true
# start the file monitor for interally restarting
Start-PodeFileMonitor
# start the server
Start-PodeServer
# at this point, if it's just a one-one off script, return
if ([string]::IsNullOrWhiteSpace($PodeContext.Server.Type)) {
return
}
# sit here waiting for termination or cancellation
while (!(Test-TerminationPressed -Key $key) -and !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) {
Start-Sleep -Seconds 1
# get the next key presses
$key = Get-ConsoleKey
# check for internal restart
if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-RestartPressed -Key $key)) {
Restart-PodeServer
}
}
Write-Host 'Terminating...' -NoNewline -ForegroundColor Yellow
$PodeContext.Tokens.Cancellation.Cancel()
}
finally {
# clean the runspaces and tokens
Close-Pode -Exit
# clean the session
$PodeContext = $null
}
}
function Start-PodeServer
{
try
{
# setup temp drives for internal dirs
Add-PodePSInbuiltDrives
# run the logic
Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.Logic -NoNewClosure
$_type = $PodeContext.Server.Type.ToUpperInvariant()
if (![string]::IsNullOrWhiteSpace($_type))
{
# start runspace for timers
Start-TimerRunspace
# start runspace for schedules
Start-ScheduleRunspace
# start runspace for gui
Start-GuiRunspace
}
# start the appropriate server
switch ($_type)
{
'SMTP' {
Start-SmtpServer
}
'TCP' {
Start-TcpServer
}
{ $_ -ieq 'HTTP' -or $_ -ieq 'HTTPS' } {
Start-WebServer
}
'SERVICE' {
Start-ServiceServer
}
}
}
catch {
throw $_.Exception
}
}
function Restart-PodeServer
{
try
{
# inform restart
Write-Host 'Restarting server...' -NoNewline -ForegroundColor Cyan
# cancel the session token
$PodeContext.Tokens.Cancellation.Cancel()
# close all current runspaces
Close-PodeRunspaces
# remove all of the pode temp drives
Remove-PodePSDrives
# clear up timers, schedules and loggers
$PodeContext.Server.Routes.Keys.Clone() | ForEach-Object {
$PodeContext.Server.Routes[$_].Clear()
}
$PodeContext.Server.Handlers.Keys.Clone() | ForEach-Object {
$PodeContext.Server.Handlers[$_] = $null
}
$PodeContext.Timers.Clear()
$PodeContext.Schedules.Clear()
$PodeContext.Server.Logging.Methods.Clear()
# clear middle/endware
$PodeContext.Server.Middleware = @()
$PodeContext.Server.Endware = @()
# set view engine back to default
$PodeContext.Server.ViewEngine = @{
'Engine' = 'html';
'Extension' = 'html';
'Script' = $null;
}
# clear up cookie sessions
$PodeContext.Server.Cookies.Session.Clear()
# clear up authentication methods
$PodeContext.Server.Authentications.Clear()
# clear up shared state
$PodeContext.Server.State.Clear()
# recreate the session tokens
dispose $PodeContext.Tokens.Cancellation
$PodeContext.Tokens.Cancellation = New-Object System.Threading.CancellationTokenSource
dispose $PodeContext.Tokens.Restart
$PodeContext.Tokens.Restart = New-Object System.Threading.CancellationTokenSource
# reload the configuration
$PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext
Write-Host " Done" -ForegroundColor Green
# restart the server
Start-PodeServer
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
function Get-PodeServerType
{
param (
[Parameter()]
[int]
$Port = 0,
[Parameter()]
[int]
$Interval = 0,
[switch]
$Smtp,
[switch]
$Tcp,
[switch]
$Https
)
if ($Smtp) {
return 'SMTP'
}
if ($Tcp) {
return 'TCP'
}
if ($Https) {
return 'HTTPS'
}
if ($Port -gt 0) {
return 'HTTP'
}
if ($Interval -gt 0) {
return 'SERVICE'
}
return ([string]::Empty)
}
function Start-ServiceServer
{
# ensure we have svc handler
if ($null -eq (Get-PodeTcpHandler -Type 'Service')) {
throw 'No Service handler has been passed'
}
# state we're running
Write-Host "Server looping every $($PodeContext.Server.Interval)secs" -ForegroundColor Yellow
# script for the looping server
$serverScript = {
try
{
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
# invoke the service logic
Invoke-ScriptBlock -ScriptBlock (Get-PodeTcpHandler -Type 'Service') -Scoped
#Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.Logic -NoNewClosure
# sleep before next run
Start-Sleep -Seconds $PodeContext.Server.Interval
}
}
catch [System.OperationCanceledException] {}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
# start the runspace for the server
Add-PodeRunspace -Type 'Main' -ScriptBlock $serverScript
}
function Pode
{
param (
[Parameter(Mandatory=$true)]
[ValidateSet('init', 'test', 'start', 'install', 'build')]
[Alias('a')]
[string]
$Action
)
# default config file name and content
$file = './package.json'
$name = Split-Path -Leaf -Path $pwd
$data = $null
# defualt 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 {
$value = $data.scripts.$Action
if ([string]::IsNullOrWhiteSpace($value) -and $Action -ieq 'start') {
$value = $data.main
}
if ([string]::IsNullOrWhiteSpace($value)) {
Write-Host "package.config does not contain a script for $($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 -Value $value
}
'start' {
Invoke-PodePackageScript -Value $value
}
'install' {
Invoke-PodePackageScript -Value $value
}
'build' {
Invoke-PodePackageScript -Value $value
}
}
}
function Invoke-PodePackageScript
{
param (
[Parameter()]
[string]
$Value
)
if ([string]::IsNullOrWhiteSpace($Value)) {
return
}
if (Test-IsPSCore) {
pwsh.exe /c "$($value)"
}
else {
powershell.exe /c "$($value)"
}
}
function Start-SmtpServer
{
# ensure we have smtp handlers
if ($null -eq (Get-PodeTcpHandler -Type 'SMTP')) {
throw 'No SMTP handler has been passed'
}
# grab the relavant port
$port = $PodeContext.Server.Endpoints[0].Port
if ($port -eq 0) {
$port = 25
}
# get the IP address for the server
$ipAddress = $PodeContext.Server.Endpoints[0].Address
if (Test-Hostname -Hostname $ipAddress) {
$ipAddress = (Get-IPAddressesForHostname -Hostname $ipAddress -Type All | Select-Object -First 1)
$ipAddress = (Get-IPAddress $ipAddress)
}
try
{
# create the listener for smtp
$endpoint = New-Object System.Net.IPEndPoint($ipAddress, $port)
$listener = New-Object System.Net.Sockets.TcpListener -ArgumentList $endpoint
# start listener
$listener.Start()
}
catch {
if ($null -ne $listener) {
$listener.Stop()
}
throw $_.Exception
}
# state where we're running
Write-Host "Listening on smtp://$($PodeContext.Server.Endpoints[0].HostName):$($port) [$($PodeContext.Threads) thread(s)]" -ForegroundColor Yellow
# script for listening out of for incoming requests
$listenScript = {
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Listener,
[Parameter(Mandatory=$true)]
[int]
$ThreadId
)
# scriptblock for the core smtp message processing logic
$process = {
# if there's no client, just return
if ($null -eq $TcpEvent.Client) {
return
}
# variables to store data for later processing
$mail_from = [string]::Empty
$rcpt_tos = @()
$data = [string]::Empty
# open response to smtp request
tcp write "220 $($PodeContext.Server.Endpoints[0].HostName) -- Pode Proxy Server"
$msg = [string]::Empty
# respond to smtp request
while ($true)
{
try { $msg = (tcp read) }
catch { break }
try {
if (!(Test-Empty $msg)) {
if ($msg.StartsWith('QUIT')) {
tcp write '221 Bye'
if ($null -ne $TcpEvent.Client -and $TcpEvent.Client.Connected) {
dispose $TcpEvent.Client -Close
}
break
}
if ($msg.StartsWith('EHLO') -or $msg.StartsWith('HELO')) {
tcp write '250 OK'
}
if ($msg.StartsWith('RCPT TO')) {
tcp write '250 OK'
$rcpt_tos += (Get-SmtpEmail $msg)
}
if ($msg.StartsWith('MAIL FROM')) {
tcp write '250 OK'
$mail_from = (Get-SmtpEmail $msg)
}
if ($msg.StartsWith('DATA'))
{
tcp write '354 Start mail input; end with <CR><LF>.<CR><LF>'
$data = (tcp read)
tcp write '250 OK'
# set event data
$SmtpEvent.From = $mail_from
$SmtpEvent.To = $rcpt_tos
$SmtpEvent.Data = $data
$SmtpEvent.Subject = (Get-SmtpSubject $data)
$SmtpEvent.Lockable = $PodeContext.Lockable
# set the email body/type
$info = (Get-SmtpBody $data)
$SmtpEvent.Body = $info.Body
$SmtpEvent.ContentType = $info.ContentType
$SmtpEvent.ContentEncoding = $info.ContentEncoding
# call user handlers for processing smtp data
Invoke-ScriptBlock -ScriptBlock (Get-PodeTcpHandler -Type 'SMTP') -Arguments $SmtpEvent -Scoped
# reset the to list
$rcpt_tos = @()
}
}
}
catch [exception] {
throw $_.exception
}
}
}
try
{
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
# get an incoming request
$task = $Listener.AcceptTcpClientAsync()
$task.Wait($PodeContext.Tokens.Cancellation.Token)
$client = $task.Result
# convert the ip
$ip = (ConvertTo-IPAddress -Endpoint $client.Client.RemoteEndPoint)
# ensure the request ip is allowed
if (!(Test-IPAccess -IP $ip) -or !(Test-IPLimit -IP $ip)) {
dispose $client -Close
}
# deal with smtp call
else {
$SmtpEvent = @{}
$TcpEvent = @{
'Client' = $client;
'Lockable' = $PodeContext.Lockable
}
Invoke-ScriptBlock -ScriptBlock $process
}
}
}
catch [System.OperationCanceledException] {}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads | ForEach-Object {
Add-PodeRunspace -Type 'Main' -ScriptBlock $listenScript `
-Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
}
# script to keep smtp server listening until cancelled
$waitScript = {
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Listener
)
try
{
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
finally {
if ($null -ne $Listener) {
$Listener.Stop()
}
}
}
Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }
}
function Get-SmtpEmail
{
param (
[Parameter()]
[string]
$Value
)
$tmp = @($Value -isplit ':')
if ($tmp.Length -gt 1) {
return $tmp[1].Trim().Trim(' <>')
}
return [string]::Empty
}
function Get-SmtpSubject
{
param (
[Parameter()]
[string]
$Data
)
return (Get-SmtpLineFromData -Data $Data -Name 'Subject')
}
function Get-SmtpBody
{
param (
[Parameter()]
[string]
$Data
)
# body info object
$BodyInfo = @{
'ContentType' = $null;
'ContentEncoding' = $null;
'Body' = $null;
}
# get the content type
$BodyInfo.ContentType = (Get-SmtpLineFromData -Data $Data -Name 'Content-Type')
# get the content encoding
$BodyInfo.ContentEncoding = (Get-SmtpLineFromData -Data $Data -Name 'Content-Transfer-Encoding')
# split the message up
$dataSplit = @($Data -isplit [System.Environment]::NewLine)
# get the index of the first blank line, and last dot
$indexOfBlankLine = $dataSplit.IndexOf([string]::Empty)
$indexOfLastDot = [array]::LastIndexOf($dataSplit, '.')
# get the body
$BodyInfo.Body = ($dataSplit[($indexOfBlankLine + 1)..($indexOfLastDot - 2)] -join [System.Environment]::NewLine)
# if there's no body, just return
if (($indexOfLastDot -eq -1) -or (Test-Empty $BodyInfo.Body)) {
return $BodyInfo
}
# decode body based on encoding
switch ($BodyInfo.ContentEncoding.ToLowerInvariant()) {
'base64' {
$BodyInfo.Body = [System.Convert]::FromBase64String($BodyInfo.Body)
}
}
# only if body is bytes, first decode based on type
switch ($BodyInfo.ContentType) {
{ $_ -ilike '*utf-7*' } {
$BodyInfo.Body = [System.Text.Encoding]::UTF7.GetString($BodyInfo.Body)
}
{ $_ -ilike '*utf-8*' } {
$BodyInfo.Body = [System.Text.Encoding]::UTF8.GetString($BodyInfo.Body)
}
{ $_ -ilike '*utf-16*' } {
$BodyInfo.Body = [System.Text.Encoding]::Unicode.GetString($BodyInfo.Body)
}
{ $_ -ilike '*utf-32*' } {
$BodyInfo.Body = [System.Text.Encoding]::UTF32.GetString($BodyInfo.Body)
}
}
return $BodyInfo
}
function Get-SmtpLineFromData
{
param (
[Parameter()]
[string]
$Data,
[Parameter()]
[string]
$Name
)
$line = (@($Data -isplit [System.Environment]::NewLine) | Where-Object {
$_ -ilike "$($Name):*"
} | Select-Object -First 1)
return ($line -ireplace "^$($Name)\:\s+", '').Trim()
}
function Write-BytesToStream
{
param (
[Parameter(Mandatory=$true)]
[byte[]]
$Bytes,
[Parameter(Mandatory=$true)]
$Stream,
[switch]
$CheckNetwork
)
try {
$ms = New-Object -TypeName System.IO.MemoryStream
$ms.Write($Bytes, 0, $Bytes.Length)
$ms.WriteTo($Stream)
$ms.Close()
}
catch {
if ($CheckNetwork -and (Test-ValidNetworkFailure $_.Exception)) {
return
}
$_.Exception | Out-Default
throw $_.Exception
}
}
function Read-StreamToEnd
{
param (
[Parameter()]
$Stream,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
if ($null -eq $Stream) {
return [string]::Empty
}
return (stream ([System.IO.StreamReader]::new($Stream, $Encoding)) {
return $args[0].ReadToEnd()
})
}
function Read-ByteLineFromByteArray
{
param (
[Parameter(Mandatory=$true)]
[byte[]]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8,
[Parameter()]
[int]
$StartIndex = 0,
[switch]
$IncludeNewLine
)
$nlBytes = Get-NewLineBytes -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-ByteLinesFromByteArray
{
param (
[Parameter(Mandatory=$true)]
[byte[]]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8,
[switch]
$IncludeNewLine
)
# lines
$lines = @()
$nlBytes = Get-NewLineBytes -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-StreamToBytes
{
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-ValueToBytes
{
param (
[Parameter()]
[object]
$Value,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
if ((Get-Type $Value).Name -ieq 'string') {
$Value = $Encoding.GetBytes($Value)
}
return $Value
}
function ConvertFrom-BytesToString
{
param (
[Parameter()]
[byte[]]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8,
[switch]
$RemoveNewLine
)
if (Test-Empty $Bytes) {
return $Bytes
}
$value = $Encoding.GetString($Bytes)
if ($RemoveNewLine) {
$value = $value.Trim("`r`n")
}
return $value
}
function Get-NewLineBytes
{
param (
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
return @{
'NewLine' = ($Encoding.GetBytes("`n") | Select-Object -First 1);
'Return' = ($Encoding.GetBytes("`r") | Select-Object -First 1);
}
}
function Remove-NewLineBytesFromArray
{
param (
[Parameter()]
$Bytes,
[Parameter()]
$Encoding = [System.Text.Encoding]::UTF8
)
$nlBytes = Get-NewLineBytes -Encoding $Encoding
$length = $Bytes.Length
if ($Bytes[$length] -eq $nlBytes.NewLine) {
$length--
}
if ($Bytes[$length] -eq $nlBytes.Return) {
$length--
}
return $Bytes[0..$length]
}
<#
This is basically like "using" in .Net
#>
function Stream
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[System.IDisposable]
$InputObject,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock
)
try {
return (Invoke-ScriptBlock -ScriptBlock $ScriptBlock -Arguments $InputObject -Return -NoNewClosure)
}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
finally {
$InputObject.Dispose()
}
}
function Dispose
{
param (
[Parameter()]
[System.IDisposable]
$InputObject,
[switch]
$Close,
[switch]
$CheckNetwork
)
if ($InputObject -eq $null) {
return
}
try {
if ($Close) {
$InputObject.Close()
}
}
catch [exception] {
if ($CheckNetwork -and (Test-ValidNetworkFailure $_.Exception)) {
return
}
$Error[0] | Out-Default
throw $_.Exception
}
finally {
$InputObject.Dispose()
}
}
function Start-TcpServer
{
# ensure we have tcp handler
if ($null -eq (Get-PodeTcpHandler -Type 'TCP')) {
throw 'No TCP handler has been passed'
}
# grab the relavant port
$port = $PodeContext.Server.Endpoints[0].Port
if ($port -eq 0) {
$port = 9001
}
# get the IP address for the server
$ipAddress = $PodeContext.Server.Endpoints[0].Address
if (Test-Hostname -Hostname $ipAddress) {
$ipAddress = (Get-IPAddressesForHostname -Hostname $ipAddress -Type All | Select-Object -First 1)
$ipAddress = (Get-IPAddress $ipAddress)
}
try
{
# create the listener for tcp
$endpoint = New-Object System.Net.IPEndPoint($ipAddress, $port)
$listener = New-Object System.Net.Sockets.TcpListener -ArgumentList $endpoint
# start listener
$listener.Start()
}
catch {
if ($null -ne $listener) {
$listener.Stop()
}
throw $_.Exception
}
# state where we're running
Write-Host "Listening on tcp://$($PodeContext.Server.Endpoints[0].HostName):$($port) [$($PodeContext.Threads) thread(s)]" -ForegroundColor Yellow
# script for listening out of for incoming requests
$listenScript = {
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Listener,
[Parameter(Mandatory=$true)]
[int]
$ThreadId
)
try
{
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
# get an incoming request
$task = $Listener.AcceptTcpClientAsync()
$task.Wait($PodeContext.Tokens.Cancellation.Token)
$client = $task.Result
# convert the ip
$ip = (ConvertTo-IPAddress -Endpoint $client.Client.RemoteEndPoint)
# ensure the request ip is allowed and deal with the tcp call
if ((Test-IPAccess -IP $ip) -and (Test-IPLimit -IP $ip)) {
$TcpEvent = @{
'Client' = $client;
'Lockalble' = $PodeContext.Lockable
}
Invoke-ScriptBlock -ScriptBlock (Get-PodeTcpHandler -Type 'TCP') -Arguments $TcpEvent -Scoped
}
# close the connection
if ($null -ne $client -and $client.Connected) {
dispose $client -Close
}
}
}
catch [System.OperationCanceledException] {}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads | ForEach-Object {
Add-PodeRunspace -Type 'Main' -ScriptBlock $listenScript `
-Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
}
# script to keep tcp server listening until cancelled
$waitScript = {
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Listener
)
try
{
while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
finally {
if ($null -ne $Listener) {
$Listener.Stop()
}
}
}
Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }
}
function Get-PodeTimer
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name
)
return $PodeContext.Timers[$Name]
}
function Start-TimerRunspace
{
if ((Get-Count $PodeContext.Timers) -eq 0) {
return
}
$script = {
while ($true)
{
$_remove = @()
$_now = [DateTime]::Now
$PodeContext.Timers.Values | Where-Object { $_.NextTick -le $_now } | ForEach-Object {
$run = $true
# increment total number of runs for timer (do we still need to count?)
if ($_.Countable) {
$_.Count++
$_.Countable = ($_.Count -lt $_.Skip -or $_.Count -lt $_.Limit)
}
# check if this run should be skipped
if ($_.Count -lt $_.Skip) {
$run = $false
}
# check if we have hit the limit, and remove
if ($run -and $_.Limit -ne 0 -and $_.Count -ge $_.Limit) {
$run = $false
$_remove += $_.Name
}
if ($run) {
try {
Invoke-ScriptBlock -ScriptBlock $_.Script -Arguments @{ 'Lockable' = $PodeContext.Lockable } -Scoped
}
catch {
$Error[0]
}
$_.NextTick = $_now.AddSeconds($_.Interval)
}
}
# remove any timers
$_remove | ForEach-Object {
$PodeContext.Timers.Remove($_)
}
Start-Sleep -Seconds 1
}
}
Add-PodeRunspace -Type 'Main' -ScriptBlock $script
}
function Timer
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[Alias('i')]
[int]
$Interval,
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[scriptblock]
$ScriptBlock,
[Parameter()]
[Alias('l')]
[int]
$Limit = 0,
[Parameter()]
[Alias('s')]
[int]
$Skip = 0
)
# lower the name
$Name = $Name.ToLowerInvariant()
# ensure the timer doesn't already exist
if ($PodeContext.Timers.ContainsKey($Name)) {
throw "Timer called $($Name) already exists"
}
# is the interval valid?
if ($Interval -le 0) {
throw "Timer $($Name) cannot have an interval less than or equal to 0"
}
# is the limit valid?
if ($Limit -lt 0) {
throw "Timer $($Name) cannot have a negative limit"
}
if ($Limit -ne 0) {
$Limit += $Skip
}
# is the skip valid?
if ($Skip -lt 0) {
throw "Timer $($Name) cannot have a negative skip value"
}
# run script if it's not being skipped
if ($Skip -eq 0) {
Invoke-ScriptBlock -ScriptBlock $ScriptBlock -Arguments @{ 'Lockable' = $PodeContext.Lockable } -Scoped
}
# add the timer
$PodeContext.Timers[$Name] = @{
'Name' = $Name;
'Interval' = $Interval;
'Limit' = $Limit;
'Count' = 0;
'Skip' = $Skip;
'Countable' = ($Skip -gt 0 -or $Limit -gt 0);
'NextTick' = [DateTime]::Now.AddSeconds($Interval);
'Script' = $ScriptBlock;
}
}
function Engine
{
param (
[Parameter()]
[ValidateNotNullOrEmpty()]
[Alias('t')]
[string]
$Engine,
[Parameter()]
[Alias('s')]
[scriptblock]
$ScriptBlock = $null,
[Parameter()]
[Alias('ext')]
[string]
$Extension
)
if ([string]::IsNullOrWhiteSpace($Extension)) {
$Extension = $Engine.ToLowerInvariant()
}
$PodeContext.Server.ViewEngine.Engine = $Engine.ToLowerInvariant()
$PodeContext.Server.ViewEngine.Extension = $Extension
$PodeContext.Server.ViewEngine.Script = $ScriptBlock
}
function Start-WebServer
{
# setup any inbuilt middleware
$inbuilt_middleware = @(
(Get-PodeAccessMiddleware),
(Get-PodeLimitMiddleware),
(Get-PodePublicMiddleware),
(Get-PodeRouteValidateMiddleware),
(Get-PodeBodyMiddleware),
(Get-PodeQueryMiddleware)
)
$PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)
# work out which endpoints to listen on
$endpoints = @()
$PodeContext.Server.Endpoints | ForEach-Object {
# get the protocol
$_protocol = (iftet $_.Ssl 'https' 'http')
# get the ip address
$_ip = "$($_.Address)"
if ($_ip -ieq '0.0.0.0') {
$_ip = '*'
}
# get the port
$_port = [int]($_.Port)
if ($_port -eq 0) {
$_port = (iftet $_.Ssl 8443 8080)
}
# if this endpoint is https, generate a self-signed cert or bind an existing one
if ($_.Ssl) {
New-PodeSelfSignedCertificate -IP $_.Address -Port $_port -Certificate $_.Certificate.Name
}
# add endpoint to list
$endpoints += @{
'Prefix' = "$($_protocol)://$($_ip):$($_port)/";
'HostName' = "$($_protocol)://$($_.HostName):$($_port)/";
}
}
# create the listener on http and/or https
$listener = New-Object System.Net.HttpListener
try
{
# start listening on defined endpoints
$endpoints | ForEach-Object {
$listener.Prefixes.Add($_.Prefix)
}
$listener.Start()
}
catch {
$Error[0] | Out-Default
if ($null -ne $Listener) {
if ($Listener.IsListening) {
$Listener.Stop()
}
dispose $Listener -Close
}
throw $_.Exception
}
# state where we're running
Write-Host "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads) thread(s)]:" -ForegroundColor Yellow
$endpoints | ForEach-Object {
Write-Host "`t- $($_.HostName)" -ForegroundColor Yellow
}
# script for listening out for incoming requests
$listenScript = {
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Listener,
[Parameter(Mandatory=$true)]
[int]
$ThreadId
)
try
{
while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
# get request and response
$task = $Listener.GetContextAsync()
$task.Wait($PodeContext.Tokens.Cancellation.Token)
try
{
$context = $task.Result
$request = $context.Request
$response = $context.Response
# reset event data
$WebEvent = @{}
$WebEvent.OnEnd = @()
$WebEvent.Auth = @{}
$WebEvent.Response = $response
$WebEvent.Request = $request
$WebEvent.Lockable = $PodeContext.Lockable
$WebEvent.Path = ($request.RawUrl -isplit "\?")[0]
$WebEvent.Method = $request.HttpMethod.ToLowerInvariant()
$WebEvent.Protocol = $request.Url.Scheme
$WebEvent.Endpoint = $request.Url.Authority
# add logging endware for post-request
Add-PodeLogEndware -WebEvent $WebEvent
# invoke middleware
if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
# get the route logic
$route = Get-PodeRoute -HttpMethod $WebEvent.Method -Route $WebEvent.Path -Protocol $WebEvent.Protocol `
-Endpoint $WebEvent.Endpoint -CheckWildMethod
# invoke route and custom middleware
if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $route.Middleware)) {
Invoke-ScriptBlock -ScriptBlock $route.Logic -Arguments $WebEvent -Scoped
}
}
}
catch {
status 500
$Error[0] | Out-Default
}
# invoke endware specifc to the current web event
$_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
Invoke-PodeEndware -WebEvent $WebEvent -Endware $_endware
# close response stream (check if exists, as closing the writer closes this stream on unix)
if ($response.OutputStream) {
dispose $response.OutputStream -Close -CheckNetwork
}
}
}
catch [System.OperationCanceledException] {}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
}
# start the runspace for listening on x-number of threads
1..$PodeContext.Threads | ForEach-Object {
Add-PodeRunspace -Type 'Main' -ScriptBlock $listenScript `
-Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ }
}
# script to keep web server listening until cancelled
$waitScript = {
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$Listener
)
try
{
while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
Start-Sleep -Seconds 1
}
}
catch [System.OperationCanceledException] {}
catch {
$Error[0] | Out-Default
throw $_.Exception
}
finally {
if ($null -ne $Listener) {
if ($Listener.IsListening) {
$Listener.Stop()
}
dispose $Listener -Close
}
}
}
Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }
}
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.0.25.0.nupkg (724f22786412) - ## / 60
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.0 | 58 | Monday, April 15, 2024 | Approved | |
Pode 2.9.0 | 184 | Monday, October 30, 2023 | Approved | |
Pode 2.8.0 | 242 | Friday, February 3, 2023 | Approved | |
Pode 2.7.2 | 138 | Tuesday, October 25, 2022 | Approved | |
Pode 2.7.1 | 116 | Thursday, July 21, 2022 | Approved | |
Pode 2.7.0 | 125 | Wednesday, June 22, 2022 | Approved | |
Pode 2.6.2 | 171 | Wednesday, March 2, 2022 | Approved | |
Pode 2.6.1 | 89 | Monday, February 21, 2022 | Approved | |
Pode 2.6.0 | 92 | Thursday, February 10, 2022 | Approved | |
Pode 2.5.2 | 104 | Tuesday, January 4, 2022 | Approved | |
Pode 2.5.1 | 101 | Tuesday, December 21, 2021 | Approved | |
Pode 2.5.0 | 114 | Saturday, November 13, 2021 | Approved | |
Pode 2.4.2 | 131 | Monday, September 13, 2021 | Approved | |
Pode 2.4.1 | 117 | Monday, August 9, 2021 | Approved | |
Pode 2.4.0 | 97 | Wednesday, July 21, 2021 | Approved | |
Pode 2.3.0 | 125 | Tuesday, June 1, 2021 | Approved | |
Pode 2.2.3 | 140 | Saturday, April 10, 2021 | Approved | |
Pode 2.2.2 | 92 | Friday, April 9, 2021 | Approved | |
Pode 2.2.1 | 96 | Saturday, March 27, 2021 | Approved | |
Pode 2.2.0 | 115 | Sunday, March 21, 2021 | Approved | |
Pode 2.1.1 | 120 | Friday, February 19, 2021 | Approved | |
Pode 2.1.0 | 1167 | Wednesday, February 3, 2021 | Approved | |
Pode 2.0.3 | 154 | Monday, December 21, 2020 | Approved | |
Pode 2.0.2 | 124 | Saturday, December 5, 2020 | Approved | |
Pode 2.0.1 | 110 | Sunday, November 29, 2020 | Approved | |
Pode 2.0.0 | 179 | Saturday, November 14, 2020 | Approved | |
Pode 1.8.4 | 174 | Friday, October 16, 2020 | Approved | |
Pode 1.8.3 | 155 | Sunday, September 20, 2020 | Approved | |
Pode 1.8.2 | 196 | Friday, July 31, 2020 | Approved | |
Pode 1.8.1 | 174 | Friday, June 26, 2020 | Approved | |
Pode 1.8.0 | 189 | Sunday, May 24, 2020 | Approved | |
Pode 1.7.3 | 187 | Sunday, May 10, 2020 | Approved | |
Pode 1.7.2 | 165 | Monday, April 27, 2020 | Approved | |
Pode 1.7.1 | 160 | Friday, April 17, 2020 | Approved | |
Pode 1.7.0 | 175 | Friday, April 10, 2020 | Approved | |
Pode 1.6.1 | 229 | Saturday, March 7, 2020 | Approved | |
Pode 1.6.0 | 189 | Tuesday, March 3, 2020 | Approved | |
Pode 1.5.0 | 225 | Sunday, February 2, 2020 | Approved | |
Pode 1.4.0 | 199 | Friday, January 10, 2020 | Approved | |
Pode 1.3.0 | 186 | Friday, December 27, 2019 | Approved | |
Pode 1.2.1 | 204 | Monday, December 2, 2019 | Approved | |
Pode 1.2.0 | 191 | Wednesday, November 13, 2019 | Approved | |
Pode 1.1.0 | 208 | Saturday, September 28, 2019 | Approved | |
Pode 1.0.1 | 203 | Wednesday, September 4, 2019 | Approved | |
Pode 1.0.0 | 195 | Monday, September 2, 2019 | Approved | |
Pode 0.32.0 | 235 | Friday, June 28, 2019 | Approved | |
Pode 0.31.0 | 199 | Tuesday, June 11, 2019 | Approved | |
Pode 0.30.0 | 198 | Sunday, May 26, 2019 | Approved | |
Pode 0.29.0 | 196 | Friday, May 10, 2019 | Approved | |
Pode 0.28.1 | 233 | Tuesday, April 16, 2019 | Approved | |
Pode 0.28.0 | 184 | Saturday, April 13, 2019 | Approved | |
Pode 0.27.3 | 204 | Thursday, April 4, 2019 | Approved | |
Pode 0.27.2 | 223 | Wednesday, March 27, 2019 | Approved | |
Pode 0.27.1 | 215 | Saturday, March 16, 2019 | Approved | |
Pode 0.27.0 | 212 | Thursday, March 14, 2019 | Approved | |
Pode 0.26.0 | 237 | Sunday, February 17, 2019 | Approved | |
Pode 0.25.0 | 232 | Tuesday, February 5, 2019 | Approved | |
Pode 0.24.0 | 260 | Friday, January 18, 2019 | Approved | |
Pode 0.23.0 | 248 | Monday, December 24, 2018 | Approved | |
Pode 0.22.0 | 240 | Friday, December 7, 2018 | Approved | |
Pode 0.21.0 | 263 | Friday, November 2, 2018 | Approved | |
Pode 0.20.0 | 266 | Saturday, October 20, 2018 | Approved | |
Pode 0.19.1 | 233 | Tuesday, October 9, 2018 | Approved | |
Pode 0.19.0 | 248 | Friday, September 14, 2018 | Approved | |
Pode 0.18.0 | 238 | Saturday, August 25, 2018 | Approved | |
Pode 0.17.0 | 209 | Sunday, August 19, 2018 | Approved | |
Pode 0.16.0 | 255 | Wednesday, August 8, 2018 | Approved | |
Pode 0.15.0 | 278 | Friday, July 13, 2018 | Approved | |
Pode 0.14.0 | 254 | Friday, July 6, 2018 | Approved | |
Pode 0.13.0 | 254 | Saturday, June 23, 2018 | Approved | |
Pode 0.12.0 | 242 | Friday, June 15, 2018 | Approved | |
Pode 0.11.3 | 280 | Sunday, June 10, 2018 | Approved | |
Pode 0.11.2 | 273 | Friday, June 8, 2018 | Approved | |
Pode 0.11.1 | 298 | Friday, June 1, 2018 | Approved | |
Pode 0.11.0 | 265 | Wednesday, May 30, 2018 | Approved | |
Pode 0.10.1 | 314 | Wednesday, May 16, 2018 | Approved | |
Pode 0.9.0 | 343 | Thursday, January 11, 2018 | Approved |
Copyright 2017-2018
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.