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 }