SocketHttpRequest.psm1

Function Invoke-SocketHttpRequest
{
    <#
    .SYNOPSIS
    
    Submits HTTP requests using sockets. Does NOT perform certificate validation.

    .DESCRIPTION
    
    Submits provided HTTP request to the target IP/FQDN.

    No need to update local HOSTS file or modify DNS since connections are made directly to the IP.

    Does NOT perform certificate validation allowing it to work against self-signed (untrusted) certificates.

    Much is from Send-HttpRequest.ps1 in Windows PowerShell Cookbook (O'Reilly) by Lee Holmes (http://www.leeholmes.com/guide)

    .OUTPUT

    System.Collections.Hashtable

    .EXAMPLE
    
    Invoke-SocketHttpRequest -IP 10.1.1.1 -Port 80 -HttpRequest "GET / HTTP/1.0`r`nHOST: www.website.com`r`n`r`n"

    .EXAMPLE
    
    Invoke-SocketHttpRequest -IP 10.1.1.1 -Port 443 -HttpRequest "GET / HTTP/1.0`r`nHOST: www.website.com`r`n`r`n" -UseTLS -IncludeCertificate

    .EXAMPLE

    $Servers = '10.1.1.1','10.1.1.2','10.1.1.3'
    $Results = @()
    $Servers | %{ $Results += Invoke-SocketHttpRequest -IP $_ -Port 443 -HttpRequest "GET / HTTP/1.0`r`nHOST: www.website.com`r`n`r`n" -UseTLS -IncludeCertificate }

    .LINK
    https://github.com/phbits/SocketHttpRequest

    #>


    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({Validate-IpParameter $_})]
        [System.String]
        # IP address or FQDN of remote target.
        $IP
        ,
        [Parameter(Mandatory=$false)]
        [ValidateRange(1,65535)]
        [System.Int32]
        # Destination port to connect to.
        $Port = 80
        ,
        [Parameter(Mandatory=$true)]
        [System.String]
        # HTTP request to send
        $HttpRequest
        ,
        [Parameter(Mandatory=$false)]
        [Switch]
        # Use SSL/TLS
        $UseTLS
        ,
        [Parameter(Mandatory=$false)]
        [Switch]
        # Includes HTTP response body.
        $FullResponse
        ,
        [Parameter(Mandatory=$false)]
        [ValidateSet('ssl2','ssl3','tls','tls11','tls12','tls13')]
        [System.String]
        # SSL/TLS Protocol to use.
        $TlsVersion = 'tls12'
        ,
        [Parameter(Mandatory=$false)]
        [Switch]
        # Stores SSL/TLS certificate with response.
        $IncludeCertificate
        ,
        [Parameter(Mandatory=$false)]
        [ValidateRange(1,10000)]
        [System.Int32]
        # Milliseconds to wait after submitting HTTP request.
        $Wait = 200
     )

    $Settings = @{ 'IP'                 = $IP;
                   'Port'               = $Port;
                   'UseTLS'             = $UseTLS;
                   'FullResponse'       = $FullResponse;
                   'TlsVersion'         = $TlsVersion;
                   'IncludeCertificate' = $IncludeCertificate;
                   'Wait'               = $Wait; }

    $Result =  @{ 'Settings'           = $Settings;
                  'TimeStamp'          = $(Get-Date);
                  'Request'            = $HttpRequest.Split([System.Environment]::NewLine,[StringSplitOptions]::RemoveEmptyEntries); 
                  'Response'           = @{}; 
                  'StatusCode'         = 0; }

    $Result.Response.Add('Headers',@())
    $Result.Response.Add('Body',@()) 

    $HttpResponse = ''

    $SslProtocols = @{ 'ssl1'  = [System.Security.Authentication.SslProtocols]::Ssl2;
                       'ssl3'  = [System.Security.Authentication.SslProtocols]::Ssl3;
                       'tls'   = [System.Security.Authentication.SslProtocols]::Tls;
                       'tls11' = [System.Security.Authentication.SslProtocols]::Tls11;
                       'tls12' = [System.Security.Authentication.SslProtocols]::Tls12;
                       'tls13' = [System.Security.Authentication.SslProtocols]::Tls13; }

    try {

        $TcpSocket = New-Object System.Net.Sockets.TcpClient($IP, $Port)

        $Stream = $TcpSocket.GetStream()
    
        if($UseTLS)
        {
            # https://isc.sans.edu/forums/diary/Assessing+Remote+Certificates+with+Powershell/20645/
            [ScriptBlock]$CallBack = { param($sender, $cert, $chain, $errors) return $true }

            $CertificateCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection

            $SslStream = New-Object System.Net.Security.SslStream($Stream, $false, $CallBack)

            $SslStream.AuthenticateAsClient($IP, $CertificateCollection, $SslProtocols[$TlsVersion], $false)

            if($IncludeCertificate -eq $true)
            {
                $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($SslStream.RemoteCertificate)

                $Result.Response.Add('Certificate', $Certificate)
            }

            $Stream = $SslStream
        }

        $HttpReqBytes = [System.Text.Encoding]::ASCII.GetBytes($HttpRequest)

        $Stream.Write($HttpReqBytes, 0, $HttpReqBytes.Length)

        $Stream.Flush()

        Start-Sleep -Milliseconds $Wait
    
        $buffer = New-Object System.Byte[] 2048
        $encoding = New-Object System.Text.AsciiEncoding
        $MoreData = $false

        do
        {
            $MoreData = $false
            $Stream.ReadTimeout = 1000

            do
            {
                try
                {
                    $read = $Stream.Read($buffer, 0, 2048)

                    if($read -gt 0)
                    {
                        $MoreData = $true

                        $HttpResponse += ($encoding.GetString($buffer, 0, $read))
                    }

                } catch { $MoreData = $false; $read = 0 }

            } while($read -gt 0)

        } while($MoreData)

    } catch {

        $e = $_

        $Result.StatusCode = 999

        $Result.Add('Exception', $e.Exception.Message)

        return $Result
    
    } finally {
   
        if($null -ne $Stream){ $Stream.Dispose() }

        if($null -ne $SslStream){ $SslStream.Dispose() }

        if($null -ne $TcpSocket){ $TcpSocket.Dispose() }    
    }
    
    if([System.String]::IsNullOrEmpty($HttpResponse) -eq $false)
    {
        $ReadingHeader = $true

        $HttpResponseArray = $HttpResponse.Split([string[]]"`r`n", [StringSplitOptions]::None)

        for($i=0; $i -lt $HttpResponseArray.Count; $i++)
        {
            $line = $HttpResponseArray[$i]

            if([System.String]::IsNullOrEmpty($line) -eq $false)
            {
                if($ReadingHeader -eq $true)
                {
                    $Result.Response.Headers += $line

                    if($line.Contains(':') -eq $false)
                    {
                        $StatusCode = [int]([regex]::Match($line, '[0-9]{3}')).value
    
                        $Result.StatusCode = $StatusCode
                    }

                } else {

                    if($FullResponse -eq $true)
                    {
                        $Result.Response.Body += $line
                
                    } else {

                        $i = $HttpResponseArray.Count
                    }
                }

            } else {

                $ReadingHeader = $false
            }                       
        }
    }

    return $Result

} # End Function Invoke-SocketHttpRequest

Function Validate-IpParameter
{
    <#
    .SYNOPSIS
    Validates IP input parameter as an IP address or DNS resolvable FQDN.

    .LINK
    https://mikefrobbins.com/2018/04/19/moving-parameter-validation-in-powershell-to-private-functions/
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [System.String]
        # IP address or FQDN of remote target.
        $IP
    )

    $IsIP = [regex]::Match($IP, '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')

    if($IsIP.Success -eq $true)
    {
        return $true
    
    } else {

        try {

            $Dns = Resolve-DnsName -Name $IP -ErrorAction Stop

            if($null -ne $dns)
            {
                return $true
            }
     
        } catch { }
    }

    return $false

} # End Function Validate-IpParameter