Welcome PowerShell User! This recipe is just one of the hundreds of useful resources contained in the PowerShell Cookbook.

If you own the book already, login here to get free, online, searchable access to the entire book's content.

If not, the Windows PowerShell Cookbook is available at Amazon, or any of your other favourite book retailers. If you want to see what the PowerShell Cookbook has to offer, enjoy this free 90 page e-book sample: "The Windows PowerShell Interactive Shell".

12.13 Program: Interact with Internet Protocols

Although it’s common to work at an abstract level with websites and web services, an entirely separate style of internet-enabled scripting comes from interacting with the remote computer at a much lower level. This lower level (called the TCP level, for Transmission Control Protocol) forms the communication foundation of most internet protocols—such as Telnet, SMTP (sending mail), POP3 (receiving mail), and HTTP (retrieving web content).

The .NET Framework provides classes that let you interact with many of the internet protocols directly: the System.Net.Mail.SmtpClient class for SMTP, the System.Net.WebClient class for HTTP, and a few others. When the .NET Framework doesn’t support an internet protocol that you need, though, you can often script the application protocol directly if you know the details of how it works.

Example 12-10 shows how to receive information about mail waiting in a remote POP3 mailbox, using the Send-TcpRequest script given in Example 12-11.

Example 12-10. Interacting with a remote POP3 mailbox
## Get the user credential
if(-not (Test-Path Variable:\mailCredential))
{
   $mailCredential = Get-Credential
}
$address = $mailCredential.UserName
$password = $mailCredential.GetNetworkCredential().Password

## Connect to the remote computer, send the commands, and receive the output
$pop3Commands = "USER $address","PASS $password","STAT","QUIT"
$output = $pop3Commands | Send-TcpRequest mail.myserver.com 110
$inbox = $output.Split("`n")[3]

## Parse the output for the number of messages waiting and total bytes
$status = $inbox |
    ConvertFrom-String -PropertyName "Response","Waiting","BytesTotal","Extra"
"{0} messages waiting, totaling {1} bytes." -f $status.Waiting, $status.BytesTotal

In Example 12-10, you connect to port 110 of the remote mail server. You then issue commands to request the status of the mailbox in a form that the mail server understands. The format of this network conversation is specified and required by the standard POP3 protocol. Example 12-10 uses the ConvertFrom-String command, which is provided in Recipe 5.15.

Example 12-11 supports the core functionality of Example 12-10. It lets you easily work with plain-text TCP protocols.

Example 12-11. Send-TcpRequest.ps1
##############################################################################
##
## Send-TcpRequest
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Send a TCP request to a remote computer, and return the response.
If you do not supply input to this script (via either the pipeline, or the
-InputObject parameter,) the script operates in interactive mode.

.EXAMPLE

PS > $http = @"
  GET / HTTP/1.1
  Host:bing.com
  `n`n
"@

$http | Send-TcpRequest bing.com 80

#>

[CmdletBinding()]
param(
    ## The computer to connect to
    [Parameter()]
    [string] $ComputerName = "localhost",

    ## A switch to determine if you just want to test the connection
    [Parameter()]
    [switch] $Test,

    ## The port to use
    [Parameter()]
    [int] $Port = 80,

    ## A switch to determine if the connection should be made using SSL
    [Parameter()]
    [switch] $UseSSL,

    ## The input string to send to the remote host
    [Parameter(ValueFromPipeline)]
    [string] $InputObject,

    ## The delay, in milliseconds, to wait between commands
    [Parameter()]
    [int] $Delay = 100
)

Set-StrictMode -Version 3

[string] $SCRIPT:output = ""

## Store the input into an array that we can scan over. If there was no input,
## then we will be in interactive mode.
$currentInput = $inputObject
if(-not $currentInput)
{
    $currentInput = @($input)
}
$scriptedMode = ([bool] $currentInput) -or $test

function Main
{
    ## Open the socket, and connect to the computer on the specified port
    if(-not $scriptedMode)
    {
        write-host "Connecting to $computerName on port $port"
    }

    try
    {
        $tcpClient = New-Object Net.Sockets.TcpClient($computerName, $port)
    }
    catch
    {
        if($test) { $false }
        else { Write-Error "Could not connect to remote computer: $_" }

        return
    }

    ## If we're just testing the connection, we've made the connection
    ## successfully, so just return $true
    if($test) { $true; return }

    ## If this is interactive mode, supply the prompt
    if(-not $scriptedMode)
    {
        write-host "Connected.  Press ^D followed by [ENTER] to exit.`n"
    }

    $stream = $tcpClient.GetStream()

    ## If we wanted to use SSL, set up that portion of the connection
    if($UseSSL)
    {
        try
        {
            $sslStream = New-Object System.Net.Security.SslStream $stream,$false
            $sslStream.AuthenticateAsClient($ComputerName)
            $stream = $sslStream
        }
        catch [System.IO.IOException]
        {
            ## Try again with explicit SSL (TLS)

            $tcpClient = new-object System.Net.Sockets.TcpClient($ComputerName, $port)
            $stream = $tcpClient.GetStream()

            $writer = new-object System.IO.StreamWriter $stream

            $writer.WriteLine("EHLO")
            $writer.Flush()

            $writer.WriteLine("STARTTLS")
            $writer.Flush()
            $null = GetOutput

            $sslStream = New-Object System.Net.Security.SslStream $stream,$false
            $sslStream.AuthenticateAsClient($ComputerName)
            $stream = $sslStream
        }
    }

    $writer = new-object System.IO.StreamWriter $stream

    while($true)
    {
        ## Receive the output that has buffered so far
        $SCRIPT:output += GetOutput

        ## If we're in scripted mode, send the commands,
        ## receive the output, and exit.
        if($scriptedMode)
        {
            foreach($line in $currentInput)
            {
                $writer.WriteLine($line)
                $writer.Flush()
                Start-Sleep -m $Delay
                $SCRIPT:output += GetOutput
            }

            break
        }
        ## If we're in interactive mode, write the buffered
        ## output, and respond to input.
        else
        {
            if($output)
            {
                foreach($line in $output.Split("`n"))
                {
                    write-host $line
                }
                $SCRIPT:output = ""
            }

            ## Read the user's command, quitting if they hit ^D
            $command = read-host
            if($command -eq ([char] 4)) { break; }

            ## Otherwise, write their command to the remote host
            $writer.WriteLine($command)
            $writer.Flush()
        }
    }

    ## Close the streams
    $writer.Close()
    $stream.Close()

    ## If we're in scripted mode, return the output
    if($scriptedMode)
    {
        $output
    }
}

## Read output from a remote host
function GetOutput
{
    ## Create a buffer to receive the response
    $buffer = new-object System.Byte[] 1024
    $encoding = new-object System.Text.AsciiEncoding

    $outputBuffer = ""
    $foundMore = $false

    ## Read all the data available from the stream, writing it to the
    ## output buffer when done.
    do
    {
        ## Allow data to buffer for a bit
        start-sleep -m 1000

        ## Read what data is available
        $foundmore = $false
        $stream.ReadTimeout = 1000

        do
        {
            try
            {
                $read = $stream.Read($buffer, 0, 1024)

                if($read -gt 0)
                {
                    $foundmore = $true
                    $outputBuffer += ($encoding.GetString($buffer, 0, $read))
                }
            } catch { $foundMore = $false; $read = 0 }
        } while($read -gt 0)
    } while($foundmore)

    $outputBuffer
}

. Main

For more information about running scripts, see Recipe 1.2.

See Also

Recipe 1.2, “Run Programs, Scripts, and Existing Tools”

Recipe 5.15, “Convert Text Streams to Objects”