The past couple of days I have been working on a Powershell based client for our REST web services.  While the client is specific to our business the core logic behind the client should apply to just about all REST based web services.  I have tried to generalize(read: remove company stuff) the code so hopefully everything still makes sense.

In our implementation of REST, we allow a person to either submit a GET request with query string parameters or attach some XML to the request and submit as a POST.  Our response will always be some xml.  We handle it with 3 xml root types.  Success, Error, and Data.  Success just lets the user know a particular POST was successful.  Error will give the user back an error code with a description.  Data will return an xml document with whatever data the user requested.  Eventually a Success I would think will also return the changes made but hey, this is version 1.

The core of each GET request is just making the request with the query strings defined.  The declaration of the core function with the parameters section is:

function Invoke-APICall {
    param(
        [string]$Username,
        [string]$Password,
        [string]$UserAgent = "Powershell API Client",
        [string]$URL,
        [xml]$XMLObject
        )

 

   1: #Create a URI instance since the HttpWebRequest.Create Method will escape the URL by default.
   2: $URI = New-Object System.Uri($URL,$true)
   3:  
   4: #Create a request object using the URI
   5: $request = [System.Net.HttpWebRequest]::Create($URI)
   6:  
   7: #Build up a nice User Agent
   8: $request.UserAgent = $(
   9: "{0} (PowerShell {1}; .NET CLR {2}; {3})" -f $UserAgent, 
  10: $(if($Host.Version){$Host.Version}else{"1.0"}),
  11: [Environment]::Version,
  12: [Environment]::OSVersion.ToString().Replace("Microsoft Windows ", "Win")
  13: )
  14:  
  15: #Establish the credentials for the request
  16: $creds = New-Object System.Net.NetworkCredential($Username,$Password)
  17: $request.Credentials = $creds
  18:  
  19: $response = $request.GetResponse()
  20:  
  21: $reader = [IO.StreamReader] $response.GetResponseStream()
  22:  
  23: #Our response will always be xml except in 404/401 case so cast as such        
  24: [xml]$responseXML = $reader.ReadToEnd()
  25:         
  26: $reader.Close()
  27:         
  28: #Let others down the pipeline have fun with our xml object
  29: Write-Output $responseXML
  30:  
  31: $response.Close()

 

To decide if this is a POST I use the following: 

if($XMLObject){ Do the POST stuff}else{Do the GET stuff}

And now the POST specific code.

   1: $creds = New-Object System.Net.NetworkCredential($Username,$Password)
   2: $request.Credentials = $creds
   3:  
   4: #Since this is a POST we need to set the method type
   5: $request.Method = "POST"
   6:  
   7: #Set the Content Type as text/xml since the content will be a block of xml.
   8: $request.ContentType = "text/xml"
   9:  
  10: #Create a new stream writer to write the xml to the request stream.
  11: $stream = New-Object IO.StreamWriter $request.GetRequestStream()
  12: $stream.AutoFlush = $True
  13: $stream.Write($($XMLObject.psbase.OuterXML),0,$($XMLObject.psbase.OuterXml.Length))
  14: $stream.Close()
  15:  
  16: #Make the request and get the response
  17: $response = $request.GetResponse()
  18:  
  19: #Create a stream reader to read the response stream.
  20: $reader = New-Object IO.StreamReader $response.GetResponseStream()
  21:  
  22: #Read the response and cast the response to XML
  23: [xml]$responseXML = $reader.ReadToEnd()
  24:  
  25: #Dump the XML out to the pipeline for others to consume
  26: Write-Output $responseXML
  27:  
  28: $response.Close()

 

There was one thing in the POST that I had trouble with at first.  When trying to execute the $stream.Write method, if I had set the $request.ContentLength property with $request.ContentLength = $$XMLObject.psbase.OuterXML.Length, it would fail stating the bytes being written were longer than the bytes specified.  So I removed the code to set the $request.ContentLength and everything seemed to work fine.  Not sure how Joel Bennett got that to work but it drove me nuts for a while.

So that just leaves the calling functions.  Here is a GET

   1: function Invoke-APIGetStuffInGroup {
   2:     param( 
   3:       [string]$Group,
   4:       [string]$Username,
   5:       [string]$Password,
   6:       [string]$UserAgent = "Powershell API Client"      
   7:    )
   8:    
   9:    
  10:         $Group = [System.Web.HttpUtility]::UrlEncode($Group)
  11:         $URL = "https://api.website.com/api/1.0/Groups/GetStuff?group=$Group"
  12:         Invoke-APICall -URL $URL -Username $Username -Password $Password -UserAgent $UserAgent
  13:         
  14: }

 

And a POST

   1: function Invoke-APIAddStuffToGroups {
   2:     param( 
   3:       [string]$Username,
   4:       [string]$Password,
   5:       [string]$UserAgent = "Powershell API Client"      
   6:    )
   7:         $xml = #Create your XML Document here
   8:         
   9:         $URL = "https://api.website.com/api/1.0/Groups/AddStuff"
  10:             
  11:         Invoke-APICall -URL $URL -XMLObject $xml -Username $Username -Password $Password -UserAgent $UserAgent
  12: }
  13:         

 

One change I might make at some point is to require a SecureString for the password since clear text can be bad.

Hope this helps someone!

-Shane

About these ads