Public/Sites.ps1

function Get-IISMSite
{
    [CmdletBinding(DefaultParameterSetName='Path')]
    param (
        [Parameter()]
        [string]
        $Name,

        [Parameter(ParameterSetName='Path')]
        [string]
        $PhysicalPath,

        [Parameter(ParameterSetName='Quick')]
        [switch]
        $Quick
    )

    # get site(s)
    if (![string]::IsNullOrWhiteSpace($Name)) {
        $result = Invoke-IISMAppCommand -Arguments "list site '$($Name)'" -NoError
    }
    else {
        $result = Invoke-IISMAppCommand -Arguments 'list sites' -NoError
    }

    if ($null -eq $result.SITE) {
        return $null
    }

    # get list of IIS apps to map to sites
    $sites = ConvertTo-IISMSiteObject -Sites $result.SITE -Quick:$Quick

    # if we have a physical path, filter sites
    if (!$Quick -and ![string]::IsNullOrWhiteSpace($PhysicalPath)) {
        $sites = @($sites | Where-Object {
            $_.Apps | Where-Object {
                $_.Directories | Where-Object {
                    $_.PhysicalPath -ieq $PhysicalPath
                }
            }
        })

        foreach ($site in $sites) {
            $site.Apps = @($site.Apps | Where-Object {
                $_.Directories | Where-Object {
                    $_.PhysicalPath -ieq $PhysicalPath
                }
            })
        }
    }

    return $sites
}

function Get-IISMSites
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [string[]]
        $Names,

        [switch]
        $Quick
    )

    return @(foreach ($name in $Names) { Get-IISMSite -Name $name -Quick:$Quick })
}

function Test-IISMSite
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    $result = Invoke-IISMAppCommand -Arguments "list site '$($Name)'" -NoError
    return ($null -ne $result.SITE)
}

function Test-IISMSiteRunning
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    return ((Get-IISMSite -Name $Name -Quick).State -ieq 'started')
}

function Stop-IISMSite
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    if (Test-IISMSiteRunning -Name $Name) {
        Invoke-IISMAppCommand -Arguments "stop site '$($Name)'" -NoParse | Out-Null
        Invoke-IISMAppCommand -Arguments "set site '$($Name)' /serverAutoStart:false" -NoParse | Out-Null
    }

    return (Get-IISMSite -Name $Name -Quick)
}

function Start-IISMSite
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    if (!(Test-IISMSiteRunning -Name $Name)) {
        Invoke-IISMAppCommand -Arguments "start site '$($Name)'" -NoParse | Out-Null
        Invoke-IISMAppCommand -Arguments "set site '$($Name)' /serverAutoStart:true" -NoParse | Out-Null
    }

    return (Get-IISMSite -Name $Name -Quick)
}

function Restart-IISMSite
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )
    
    Stop-IISMSite -Name $Name | Out-Null
    Start-IISMSite -Name $Name | Out-Null
    return (Get-IISMSite -Name $Name -Quick)
}

function Get-IISMSiteBindings
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    # get site
    $result = Invoke-IISMAppCommand -Arguments "list site '$($Name)'" -NoError
    if ($null -eq $result.SITE) {
        return $null
    }

    # parse the list of binding
    return @(ConvertTo-IISMBindingObject -Site $result.SITE)
}

function Get-IISMSitePhysicalPath
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $AppName = '/',

        [Parameter()]
        [string]
        $DirName
    )

    $AppName = Add-IISMSlash -Value $AppName
    $DirName = Add-IISMSlash -Value $DirName

    $dirs = ((Get-IISMSite -Name $Name).Apps | Where-Object {
        $_.Path -ieq $AppName
    } | Select-Object -First 1).Directories
    
    return ($dirs | Where-Object {
        $_.Path -ieq $DirName
    } | Select-Object -First 1).PhysicalPath
}

function Get-IISMSiteAppPool
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $AppName = '/'
    )

    $AppName = Add-IISMSlash -Value $AppName

    return ((Get-IISMSite -Name $Name).Apps | Where-Object {
        $_.Path -ieq $AppName
    } | Select-Object -First 1).AppPool.Name
}

function Reset-IISMSiteAppPool
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    (Get-IISMSite -Name $Name).Apps.AppPool.Name | Sort-Object -Unique | ForEach-Object {
        Reset-IISMAppPool -Name $_ | Out-Null
    }
}

function Edit-IISMSitePhysicalPath
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $AppName = '/',

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

        [switch]
        $CreatePath
    )

    $AppName = Add-IISMSlash -Value $AppName

    # error if the site doesn't exist
    if (!(Test-IISMSite -Name $Name)) {
        throw "Website '$($Name)' does not exist in IIS"
    }

    # get the site info
    $site = Get-IISMSite -Name $Name

    # error if this site doesn't have the supplied app
    $app = ($site.Apps | Where-Object { $_.Path -ieq $AppName })
    if ($null -eq $app) {
        throw "The app '$($AppName)' does not exist against the website '$($Name)' in IIS"
    }

    # if create flag passed, make the path
    if ($CreatePath -and !(Test-Path $PhysicalPath)) {
        New-Item -Path $PhysicalPath -ItemType Directory -Force | Out-Null
    }

    # update the physical path
    Update-IISMDirectory -SiteName $Name -AppName $AppName -PhysicalPath $PhysicalPath | Out-Null

    # return the site
    return (Get-IISMSite -Name $Name)
}

function Remove-IISMSite
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    if (Test-IISMSite -Name $Name) {
        # first remove all bindings - in an attempt to remove cert bindings
        Remove-IISMSiteBindings -Name $Name

        # then, remove the site and everything else
        Invoke-IISMAppCommand -Arguments "delete site '$($Name)'" -NoParse | Out-Null
    }

    return (Get-IISMSite)
}

function Test-IISMSiteBinding
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [ValidateSet('ftp', 'http', 'https', 'msmq.formatname', 'net.msmq', 'net.pipe', 'net.tcp')]
        [string]
        $Protocol,

        [Parameter()]
        [int]
        $Port,

        [Parameter()]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname
    )

    # error if the site doesn't exist
    if (!(Test-IISMSite -Name $Name)) {
        throw "Website '$($Name)' does not exist in IIS"
    }

    # get the website bindings
    $bindings = Get-IISMSiteBindings -Name $Name
    foreach ($b in $bindings) {
        if ($b.Protocol -ieq $Protocol -and $b.Port -eq $Port -and $b.IPAddress -ieq $IPAddress -and $b.Hostname -ieq $Hostname) {
            return $true
        }
    }

    return $false
}

function Remove-IISMSiteBindings
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    # error if the site doesn't exist
    if (!(Test-IISMSite -Name $Name)) {
        throw "Website '$($Name)' does not exist in IIS"
    }

    # remove all bindings
    @(Get-IISMSiteBindings -Name $Name) | ForEach-Object {
        Remove-IISMSiteBinding -Name $Name -Protocol $_.Protocol -Port $_.Port -IPAddress $_.IPAddress -Hostname $_.Hostname | Out-Null
    }
}

function Remove-IISMSiteBinding
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [ValidateSet('ftp', 'http', 'https', 'msmq.formatname', 'net.msmq', 'net.pipe', 'net.tcp')]
        [string]
        $Protocol,

        [Parameter()]
        [int]
        $Port,

        [Parameter()]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname
    )

    # error if the site doesn't exist
    if (!(Test-IISMSite -Name $Name)) {
        throw "Website '$($Name)' does not exist in IIS"
    }

    # do nothing if binding doesn't exist
    if (!(Test-IISMSiteBinding -Name $Name -Protocol $Protocol -Port $Port -IPAddress $IPAddress -Hostname $Hostname)) {
        return
    }

    # if https, attempt to unbind cert first
    if ($Protocol -ieq 'https') {
        Remove-IISMSiteBindingCertificate -Port $Port -IPAddress $IPAddress -Hostname $Hostname
    }

    $binding = Get-IISMBindingCommandString -Protocol $Protocol -Port $Port -IPAddress $IPAddress -Hostname $Hostname
    Invoke-IISMAppCommand -Arguments "set site '$($Name)' /-`"$($binding)`"" -NoParse | Out-Null
    return (Get-IISMSiteBindings -Name $Name)
}

function Remove-IISMSiteDefaultBinding
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    return (Remove-IISMSiteBinding -Name $Name -Protocol http -Port 80 -IPAddress '*')
}

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

        [Parameter(Mandatory=$true)]
        [ValidateSet('ftp', 'http', 'https', 'msmq.formatname', 'net.msmq', 'net.pipe', 'net.tcp')]
        [string]
        $Protocol,

        [Parameter()]
        [int]
        $Port,

        [Parameter()]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname,

        [Parameter()]
        [string]
        $CertificateThumbprint
    )

    # error if the site doesn't exist
    if (!(Test-IISMSite -Name $Name)) {
        throw "Website '$($Name)' does not exist in IIS"
    }

    # attempt to remove the binding first, if it exists
    Remove-IISMSiteBinding -Name $Name -Protocol $Protocol -Port $Port -IPAddress $IPAddress -Hostname $Hostname | Out-Null

    # add the binding
    $binding = Get-IISMBindingCommandString -Protocol $Protocol -Port $Port -IPAddress $IPAddress -Hostname $Hostname
    Invoke-IISMAppCommand -Arguments "set site '$($Name)' /+`"$($binding)`"" -NoParse | Out-Null

    # if https, bind a certificate if thumbprint supplied
    if ($Protocol -ieq 'https' -and ![string]::IsNullOrWhiteSpace($CertificateThumbprint)) {
        Set-IISMSiteBindingCertificate -CertificateThumbprint $CertificateThumbprint -Port $Port -IPAddress $IPAddress -Hostname $Hostname
    }

    return (Get-IISMSiteBindings -Name $Name)
}

function Edit-IISMSiteAppPool
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $AppName ='/',

        [Parameter(Mandatory=$true)]
        [string]
        $AppPoolName
    )

    # error if the site doesn't exist
    if (!(Test-IISMSite -Name $Name)) {
        throw "Website '$($Name)' does not exist in IIS"
    }

    # error if the app doesn't exist
    $AppName = Add-IISMSlash -Value $AppName
    $FullAppName = "$($Name)$($AppName)"

    if (!(Test-IISMApp -SiteName $Name -Name $AppName)) {
        throw "Application '$($FullAppName)' does not exist in IIS"
    }

    # if the app-pool doesn't exist, create a default one
    if (!(Test-IISMAppPool -Name $AppPoolName)) {
        New-IISMAppPool -Name $AppPoolName | Out-Null
    }

    # bind the app-pool to the site's app
    Invoke-IISMAppCommand -Arguments "set app '$($FullAppName)' /applicationPool:'$($AppPoolName)'" -NoParse | Out-Null

    # return the site
    return (Get-IISMSite -Name $Name)
}

function New-IISMSite
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $AppPoolName = 'DefaultAppPool',

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

        [Parameter()]
        [pscredential]
        $Credentials,

        [switch]
        $CreatePath,

        [switch]
        $DisableAutoStart
    )

    # error if site already exists
    if (Test-IISMSite -Name $Name) {
        throw "Website '$($Name)' already exists in IIS"
    }

    # if the app-pool doesn't exist, create a default one
    if (!(Test-IISMAppPool -Name $AppPoolName)) {
        New-IISMAppPool -Name $AppPoolName | Out-Null
    }

    # if create flag passed, make the path
    if ($CreatePath -and !(Test-Path $PhysicalPath)) {
        New-Item -Path $PhysicalPath -ItemType Directory -Force | Out-Null
    }

    # create the site in IIS
    $_args = "/name:'$($Name)' /physicalPath:'$($PhysicalPath)'"
    Invoke-IISMAppCommand -Arguments "add site $($_args)" -NoParse | Out-Null

    # set the physical vdir path creds
    if ($null -ne $Credentials) {
        Set-IISMDirectoryCredentials -SiteName $Name -Credentials $Credentials
    }

    # flag the site's auto start mode
    Invoke-IISMAppCommand -Arguments "set site '$($Name)' /serverAutoStart:$(!$DisableAutoStart)" -NoParse | Out-Null

    # bind the app-pool to the site's default app
    if ($AppPoolName -ine 'DefaultAppPool') {
        Edit-IISMSiteAppPool -Name $Name -AppName '/' -AppPoolName $AppPoolName | Out-Null
    }

    Wait-IISMBackgroundTask -ScriptBlock { Test-IISMSite -Name $Name }

    # return the site
    return (Get-IISMSite -Name $Name)
}

function Set-IISMSiteBindingCertificate
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $CertificateThumbprint,

        [Parameter(Mandatory=$true)]
        [int]
        $Port,

        [Parameter()]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname
    )

    # error if no ip/hostname
    if ([string]::IsNullOrWhiteSpace($Hostname) -and [string]::IsNullOrWhiteSpace($IPAddress)) {
        throw "A Hostname or an IP Address is required when binding a certificate"
    }

    # error if already bound with cert
    if (Test-IISMSiteBindingCertificate -Port $Port -IPAddress $IPAddress -Hostname $Hostname) {
        throw "The binding '$($IPAddress):$($Port):$($Hostname)' is already bound with a certificate"
    }

    $appId = '{a3ba417c-dc1d-446b-95a5-a306ab26c1af}'

    # bind cert using hostname
    if (![string]::IsNullOrWhiteSpace($Hostname) -and $Hostname -ine '*') {
        $addr = "$($Hostname):$($Port)"
        $result = (Invoke-IISMNetshCommand -Arguments "http add sslcert hostnameport=$($addr) certhash=$($CertificateThumbprint) certstorename=MY appid='$($appId)'" -NoError)
    }

    # else, bind using IP address
    else {
        $addr = "$($IPAddress):$($Port)"
        $result = (Invoke-IISMNetshCommand -Arguments "http add sslcert ipport=$($addr) certhash=$($CertificateThumbprint) appid='$($appId)'" -NoError)
    }

    if ($LASTEXITCODE -ne 0 -or !$?) {
        throw "Failed to bind certificate against '$($addr)':`n$($result)"
    }
}

function Remove-IISMSiteBindingCertificate
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]
        $Port,

        [Parameter()]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname
    )

    # error if no ip/hostname
    if ([string]::IsNullOrWhiteSpace($Hostname) -and [string]::IsNullOrWhiteSpace($IPAddress)) {
        throw "A Hostname or an IP Address is required when removing a bound certificate"
    }

    # do nothing if not bound
    if (!(Test-IISMSiteBindingCertificate -Port $Port -IPAddress $IPAddress -Hostname $Hostname)) {
        return
    }

    # delete cert using hostname
    if (![string]::IsNullOrWhiteSpace($Hostname) -and $Hostname -ine '*') {
        $addr = "$($Hostname):$($Port)"
        $result = (Invoke-IISMNetshCommand -Arguments "http delete sslcert hostnameport=$($addr)" -NoError)
    }

    # else, delete using IP address
    else {
        $addr = "$($IPAddress):$($Port)"
        $result = (Invoke-IISMNetshCommand -Arguments "http delete sslcert ipport=$($addr)" -NoError)
    }

    if ($LASTEXITCODE -ne 0 -or !$?) {
        throw "Failed to delete certificate against '$($addr)':`n$($result)"
    }
}

function Test-IISMSiteBindingCertificate
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]
        $Port,

        [Parameter()]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname
    )

    return ($null -ne (Get-IISMSiteBindingCertificate -Port $Port -IPAddress $IPAddress -Hostname $Hostname))
}

function Get-IISMSiteBindingCertificate
{
    [CmdletBinding(DefaultParameterSetName='Port')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='Port')]
        [int]
        $Port,

        [Parameter(ParameterSetName='Port')]
        [string]
        $IPAddress,

        [Parameter(ParameterSetName='Port')]
        [string]
        $Hostname,

        [Parameter(Mandatory=$true, ParameterSetName='Thumbprint')]
        [string]
        $Thumbprint
    )

    if ($PSCmdlet.ParameterSetName -ieq 'port') {
        # get netsh details by ip address
        $details = (Invoke-IISMNetshCommand -Arguments "http show sslcert ipport=$($IPAddress):$($Port)" -NoError)

        # if that threw an error, and we have a hostname, check that
        if ($LASTEXITCODE -ne 0 -and ![string]::IsNullOrWhiteSpace($Hostname) -and $Hostname -ine '*') {
            $details = (Invoke-IISMNetshCommand -Arguments "http show sslcert hostnameport=$($Hostname):$($Port)" -NoError)
        }

        # get the thumbprint from the output
        $Thumbprint = (($details -imatch 'Certificate Hash\s+:\s+([a-z0-9]+)') -split ':')[1]
        if (![string]::IsNullOrWhiteSpace($Thumbprint)) {
            $Thumbprint = $Thumbprint.Trim()
        }
    }

    # if no thumbprint, return null
    if ([string]::IsNullOrWhiteSpace($Thumbprint)) {
        return $null
    }

    # get cert subject if on windows
    if (!(Test-IsUnix)) {
        $subject = (Get-ChildItem "Cert:/LocalMachine/My/$($Thumbprint)").Subject
    }

    # return the cert details
    return @{
        Thumbprint = $Thumbprint
        Subject = $subject
    }
}

function Test-IISMSiteIsFtp
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    $bindings = @(Get-IISMSiteBindings -Name $Name)
    return ($bindings.Protocol -icontains 'ftp')
}