Unpacking Software Livestream

Join our monthly Unpacking Software livestream to hear about the latest news, chat and opinion on packaging, software deployment and lifecycle management!

Learn More

Chocolatey Product Spotlight

Join the Chocolatey Team on our regular monthly stream where we put a spotlight on the most recent Chocolatey product releases. You'll have a chance to have your questions answered in a live Ask Me Anything format.

Learn More

Chocolatey Coding Livestream

Join us for the Chocolatey Coding Livestream, where members of our team dive into the heart of open source development by coding live on various Chocolatey projects. Tune in to witness real-time coding, ask questions, and gain insights into the world of package management. Don't miss this opportunity to engage with our team and contribute to the future of Chocolatey!

Learn More

Calling All Chocolatiers! Whipping Up Windows Automation with Chocolatey Central Management

Webinar from
Wednesday, 17 January 2024

We are delighted to announce the release of Chocolatey Central Management v0.12.0, featuring seamless Deployment Plan creation, time-saving duplications, insightful Group Details, an upgraded Dashboard, bug fixes, user interface polishing, and refined documentation. As an added bonus we'll have members of our Solutions Engineering team on-hand to dive into some interesting ways you can leverage the new features available!

Watch On-Demand
Chocolatey Community Coffee Break

Join the Chocolatey Team as we discuss all things Community, what we do, how you can get involved and answer your Chocolatey questions.

Watch The Replays
Chocolatey and Intune Overview

Webinar Replay from
Wednesday, 30 March 2022

At Chocolatey Software we strive for simple, and teaching others. Let us teach you just how simple it could be to keep your 3rd party applications updated across your devices, all with Intune!

Watch On-Demand
Chocolatey For Business. In Azure. In One Click.

Livestream from
Thursday, 9 June 2022

Join James and Josh to show you how you can get the Chocolatey For Business recommended infrastructure and workflow, created, in Azure, in around 20 minutes.

Watch On-Demand
The Future of Chocolatey CLI

Livestream from
Thursday, 04 August 2022

Join Paul and Gary to hear more about the plans for the Chocolatey CLI in the not so distant future. We'll talk about some cool new features, long term asks from Customers and Community and how you can get involved!

Watch On-Demand
Hacktoberfest Tuesdays 2022

Livestreams from
October 2022

For Hacktoberfest, Chocolatey ran a livestream every Tuesday! Re-watch Cory, James, Gary, and Rain as they share knowledge on how to contribute to open-source projects such as Chocolatey CLI.

Watch On-Demand

Pode

This is not the latest version of Pode available.

  • 1
  • 2
  • 3

0.21.0 | Updated: 02 Nov 2018

Downloads:

15,340

Downloads of v 0.21.0:

258

Maintainer(s):

Software Author(s):

  • Badgerati

Pode 0.21.0

This is not the latest version of Pode available.

  • 1
  • 2
  • 3

All Checks are Passing

3 Passing Tests


Validation Testing Passed


Verification Testing Passed

Details

Scan Testing Successful:

No detections found in any package files

Details
Learn More

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:

NOTE

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

  • 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

3. Copy Your Script

choco upgrade pode -y --source="'INTERNAL REPO URL'" --version="'0.21.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.21.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.21.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.21.0'
end

See docs at https://docs.chef.io/resource_chocolatey_package.html.


cChocoPackageInstaller pode
{
    Name     = "pode"
    Version  = "0.21.0"
    Source   = "INTERNAL REPO URL"
}

Requires cChoco DSC Resource. See docs at https://github.com/chocolatey/cChoco.


package { 'pode':
  ensure   => '0.21.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.

Package Approved

This package was approved as a trusted package on 02 Nov 2018.

Description

Pode is a Cross-Platform PowerShell framework that allows you to host REST APIs, Web Sites, and TCP/SMTP Servers

Features

  • Can run on *nix environments using PowerShell Core
  • Host REST APIs, Web Pages, Static Content, TCP and SMTP server
  • Multiple threads can be used to response to incoming requests
  • Inbuilt template engine, with support for third-parties
  • Setup async timers to be used as one off tasks, or for housekeeping services
  • Ability to schedule async tasks using cron expressions
  • Supports logging to CLI, Files, and custom loggers to other services like LogStash, etc.
  • 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
  • Support for generating/binding self-signed certificates, and signed certificates on Windows
  • Support for middleware and sessions on web servers
  • Can use authentication on requests, which can either be sessionless or session persistent

src\Pode.psd1
#
# 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.21.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 that allows you 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'
    )

    # 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')

            # 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'

        }
    }
}
src\Pode.psm1
# is the current platform unix?
function Test-IsUnix
{
    return $PSVersionTable.Platform -ieq 'unix'
}

# is the current console running as admin?
function Test-AdminUser
{
    # 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
    }
}


if (!(Test-AdminUser)) {
    throw 'Must be running with administrator priviledges to use Pode module'
}


# 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 (Test-IsUnix) {
    $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)
src\Tools\Authentication.ps1
function Auth
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('use', 'check')]
        [Alias('a')]
        [string]
        $Action,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [Alias('n')]
        [string]
        $Name,

        [Parameter()]
        [Alias('v')]
        [scriptblock]
        $Validator,

        [Parameter()]
        [Alias('p')]
        [scriptblock]
        $Parser,

        [Parameter()]
        [Alias('o')]
        [hashtable]
        $Options,

        [switch]
        [Alias('c')]
        $Custom
    )

    if ($Action -ieq 'use') {
        if (Test-Empty $Validator) {
            throw "Authentication method '$($Name)' is missing required Validator script"
        }

        if ($Custom -and (Test-Empty $Parser)) {
            throw "Custom authentication method '$($Name)' is missing required Parser script"
        }
    }

    switch ($Action.ToLowerInvariant())
    {
        'use' {
            Invoke-AuthUse -Name $Name -Validator $Validator -Parser $Parser -Options $Options
        }

        'check' {
            return (Invoke-AuthCheck -Name $Name -Options $Options)
        }
    }
}

function Invoke-AuthUse
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [scriptblock]
        $Validator,

        [Parameter()]
        [scriptblock]
        $Parser,

        [Parameter()]
        [hashtable]
        $Options,

        [switch]
        $Custom
    )

    # get the auth data
    $AuthData = (Get-PodeAuthMethod -Name $Name -Validator $Validator -Parser $Parser -Custom:$Custom)

    # ensure the name doesn't already exist
    if ($PodeSession.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 = @{
        'Options' = $Options;
        'Parser' = $AuthData.Parser;
        'Validator' = $AuthData.Validator;
        'Custom' = $AuthData.Custom;
    }

    # apply auth method to session
    $PodeSession.Server.Authentications[$AuthData.Name] = $obj
}

function Invoke-AuthCheck
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter()]
        [hashtable]
        $Options
    )

    # ensure the auth type exists
    if (!$PodeSession.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 = $PodeSession.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 dat for use to pass to the validator
            if ($auth.Custom) {
                $data = (Invoke-ScriptBlock -ScriptBlock $auth.Parser -Arguments @($s, $auth.Options) -Return -Splat)
                $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)]
        [scriptblock]
        $Validator,

        [Parameter()]
        [scriptblock]
        $Parser,

        [switch]
        $Custom
    )

    # first, is it just a custom type?
    if ($Custom) {
        return @{
            'Name' = $Name;
            'Custom' = $true;
            'Parser' = $Parser;
            'Validator' = $Validator;
        }
    }

    # otherwise, check the inbuilt ones
    switch ($Name.ToLowerInvariant())
    {
        'basic' {
            return (Get-PodeAuthBasic -ScriptBlock $Validator)
        }

        'form' {
            return (Get-PodeAuthForm -ScriptBlock $Validator)
        }
    }

    # if we get here, check if a parser was passed for custom type
    if (Test-Empty $Parser) {
        throw "Authentication method '$($Name)' does not exist as an inbuilt type, nor has a Parser been passed for a custom type"
    }

    # a parser was passed, so it is a custom type
    return @{
        'Name' = $Name;
        '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-PodeAuthBasic
{
    param (
        [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) -Return -Splat)
    }

    return @{
        'Name' = 'Basic';
        'Custom' = $false;
        'Parser' = $parser;
        'Validator' = $ScriptBlock;
    }
}

function Get-PodeAuthForm
{
    param (
        [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) -Return -Splat)
    }

    return @{
        'Name' = 'Form';
        'Custom' = $false;
        'Parser' = $parser;
        'Validator' = $ScriptBlock;
    }
}
src\Tools\ContentTypes.ps1
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') }
    }
}
src\Tools\Cookies.ps1
function Session
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [hashtable]
        $Options
    )

    # check that session logic hasn't already been defined
    if (!(Test-Empty $PodeSession.Server.Cookies.Session)) {
        throw 'Session middleware logic has already been defined'
    }

    # ensure a secret was actually passed
    if (Test-Empty $Options.Secret) {
        throw 'A secret key is required for session cookies'
    }

    # ensure the override generator is a scriptblock
    if (!(Test-Empty $Options.GenerateId) -and (Get-Type $Options.GenerateId).Name -ine 'scriptblock') {
        throw "Session GenerateId should be a ScriptBlock, but got: $((Get-Type $Options.GenerateId).Name)"
    }

    # ensure the override store has the required methods
    if (!(Test-Empty $Options.Store)) {
        $members = @($Options.Store | Get-Member | Select-Object -ExpandProperty Name)
        @('delete', 'get', 'set') | ForEach-Object {
            if ($members -inotcontains $_) {
                throw "Custom session store does not implement the required '$($_)' method"
            }
        }
    }

    # ensure the duration is not <0
    $Options.Duration = [int]($Options.Duration)
    if ($Options.Duration -lt 0) {
        throw "Session duration must be 0 or greater, but got: $($Options.Duration)s"
    }

    # get the appropriate store
    $store = $Options.Store

    # if no custom store, use the inmem one
    if (Test-Empty $store) {
        $store = (Get-PodeSessionCookieInMemStore)
        Set-PodeSessionCookieInMemClearDown
    }

    # set options against session
    $PodeSession.Server.Cookies.Session = @{
        'Name' = (coalesce $Options.Name 'pode.sid');
        'SecretKey' = $Options.Secret;
        'GenerateId' = (coalesce $Options.GenerateId { return (Get-NewGuid) });
        'Store' = $store;
        'Info' = @{
            'Duration' = [int]($Options.Duration);
            'Extend' = [bool]($Options.Extend);
            'Secure' = [bool]($Options.Secure);
            'Discard' = [bool]($Options.Discard);
        };
    }

    # bind session middleware to attach session function
    return {
        param($s)

        # if session already set, return
        if ($s.Session) {
            return $true
        }

        try
        {
            # get the session cookie
            $s.Session = Get-PodeSessionCookie -Request $s.Request

            # if no session on browser, create a new one
            if (!$s.Session) {
                $s.Session = (New-PodeSessionCookie)
                $new = $true
            }

            # get the session's data
            elseif ($null -ne ($data = $PodeSession.Server.Cookies.Session.Store.Get($s.Session.Id))) {
                $s.Session.Data = $data
                Set-PodeSessionCookieDataHash -Session $s.Session
            }

            # session not in store, create a new one
            else {
                $s.Session = (New-PodeSessionCookie)
                $new = $true
            }

            # add helper methods to session
            Set-PodeSessionCookieHelpers -Session $s.Session

            # add cookie to response if it's new or extendible
            if ($new -or $s.Session.Cookie.Extend) {
                Set-PodeSessionCookie -Response $s.Response -Session $s.Session
            }

            # assign endware for session to set cookie/storage
            $s.OnEnd += {
                param($s)

                # if auth is in use, then assign to session store
                if (!(Test-Empty $s.Auth) -and $s.Auth.Store) {
                    $s.Session.Data.Auth = $s.Auth
                }

                Invoke-ScriptBlock -ScriptBlock $s.Session.Save -Arguments @($s.Session, $true) -Splat
            }
        }
        catch {
            $Error[0] | Out-Default
            return $false
        }

        # move along
        return $true
    }
}

function Get-PodeSessionCookie
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Request
    )

    # get the session from cookie
    $cookie = $Request.Cookies[$PodeSession.Server.Cookies.Session.Name]
    if ((Test-Empty $cookie) -or (Test-Empty $cookie.Value)) {
        return $null
    }

    # ensure the session was signed
    $session = (Invoke-CookieUnsign -Signature $cookie.Value -Secret $PodeSession.Server.Cookies.Session.SecretKey)
    if (Test-Empty $session) {
        return $null
    }

    # return session cookie data
    $data = @{
        'Name' = $cookie.Name;
        'Id' = $session;
        'Cookie' = $PodeSession.Server.Cookies.Session.Info;
        'Data' = @{};
    }

    $data.Cookie.TimeStamp = $cookie.TimeStamp
    return $data
}

function Set-PodeSessionCookie
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Response,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    # sign the session
    $signedValue = (Invoke-CookieSign -Value $Session.Id -Secret $PodeSession.Server.Cookies.Session.SecretKey)

    # create a new cookie
    $cookie = [System.Net.Cookie]::new($Session.Name, $signedValue)
    $cookie.Secure = $Session.Cookie.Secure
    $cookie.Discard = $Session.Cookie.Discard

    # calculate the expiry
    $cookie.Expires = (Get-PodeSessionCookieExpiry -Session $Session)

    # assign cookie to response
    $Response.AppendCookie($cookie) | Out-Null
}

function Remove-PodeSessionCookie
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Response,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    # remove the cookie from the response, and reset it to expire
    $cookie = $Response.Cookies[$Session.Name]
    $cookie.Discard = $true
    $cookie.Expires = [DateTime]::UtcNow.AddDays(-2)
    $Response.AppendCookie($cookie) | Out-Null

    # remove session from store
    Invoke-ScriptBlock -ScriptBlock $Session.Delete -Arguments @($Session) -Splat

    # blank the session
    $Session.Clear()
}

function New-PodeSessionCookie
{
    $sid = @{
        'Name' = $PodeSession.Server.Cookies.Session.Name;
        'Id' = (Invoke-ScriptBlock -ScriptBlock $PodeSession.Server.Cookies.Session.GenerateId -Return);
        'Cookie' = $PodeSession.Server.Cookies.Session.Info;
        'Data' = @{};
    }

    Set-PodeSessionCookieDataHash -Session $sid

    $sid.Cookie.TimeStamp = [DateTime]::UtcNow
    return $sid
}

function Set-PodeSessionCookieDataHash
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    $Session.Data = (coalesce $Session.Data @{})
    $Session.DataHash = (Invoke-SHA256Hash -Value ($Session.Data | ConvertTo-Json))
}

function Test-PodeSessionCookieDataHash
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    if (Test-Empty $Session.DataHash) {
        return $false
    }

    $Session.Data = (coalesce $Session.Data @{})
    $hash = (Invoke-SHA256Hash -Value ($Session.Data | ConvertTo-Json))
    return ($Session.DataHash -eq $hash)
}

function Get-PodeSessionCookieExpiry
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    $expiry = (iftet $Session.Cookie.Extend ([DateTime]::UtcNow) $Session.Cookie.TimeStamp)
    $expiry = $expiry.AddSeconds($Session.Cookie.Duration)
    return $expiry
}

function Set-PodeSessionCookieHelpers
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    # force save a session's data to the store
    $Session | Add-Member -MemberType NoteProperty -Name Save -Value {
        param($session, $check)

        # only save if check and hashes different
        if ($check -and (Test-PodeSessionCookieDataHash -Session $session)) {
            return
        }

        # generate the expiry
        $expiry = (Get-PodeSessionCookieExpiry -Session $session)

        # save session data to store
        $PodeSession.Server.Cookies.Session.Store.Set($session.Id, $session.Data, $expiry)

        # update session's data hash
        Set-PodeSessionCookieDataHash -Session $session
    }

    # delete the current session
    $Session | Add-Member -MemberType NoteProperty -Name Delete -Value {
        param($session)

        # remove data from store
        $PodeSession.Server.Cookies.Session.Store.Delete($session.Id)

        # clear session
        $session.Clear()
    }
}

function Get-PodeSessionCookieInMemStore
{
    $store = New-Object -TypeName psobject

    # add in-mem storage
    $store | Add-Member -MemberType NoteProperty -Name Memory -Value @{}

    # delete a sessionId and data
    $store | Add-Member -MemberType ScriptMethod -Name Delete -Value {
        param($sessionId)
        $this.Memory.Remove($sessionId) | Out-Null
    }

    # get a sessionId's data
    $store | Add-Member -MemberType ScriptMethod -Name Get -Value {
        param($sessionId)

        $s = $this.Memory[$sessionId]

        # if expire, remove
        if ($null -ne $s -and $s.Expiry -lt [DateTime]::UtcNow) {
            $this.Memory.Remove($sessionId) | Out-Null
            return $null
        }

        return $s.Data
    }

    # update/insert a sessionId and data
    $store | Add-Member -MemberType ScriptMethod -Name Set -Value {
        param($sessionId, $data, $expiry)

        $this.Memory[$sessionId] = @{
            'Data' = $data;
            'Expiry' = $expiry;
        }
    }

    return $store
}

function Set-PodeSessionCookieInMemClearDown
{
    # cleardown expired inmem session every 10 minutes
    schedule '__pode_session_inmem_cleanup__' '0/10 * * * *' {
        $store = $PodeSession.Server.Cookies.Session.Store
        if (Test-Empty $store.Memory) {
            return
        }

        # remove sessions that have expired
        $now = [DateTime]::UtcNow
        $store.Memory.Keys | ForEach-Object {
            if ($store.Memory[$_].Expiry -lt $now) {
                $store.Memory.Remove($_)
            }
        }
    }
}
src\Tools\CronParser.ps1
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
}
src\Tools\Cryptography.ps1
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
}
src\Tools\Endware.ps1
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
    $PodeSession.Server.Endware += $ScriptBlock
}
src\Tools\FileMonitor.ps1
function Start-PodeFileMonitor
{
    if (!$PodeSession.Server.FileMonitor) {
        return
    }

    # what folder and filter are we moitoring?
    $folder = $PodeSession.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

    # listen out of file created, changed, deleted events
    Register-ObjectEvent -InputObject $watcher -EventName 'Created' -SourceIdentifier (Get-PodeFileMonitorName Create) -Action {
        $Event.MessageData.Timer.Stop()
        $Event.MessageData.Timer.Start()
    } -MessageData @{ 'Timer' = $timer; } -SupportEvent

    Register-ObjectEvent -InputObject $watcher -EventName 'Changed' -SourceIdentifier (Get-PodeFileMonitorName Update) -Action {
        $Event.MessageData.Timer.Stop()
        $Event.MessageData.Timer.Start()
    } -MessageData @{ 'Timer' = $timer; } -SupportEvent

    Register-ObjectEvent -InputObject $watcher -EventName 'Deleted' -SourceIdentifier (Get-PodeFileMonitorName Delete) -Action {
        $Event.MessageData.Timer.Stop()
        $Event.MessageData.Timer.Start()
    } -MessageData @{ 'Timer' = $timer; } -SupportEvent

    # listen out for timer ticks to reset server
    Register-ObjectEvent -InputObject $timer -EventName 'Elapsed' -SourceIdentifier (Get-PodeFileMonitorTimerName) -Action {
        $Event.MessageData.Session.Tokens.Restart.Cancel()
        $Event.Sender.Stop()
    } -MessageData @{ 'Session' = $PodeSession; } -SupportEvent
}

function Stop-PodeFileMonitor
{
    if ($PodeSession.Server.FileMonitor) {
        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'
}
src\Tools\Handlers.ps1
function Get-PodeTcpHandler
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('SMTP', 'TCP', 'Service')]
        [string]
        $Type
    )

    return $PodeSession.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 $PodeSession.Server.Handlers[$Type]) {
        throw "Handler for $($Type) already defined"
    }

    # add the handler
    $PodeSession.Server.Handlers[$Type] = $ScriptBlock
}
src\Tools\Helpers.ps1

# 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 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' command"
    }

    # 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 Test-IPAddress
{
    param (
        [Parameter()]
        [string]
        $IP
    )

    if ((Test-Empty $IP) -or $IP -ieq '*' -or $IP -ieq 'all') {
        return $true
    }

    try {
        [System.Net.IPAddress]::Parse($IP) | Out-Null
        return $true
    }
    catch [exception] {
        return $false
    }
}

function ConvertTo-IPAddress
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Endpoint
    )

    return [System.Net.IPAddress]::Parse(([System.Net.IPEndPoint]$Endpoint).Address.ToString())
}

function Test-IPAddressLocal
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $IP
    )

    return (@('0.0.0.0', '*', '127.0.0.1', 'all') -icontains $IP)
}

function Test-IPAddressAny
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $IP
    )

    return (@('0.0.0.0', '*', 'all') -icontains $IP)
}

function Get-IPAddress
{
    param (
        [Parameter()]
        [string]
        $IP
    )

    if ((Test-Empty $IP) -or $IP -ieq '*' -or $IP -ieq 'all') {
        return [System.Net.IPAddress]::Any
    }

    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')]
        [string]
        $Type,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        $Parameters,

        [switch]
        $Forget
    )

    try
    {
        $ps = [powershell]::Create()
        $ps.RunspacePool = $PodeSession.RunspacePools[$Type]
        $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 {
            $PodeSession.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 $PodeSession.Runspaces)) {
            # sleep for 1s before doing this, to let listeners dispose
            Start-Sleep -Seconds 1

            # now dispose runspaces
            $PodeSession.Runspaces | Where-Object { !$_.Stopped } | ForEach-Object {
                dispose $_.Runspace
                $_.Stopped = $true
            }

            $PodeSession.Runspaces = @()
        }

        # dispose the runspace pools
        if ($ClosePool -and $null -ne $PodeSession.RunspacePools) {
            $PodeSession.RunspacePools.Values | Where-Object { !$_.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 ($PodeSession.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
                    $PodeSession.Tokens.Cancellation.Cancel()
                    break
                }
            }

            Start-Sleep -Milliseconds 10
        }
    }
}

function Close-Pode
{
    param (
        [switch]
        $Exit
    )

    Close-PodeRunspaces -ClosePool
    Stop-PodeFileMonitor

    try {
        dispose $PodeSession.Tokens.Cancellation
        dispose $PodeSession.Tokens.Restart
    } catch {
        $Error[0] | Out-Default
    }

    if ($Exit -and $PodeSession.Server.Type -ine 'script') {
        Write-Host " Done" -ForegroundColor Green
    }
}

<#
# 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 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 = $PodeSession.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)
}

<#
    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 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-PodeContent
{
    param (
        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        $Content
    )

    if (Test-Empty $Content) {
        return $Content
    }

    switch ($ContentType) {
        { $_ -ilike '*/json' } {
            $Content = ($Content | ConvertFrom-Json)
        }

        { $_ -ilike '*/xml' } {
            $Content = [xml]($Content)
        }

        { $_ -ilike '*/csv' } {
            $Content = ($Content | ConvertFrom-Csv)
        }

        { $_ -ilike '*/x-www-form-urlencoded' } {
            $Content = (ConvertFrom-NameValueToHashTable -Collection ([System.Web.HttpUtility]::ParseQueryString($Content)))
        }
    }

    return $Content
}

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)))
}
src\Tools\Logging.ps1
function Get-PodeLogger
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeSession.Loggers[$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 ($PodeSession.DisableLogging -or (Get-Count $PodeSession.Loggers) -eq 0) {
        return
    }

    $LogObject.Response.StatusCode = $Response.StatusCode
    $LogObject.Response.StatusDescription = $Response.StatusDescription

    if ($Response.ContentLength64 -gt 0) {
        $LogObject.Response.Size = $Response.ContentLength64
    }

    $PodeSession.RequestsToLog.Add($LogObject) | Out-Null
}

function Start-LoggerRunspace
{
    if ((Get-Count $PodeSession.Loggers) -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 $PodeSession.RequestsToLog) -eq 0) {
                Start-Sleep -Seconds 1
                continue
            }

            # safetly pop off the first log request from the array
            $r = $null

            lock $PodeSession.RequestsToLog {
                $r = $PodeSession.RequestsToLog[0]
                $PodeSession.RequestsToLog.RemoveAt(0) | Out-Null
            }

            # convert the request into a log string
            $str = (Get-RequestString $r)

            # apply log request to supplied loggers
            $PodeSession.Loggers.Keys | ForEach-Object {
                switch ($_.ToLowerInvariant())
                {
                    'terminal' {
                        $str | Out-Default
                    }

                    'file' {
                        $details = $PodeSession.Loggers[$_]
                        $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 $PodeSession.Loggers[$_] -Arguments @{
                            'Log' = $r;
                            'Lockable' = $PodeSession.Lockable;
                        }
                    }
                }
            }

            # small sleep to lower cpu usage
            Start-Sleep -Milliseconds 100
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $script
}

function Logger
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter()]
        [object]
        $Details = $null
    )

    # is logging disabled?
    if ($PodeSession.DisableLogging) {
        Write-Host "Logging has been disabled for $($Name)" -ForegroundColor DarkCyan
        return
    }

    # lower the name
    $Name = $Name.ToLowerInvariant()

    # ensure the logger doesn't already exist
    if ($PodeSession.Loggers.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)
    $PodeSession.Loggers[$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 ($PodeSession.Loggers.Count -eq 1) {
        Start-LoggerRunspace
    }
}
src\Tools\Middleware.ps1
function Invoke-PodeMiddleware
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $WebEvent,

        [Parameter()]
        $Middleware
    )

    # if there's no middleware, do nothing
    if (Test-Empty $Middleware) {
        return $true
    }

    # 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 = ($PodeSession.Server.Middleware | Where-Object { $_.Name -ieq $Name })

    # if override there, remove it from middleware
    if ($override) {
        $PodeSession.Server.Middleware = @($PodeSession.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($s)

        # get the static file path
        $path = Get-PodeStaticRoutePath -Path $s.Path
        if ($null -eq $path) {
            return $true
        }

        # write the file to the response
        Write-ToResponseFromFile -Path $path

        # 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
            if ($null -eq $route) {
                $route = Get-PodeRoute -HttpMethod '*' -Route $s.Path
            }

            # 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($s)

        try
        {
            # read any post data
            $data = stream ([System.IO.StreamReader]::new($s.Request.InputStream, $s.Request.ContentEncoding)) {
                param($r)
                return $r.ReadToEnd()
            }

            # attempt to parse that data
            $data = ConvertFrom-PodeContent -ContentType $s.Request.ContentType -Content $data

            # set session data
            $s.Data = $data

            # 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)]
        [ValidateNotNull()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [Alias('n')]
        [string]
        $Name
    )

    # if a name was supplied, ensure is doesn't already exist
    if (!(Test-Empty $Name)) {
        if (($PodeSession.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) {
            throw "Middleware with defined name of $($Name) already exists"
        }
    }

    # add the scriptblock to array of middleware that needs to be run
    $PodeSession.Server.Middleware += @{
        'Name' = $Name;
        'Logic' = $ScriptBlock;
    }
}
src\Tools\NameGenerator.ps1
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])"
}
src\Tools\Responses.ps1
# write data to main http response
function Write-ToResponse
{
    param (
        [Parameter()]
        $Value,

        [Parameter()]
        [string]
        $ContentType = $null
    )

    if (Test-Empty $Value) {
        return
    }

    $res = $WebEvent.Response
    if ($null -eq $res -or $null -eq $res.OutputStream -or !$res.OutputStream.CanWrite) {
        return
    }

    if (!(Test-Empty $ContentType)) {
        $res.ContentType = $ContentType
    }

    if ((Get-Type $Value).Name -ieq 'string') {
        $Value = [System.Text.Encoding]::UTF8.GetBytes($Value)
    }

    $res.ContentLength64 = $Value.Length

    try {
        $memory = New-Object -TypeName System.IO.MemoryStream
        $memory.Write($Value, 0, $Value.Length)
        $memory.WriteTo($res.OutputStream)
        $memory.Close()
    }
    catch {
        if (Test-ValidNetworkFailure $_.Exception) {
            return
        }

        $Error[0] | Out-Default
        throw $_.Exception
    }
}

function Write-ToResponseFromFile
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Path
    )

    # 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

    if ((Test-Empty $ext) -or $ext -ine $PodeSession.Server.ViewEngine.Extension) {
        $content = Get-ContentAsBytes -Path $Path
        Write-ToResponse -Value $content -ContentType (Get-PodeContentType -Extension $ext)
        return
    }

    # generate dynamic content
    $content = [string]::Empty

    switch ($PodeSession.Server.ViewEngine.Engine)
    {
        'pode' {
            $content = Get-Content -Path $Path -Raw -Encoding utf8
            $content = ConvertFrom-PodeFile -Content $content
        }

        default {
            if ($null -ne $PodeSession.Server.ViewEngine.Script) {
                $content = (Invoke-ScriptBlock -ScriptBlock $PodeSession.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 download files from public/static-route directories
    $Path = Get-PodeStaticRoutePath -Path $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 = [System.IO.File]::OpenRead($Path)

    # 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 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,

        [switch]
        [Alias('m')]
        $Moved
    )

    if (Test-Empty $Url) {
        $uri = $WebEvent.Request.Url

        $Protocol = $Protocol.ToLowerInvariant()
        if (Test-Empty $Protocol) {
            $Protocol = $uri.Scheme
        }

        if ($Port -le 0) {
            $Port = $uri.Port
        }

        $PortStr = [string]::Empty
        if ($Port -ne 80 -and $Port -ne 443) {
            $PortStr = ":$($Port)"
        }

        $Url = "$($Protocol)://$($uri.Host)$($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 += ".$($PodeSession.Server.ViewEngine.Extension)"
    }

    # only look in the view directory
    $Path = Join-ServerRoot '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 = $PodeSession.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 $PodeSession.Server.ViewEngine.Script) {
                $content = (Invoke-ScriptBlock -ScriptBlock $PodeSession.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 += ".$($PodeSession.Server.ViewEngine.Extension)"
    }

    # only look in the view directory
    $Path = Join-ServerRoot 'views' $Path

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path)) {
        return
    }

    # run any engine logic
    $engine = $PodeSession.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 $PodeSession.Server.ViewEngine.Script) {
                $content = (Invoke-ScriptBlock -ScriptBlock $PodeSession.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
        }
    }
}
src\Tools\Routes.ps1
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
    )

    # is this a static route?
    $isStatic = ($HttpMethod -ieq 'static')

    # first ensure we have the method
    $method = $PodeSession.Server.Routes[$HttpMethod]
    if ($null -eq $method) {
        return $null
    }

    # if we have a perfect match for the route, return it
    $found = $method[$Route]
    if (!$isStatic -and $null -ne $found) {
        return @{
            'Logic' = $found.Logic;
            'Middleware' = $found.Middleware;
            '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 = $method[$valid]
        $Route -imatch "$($valid)$" | Out-Null

        if ($isStatic) {
            return @{
                'Folder' = $found.Path;
                'Defaults' = $found.Defaults;
                'File' = $Matches['file'];
            }
        }
        else {
            return @{
                'Logic' = $found.Logic;
                'Middleware' = $found.Middleware;
                'Parameters' = $Matches;
            }
        }
    }
}

function Get-PodeStaticRoutePath
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path
    )

    # attempt to get a static route
    $route = Get-PodeRoute -HttpMethod 'static' -Route $Path

    # if we have a defined static route, use that
    if ($null -ne $route) {
        # if there's no file, we need to check defaults
        if ([string]::IsNullOrWhiteSpace($route.File) -and (Get-Count @($route.Defaults)) -gt 0)
        {
            if ((Get-Count @($route.Defaults)) -eq 1) {
                $route.File = @($route.Defaults)[0]
            }
            else {
                foreach ($def in $route.Defaults) {
                    if (Test-PodePath (Join-ServerRoot $route.Folder $def) -NoStatus) {
                        $route.File = $def
                        break
                    }
                }
            }
        }

        return (Join-ServerRoot $route.Folder $route.File)
    }

    # else, use the public static directory (but only if path is a file)
    if (Test-PathIsFile $Path) {
        return (Join-ServerRoot 'public' $Path)
    }

    # otherwise, just return null
    return $null
}

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
    )

    if ($HttpMethod -ieq 'static') {
        Add-PodeStaticRoute -Route $Route -Path ([string](@($Middleware))[0]) -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
    }
}

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
    )

    # 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
        }
    }

    # lower the method
    $HttpMethod = $HttpMethod.ToLowerInvariant()

    # 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"
    }

    # ensure the route has appropriate slashes
    $Route = Update-PodeRouteSlashes -Route $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-_]+?)")
    }

    # ensure route doesn't already exist
    if ($PodeSession.Server.Routes[$HttpMethod].ContainsKey($Route)) {
        throw "[$($HttpMethod)] $($Route) is already defined"
    }

    # 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
    $PodeSession.Server.Routes[$HttpMethod][$Route] = @{
        'Logic' = $ScriptBlock;
        'Middleware' = $Middleware;
    }
}

function Add-PodeStaticRoute
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Route,

        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $Defaults
    )

    # 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
    if (Test-Empty $Path) {
        throw "No path supplied for $($HttpMethod) definition"
    }

    if (!(Test-Path (Join-ServerRoot $Path))) {
        throw "Folder supplied for $($HttpMethod) route does not exist: $($Path)"
    }

    # ensure the route has appropriate slashes
    $Route = Update-PodeRouteSlashes -Route $Route -Static

    # ensure route doesn't already exist
    if ($PodeSession.Server.Routes[$HttpMethod].ContainsKey($Route)) {
        throw "[$($HttpMethod)] $($Route) is already defined"
    }

    # setup default static files
    if ($null -eq $Defaults) {
        $Defaults = Get-PodeStaticRouteDefaults
    }

    # add the route path
    $PodeSession.Server.Routes[$HttpMethod][$Route] = @{
        'Path' = $Path;
        'Defaults' = $Defaults;
    }
}

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
{
    $config = $PodeSession.Server.Configuration
    if (!(Test-Empty $config) -and $null -ne $config.web.static.defaults) {
        return @($config.web.static.defaults)
    }

    return @(
        'index.html',
        'index.htm',
        'default.html',
        'default.htm'
    )
}
src\Tools\Schedules.ps1
function Get-PodeSchedule
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeSession.Schedules[$Name]
}

function Start-ScheduleRunspace
{
    if ((Get-Count $PodeSession.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
            $PodeSession.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' = $PodeSession.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 += @($PodeSession.Schedules.Values |
                Where-Object { ($null -ne $_.EndTime -and $_.EndTime -lt $_now) }).Name

            # remove any schedules
            $_remove | ForEach-Object {
                if ($PodeSession.Schedules.ContainsKey($_)) {
                    $PodeSession.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 ($PodeSession.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
    $PodeSession.Schedules[$Name] = @{
        'Name' = $Name;
        'StartTime' = $StartTime;
        'EndTime' = $EndTime;
        'Cron' = $exp;
        'Limit' = $Limit;
        'Count' = 0;
        'Countable' = ($Limit -gt 0);
        'Script' = $ScriptBlock;
    }
}
src\Tools\Security.ps1
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 = $PodeSession.Server.Limits.Rules[$type]
    $active = $PodeSession.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 = $PodeSession.Server.Access.Allow[$type]
    $deny = $PodeSession.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 = $PodeSession.Server.Limits.Rules[$type]

    # setup up perm type
    if ($null -eq $rules) {
        $PodeSession.Server.Limits.Rules[$type] = @{}
        $PodeSession.Server.Limits.Active[$type] = @{}
        $rules = $PodeSession.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 = $PodeSession.Server.Access[$Permission][$type]
    $oppType = $PodeSession.Server.Access[$opp][$type]

    # setup up perm type
    if ($null -eq $permType) {
        $PodeSession.Server.Access[$Permission][$type] = @{}
        $permType = $PodeSession.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();
        };
    })
}
src\Tools\Server.ps1
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,

        [switch]
        $Smtp,

        [switch]
        $Tcp,

        [switch]
        $Http,

        [switch]
        $Https,

        [switch]
        $DisableTermination,

        [switch]
        $DisableLogging,

        [switch]
        $FileMonitor
    )

    # ensure the session is clean
    $PodeSession = $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
        $PodeSession = New-PodeSession -ScriptBlock $ScriptBlock `
            -Port $Port `
            -IP $IP `
            -Threads $Threads `
            -Interval $Interval `
            -ServerRoot $MyInvocation.PSScriptRoot `
            -ServerType $ServerType `
            -DisableLogging:$DisableLogging `
            -FileMonitor:$FileMonitor

        # set a default port for the server type
        Set-PodePortForServerType

        # parse ip:port to listen on (if both have been supplied)
        if (!(Test-Empty $IP) -or $PodeSession.Server.IP.Port -gt 0) {
            listen -IPPort "$($IP):$($PodeSession.Server.IP.Port)" -Type $PodeSession.Server.Type
        }

        # 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 ($PodeSession.Server.Type -ieq 'script') {
            return
        }

        # sit here waiting for termination
        while (!(Test-TerminationPressed -Key $key)) {
            Start-Sleep -Seconds 1

            # get the next key presses
            $key = Get-ConsoleKey

            # check for internal restart
            if (($PodeSession.Tokens.Restart.IsCancellationRequested) -or (Test-RestartPressed -Key $key)) {
                Restart-PodeServer
            }
        }

        Write-Host 'Terminating...' -NoNewline -ForegroundColor Yellow
        $PodeSession.Tokens.Cancellation.Cancel()
    }
    finally {
        # clean the runspaces and tokens
        Close-Pode -Exit

        # clean the session
        $PodeSession = $null
    }
}

function Start-PodeServer
{
    try
    {
        # run the logic
        Invoke-ScriptBlock -ScriptBlock $PodeSession.Server.Logic -NoNewClosure

        $_type = $PodeSession.Server.Type.ToUpperInvariant()
        if ($_type -ine 'script')
        {
            # start runspace for timers
            Start-TimerRunspace

            # start runspace for schedules
            Start-ScheduleRunspace
        }

        # start the appropriate server
        switch ($_type)
        {
            'SMTP' {
                Start-SmtpServer
            }

            'TCP' {
                Start-TcpServer
            }

            'HTTP' {
                Start-WebServer
            }

            'HTTPS' {
                Start-WebServer -Https
            }

            'SERVICE' {
                Start-ServiceServer
            }
        }
    }
    catch {
        $Error[0] | Out-Default
        throw $_.Exception
    }
}

function Restart-PodeServer
{
    try
    {
        # inform restart
        Write-Host 'Restarting server...' -NoNewline -ForegroundColor Cyan

        # cancel the session token
        $PodeSession.Tokens.Cancellation.Cancel()

        # close all current runspaces
        Close-PodeRunspaces

        # clear up timers, schedules and loggers
        $PodeSession.Server.Routes.Keys.Clone() | ForEach-Object {
            $PodeSession.Server.Routes[$_].Clear()
        }

        $PodeSession.Server.Handlers.Keys.Clone() | ForEach-Object {
            $PodeSession.Server.Handlers[$_] = $null
        }

        $PodeSession.Timers.Clear()
        $PodeSession.Schedules.Clear()
        $PodeSession.Loggers.Clear()

        # clear middle/endware
        $PodeSession.Server.Middleware = @()
        $PodeSession.Server.Endware = @()

        # clear up view engine
        $PodeSession.Server.ViewEngine.Clear()

        # clear up cookie sessions
        $PodeSession.Server.Cookies.Session.Clear()

        # clear up authentication methods
        $PodeSession.Server.Authentications.Clear()

        # clear up shared state
        $PodeSession.Server.State.Clear()

        # recreate the session tokens
        dispose $PodeSession.Tokens.Cancellation
        $PodeSession.Tokens.Cancellation = New-Object System.Threading.CancellationTokenSource

        dispose $PodeSession.Tokens.Restart
        $PodeSession.Tokens.Restart = New-Object System.Threading.CancellationTokenSource

        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 'SCRIPT'
}

function Set-PodePortForServerType
{
    if ($PodeSession.Server.IP.Port -gt 0) {
        return
    }

    switch ($PodeSession.Server.Type.ToUpperInvariant())
    {
        'SMTP' {
            $PodeSession.Server.IP.Port = 25
        }

        'HTTP' {
            $PodeSession.Server.IP.Port = 8080
        }

        'HTTPS' {
            $PodeSession.Server.IP.Port = 8443
        }
    }
}
src\Tools\ServiceServer.ps1
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 $($PodeSession.Server.Interval)secs" -ForegroundColor Yellow

    # script for the looping server
    $serverScript = {
        try
        {
            while (!$PodeSession.Tokens.Cancellation.IsCancellationRequested)
            {
                # invoke the service logic
                Invoke-ScriptBlock -ScriptBlock (Get-PodeTcpHandler -Type 'Service') -Scoped
                #Invoke-ScriptBlock -ScriptBlock $PodeSession.Server.Logic -NoNewClosure

                # sleep before next run
                Start-Sleep -Seconds $PodeSession.Server.Interval
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $Error[0] | Out-Default
            throw $_.Exception
        }
    }

    # start the runspace for the server
    Add-PodeRunspace -Type 'Main' -ScriptBlock $serverScript
}
src\Tools\Session.ps1
function New-PodeSession
{
    param (
        [scriptblock]
        $ScriptBlock,

        [int]
        $Port = 0,

        [string]
        $IP = $null,

        [int]
        $Threads = 1,

        [int]
        $Interval = 0,

        [string]
        $ServerRoot,

        [ValidateSet('HTTP', 'HTTPS', 'SCRIPT', 'SERVICE', 'SMTP', 'TCP')]
        [string]
        $ServerType,

        [string]
        $Name = $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 session object
    $session = 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 DisableLogging -Value $DisableLogging -PassThru |
        Add-Member -MemberType NoteProperty -Name Loggers -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 type, name, logic and root
    $session.Server.Name = $Name
    $session.Server.Type = $ServerType
    $session.Server.Root = $ServerRoot
    $session.Server.Logic = $ScriptBlock
    $session.Server.Interval = $Interval
    $session.Server.FileMonitor = $FileMonitor

    # check if there is any global configuration
    $session.Server.Configuration = @{}

    $configPath = (Join-ServerRoot -Folder '.' -FilePath 'pode.json' -Root $ServerRoot)
    if (Test-PodePath -Path $configPath  -NoStatus) {
        $session.Server.Configuration = (Get-Content $configPath -Raw | ConvertFrom-Json)
    }

    # set the IP address details
    $session.Server.IP = @{
        'Address' = $null;
        'Port' = $Port;
        'Name' = 'localhost';
        'Ssl' = ($ServerType -ieq 'https');
        'Certificate' = @{
            'Name' = $null;
        };
    }

    # shared state between runspaces
    $session.Server.State = @{}

    # session engine for rendering views
    $session.Server.ViewEngine = @{
        'Engine' = 'html';
        'Extension' = 'html';
        'Script' = $null;
    }

    # routes for pages and api
    $session.Server.Routes = @{
        'delete' = @{};
        'get' = @{};
        'head' = @{};
        'merge' = @{};
        'options' = @{};
        'patch' = @{};
        'post' = @{};
        'put' = @{};
        'trace' = @{};
        'static' = @{};
        '*' = @{};
    }

    # handlers for tcp
    $session.Server.Handlers = @{
        'tcp' = $null;
        'smtp' = $null;
        'service' = $null;
    }

    # setup basic access placeholders
    $session.Server.Access = @{
        'Allow' = @{};
        'Deny' = @{};
    }

    # setup basic limit rules
    $session.Server.Limits = @{
        'Rules' = @{};
        'Active' = @{};
    }

    # cookies and session logic
    $session.Server.Cookies = @{
        'Session' = @{};
    }

    # authnetication methods
    $session.Server.Authentications = @{}

    # create new cancellation tokens
    $session.Tokens = @{
        'Cancellation' = New-Object System.Threading.CancellationTokenSource;
        'Restart' = New-Object System.Threading.CancellationTokenSource;
    }

    # requests that should be logged
    $session.RequestsToLog = New-Object System.Collections.ArrayList

    # middleware that needs to run
    $session.Server.Middleware = @()

    # endware that needs to run
    $session.Server.Endware = @()

    # runspace pools
    $session.RunspacePools = @{
        'Main' = $null;
        'Schedules' = $null;
    }

    # session state
    $session.Lockable = [hashtable]::Synchronized(@{})
    $state = [initialsessionstate]::CreateDefault()
    $state.ImportPSModule((Get-Module -Name Pode).Path)

    $_session = New-PodeStateSession $session

    $variables = @(
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PodeSession', $_session, $null),
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'Console', $Host, $null)
    )

    $variables | ForEach-Object {
        $state.Variables.Add($_)
    }

    # setup runspaces
    $session.Runspaces = @()

    # setup main runspace pool
    $threadsCounts = @{
        'Default' = 1;
        'Timer' = 1;
        'Log' = 1;
        'Schedule' = 1;
        'Misc' = 1;
    }

    $totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum + $Threads
    $session.RunspacePools.Main = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $state, $Host)
    $session.RunspacePools.Main.Open()

    # setup schedule runspace pool
    $session.RunspacePools.Schedules = [runspacefactory]::CreateRunspacePool(1, 2, $state, $Host)
    $session.RunspacePools.Schedules.Open()

    # return the new session
    return $session
}

function New-PodeStateSession
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    return (New-Object -TypeName psobject |
        Add-Member -MemberType NoteProperty -Name Threads -Value $Session.Threads -PassThru |
        Add-Member -MemberType NoteProperty -Name Timers -Value $Session.Timers -PassThru |
        Add-Member -MemberType NoteProperty -Name Schedules -Value $Session.Schedules -PassThru |
        Add-Member -MemberType NoteProperty -Name RunspacePools -Value $Session.RunspacePools -PassThru |
        Add-Member -MemberType NoteProperty -Name Tokens -Value $Session.Tokens -PassThru |
        Add-Member -MemberType NoteProperty -Name DisableLogging -Value $Session.DisableLogging -PassThru |
        Add-Member -MemberType NoteProperty -Name Loggers -Value $Session.Loggers -PassThru |
        Add-Member -MemberType NoteProperty -Name RequestsToLog -Value $Session.RequestsToLog -PassThru |
        Add-Member -MemberType NoteProperty -Name Lockable -Value $Session.Lockable -PassThru |
        Add-Member -MemberType NoteProperty -Name Server -Value $Session.Server -PassThru)
}

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 $PodeSession -or $null -eq $PodeSession.Server.State) {
            return $null
        }

        switch ($Action.ToLowerInvariant())
        {
            'set' {
                $PodeSession.Server.State[$Name] = $Object
            }

            'get' {
                $Object = $PodeSession.Server.State[$Name]
            }

            'remove' {
                $Object = $PodeSession.Server.State[$Name]
                $PodeSession.Server.State.Remove($Name) | Out-Null
            }
        }

        return $Object
    }
    catch {
        $Error[0] | Out-Default
        throw $_.Exception
    }
}

function Listen
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [Alias('ipp')]
        [string]
        $IPPort,
        
        [Parameter()]
        [ValidateSet('HTTP', 'HTTPS', 'SMTP', 'TCP')]
        [Alias('t')]
        [string]
        $Type,

        [Parameter()]
        [Alias('cert')]
        [string]
        $Certificate = $null
    )

    $hostRgx = '(?<host>(\[[a-z0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d+|\*|all))'
    $portRgx = '(?<port>\d+)'
    $cmbdRgx = "$($hostRgx)\:$($portRgx)"

    # validate that we have a valid ip:port address
    if (!($IPPort -imatch "^$($cmbdRgx)$" -or $IPPort -imatch "^$($hostRgx)[\:]{0,1}" -or $IPPort -imatch "[\:]{0,1}$($portRgx)$")) {
        throw "Failed to parse '$($IPPort)' as a valid IP:Port address"
    }

    # grab the ip address
    $_host = $Matches['host']
    if (Test-Empty $_host) {
        $_host = '*'
    }

    # ensure we have a valid ip address
    if (!(Test-IPAddress -IP $_host)) {
        throw "Invalid IP address has been supplied: $($IP)"
    }

    # grab the port
    $_port = $Matches['port']
    if (Test-Empty $_port) {
        $_port = 0
    }

    # ensure the port is valid
    if ($_port -lt 0) {
        throw "Port cannot be negative: $($_port)"
    }

    # set the ip for the session
    $PodeSession.Server.IP.Address = (Get-IPAddress $_host)
    if (!(Test-IPAddressLocal -IP $PodeSession.Server.IP.Address)) {
        $PodeSession.Server.IP.Name = $PodeSession.Server.IP.Address
    }

    # set the port for the session
    $PodeSession.Server.IP.Port = $_port

    # set the server type
    $PodeSession.Server.Type = $Type

    # if the server type is https, set cert details
    if ($Type -ieq 'https') {
        $PodeSession.Server.IP.Ssl = $true
        $PodeSession.Server.IP.Certificate.Name = $Certificate
    }
}

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
    $PodeSession.RunspacePools.Values | ForEach-Object {
        $_.InitialSessionState.ImportPSModule($_path)
    }
}
src\Tools\Setup.ps1
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)"
    }
}
src\Tools\SmtpServer.ps1
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 = $PodeSession.Server.IP.Port
    if ($port -eq 0) {
        $port = 25
    }

    # create the listener for smtp
    $endpoint = New-Object System.Net.IPEndPoint($PodeSession.Server.IP.Address, $port)
    $listener = New-Object System.Net.Sockets.TcpListener -ArgumentList $endpoint

    try
    {
        # start listener
        $listener.Start()
    }
    catch {
        $Error[0] | Out-Default

        if ($null -ne $listener) {
            $listener.Stop()
        }

        throw $_.Exception
    }

    # state where we're running
    Write-Host "Listening on smtp://$($PodeSession.Server.IP.Name):$($port) [$($PodeSession.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 $($PodeSession.Server.IP.Name) -- 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.Lockable = $PodeSession.Lockable

                            # 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 (!$PodeSession.Tokens.Cancellation.IsCancellationRequested)
            {
                # get an incoming request
                $task = $Listener.AcceptTcpClientAsync()
                $task.Wait($PodeSession.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' = $PodeSession.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..$PodeSession.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 (!$PodeSession.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
}
src\Tools\TcpServer.ps1
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 = $PodeSession.Server.IP.Port
    if ($port -eq 0) {
        $port = 9001
    }

    $endpoint = New-Object System.Net.IPEndPoint($PodeSession.Server.IP.Address, $port)
    $listener = New-Object System.Net.Sockets.TcpListener -ArgumentList $endpoint

    try
    {
        # start listener
        $listener.Start()
    }
    catch {
        $Error[0] | Out-Default

        if ($null -ne $listener) {
            $listener.Stop()
        }

        throw $_.Exception
    }

    # state where we're running
    Write-Host "Listening on tcp://$($PodeSession.Server.IP.Name):$($port) [$($PodeSession.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 (!$PodeSession.Tokens.Cancellation.IsCancellationRequested)
            {
                # get an incoming request
                $task = $Listener.AcceptTcpClientAsync()
                $task.Wait($PodeSession.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' = $PodeSession.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..$PodeSession.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 (!$PodeSession.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 }
}
src\Tools\Timers.ps1
function Get-PodeTimer
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeSession.Timers[$Name]
}

function Start-TimerRunspace
{
    if ((Get-Count $PodeSession.Timers) -eq 0) {
        return
    }

    $script = {
        while ($true)
        {
            $_remove = @()
            $_now = [DateTime]::Now

            $PodeSession.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' = $PodeSession.Lockable } -Scoped
                    }
                    catch {
                        $Error[0]
                    }

                    $_.NextTick = $_now.AddSeconds($_.Interval)
                }
            }

            # remove any timers
            $_remove | ForEach-Object {
                $PodeSession.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 ($PodeSession.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' = $PodeSession.Lockable } -Scoped
    }

    # add the timer
    $PodeSession.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;
    }
}
src\Tools\WebServer.ps1
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()
    }

    $PodeSession.Server.ViewEngine.Engine = $Engine.ToLowerInvariant()
    $PodeSession.Server.ViewEngine.Extension = $Extension
    $PodeSession.Server.ViewEngine.Script = $ScriptBlock
}

function Start-WebServer
{
    param (
        [switch]
        $Https
    )

    # grab the protocol
    $protocol = (iftet $Https 'https' 'http')

    # grab the ip address
    $_ip = "$($PodeSession.Server.IP.Address)"
    if ($_ip -ieq '0.0.0.0') {
        $_ip = '*'
    }

    # grab the port
    $port = $PodeSession.Server.IP.Port
    if ($port -eq 0) {
        $port = (iftet $Https 8443 8080)
    }

    # if it's https, generate a self-signed cert or bind an existing one
    if ($Https -and $PodeSession.Server.IP.Ssl) {
        New-PodeSelfSignedCertificate -IP $PodeSession.Server.IP.Address -Port $port -Certificate $PodeSession.Server.IP.Certificate.Name
    }

    # setup any inbuilt middleware
    $inbuilt_middleware = @(
        (Get-PodeAccessMiddleware),
        (Get-PodeLimitMiddleware),
        (Get-PodePublicMiddleware),
        (Get-PodeRouteValidateMiddleware),
        (Get-PodeBodyMiddleware),
        (Get-PodeQueryMiddleware)
    )

    $PodeSession.Server.Middleware = ($inbuilt_middleware + $PodeSession.Server.Middleware)

    # create the listener on http and/or https
    $listener = New-Object System.Net.HttpListener

    try
    {
        # start listening on ip:port
        $listener.Prefixes.Add("$($protocol)://$($_ip):$($port)/")
        $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 $($protocol)://$($PodeSession.Server.IP.Name):$($port)/ [$($PodeSession.Threads) thread(s)]" -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 !$PodeSession.Tokens.Cancellation.IsCancellationRequested)
            {
                # get request and response
                $task = $Listener.GetContextAsync()
                $task.Wait($PodeSession.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 = $PodeSession.Lockable
                    $WebEvent.Path = ($request.RawUrl -isplit "\?")[0]
                    $WebEvent.Method = $request.HttpMethod.ToLowerInvariant()

                    # add logging endware for post-request
                    Add-PodeLogEndware -WebEvent $WebEvent

                    # invoke middleware
                    if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $PodeSession.Server.Middleware)) {
                        # get the route logic
                        $route = Get-PodeRoute -HttpMethod $WebEvent.Method -Route $WebEvent.Path
                        if ($null -eq $route) {
                            $route = Get-PodeRoute -HttpMethod '*' -Route $WebEvent.Path
                        }

                        # 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 + @($PodeSession.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..$PodeSession.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 !$PodeSession.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 }
}
tools\ChocolateyInstall.ps1
$ErrorActionPreference = 'Stop'

# Install Module
# Determine which Program Files path to use
if (![string]::IsNullOrWhiteSpace($env:ProgramFiles)) {
    $modulePath = Join-Path $env:ProgramFiles (Join-Path 'WindowsPowerShell' 'Modules')
}
else {
    $modulePath = Join-Path ${env:ProgramFiles(x86)} (Join-Path 'WindowsPowerShell' 'Modules')
}

# Check to see if we need to create the Modules path
if (!(Test-Path $modulePath))
{
    Write-Host "Creating path: $modulePath"
    New-Item -ItemType Directory -Path $modulePath -Force | Out-Null
    if (!$?) {
        throw "Failed to create: $modulePath"
    }
}

# Check to see if Modules path is in PSModulePaths
$psModules = $env:PSModulePath
if (!$psModules.Contains($modulePath))
{
    Write-Host 'Adding module path to PSModulePaths'
    $psModules += ";$modulePath"
    Install-ChocolateyEnvironmentVariable -VariableName 'PSModulePath' -VariableValue $psModules -VariableType Machine
    $env:PSModulePath = $psModules
}

# Create Pode module
$podeModulePath = Join-Path $modulePath 'Pode'
if (!(Test-Path $podeModulePath))
{
    Write-Host 'Creating Pode module directory'
    New-Item -ItemType Directory -Path $podeModulePath -Force | Out-Null
    if (!$?) {
        throw "Failed to create: $podeModulePath"
    }
}

# Copy contents to module
Write-Host 'Copying Pode to module path'

try
{
    Push-Location (Join-Path $env:ChocolateyPackageFolder 'src')

    New-Item -ItemType Directory -Path (Join-Path $podeModulePath 'Tools') -Force | Out-Null
    Copy-Item -Path ./Tools/* -Destination (Join-Path $podeModulePath 'Tools') -Force | Out-Null
    Copy-Item -Path ./Pode.psm1 -Destination $podeModulePath -Force | Out-Null
    Copy-Item -Path ./Pode.psd1 -Destination $podeModulePath -Force | Out-Null
}
finally
{
    Pop-Location
}
tools\ChocolateyUninstall.ps1
# Determine which Program Files path to use
if (![string]::IsNullOrWhiteSpace($env:ProgramFiles))
{
    $modulePath = Join-Path $env:ProgramFiles (Join-Path 'WindowsPowerShell' 'Modules')
}
else
{
    $modulePath = Join-Path ${env:ProgramFiles(x86)} (Join-Path 'WindowsPowerShell' 'Modules')
}

# Delete Pode module
$podeModulePath = Join-Path $modulePath 'Pode'
if (Test-Path $podeModulePath)
{
    Write-Host 'Deleting Pode module directory'
    Remove-Item -Path $podeModulePath -Recurse -Force | Out-Null
    if (!$?)
    {
        throw "Failed to delete: $podeModulePath"
    }
}

Log in or click on link to see number of positives.

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.8.0 236 Friday, February 3, 2023 Approved
Pode 2.7.2 130 Tuesday, October 25, 2022 Approved
Pode 2.7.1 112 Thursday, July 21, 2022 Approved
Pode 2.7.0 122 Wednesday, June 22, 2022 Approved
Pode 2.6.2 165 Wednesday, March 2, 2022 Approved
Pode 2.6.1 87 Monday, February 21, 2022 Approved
Pode 2.6.0 87 Thursday, February 10, 2022 Approved
Pode 2.5.2 99 Tuesday, January 4, 2022 Approved
Pode 2.5.1 94 Tuesday, December 21, 2021 Approved
Pode 2.5.0 111 Saturday, November 13, 2021 Approved
Pode 2.4.2 127 Monday, September 13, 2021 Approved
Pode 2.4.1 112 Monday, August 9, 2021 Approved
Pode 2.4.0 94 Wednesday, July 21, 2021 Approved
Pode 2.3.0 120 Tuesday, June 1, 2021 Approved
Pode 2.2.3 136 Saturday, April 10, 2021 Approved
Pode 2.2.2 90 Friday, April 9, 2021 Approved
Pode 2.2.1 94 Saturday, March 27, 2021 Approved
Pode 2.2.0 111 Sunday, March 21, 2021 Approved
Pode 2.1.1 113 Friday, February 19, 2021 Approved
Pode 2.1.0 1164 Wednesday, February 3, 2021 Approved
Pode 2.0.3 148 Monday, December 21, 2020 Approved
Pode 2.0.2 121 Saturday, December 5, 2020 Approved
Pode 2.0.1 104 Sunday, November 29, 2020 Approved
Pode 2.0.0 175 Saturday, November 14, 2020 Approved
Pode 1.8.4 166 Friday, October 16, 2020 Approved
Pode 1.8.3 150 Sunday, September 20, 2020 Approved
Pode 1.8.2 190 Friday, July 31, 2020 Approved
Pode 1.8.1 169 Friday, June 26, 2020 Approved
Pode 1.8.0 180 Sunday, May 24, 2020 Approved
Pode 1.7.3 182 Sunday, May 10, 2020 Approved
Pode 1.7.2 162 Monday, April 27, 2020 Approved
Pode 1.7.1 154 Friday, April 17, 2020 Approved
Pode 1.7.0 171 Friday, April 10, 2020 Approved
Pode 1.6.1 223 Saturday, March 7, 2020 Approved
Pode 1.6.0 184 Tuesday, March 3, 2020 Approved
Pode 1.5.0 220 Sunday, February 2, 2020 Approved
Pode 1.4.0 193 Friday, January 10, 2020 Approved
Pode 1.3.0 179 Friday, December 27, 2019 Approved
Pode 1.2.1 200 Monday, December 2, 2019 Approved
Pode 1.2.0 185 Wednesday, November 13, 2019 Approved
Pode 1.1.0 201 Saturday, September 28, 2019 Approved
Pode 1.0.1 191 Wednesday, September 4, 2019 Approved
Pode 1.0.0 192 Monday, September 2, 2019 Approved
Pode 0.32.0 230 Friday, June 28, 2019 Approved
Pode 0.31.0 193 Tuesday, June 11, 2019 Approved
Pode 0.30.0 194 Sunday, May 26, 2019 Approved
Pode 0.29.0 192 Friday, May 10, 2019 Approved
Pode 0.28.1 227 Tuesday, April 16, 2019 Approved
Pode 0.28.0 180 Saturday, April 13, 2019 Approved
Pode 0.27.3 200 Thursday, April 4, 2019 Approved
Pode 0.27.2 218 Wednesday, March 27, 2019 Approved
Pode 0.27.1 210 Saturday, March 16, 2019 Approved
Pode 0.27.0 209 Thursday, March 14, 2019 Approved
Pode 0.26.0 233 Sunday, February 17, 2019 Approved
Pode 0.25.0 228 Tuesday, February 5, 2019 Approved
Pode 0.24.0 252 Friday, January 18, 2019 Approved
Pode 0.23.0 245 Monday, December 24, 2018 Approved
Pode 0.22.0 236 Friday, December 7, 2018 Approved
Pode 0.21.0 258 Friday, November 2, 2018 Approved
Pode 0.20.0 260 Saturday, October 20, 2018 Approved
Pode 0.19.1 228 Tuesday, October 9, 2018 Approved
Pode 0.19.0 244 Friday, September 14, 2018 Approved
Pode 0.18.0 234 Saturday, August 25, 2018 Approved
Pode 0.17.0 203 Sunday, August 19, 2018 Approved
Pode 0.16.0 249 Wednesday, August 8, 2018 Approved
Pode 0.15.0 274 Friday, July 13, 2018 Approved
Pode 0.14.0 246 Friday, July 6, 2018 Approved
Pode 0.13.0 247 Saturday, June 23, 2018 Approved
Pode 0.12.0 239 Friday, June 15, 2018 Approved
Pode 0.11.3 275 Sunday, June 10, 2018 Approved
Pode 0.11.2 271 Friday, June 8, 2018 Approved
Pode 0.11.1 291 Friday, June 1, 2018 Approved
Pode 0.11.0 258 Wednesday, May 30, 2018 Approved
Pode 0.10.1 310 Wednesday, May 16, 2018 Approved
Pode 0.9.0 332 Thursday, January 11, 2018 Approved

This package has no dependencies.

Discussion for the Pode Package

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.
comments powered by Disqus