PowerShellでWebリクエスト

2020-12-23 | コメント(0)

WebサイトへHTTPリクエストを投げて結果のレスポンスを取得するコードです。

PowerShellで実行していますが、.NET Framework のオブジェクトを使用しているので、C#、VB.NET でも同じ様なコードで実行できるはずです。

関連する Microsoft の説明サイト
方法: WebRequest クラスを使用してデータを要求する

Microsoft の説明サイトで "https://docs.microsoft.com" へアクセスしているサンプルがあるので、当ページのコード例も docs.microsoft.com へアクセスしている内容にしていますが、なるべくこのホスト名部分は自分のWebサーバー等に書き換えてアクセスするようにしてください。

Webブラウザが実際に発するHTTP要求は、当ページのコード例の他に Accept-**** や ユーザーエージェント 等のヘッダー値があるので、サーバー側からみて通常の要求からすると、不正な要求、もしくは攻撃されていると見なされることにもなるので、なるべく自前のWebサーバーでアクセス確認するようにしてください。

ソケット接続でHTTPリクエストを投げる

まずは、System.Net.Sockets.TcpClient を使用して、HTTPリクエストを直接投げて、サーバーの応答を取得するコードです。

平文のHTTPリクエストのため、80番ポートへ接続して応答を取得します。つまり http のリクエストです。

SSL/TLSハンドシェイクのコードが書ければ、https, 443番ポートへ接続するように組み立てられますが、ハンドシェイクを自前で行うのは難しいので行いません。

cls

$response = ""

$request = @"
GET / HTTP/1.1
HOST: docs.microsoft.com
"@

$request += "`r`n`r`n"

$requestBytes = [System.Text.Encoding]::UTF8.GetBytes($request)

$client = New-Object System.Net.Sockets.TcpClient
try {
    $client.Connect("docs.microsoft.com", 80)
    try {
        $stream = $client.GetStream()
        try {
            $stream.Write($requestBytes, 0, $requestBytes.Length)

            sleep -Seconds 3

            $readBytes = New-Object byte[] 400
            $readLen = $stream.Read($readBytes, 0, $readBytes.Length)

            $response = [System.Text.Encoding]::UTF8.GetString($readBytes, 0, $readLen)
        } finally {
            $stream.Dispose()
            $stream = $null
        }
    } finally {
        $client.Close()
    }
} finally {
    $client.Dispose()
    $client = $null
}

echo $response

HTTPの理解のために捕捉すると、リクエストの1行目は、リクエストライン (Request Line) というものです。

"GET / HTTP/1.1" という行のところで、"動詞 要求URL HTTPプロトコルバージョン" というフォーマットの決まりがあります。

要求URLは "/" なので、そのサーバーのルートページを要求するものです。

リクエストの2行目以降は、リクエストヘッダーです。HTTPリクエストの各種ヘッダーを、"token: value" の形式で指定します。

HTTP/0.9 や HTTP/1.0 の頃は、ヘッダ値が無くてもアクセス先のページが取得できましたが、HTTP/1.1以降で昨今のWebサーバーでは、最低限 HOSTヘッダー値が必要になることがほとんどです。HOSTヘッダー値が無いと、400エラー Bad Request になります。

リクエストヘッダーは、空文字改行で終わるため、最後にCRLFを2つ追加します。$request += "`r`n`r`n" のところです。

リクエストヘッダーの後には、リクエストボディを含めることができます。リクエストボディはPOST要求時のフォーム値データ相当になりますが、その場合はリクエストヘッダーにデータ長も指定する必要もあります。

ここでは、GET要求だけの確認なので、POST要求の説明は割愛します。

コードでは、リクエストを要求(ソケットへ書込)した後に、sleep -Seconds 3 で、3秒待機しています。これは、直ぐに取得をしてもサーバー側の出力より処理が早すぎて取得できないためです。

本来のソケット処理としてはソケットの PEEK 処理で取得データが確認できるまで待つという処理をするのが正しいのですが、そのWAITコードを書くのが面倒なので、3秒程度の待機にしています。

待機した後に、レスポンスを取得(ソケットから読込)して文字列値にしています。

コンテンツの全てを読込むには、ソケットからの読取データが無くなるまでループして取得するか、Content-Length (がある場合) で指定されたバイト長を読み込む必要がありますが、HTTPレスポンスのヘッダーを確認したいためのコードなので、適当に400バイトだけ取得して終了させています。

WebRequestとWebResponseを使う

つぎに、.NET Framework の System.Net.WebRequest と System.Net.WebResponse を使用してHTTP要求、レスポンスを取得するコードです。

WebRequestでは、http と https のどちらでもWebリクエストを行うことができるので、SSLサイトにもアクセスすることができます。

リクエスト方法は、Create の引数に "https://docs.microsoft.com/" というようにプロトコルも含めたURLを渡すだけです。

WebRequest, WebResponse は高機能で、http:// ~~ であれば 80番ポートへアクセス、https:// ~~ であれば 443番ポートへアクセスします。SSL/TLSハンドシェイクも自動的に行ってくれるので便利です。

高機能という事に相反して逆なことを言うと、特定なプロパティを未指定にすることが出来ない等、細かい指定が出来ないところもあります。例えば、HOSTヘッダ値は、Hostプロパティになりますが、空文字にすることが出来ません。Headers から "HOST" を Remove することもできません。これらはプロパティ値を変更する時に例外が発生してしまうので、Webサーバーにリクエストを要求する以前に準備ができないことになります。

cls

$responseBody = ""

$webRequest = [System.Net.WebRequest]::Create("https://docs.microsoft.com/")
#$webRequest.Host = "docs.microsoft.com"

$webRequest.AllowAutoRedirect = $false

try {
    $webResponse = $webRequest.GetResponse()
} catch [System.Net.WebException] {
    $webResponse = $_.Exception.Response

    if (-not $webResponse) {
        $_
    }
}

if ($webResponse) {
    try {
        $version = [string]$webResponse.ProtocolVersion
        $statusCode = [int]$webResponse.StatusCode
        $status = $webResponse.StatusDescription

        echo "HTTP/$version $statusCode $status"

        foreach ($key in $webResponse.Headers.Keys) {
            $val = $webResponse.Headers[$key]
            echo "$($key): $($val)"
        }

        $stream = $webResponse.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($stream)
        try {
            $responseBody = $reader.ReadToEnd()
            $responseBody = $responseBody.TrimStart(" `r`n".ToCharArray())
            if ($responseBody.Length -gt 300) {
                $responseBody = $responseBody.Remove(300)
            }
        } finally {
            $reader.Dispose()
            $reader = $null
            $stream.Dispose()
            $stream = $null
        }
    } finally {
        $webResponse.Dispose()
        $webResponse = $null
    }
}

echo $responseBody

WebRequestでは、GetResponse() で WebResponse としてレスポンスが得られます。

WebRequestのHostプロパティは、HTTPリクエストのHOSTヘッダ値になりますが、Createした時のURLから、FQDNが自動的に設定されるので、通常は変更する必要はありません。

WebRequestの動きは、サーバー応答がリダイレクトされる場合(301, 302)、リダイレクトした先のページを取得しようとします。リダイレクト先のページでは無く、要求ページの応答を直接見たいので、AllowAutoRedirectをFalseにして、リダイレクトの動作を抑止させます。

また、応答コードが 400番台, 500番台だと例外が発生してしまうので、try ~ catch で例外処理します。

例外発生時には、WebException例外からレスポンスを取得します。例外オブジェクトの .Exception.Response プロパティが WebResponse です。但し接続が出来ない、接続が切られた等でレスポンスが得られない場合もあるので、そのような場合は Response プロパティには何も無いので後続の処理を行わないように切り分けておく必要があります。

例外処理によって、400番台, 500番台 のエラーが発生してもレスポンスが得られることになるため、200番台, 300番台, 400番台, 500番台 のすべてで、HTTPレスポンスヘッダが確認できるようになります。

コードでは、コンテンツ部分(レスポンスボディ)を、300文字以降切り捨てています。

カテゴリ:

コメントする

※HTMLタグは使えません

Author

あきちゃん

主に、.NETでWebシステムの設計と開発をしています。
(茨城県在住, 都内勤務)
プロフィール