PowerShell Wrapper for Http Namespaces

on Friday, October 31, 2014

When hosting HTTP WCF services as a self-hosted Windows Services the server needs to have the HTTP Namespace reserved. The reservation allows for the domain account which runs the service to setup a listener on a particular port, for a particular address.

There are some tools already available which can help in this process:

  • HTTP Namespace Manager – A nice GUI interface, which is easy to understand and setup. It also works on Server Core Servers.
  • httpcfg – Windows Server 2003
  • netsh – Windows Server 2008+

But, there are no PowerShell wrappers for these commands. So, here’s a wrapper that provides:

  • Add-HttpNamespace
  • Get-HttpNamespace
  • Get-HttpNamespaces
  • Test-HttpNamespaceExists

There’s no remove because I haven’t needed it yet. A namespace is usually associated with a particular port, and I haven’t been involved in a situation where a port needed to be reused.

<#
.SYNOPSIS
    Parses the output from netsh to turn them in PSObjects.
#>
Function Get-HttpNamespaces {
[CmdletBinding()]
[OutputType([PSObject[]])]
Param()

    # the $propsReady variable causes alot of errors to occur, but the results are accurate.
    # so this helps hide the errors
    $originalErrorAction = $ErrorActionPreference
    $ErrorActionPreference = 'SilentlyContinue'

    try {

        # pull the data from netsh
        $urlaclOutput = . netsh http show urlacl

        # parse the data into PSObjects
        $httpNamespaces = New-Object System.Collections.Generic.List[PSObject]
        $props = @{}
        $userProps = @{}
        $userRdy = $false
        for($i = 0; $i -lt $urlaclOutput.Count; $i++) {
            $line = $urlaclOutput[$i].Trim()

            $split = $line.Split(":", [StringSplitOptions]::RemoveEmptyEntries)

            $first = ""
            if($split.Count -gt 0) { $first = $split[0] }
        
            # line parsing
            switch($first.Trim()) {
                "Reserved URL" {
                    $props.ReservedUrl = $line.Substring(25).Trim()
                    $users = New-Object System.Collections.Generic.List[PSObject]
                }
                "User" {
                    if($userRdy) {
                        $user = New-Object PSObject -Property $userProps
                        $users.Add($user)

                        $userProps = @{}
                        $userRdy = $false
                    }

                    $userProps.User = $split[1].Trim()
                }
                "Listen" { $userProps.Listen = $split[1].Trim() }
                "Delegate" {
                    $userProps.Delegate = $split[1].Trim()
                    $userRdy = $true
                }
                "SDDL" {
                    $userProps.SDDL = $line.Substring(5).Trim()
                    $userRdy = $true
                }
                "" {
                    if($userRdy) {
                        # user
                        $user = New-Object PSObject -Property $userProps
                        $users.Add($user)

                        $userProps = @{}

                        # url
                        $props.Users = $users.ToArray()

                        $cnObj = New-Object PSObject -Property $props
                        $httpNamespaces.Add($cnObj)

                        $props = @{}

                        # reset flag
                        $userRdy = $false
                    }
                }
            }
        }
    } finally {
        $ErrorActionPreference = $originalErrorAction # revert the error action
    }

    return $httpNamespaces.ToArray()
}


<#
.SYNOPSIS
    Retrieves the namespace information for a given namespace. It will also search for namespaces
    which match but the host names have been replaced with + or * symbols.
#>
Function Get-HttpNamespace {
[CmdletBinding()]
[OutputType([PSObject])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $HttpNamespace
)

    $httpNamespaces = Get-HttpNamespaces

    # get * and + versions of the url ready
    $starNamespace = $HttpNamespace
    $plusNamespace = $HttpNamespace
    $namespaceRegex = [regex] "http.*://(.*):.*/.*"
    if($HttpNamespace -match $namespaceRegex) {
        $hostname = $Matches[1]
        $starNamespace = $HttpNamespace.Replace($hostname, "*")
        $plusNamespace = $HttpNamespace.Replace($hostname, "+")
    }

    # sometimes the http namespaces get /'s added to the end
    $namespace = $httpNamespaces |? {
                            $_.ReservedUrl -eq $HttpNamespace `
                    -or     $_.ReservedUrl -eq ($HttpNamespace + '/') `
                    -or     $_.ReservedUrl -eq $starNamespace `
                    -or     $_.ReservedUrl -eq ($starNamespace + '/') `
                    -or     $_.ReservedUrl -eq $plusNamespace `
                    -or     $_.ReservedUrl -eq ($plusNamespace + '/')
                }

    return $namespace
}



<#
.SYNOPSIS
    Checks if a namespace already exists. It will also search if the namespace has had its host name
    replaced with + or * symbols.
#>
Function Test-HttpNamespaceExists {
[CmdletBinding()]
[OutputType([bool])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $HttpNamespace
)

    $namespace = Get-HttpNamespace $HttpNamespace

    return $namespace -ne $null
}



<#
.SYNOPSIS
    Adds a new Http Namespace. This will automatically swap out the host name for a + symbol. The
    + symbol allows the Http Namespace to bind on all NIC addresses.
#>
Function Add-HttpNamespace {
[CmdletBinding()]
[OutputType([PSObject])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $HttpNamespace,
    [Parameter(Mandatory = $true)]
    [string] $DomainAccount
)

    $create = $true
    if(Test-HttpNamespaceExists $HttpNamespace) {
        # it already exists, so maybe not create it
        $create = $false

        $namespace = Get-HttpNamespace $HttpNamespace
        # but, if the given DomainAccount doesn't exist then create it
        $user = $namespace.users |? { $_.user -eq $DomainAccount }
        if($user) {
            Write-Warning "NET $env:COMPUTERNAME - Http Namespace '$HttpNamespace' already contains a rule for '$DomainAccount'. Skipping creation."
            return
        } else {
            $create = $true
        }
    }

    if($create) {
        # the standard pattern to use is http://+:port/servicename.
        #   eg. http://contoso01:15110/EmployeeService would become http://+:15110/EmployeeService
        $plusNamespace = $HttpNamespace
        $namespaceRegex = [regex] "http.*://(.*):.*/.*"
        if($HttpNamespace -match $namespaceRegex) {
            $hostname = $Matches[1]
            $plusNamespace = $HttpNamespace.Replace($hostname, "+")
        } else {
            throw "NET $env:COMPUTERNAME - Http Namespace '$HttpNamespace' could not be parsed into plus format before being added. Plus format " + `
                "looks like http://+:port/servicename. For example, http://contoso01:15110/EmployeeService would be formatted into " + `
                "http://+:15110/EmployeeService."
        }

        # ensure the full domain account name is used
        $fullDomainAccount = Get-FullDomainAccount $DomainAccount

        # create the permission
        Write-Warning "NET $env:COMPUTERNAME - Adding Http Namespace '$Httpnamespace' for account '$fullDomainAccount'"
        $results = . netsh http add urlacl url=$plusNamespace user=$fullDomainAccount listen=yes delegate=yes
        Write-Host "NET $env:COMPUTERNAME - Added Http Namespace '$Httpnamespace' for account '$fullDomainAccount'"
    }

    $namespace = Get-HttpNamespace $HttpNamespace
    return $namespace
}

3 comments:

Anonymous said...

Where does Get-FullDomainAccount come from? Doesn't exist for me.

smaglio81 said...

Sorry about that. Here it is: http://stevenmaglio.blogspot.com/2016/04/get-fulldomain-account.html

Anonymous said...

Nice post. Thank you.

Post a Comment


Creative Commons License
This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.