Public/Sites.ps1

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

        [Parameter()]
        [string]
        $PhysicalPath
    )

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

    # just return if there are no results
    if ($null -eq $result) {
        return $null
    }

    # get list of IIS apps to map to sites
    $apps = Get-IISMApps
    $sites = ConvertTo-IISMSiteObject -Sites $result.SITE -Apps $apps

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

    return $sites
}

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

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

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

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

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

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

    return (Get-IISMSites -Name $Name)
}

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

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

    return (Get-IISMSites -Name $Name)
}

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

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

    return (Get-IISMSites -Name $Name).Bindings
}

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

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

    $AppName = Add-IISMSlash -Value $AppName

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

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

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

    $AppName = Add-IISMSlash -Value $AppName

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

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

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

        [Parameter(Mandatory=$true)]
        [Alias('p')]
        [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-IISMSites -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-IISMSites -Name $Name)
}

function Remove-IISMSite
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Alias('n')]
        [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-IISMSites)
}

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

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

        [Parameter()]
        [int]
        $Port,

        [Parameter()]
        [Alias('ip')]
        [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)]
        [Alias('n')]
        [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)]
        [Alias('n')]
        [string]
        $Name,

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

        [Parameter()]
        [int]
        $Port,

        [Parameter()]
        [Alias('ip')]
        [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)]
        [Alias('n')]
        [string]
        $Name
    )

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

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

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

        [Parameter()]
        [int]
        $Port,

        [Parameter()]
        [Alias('ip')]
        [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)]
        [Alias('n')]
        [string]
        $Name,

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

        [Parameter(Mandatory=$true)]
        [Alias('apn')]
        [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-IISMSites -Name $Name)
}

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

        [Parameter()]
        [Alias('apn')]
        [string]
        $AppPoolName,

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

        [switch]
        $CreatePath
    )

    # if no app-pool name, set to the site name
    if ([string]::IsNullOrWhiteSpace($AppPoolName)) {
        $AppPoolName = $Name
    }

    # 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

    # bind the app-pool to the site's default app
    Update-IISMSiteAppPool -Name $Name -App '/' -AppPoolName $AppPoolName | Out-Null
    Wait-IISMBackgroundTask -ScriptBlock { Test-IISMSite -Name $Name }

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

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

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

        [Parameter()]
        [Alias('ip')]
        [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()]
        [Alias('ip')]
        [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()]
        [Alias('ip')]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname
    )

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

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

        [Parameter()]
        [Alias('ip')]
        [string]
        $IPAddress,

        [Parameter()]
        [string]
        $Hostname
    )

    # 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/$($t)").Subject
    }

    # return the cert details
    return (New-Object -TypeName psobject |
        Add-Member -MemberType NoteProperty -Name Thumbprint -Value $thumbprint -PassThru |
        Add-Member -MemberType NoteProperty -Name Subject -Value $subject -PassThru)
}