Wednesday, June 19, 2019

Accessing The Mirth Connect API from PowerShell

Feel free to review my standard disclaimer which also includes some excuses as to why everything below could be just plain wrong.


A recent project I have been working on to migrate databases between SQL instances required me to update settings stored in the Configuration Map of a Mirth Connect appliance.  I had scripted out the migration of the databases, creation of associated SQL logins, creation of SQL jobs, etc.. and this represented one of the only manual tasks that remained.  About halfway through the project I decided to see if I could automate the updates to the Configuration Map via the Mirth Connect API from PowerShell.

The Mirth Connect API is accessible via the following URL https://appliance.domain.com:8443/api/ and presents a standard Swagger UI.  One of the more difficult parts of accessing the API was figuring out the authentication.  Most of the APIs I have worked with in the past used token based authentication passed via a Basic authentication header.  The Mirth Connect API however, utilizes username and password authentication.

While some of the information I found seemed to indicate that Basic authentication using a username and password was supported, I was unable to get this to work.  I found some comments in forum posts that perhaps the Basic authentication method did not work when external LDAP was used, however I was not able to find anything to officially support this.  That left me with posting the credentials to the login page and obtaining a session cookie.  I will point out that there is a bit of borrowed code below (as if most of the code isn't borrowed one way or another) used to allow connection to hosts with self-signed or invalid certificates.  This was borrowed from here and here.

function Get-ApiSessionToken()
{
 param
 (
   [Parameter(Mandatory=$true)][string]$HostAddress
  ,[Parameter(Mandatory=$true)][PSCredential]$Credential 
 )
 
 #CONVERT SECURE STRING RETURNED BY PS CREDENTIALS TO PLAIN STRING
 $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password) 
 $plainString = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
 [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
 
 #BUILD POST TO LOGIN PAGE
 $Data = "username={0}&password={1}" -f ($Credential.UserName),$plainString
 
 #BORROWED CODE USED TO ALLOW CONNECTION TO HOSTS WITH SELF SIGNED/INVALID CERTS
 Add-Type @"
 using System.Net;
 using System.Security.Cryptography.X509Certificates;
 public class TrustAllCertsPolicy : ICertificatePolicy {
  public bool CheckValidationResult(
   ServicePoint srvPoint, X509Certificate certificate,
   WebRequest request, int certificateProblem) {
   return true;
  }
 }
"@
 [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
 
 #SET COMMUNICATION TO USE TLS1.2
 [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
 
 #BUILD HEADER AND URI 
 $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
 $lUri = Join-Parts ($HostAddress,'/api/users/_login') '/'
 if(-Not ($Uri -Like "http*"))
 {
  $lUri = "https://{0}" -f $lUri
 }
  
 try
 {
  $headers.Add("Accept", 'application/xml')
  #WEBREQUEST USED INSTEAD OF INVOKE RESTMETHOD AS THE SESSION COOKIE IS PASSED IN THE RESPONSE HEADER WHICH IS NOT AVAILABLE VIA RESTMETHOD
  $response = Invoke-WebRequest -Method "POST" -Uri $lUri -Headers $headers -Body $Data -ContentType 'application/x-www-form-urlencoded'
 }
 catch
 {
  if(-Not ($_.Exception.Message -Like "*(404) Not Found*"))
  {
   Write-Host ($_.Exception | Format-List -force | Out-String) -ForegroundColor "Red"
  }
 }
 return $response.Headers["Set-Cookie"]
}
$mycreds = Get-Credential
$sessionToken = Get-ApiSessionToken -HostAddress "appliance.domain.com:8443" -Credential $mycreds



Now that I had a cookie, I had no idea how to use it.  It turns out the Invoke-WebRequest, Invoke-RestMethod, and similar functions accept a WebRequestSession object as a parameter and the cookie is defined in the session object.  Unfortunately, I am fairly ignorant when it comes to session cookies so I am unsure if what was returned is a standard format.  It was necessary, however to parse the response to populate the cookie object.   

$SessionToken = "JSESSIONID=far8zqbhf93r15rej52qeab7u;Path=/api;Secure"

$Cookie = New-Object System.Net.Cookie
$Cookie.Name = $SessionToken.Split("=")[0]
$Cookie.Value = ($SessionToken.Split("=")[1]).Split(";")[0]
$Cookie.Domain = ([System.Uri]$Uri).DnsSafeHost

$WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$WebSession.Cookies.Add($Cookie)

At this point I had what I needed to create a generic function for making Mirth Connect API calls.

function Invoke-MirthApiCall()
{
 param
 (
   [Parameter(Mandatory=$true)][string]$Uri
  ,[Parameter(Mandatory=$true)][string]$SessionToken
  ,[Parameter(Mandatory=$false)][string]$Method = "GET"
  ,[Parameter(Mandatory=$false)][string]$Data
 )
 
 if(($Method -eq "post" -Or $Method -eq "patch") -And [string]::IsNullOrEmpty($Data))
 {
  Write-Host "Post and patch methods require data parameter" -ForegroundColor "Red"
 }
 else
 {
  #BORROWED CODE USED TO ALLOW CONNECTION TO HOSTS WITH SELF SIGNED/INVALID CERTS
  Add-Type @"
  using System.Net;
  using System.Security.Cryptography.X509Certificates;
  public class TrustAllCertsPolicy : ICertificatePolicy {
   public bool CheckValidationResult(
    ServicePoint srvPoint, X509Certificate certificate,
    WebRequest request, int certificateProblem) {
    return true;
   }
  }
"@
  [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
  
  #SET COMMUNICATION TO USE TLS1.2
  [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
  
  #BUILD HEADER AND URI 
  $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"  
  if(-Not ($Uri -Like "http*"))
  {
   $Uri = "https://{0}" -f $Uri
  }
  
  #SETUP WEBREQUESTSESSION OBJECT WITH THE AUTHENTICATION COOKIE 
  $Cookie = New-Object System.Net.Cookie
  $Cookie.Name = $SessionToken.Split("=")[0]
  $Cookie.Value = ($SessionToken.Split("=")[1]).Split(";")[0]
  $Cookie.Domain = ([System.Uri]$Uri).DnsSafeHost

  $WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
  $WebSession.Cookies.Add($Cookie) 
  
  #CALL INVOKE-WEBREQUEST WITH THE APPROPRIATE PARAMS FOR THE METHOD
  try
  {
   if($Method -eq "post" -Or $Method -eq "patch" -Or $Method -eq "put")
   {
    $headers.Add("Accept", 'application/xml')
    $results = Invoke-RestMethod -Method $Method -Uri $Uri -Headers $headers -Body $Data -ContentType 'application/xml' -WebSession $WebSession 
   }
   else
   {
    $headers
    $results = Invoke-RestMethod -Method $Method -Uri $Uri -Headers $headers  -WebSession $WebSession
   }
   Write-Output $results
  }
  catch
  {
   if(-Not ($_.Exception.Message -Like "*(404) Not Found*"))
   {
    Write-Host ($_.Exception | Format-List -force | Out-String) -ForegroundColor "Red"
   }
  }
 }
}

This all comes together in a couple of very simple functions to get and set the Mirth Connect Configuration Map.

function Get-ConfigMap
{
 param
 (
   [Parameter(Mandatory=$true)][string]$HostAddress
  ,[Parameter(Mandatory=$true)][PSCredential]$Credential
 )
 
 $sessionToken = Get-ApiSessionToken -HostAddress $HostAddress -Credential $Credential
 
 $lUri = Join-Parts ($HostAddress,'/api/server/configurationMap') '/'
 $response = Invoke-MirthApiCall -Uri $lUri -SessionToken $sessionToken
 
 return  [xml]($response.OuterXml)
 
}

function Set-ConfigMap
{
 param
 (
   [Parameter(Mandatory=$true)][string]$HostAddress
  ,[Parameter(Mandatory=$true)][PSCredential]$Credential
  ,[Parameter(Mandatory=$true)][xml]$ConfigMap
 )
 
 $sessionToken = Get-ApiSessionToken -HostAddress $HostAddress -Credential $Credential
 
 $lUri = Join-Parts ($HostAddress,'/api/server/configurationMap') '/'
 $response = Invoke-MirthApiCall -Uri $lUri -SessionToken $sessionToken -Method "PUT" -Data ($ConfigMap.OuterXml)
 
 return $response
 
}

Just so the code above is completely functional, I wanted to include a small function I found and use to concatenate URL parts.

Function Join-Parts {
    param ([string[]] $Parts, [string] $Seperator = '')
    $search = '(?<!:)' + [regex]::Escape($Seperator) + '+'  #Replace multiples except in front of a colon for URLs.
    $replace = $Seperator
    ($Parts | ? {$_ -and $_.Trim().Length}) -join $Seperator -replace $search, $replace
}


1 comment:

  1. Beautiful. Thanks for posting this very useful information!

    ReplyDelete