Private/WebUtil.ps1

Function Get-FreeWebPort {
    <#
    .Synopsis
    Find a free port for web application.

    .Description
    Find a free port for web application, starting from a given port, and increment if needed.

    .Parameter startingPort
    The port to start with.

    .Example
    # find a free port starting with 8090
    Get-FreeWebPort 8090
    #>
    
    [cmdletbinding()]
    [OutputType([int])]
    param( 
        [int] $startingPort
    )
    Process
    {        
        $usedPorts = @()
        Import-Module WebAdministration
        
        foreach ($bind in (Get-WebBinding).bindingInformation) {
            $port = Get-BindingPort $bind
            $usedPorts += $port 
        }
        
        $choosenPort = $startingPort
        while ($usedPorts.contains($choosenPort)) {
            $choosenPort += 10
        }
        
        return $choosenPort
    }
}

function Get-BindingPort($bind) {
    Write-Verbose "Extracting port from binding $bind"
    $startIndex = $bind.indexof(':')+1
    $endIndex = $bind.lastindexof(':')
    if ($endIndex -gt $startIndex) {
        $port = $bind.Substring($startIndex, ($endIndex-$startIndex))
    } else {
        $port = $bind.Substring($startIndex)
    }
    Write-Verbose "Extracted port = $port"
    return [int] $port
}

function Find-WebSite($Port) {
    $sites = Get-WebSite
    foreach ($site in $sites) {
        $matchSite = Find-SiteMatchPort $site $Port
        if ($matchSite) {
            return $site
        }
    }

    return $null
}

function Find-SiteMatchPort($Site, $Port) {
   $bindings = $Site.bindings.Collection.Where({$_.protocol.StartsWith('http')})
   foreach ($binding in $bindings.bindingInformation) {
        $sitePort = Get-BindingPort $binding
        $siteName = $Site.Name
        Write-Verbose "Comparing site $sitePort with $Port"
        if ($sitePort -eq $Port) {
            Write-Verbose "Site $siteName match port $Port"
            return $true
        }
   }

   return $false
}

function Set-WebAuthentication($Tenant, $WebSite, $AppName, $Value, [switch] $NoSave) {
    $Path = $WebSite
    $iisPath = "IIS:Sites\$Website"
    Write-Verbose "Path is $Path"
    if ($Value -eq "Windows") {
        Set-WebConfigurationProperty -filter /system.webServer/security/authentication/windowsAuthentication -name enabled -value $true -PSPath IIS:\ -location "$Path"
        Set-WebConfigurationProperty -filter /system.webServer/security/authentication/AnonymousAuthentication -name enabled -value $false -PSPath IIS:\ -location "$Path"
        $config = (Get-WebConfiguration system.web/authentication $iisPath)
        $config.mode = "Windows"
        $config | Set-WebConfiguration system.web/authentication
    }

    if ($Value -eq "Formular") {
        Set-WebConfigurationProperty -filter /system.webServer/security/authentication/windowsAuthentication -name enabled -value $false -PSPath IIS:\ -location "$Path"
        Set-WebConfigurationProperty -filter /system.webServer/security/authentication/AnonymousAuthentication -name enabled -value $true -PSPath IIS:\ -location "$Path"
        $config = (Get-WebConfiguration system.web/authentication $iisPath)
        $config.mode = "Forms"
        $config | Set-WebConfiguration system.web/authentication
    }

    if (!($NoSave)) {
        Store-WebConf $Tenant "Authentication" $Value
    }

    Write-Verbose "Configuration saved"
}

function Store-WebConf($Tenant, $Key, $Value) {
    $path = Get-WebConfigPathFromTenant $Tenant
    if (!(Test-Path $path)) {
        mkdir $path | Out-Null
    }

    $filePath = "$path$Key"
    Write-Verbose "Writing $Value to $filePath"
    Set-Content $filePath $Value
}

function Get-WebConf($Tenant, $Key) {
    $path = Get-WebConfigPathFromTenant $Tenant
    $filePath = "$path$Key"
    if (!(Test-Path $filePath)) {
        Write-Verbose "Path $filePath does not exist"
        return $null
    }

    Write-Verbose "Reading $filePath"
    $value = Get-Content $filePath
    Write-Verbose "$Key : $value"
    return $value
}

function Get-SSLConf($Tenant) {
    $ssljson = [string] (Get-WebConf $Tenant "SSL")
    if ($ssljson -eq $null) {
        
        $ssl = @{
            "Enable" = $false;
        }
    } else {
        $ssl = ConvertFrom-Json $ssljson
    }

    return $ssl
}

function Get-AuthenticationConf($Tenant) {
    $authentication = Get-WebConf $Tenant "Authentication"
    if ($authentication -eq $null) {
            $authentication = "Windows"
    }

    return $authentication
}

function Apply-WebConfOnWebServices($webPath) {
    $sslConf = Get-SSLConf $Tenant
    if ($sslConf -eq $null) {
        $ssl = "false"
    } else {
        $ssl = $sslConf.Enable
    }

    $authentication = Get-AuthenticationConf $Tenant
    if ($authentication -eq $null) {
        $authentication = "Windows"
    }

    Write-Output "Web configuration - SSL: $ssl, Authentication: $authentication"
    $servicePath = "$webPath\services.config"
    $backupPath = "$webPath\services.backup"

    Backup-ServiceConfig $servicePath $backupPath
    Write-ServiceInit $servicePath
    Write-ServicesHeader $servicePath
    Write-Services $servicePath $authentication $ssl
    Write-ServicesFooter $servicePath
}

function Backup-ServiceConfig($Path, $Backup) {
    if (Test-Path $Path) {
        Write-Verbose "Creating backup of $Path to $Backup"
        Copy-Item $Path $Backup -force | Out-Null
    }
}

function Write-ServiceInit($Path) {
    Write-Verbose "Initializing $Path"
    New-Item $Path -force | Out-Null
}

function Write-ServicesHeader($Path) {
    Add-Content $Path "<?xml version=`"1.0`"?>"
    Add-Content $Path "<services>"
}

function Write-ServicesFooter($Path) {
    Add-Content $Path "</services>"
}

function Write-Services($Path, $Authentication, $SSL) {
    foreach($pair in $services.GetEnumerator()) {
        $service = $($pair.Name)
        $behavior = $($pair.Value)
        $contract = $endpointContract[$service]
        if ($contract -eq $null) {
            $contract = $service
        }

        Write-Verbose "Registering service $service"
        Write-ServiceHeader $Path $service
        $binding = Get-Binding $Authentication "false"
        Write-EndPoint $Path $behavior $binding $contract
        if ($SSL -eq "true") {
            $binding = Get-Binding $Authentication "true"
            Write-EndPoint $Path $behavior $binding $contract
        }

        Write-ServiceFooter $Path
    }
}

function Get-Binding($Authentication, $SSL) {
    if ($Authentication -eq "Windows") {
        if ($SSL -eq "true") {
            return "securedWebBinding"
        } else {
            return "defaultWebBinding"
        }
    } else {
        if ($SSL -eq "true") {
            return "anoSecuredWebBinding"
        } else {
            return "anoWebBinding"
        }
    }
}

function Write-ServiceHeader($Path, $Service) {
    $header = "`t" + $serviceHeaderTemplate.Replace("[SERVICE]", $Service)
    Add-Content $Path $header
}

function Write-EndPoint($Path, $Behavior, $Binding, $Service) {
    $endpoint = "`t`t" + $endPointTemplate.Replace("[BEHAVIOR]", $Behavior).Replace("[BINDING]", $Binding).Replace("[SERVICE]", $Service)
    Add-Content $Path $endpoint
}

function Write-ServiceFooter($Path) {
    Add-Content $Path "`t</service>"
}

$serviceHeaderTemplate = "<service behaviorConfiguration=`"httpGetEnablingBehavior`" name=`"[SERVICE]`">"
$endPointTemplate = "<endpoint address=`"`" behaviorConfiguration=`"[BEHAVIOR]`" binding=`"webHttpBinding`" bindingConfiguration=`"[BINDING]`" contract=`"[SERVICE]`"/>"

$services = @{
        "Ortems.PlannerOne.Web.UserControl.CalendarExceptions.CalendarException" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Indicators.IndicatorsService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Scheduling.ConfirmationLoader" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Sequencing.SequencingService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Display.Custom.TaskColorService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Filter.FilterService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Views.ResourceSequence.ResourceSequenceService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.DetailsPanel.InfoService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Utilities.UtilitiesService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Display.RefreshService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Display.ViewsService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Display.Custom.RowService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Display.Custom.Task" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Robots.RobotsService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Search.SearchService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Action.ManageTask.TaskManagerService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Views.Calendar.Service" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.ActivityComputation.ActivityComputationService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Service.SelectionService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Views.JobOverview.JobOverviewService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.Display.Total.TotalService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Service.ViewsService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.SwitchPlanning.SwitchPlanningService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.MoveTasks.MoveTasksService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Api.PlanningProduction" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Api.PlanningProject" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.UserControl.WebGantt.Gantt" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Admin.PerformanceService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Admin.PerformanceSessionService" = "webHttp";
        "Ortems.PlannerOne.Web.Admin.WebSecurityService" = "webHttp";
        "Ortems.PlannerOne.Web.UserControl.Action.Assignment.ChooseResource" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Configure.ConfigurationService" = "webHttp";
        "Ortems.PlannerOne.Web.Admin.ICalConfigurationService" = "webHttp";
        "Ortems.PlannerOne.Web.UserControl.PinTasks.PinTasksService" = "webScriptEnablingBehavior";
        "Ortems.PlannerOne.Web.Api.CacheInfo" = "webHttp";
}

$endpointContract = @{
    "Ortems.PlannerOne.Web.Api.PlanningProduction" = "Ortems.PlannerOne.Web.Api.IPlanningProductionContract";
    "Ortems.PlannerOne.Web.Api.PlanningProject" = "Ortems.PlannerOne.Web.Api.IPlanningContract"
}

function Get-P1WebApplication([string]$Tenant) {
    $info = Get-P1Tenant $Tenant
    $host = $info.WebHost
    $port = $info.SitePort
    $app = $info.WebApplicationName
    Write-Output "Host: $host, Port: $port, Application name: $app"

    $site = Find-WebSite $port
    $siteName = $site.Name
    if ($site -eq $null) {
        Write-Warning "No site use binding $port configured for tenant Web Application $app of tenant $Tenant. Operation canceled."
        return $null
    } else {
        Write-Verbose "Site found"
    }

    $webApp = Get-WebApplication -Name $app -Site $siteName
    if ($webApp -eq $null) {
        Write-Warning "Cannot find Web Application $app for port $port"
        return $null
    } else {
        Write-Verbose "Web Application found"
    }

    return $webApp
}

function Register-P1WebServices($Tenant) {
    Write-Section "Registering Web services for current security"
    if (!(Test-Tenant $Tenant)) {
        Write-Warning "Tenant $Tenant does not exist."
        Write-Warning "Operation canceled."
        return;
    }

    $info = Get-P1Tenant $Tenant
    $host = $info.WebHost
    $port = $info.SitePort
    $app = $info.WebApplicationName
    Write-Output "Host: $host, Port: $port, Application name: $app"

    $site = Find-WebSite $port
    if ($site -eq $null) {
        Write-Warning "No site use binding $port configured for tenant Web Application $app of tenant $Tenant. Operation canceled."
        return $null
    } else {
        Write-Verbose "Site found"
    }

    $path = $site.PhysicalPath

    Write-Verbose "Physical path is $path"
    Apply-WebConfOnWebServices $path
    Write-OK "REST services registered with security for tenant $Tenant"
}

function Set-WebAuthenticationInternal([string] $Tenant, [switch] $Windows, [switch] $Formular, [switch] $NoSave) {
    $info = Get-P1Tenant $Tenant
    $host = $info.WebHost
    $port = $info.SitePort
    $app = $info.WebApplicationName
    Write-Verbose "Host: $host, Port: $port, Application name: $app"

    $site = Find-WebSite $port
    $siteName = $site.Name
    if ($site -eq $null) {
        Write-Warning "No site use binding $port configured for tenant Web Application $app of tenant $Tenant. Operation canceled."
        return
    } else {
        Write-Verbose "Site found"
    }


    $webApp = Get-WebApplication -Name $app -Site $siteName
    if ($webApp -eq $null) {
        Write-Warning "Cannot find Web Application $app for port $port"
    } else {
        Write-Verbose "Web Application found"
    }

    $webAppPath = $webApp.PhysicalPath
    $sitePath = $site.PhysicalPath
    $siteConfigSource = "$webAppPath\site.config"
    $siteConfigTarget = "$sitePath\web.config"
    Write-Verbose "Copying $siteConfigSource to $siteConfigTarget"
    Copy-Item $siteConfigSource $siteConfigTarget -Force

    $mode = ""
    if ($Windows) {
        $mode = "Windows"
    }

    if ($Formular) {
        $mode = "Formular"
    }

    Write-Verbose "Mode will be set to $mode"

    Set-WebAuthentication $Tenant $siteName $app $mode -NoSave:$NoSave.IsPresent
}

function Set-WebSSLInternal([string] $Tenant, [switch] $Enable, [switch] $Disable, [int] $SSLPort, [string] $CertificateStore, [string] $CertificateFriendlyName, [string] $CertificateThumbprint, [string] $HostedIP, [switch] $NoSave) {
    if ($SSLPort -eq 0) {
        Write-Warning "No SSL Port define, trying with 443."
        $SSLPort = 443
    }
        
    $info = Get-P1Tenant $Tenant
    $host = $info.WebHost
    $port = $info.SitePort
    $app = $info.WebApplicationName
    Write-Verbose "Host: $host, Port: $port, Application name: $app"

    $site = Find-WebSite $port
    $siteName = $site.Name
    if ($site -eq $null) {
        Write-Warning "No site use binding $port configured for tenant Web Application $app of tenant $Tenant. Operation canceled."
        return
    } else {
        Write-Verbose "Site found"
    }

    $bindingHostedIP = $HostedIP
    if ($HostedIP -eq "" -or $HostedIP -eq "0.0.0.0") {
        $HostedIP = "0.0.0.0"
        $bindingHostedIP = "*"
    }

    # Disable binding for site
    if ($Disable) {
        Write-Verbose "Removing binding $SSLPort on site $siteName"
        try {
            Remove-WebBinding -Name "$siteName" -Port $SSLPort -ErrorAction Stop
        } catch {
            Write-KO "Web application binding for port $SSLPort does not exist"
        }

        Write-Verbose "Removing SSL binding record"
        try {
            Remove-Item "IIS:\SslBindings\$HostedIP!$SSLPort" -ErrorAction Stop
        } catch {
            Write-KO "SSL Binding IIS:\SslBindings\$HostedIP!$SSLPort does not exist"
        }

        if (!($NoSave)) {
            $conf = @{
                "Enable" = $false;
            }

            $jsonConf = ConvertTo-Json $conf
            Store-WebConf $Tenant "SSL" $jsonConf
        }

        Write-OK "Web SSL disabled for tenant $Tenant"
        return
    }

    # Create binding for site
    Write-Verbose "Creating new binding $SSLPort on site $siteName"
    try {
        New-WebBinding -Name "$siteName" -IPAddress "$bindingHostedIP" -Port $SSLPort -Protocol https
    } catch {
        Write-KO "Port is already define on site"
        return
    }

    # Create SSL Binding in IIS
    if ($CertificateStore -eq "") {
        $CertificateStore = "My"
    }

    if ($CertificateFriendlyName -ne "" -and $CertificateThumbprint -ne "" ) {
        Write-Warning "You can only use one certificate option between CertificateFriendlyName and CertificateThumbprint"
        return
    }

    if ($CertificateFriendlyName -eq "" -and $CertificateThumbprint -eq "" ) {
        Write-Warning "You must choose one certificate option between CertificateFriendlyName and CertificateThumbprint"
        return
    }

    $certificateLocation = "cert:\LocalMachine\$CertificateStore"
    Write-Verbose "Certificate location is $certificateLocation"
    if ($CertificateFriendlyName -ne "") {
        Write-Verbose "Searching for certificate with friendly name $CertificateFriendlyName"
        $certificate = Get-ChildItem $certificateLocation | where-object { $_.FriendlyName -eq "$CertificateFriendlyName" }
    }

    if ($CertificateThumbprint -ne "") {
        Write-Verbose "Searching for certificate with thumbprint $CertificateThumbprint"
        $certificate = Get-ChildItem $certificateLocation | where-object { $_.Thumbprint -eq "$CertificateThumbprint" }
    }

    if ($certificate -eq $null) {
        Write-KO "Cannot find certificate $CertificateFriendlyName $CertificateThumbprint in $certificateLocation"
        return
    } else {
        # Thumbprint is store in configuration
        $CertificateThumbprint = $certificate.Thumbprint
        Write-Verbose "Certificate has been found"
    }
    
    Write-Verbose "Writing SSL binding IIS:\SslBindings\$HostedIP!$SSLPort"
    try {
        $certificate | New-Item "IIS:\SslBindings\$HostedIP!$SSLPort"
    } catch {
        Write-KO "IIS SSL binding IIS:\SslBindings\$HostedIP!$SSLPort already exist"
    }

    Write-Verbose "SSL binding written"

    if (!($NoSave)) {
        $conf = @{
            "Enable" = $true;
            "Port" = $SSLPort;
            "Store" = "$CertificateStore";
            "Thumbprint" = "$CertificateThumbprint";
            "IP" = "$HostedIP";
        }

        $jsonConf = ConvertTo-Json $conf
        Store-WebConf $Tenant "SSL" $jsonConf
    }
}

function EscapeQueryData($parameter) {
    $escaped = [System.Uri]::EscapeDataString($parameter)
    $escaped = $escaped.Replace(".", "%2E")
    return $escaped
}

function FixUrl($url) {
    # protocol for which to prevent unescaping. Change to HTTPS if necessary.
    $protocol = "http"
    # flag's value
    $UnEscapeDotsAndSlashes = 0x2000000

    # GetSyntax method, which is static internal, gets registered parsers for given protocol
    $getSyntax = [System.UriParser].GetMethod("GetSyntax", 40)
    # field m_Flags contains information about Uri parsing behaviour
    $flags = [System.UriParser].GetField("m_Flags", 36)

    $parser = $getSyntax.Invoke($null, $protocol)
    # get the current Flag settings
    $currentValue = $flags.GetValue($parser)

    # check if un-escaping enabled
    if (($currentValue -band $UnEscapeDotsAndSlashes) -eq $UnEscapeDotsAndSlashes) {
      $newValue = $currentValue -bxor $UnEscapeDotsAndSlashes
      # disable unescaping by removing UnEscapeDotsAndSlashes flag
      $flags.SetValue($parser, $newValue)
    }

    $uri = new-object Uri($url)
    return $uri
}

function Update-MIMETypes() {
    Write-Section "Registering MIME Types..."
    Import-Module WebAdministration

    $targetMimeType = 'application/json'
    # check if mime type already exist
    $jsonMimeType = Get-WebConfigurationProperty -Filter "//staticContent/mimeMap[@fileExtension='.json']" -Name mimeType
    
    if ($jsonMimeType) {
        # if exist, warning! (Could be a wrong mapping)
        $mimeTypeValue = $jsonMimeType.Value
        Write-OK ".json file extension already exists and is associated to content type: $mimeTypeValue"
    } else {
        # if not exist, add it
        Write-Verbose "Adding .json file extension to be associated to content type: $targetMimeType"
        Add-WebConfigurationProperty //staticContent -Name collection -Value @{fileExtension='.json'; mimeType=$targetMimeType} -ErrorAction SilentlyContinue
        
        if ($error.Count -gt 0) {
            Write-KO "Something wrong happened: $error[0].Exception.Message"
        } else {
            Write-OK ".json file extension created"
        }
    }
}