PowerShell als HTTP-Client

Es ist per PowerShell sehr einfach möglich, per HTTP verschiedene Dinge zu automatisieren. Bis PowerShell 2.0 mussten Sie dazu allerdings noch direkt auf .NET-Klassen wie "System.Net.WebClient" zurückgreifen. Mit PowerShell 3.0 gibt es die zwei neue Commandlets "Invoke-Webrequest" und "Invoke-RESTMethod".

Bewertungskriterien

Ehe ich nun gleich die verschiedenen Möglichkeiten vorstelle, möchte ich erst einmal die Kriterien für die Bewertung vorstellen und die Wege als Tabelle auflisten.

  • Technik
    Es gibt Lösungen als PowerShell, solche mit einer .NET Klasse oder die Nutzung von COM-Objekten
  • Restricted Header
    Es gibt Header (z.B. "Referer"), die man nutzen möchte aber nicht alle Methoden erlauben die Vorgabe dieser Header.
  • Parallelität
    Die meisten Varianten warten nach dem Absetzen der Anfrage auf die Rückkehr. Da macht die Programmierung einfach aber wer aus Performance-Gründen parallel arbeiten will, muss dann selbst Code darum schreiben.
  • Fehler-Ergebniscode und Inhalt beim Fehler
    Einige Wege liefern wirklich z.B. einen 401-Fehler, während andere eine Exception werden.
  • Ergebnisform
    Interessant ist auch, ob beim Download einer Webseite z.B. nur der HTML-Body als Text kommt, ein Zugriff auf den kompletten Datenstrom möglich ist oder sogar ein fertig zerlegtes HTML-DOM-Objekt zu erwarten ist.

Bei meinen Versuchen zu UCWA - Unified Communication Web API habe ich alle Versionen durchprobiert, da ich hier eine URL mit einem 401 abrufen muss und dennoch die Header brauche.

  • Restricted Header
    Sagt aus, ob am Beispiel von "Referer" der Header angepasst werden kann
  • Seriell
    Ja bedeutet, dass das Skript auf das Ergebnis wartet.

Hier die verschiedenen Methoden im Schnellüberblick

Methode Technik Restricted
Header
Seriell Tempo Verhalten bei

Ja

2xx  3xx 4xx NoDNS Notreach

Invoke-Webrequest

CMDLet

Nein

Ja

Langsam

OK

Redir

Ex

Ex

Ex

Ex

System.Net.WebClient

NET

Nein

Ja

Ja

OK

Redir

Ex

Ex

Ex

Ex

System.Net.HTTPWebrequest

NET

Ja

Nein

Ja

OK

Redir

Ex

Ex

Ex

Ex

System.Net.Http.HttpClient

NET

Ja

Ja

Nein

OK

Redir

Ex

Ex

Ex

Ex

XMLHttpRequest

COM

Ja

Ja

200

300

400

502

Ex

Ex

Ex

Bits-Request

 

 

 

 

 

 

 

 

 

Hinweise zur Bedeutung:

  • OK1
    Der Aufruf gibt ein Objekt zurück
  • OK2
    Der Aufruf gibt einen String mit den Inhalt zurück
  • ReDir
    Wenn die 3xx Antwort ein Verweis auf eine andere URL ist, dann macht die Methode automatisch einen zweiten GET auf die neue URL. Der aufrufende Prozess muss sich also nicht um die Behandlung eines 3xx Reply kümmern
  • Ex
    Die Methode wirft eine "Exception". Diese können Sie mit einem Try/Catch abfangen und die Excetption auslesen. Keine der Methoden liefert bei einer Exception aber den Fehler als Rückgabe des Aufrufs. Hier ein Bespiel so einer Try/Catch Konstruktion
Try {
   $result = Invoke-WebRequest `
                 -uri "http://www.msxfaq.de/.htaccess" `
                 -Method GET
   $httperrorcode = $result.Statuscode
} 
Catch [System.Net.WebException]{
   [System.Net.HttpWebResponse]$result = [System.Net.HttpWebResponse] $_.Exception.Response
   $httperrorcode  = $result.StatusCode.Value__
   $error.clear()
}

Sie sehen wie unterschiedlich die verschiedenen Methoden und Klassen agieren. Da ist es nicht immer richtig den einfachen Weg zu wählen, wenn bestimmte Ergebnisse benötigt werden. Nun die einzelnen Methoden in Beispielcodes:

SSL und Zertifikat

Wenn Sie mit einem Browser eine HTTPS-Adresse ansprechen und es Probleme mit dem Zertifikat gibt. So könnte der Name nicht übereinstimmen, die Gültigkeit abgelaufen, die ausstellende CA nicht vertrauenswürdig oder das Zertifikat sogar zurückgezogen worden sein. Als interaktiver Anwender bekommen Sie in der Regel eine Rückfrage, ob sie dennoch weiter machen möchten. Ein Skript kann nur einen Fehler werfen, den sie abfangen müssen. Oder Sie hinterlegen eine Funktion, die den Umgang mit der Fehlermeldung regelt. Folgende kurze Zeile liefert immer ein "True" für Zertifikatwarnungen der aktuellen Powershell Session.

# disabled SSL Checks if required
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}

Sie sollten es aber nicht zur Gewohnheit werden lassen, Zertifikatwarnungen und Fehler zu ignorieren. Besser ist es das Zertifikat auf der Gegenseite zu korrigieren. Dieser Einzeiler kann aber nicht alle Fehler abfangen. Die umfangreichere Lösung erfordert einen eigenen Type.

TLS Protokoll mit PowerShell

Normalerweise handeln Client und Server ein "passendes" TLS-Protokoll aus und versucht das "bester" zu nutzen. Als Client können Sie aber nicht sicher sein, ob nicht auf dem Weg jemand das "Angebot" des Servers beim TLS-Handshake absichtlich verändert, so dass Sie gar nicht wissen, was der Server kann. Sie können aber ihrerseits z.B. TLS 1.2 erzwingen und damit keine schwächeren Verfahren annehmen. Das geht z.B. wie folgt:

[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12

Ab dem Moment nutzen Invoke-WebRequest und andere eben nur noch TLS 1.2. Wenn Sie aber auch ein Fallback auf schwächere Verfahren erlauben wollen, dann müssen Sie einfach mehrere Werte binär mit "oder" verbinden

[Net.ServicePointManager]::SecurityProtocol = `
        [Net.SecurityProtocolType]::Tls `
   -bor [Net.SecurityProtocolType]::Tls11 `
   -bor [Net.SecurityProtocolType]::Tls12

Die Einstellungen gelten nur für die aktuelle Session

Invoke-Webrequest / Invoke-RestMethod (PS 3.0 oder höher)

Ab PowerShell 3.0 können Sie sehr viele Aufgaben als HTTP-Client in einer Zeile lösen, z.B.: den Download einer Datei.

$data= Invoke-WebRequest `
   -Uri "http://www.msxfaq.de" `
   -Outfile msxfaq-homepage.htm

Analog dazu können Sie auch Daten per POST an eine Webseite hochladen.

$data= Invoke-WebRequest `
   -Method post `
   -Uri "http://localhost:81/post.txt" `
   -Body "bodytest"

Das macht es natürlich schon sehr viel einfacher mit einem Webserver zu interagieren.

Die Antwort im Erfolgsfalle ist ein HtmlWebResponseObject.

Interessant sind hier bei die beiden Properties "StatusCode" und "StatusDescription". Der Inhalt ist in dem Property "Content". Wer dem kompletten HTML-Inhalt samt Header benötigt, nutzt "RawContent". Interessanter sind aber vor allem die vorgearbeiteten Properties wie z.B."Links", welches alle Hyperlinks ausgibt.

Hinweis
Die Rückgabe enthält nur dann ein Objekt, wenn die Abfrage erfolgreich war. Fehler werden immer mit einer "Exception" abgefangen. Erwarten Sie also nie in $data.Statuscode einen 401 o.ä. 

Achtung:
Invoke-Webrequest nutzt per Default DOM des IE. Wenn dieser nicht installiert oder limitiert ist, kann das Parsen nicht funktionieren. Nutzen Sie dann den Parameter "-UseBasicParsing". Leider werden dann nur Links, Forms, Images und Header auseinander genommen aber das Feld "ParsedHTML" bleibt leer.

Achtung:
Beide Commandlets senden nicht zwingend den kompletten Request in einem Paket. Es ist durchaus erlaubt den Request auf mehrere Pakete aufzuteilen und dem Ziel über das Feld "Content-Length" mitzuteilen, wie groß die Daten sind. Das Ziel muss dann mit einem "100 Continue" eine Zwischenbestätigung senden. Leider "verstehen" das gerade kleine Geräte das nicht immer, wie ich auf PRTG Edimax SP2101W z.B. herausgefunden habe.

Zudem scheint im Hintergrund ein Objekte bestehen zu bleiben, welches bei Folgeanfragen wieder genutzt wird. Im Wireshark ist schön zu sehen, dass die gleichen Anfragen im Sekundenabstand immer den gleichen Source-Port nutzen:

Hinweis:
Der Download ist deutlich schneller, wenn Sie die Variable $ProgressPreference = 'SilentlyContinue' setzen. Das erspart ihnen den "Progress-Balken"

Invoke-Webrequest und Performance

Eines muss man Invoke-Webrequest aber ankreiden: Es ist nicht besonders schnell. Hier ist mal ein ganz einfacher Code, der 100mal die Homepage der msxfaq.de herunter lädt:

1..100 | %{Invoke-WebRequest http://www.msxfaq.de}

Schon im Powershell-Fenster ist zu sehen, dass jeder Request ca. 1 Sekunde dauert und auch im Taskmanager ist zu sehen, dass die PowerShell hier nur eine Verbindung zur 217.160.0.234 pflegt aber mit 100kbit/Sek ist das alles andere als schnell:

Invoke-Webrequest ist also der "Quickie" um eine HTML-Seite zu bekommen. Das stimmt so aber nicht. Das Commandlet macht sich nach dem Empfang der Seite nämlich noch die Mühe diese zu parsen und auf dem Bildschirm auszugeben. Beides lässt sich abschalten oder reduzieren mit deutlichen Auswirkungen auf die Performance.

Durch den Einsatz des Parameters "-UseBasicParsing" werden die Felder "Forms", "InputFields" und "ParsedHTML" des Ergebnisses nicht gefüllt. "Content" und "RAW-Content" gibt es aber ebenso wie "Links", "Images" und "Headers". BasicParsing liefert also schon viele Daten für weitergehende Downloads aber verzichtet auf das Rendern des Inhalts, was aber kaum mehr Zeit braucht.

Diese Werte habe ich in PowerShell per "Single Thread" ermittelt. Wenn Sie in ihrem Code Multithreading nutzen, können die Werte abweichen.

Aufruf 100 Mal Durchsatz
23771 bytes

Der einfache Aufruf mit Bildschirmausgabe und vollem Parsing.

$start=get-date
1..100 | %{Invoke-WebRequest http://www.msxfaq.de}
(get-date) - $start

150,0 Sek

158 kBit/Sec

Aufruf mit UserBasicParsing

$start=get-date
1..100 | %{Invoke-WebRequest http://www.msxfaq.de -UseBasicParsing}
(get-date) - $start

12,3 Sek

1,932  kBit/Sek

Unterdrücken der Ausgabe mit Out-Null

$start=get-date
1..100 | %{Invoke-WebRequest http://www.msxfaq.de | out-null}
(get-date) - $start

7,7 Sek

3087 kBit/Sek

UseBasicParsing und Ausgabe mit Out-Null verwerfen

$start=get-date
1..100 | %{Invoke-WebRequest http://www.msxfaq.de -UseBasicParsing | out-null}
(get-date) - $start

9,4 Sek

2528 kBit/Sek

Als Gegencheck habe ich mal eine native .NET-Klasse genutzt.

$webclient = New-Object Net.WebClient
$webclient.Headers.Add("User-agent", "Mozilla/5.0 (Windows NT; Windows NT 10.0; de-DE)");
$webclient.CachePolicy = new-object System.Net.Cache.RequestCachePolicy ("bypasscache") # Skip Cache

$start=get-date
1..100 | %{$null=$webclient.DownloadString("http://www.msxfaq.de")}
(get-date) - $start

$start=get-date
1..100 | %{$webclient.DownloadString("http://www.msxfaq.de") | out-null}
(get-date) - $start

Dieser Weg ist also nicht schneller und sogar die Ausgabe nach "null" ist ein deutlicher Unterschied. Allerdings tippe ich hier eher auf meine Messfehler, denn Invoke-Webrequest nutzt wohl auch den gleichen Unterbau.

14 Sek
10 Sek

 

Mittlerweile liefert Microsoft auch "CURL.EXE" mit, was aber deutlich langsamer ist.

measure-command {1..100 | %{curl https://www.msxfaq.de}}

Das dürfte aber am Start der CMD-Shell liegen.

30s

 

Es ist offensichtlich, dass die Ausgabe der Daten auf den Bildschirm der Bremser ist. Wer die Ausgaben nicht braucht und nur Last erzeugen will, kann das mit "| Out-Null" oder der Zuweisung in eine Variable unterbinden. Anscheinend verlangsamt das "-UseBasicParsing" sogar die Verarbeitung. Allerdings muss man wissen, dass bei automatisierten Prozessen der Zugriff auf die Browser DOM-Objekte nicht immer möglich ist. Wenn ich die Dauer mit "Measure-Comand" messen will, welches auch die Ausgaben unterdrückt, ist auch alles schneller.

Invoke-Webrequest und 301 Moved Permanently

Immer mehr Webseiten erfordern eine SSL-Verschlüsselung und lösen dies dahingehend, dass Sie einen Zugriff per HTTP mit einem Redirect auf die SSL-Verbindung forcieren. Ich habe das mal mit frankysweb.de nachgespielt. Mit Fiddler lässt sich das Verhalten sehr gut analysieren.

Invoke-Webrequest unterstützt die Umleitung und liefert die Zieladresse als Ergebnis zurück. Per Default folgt Invoke-WebRequest bis zu 5 Weiterleitungen. Die Anzahl kann mit dem Parameter -MaximumRedirection angepasst werden.

Mit der PowerShell 7 hat sich das verhalten etwas geändert. Hier scheint Invoke-Webrequest nicht mehr zu folgen sondern einen Fehler nach $error zu melden.

Ursache ist hier die Umleitung von HTTPS zu HTTP, die das darunterliegende NET Core Framework nicht macht.

Der Fehler ist also nicht in der PowerShell selbst. Es wäre nett gewesen, wenn das Handling dieses Sonderfalles z.B.: durch einen Parameter "-Allow HTTPStoHTTPredirect:$true" o.ä. möglich wäre. Dem ist aber nicht so. Ich muss ich dann selbst z.B. mit einem Try/Catch arbeiten und die Location nachverfolgen.

Invoke-Webrequest und UTF-8

Eine sehr unschöne Fehlermeldung bekommen Sie manchmal, wenn Sie Invoke-Webrequest gegen einen Webserver nutzen, der Daten als UTF-8 ausliefert. Auch dieses Verhalten ist mir bei frankysweb.de aufgefallen, was aber keinesfalls als Fehler der Webseite angesehen werden darf.

Die Fehlermeldung in der Powershell liefert darauf hin folgendes:

Invoke-WebRequest : ""UTF-8"" ist kein unterstützter Codierungsname. Informationen zum Definieren einer benutzerdefinierten
Codierung entnehmen Sie der Dokumentation zur Encoding.RegisterProvider-Methode.
Parametername: name

Wenn ich bei der Anforderung der Daten kein Encoding vorgebe, liefert der WebServer einfach aus, was er als Default hat und Invoke-Webrequest weiß einfach nicht, wie man mit UTF-8 als Objekt umgeht. Ich habe versucht über die Header "Content-Type" und "Accept-Charset" versucht eine Codierung vorzugeben aber der WebServer hat das einfach ignoriert.

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Accept-Charset", 'UTF-8')
Invoke-WebRequest -headers $headers

Sobald der WebServer "UTF-8" sendet, scheint Invoke-Webrequest als auch Invoke-RESTMethod zu streiken. Diverse Quellen behaupten, dass dies ein Bug wäre. Interessanteweise kann man mit dem Parameter "-outfile" den Content problemlos in eine Datei schreiben lassen.

Eine Lösung für Invoke-WebRequest habe ich nicht. Ich nutzt stattdessen dann die NET.WebClient-Klasse:

$webclient= New-Object Net.WebClient
$rssdata = $webclient.DownloadString("https://www.frankysweb.de/feed/")

Der Inhalt ist weiterhin UTF-8 mit Umlauten:

Wenn ich die Rückgabe aber einfach verarbeiten würde, dann sind die Umlaute korrupt. PowerShell interpretiert den String als UNICODE

Auch diese Klasse erkennt anhand des Content-Type nicht automatisch das Encoding. Das kann ich hier aber vorab spezifizieren, so dass der komplette Code dann wie folgt aussieht:

$webclient = New-Object Net.WebClient

#Optional Proxy
$webclient.Proxy = New-Object System.net.WebProxy("http://squid:3128/", $true)
$webclient.Proxy.UseDefaultCredentials= $true 

$webclient.Encoding = [System.Text.Encoding]::UTF8
$rssdata = $webclient.DownloadString("https://www.frankysweb.de/feed/")
#Ausgabe der Items an die Pipeline
([xml]$rssdata).rss.channel.Item

Natürlich ist es schade, dass Invoke-Webrequest nicht mit UTF-8 umgehen kann und selbst Der WebClient braucht etwas Hilfe, da er ansonsten die Rückgabe als ISO-8859-1 (Windows-1252) codiert, selbst wenn der Content-Type in der Antwort etwas anderes mitteilt.

Exception Handling

Wenn Sie mit Invoke-Webrequest oder Invoke-RestMethod eine URL ansprechen, und der Server z.B. mit einem 4xx Fehler antwortet, dann liefert PowerShell eine Exception.

Hier noch mal als Text für Google und Co

PS C:\> $result = invoke-webrequest https://www.msxfaq.de/gibtesnicht.htm
invoke-webrequest : MSXFAQ.DE:404 Fehlerseite
Die von ihnen angesurfte Seite ist anscheinend nicht mehr vorhanden oder wurde an eine andere Stelle umgezogen.
Moment bitte, wir leiten Sie weiter
In Zeile:1 Zeichen:11
+ $result = invoke-webrequest https://www.msxfaq.de/gibtesnicht.htm
+           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

Das Problem dabei ist, dass die Variable "$Result" in dem Fall nicht belegt wird. Wenn ihnen der Fehler reicht, ist das zu vertreten. Wenn Sie aber z.B. eine REST-Methode aufrufen, dann ist der HTTP-Fehler ziemlich unwichtig. Dann interessiert mich schon die detaillierte Meldung im Payload. Hier am Beispiel eines Graph-Aufrufs:

Diese Information kann ich aber über eine Try/Catch-Konstruktion abfangen:

    try {
        $result =Invoke-WebRequest https://www.msxfaq.de/gibtesnicht.htm
    }
    catch {
        $resultstream = $_.Exception.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($resultstream)
        $errorbody = $global:reader.ReadToEnd();
    }

So kann ich über $errorbody dann auch die Details des Fehlers weiter verarbeiten.

Achtung: Powershell 2und PowerShell 5+ verhalten sich hier unterschiedlich.

System.Net.WebClients (bis PS 2.0)

Per PowerShell können auch HTTP-Anfragen an Webserver gestellt werden. So können z.B. automatisch Daten von Webservern herunter geladen werden. Dreh und Angelpunkt ist die System.Net.WebClient-Klasse mit ihren Properties.

PS C:\> $webclient = new-object System.Net.WebClient
PS C:\> $webclient

AllowReadStreamBuffering  : False
AllowWriteStreamBuffering : False
Encoding                  : System.Text.SBCSCodePageEncoding
BaseAddress               :
Credentials               : useDefaultCredentials     : False
Headers                   : {}
QueryString               : {}
ResponseHeaders           :
Proxy                     : System.Net.WebRequest+WebProxyWrapper
CachePolicy               :
IsBusy                    : False
Site                      :
Container                 :

Hier können Sie dann natürlich auch die Header komplett flexibel setzen:

$webclient.Headers.Add("UserAgent", "User agent to send");
$webclient.Headers.Add("Referer", "string");

Und interessanter noch die Methoden. Hier eine Auswahl:

   TypeName: System.Net.WebClient
Name                      MemberType Definition
----                      ---------- ----------
DownloadData              Method     byte[] DownloadData(string address), by...
DownloadDataAsync         Method     void DownloadDataAsync(uri address), vo...
DownloadFile              Method     void DownloadFile(string address, strin...
DownloadFileAsync         Method     void DownloadFileAsync(uri address, str...
DownloadString            Method     string DownloadString(string address), ...
DownloadStringAsync       Method     void DownloadStringAsync(uri address), ...
OpenRead                  Method     System.IO.Stream OpenRead(string addres...
OpenReadAsync             Method     void OpenReadAsync(uri address), void O...
OpenWrite                 Method     System.IO.Stream OpenWrite(string addre...
OpenWriteAsync            Method     void OpenWriteAsync(uri address), void ...
UploadData                Method     byte[] uploadData(string address, byte[...
UploadDataAsync           Method     void uploadDataAsync(uri address, byte[...
UploadFile                Method     byte[] uploadFile(string address, strin...
UploadFileAsync           Method     void uploadFileAsync(uri address, strin...
UploadString              Method     string uploadString(string address, str...
UploadStringAsync         Method     void uploadStringAsync(uri address, str...
UploadValues              Method     byte[] uploadValues(string address, Sys...
UploadValuesAsync         Method     void uploadValuesAsync(uri address, Sys...

Damit ist die Basis gelegt und der Abruf einer einfachen HTTP-Information ohne Proxy ist in zwei Zeilen geschafft:

$webclient = new-object System.Net.WebClient
$webclient.DownloadString("http://www.msxfaq.de")

Einen Fehler müssen Sie natürlich mit Try/Catch-Konstruktionen selbst abfangen

System.Net.HTTPWebrequest

Diese Klasse ist eine auf HTTP "optimierte" Version der generischen Webrequest-Klasse. Während ein Webrequest quasi jede URI bedienen kann, ist diese Klasse auf HTTP/HTTPS fokussiert. Die Klasse kennt dabei als Attribute auch HTTP-spezifische Eigenschaften.

$URL = "http://www.msxfaq.de"
$httprequest=[system.Net.HttpWebRequest]::Create($URL);
$data = $httprequest.getresponse();
$stat = $data.statuscode;
$data.Close();

System.Net.Http.HttpClient

Zuletzt gibt es auch noch diese Klasse im .NET-Framework. Dazu gleich ein Hinweis:

Hinweis Die Namespaces System.Net.Http und System.Net.Http.Headers sind in zukünftigen Windows-Versionen möglicherweise nicht mehr zur Verwendung in Windows Store-Apps verfügbar. Verwenden Sie ab Windows 8.1 und Windows Server 2012 R2 stattdessen Windows.Web.Http.HttpClient im Windows.Web.Http-Namespace und den zugehörigen Namespaces Windows.Web.Http.Headers und Windows.Web.Http.Filters für Windows Store-Apps.
Quelle: http://msdn.microsoft.com/de-de/library/windows/apps/hh781239.aspx

Wenn Sie keine "DefaultRequestHeader" addieren, dann ist der Request wirklich "leer". Einige Webserver lehnen so etwas gerne mit einen "400 Bad Request" ab. Meist reicht es einen "User-Agent" mit zu senden.

Allerdings geht das wirklich erst ab Windows 8.1 und Windows Server 2012R2- Dann bleibe ich erst mal beim System.net.http.httpClient. Im Gegensatz zu den anderen drei Optionen läuft diese Klasse asynchron, d.h. die Kontrolle wird sofort an den aufrufenden Prozess zurück gegeben. Das ist von Vorteil, wenn man parallelisieren muss aber bei einem Aufruf müssen wir warten.

Add-Type -AssemblyName System.Net.Http

# Optional Handler erstellen, z.B. fuer Proxy, Credentials, Redirect, SSLProperties
$HttpClientHandler =New-Object System.Net.Http.HttpClientHandler 

# Optional Proxy und ProxyAuthentication
$HttpClientHandler.Proxy = New-Object System.net.WebProxy("http://proxyserver:3128/", $true)
$HttpClientHandler.Proxy.UseDefaultCredentials=$true

$httpclient = New-Object System.Net.Http.HttpClient($HttpClientHandler)
# oder ohne HttpClientHandler 
#$httpclient = New-Object System.Net.Http.HttpClient

#Optionale Header
$httpclient.DefaultRequestHeaders.add("User-Agent", "Mozilla/5.0 MSXFAQSample");

write-verbose "Start Request"
$response = $httpclient.GetStringAsync($URL)

Try {
   write-host "Wait für completion"
   $response.wait() # wait für end of http-tasks
   add-member -InputObject $response -Name LastStatus -MemberType noteproperty -Value "OK" -Force
}
Catch {
   write-host "Download ERROR $URL"
   write-host ("Error:" + $response.Exception.InnerException.message)
   add-member -InputObject $response -Name LastStatus -MemberType noteproperty -Value ($winhttptask.Exception.InnerException.message) -Force
   $error.removerange(0,1) #remove last error
}

if ($winhttptask.LastStatus -eq "OK") {
   write-host "Download OK"
   write-host ("Download Data 40char " + $response.result.substring(1,40))
}
else {
   write-host ("Found error:" + $response.LastStatus)
}

Aber auch dieser Request liefert als Response nur dann eine Antwort, wenn dieser fehlerfrei war. Wenn auch hier die Abfrage mit einem 4xx Fehler bedient wurde, dann ist auch dieser Response leer.

Ich habe noch keinen Weg gefunden, bei einem 407 neben dem Fehlercode auch die Response-Header des 407 zu erhalten.

XMLHTTPRequest

Zwar ist dies ein COM-Objekt und kann auch aus der PowerShell heraus genutzt werden. Allerdings ist diese Schnittstelle ein universeller Zugang für verschiedene Browser und durchaus mit den gleichen Methoden und Eigenschaften über die verschiedenen Betriebssysteme vorhanden.

$XMLHTTPRequest = New-Object -ComObject Msxml2.XMLHTTP
$XMLHTTPRequest.open('GET', $URL, $false)
#$XMLHTTPRequest.open('POST', $URL, $false)
$XMLHTTPRequest.setRequestHeader("Referer", "http://www.msxfaq.de")
#$XMLHTTPRequest.setRequestHeader("Content-type",   "application/x-www-form-URLencoded")
#$XMLHTTPRequest.setRequestHeader("Content-length", $parameters.length)
$XMLHTTPRequest.setRequestHeader("Referer", "http://www.msxfaq.de")
$XMLHTTPRequest.setRequestHeader("Test", "http://www.msxfaq.de")

Try {
   write-host "Download START $URL"
   $XMLHTTPRequest.send($null)
   write-host "Download DONE $URL"
}
Catch {
   write-host "Download ERROR $URL"
   write-host ("Error:" + $_.Exception.InnerException.Message)
   $error.removerange(0,1) # remove last error
}
write-host ("Status    :" + $XMLHTTPRequest.status)
write-host ("Statustext:" + $XMLHTTPRequest.statusText)

Hier die Information über das Objekt selbst:

PS C:\> $XMLHTTPRequest |gm


   TypeName: System.__ComObject#{ed8c108d-4349-11d2-91a4-00c04f7969e8}

Name                  MemberType Definition
----                  ---------- ----------
abort                 Method     void abort ()
getAllResponseHeaders Method     string getAllResponseHeaders ()
getResponseHeader     Method     string getResponseHeader (string)
open                  Method     void open (string, string, Variant, Variant...
send                  Method     void send (Variant)
setRequestHeader      Method     void setRequestHeader (string, string)
onreadystatechange    Property   IDispatch onreadystatechange () {set}
readyState            Property   int readyState () {get}
responseBody          Property   Variant responseBody () {get}
responseStream        Property   Variant responseStream () {get}
responseText          Property   string responseText () {get}
responseXML           Property   IDispatch responseXML () {get}
status                Property   int status () {get}
statusText            Property   string statusText () {get}

BITS-Transfer

Bits wird schon länger von Windows angeboten und verschiedene Programme, wie z.B. Outook nutzen Bits um den Download von Dateien zu delegieren. Bits ist ein Hintergrunddienst, der angeblich auch die Bandbreite berücksichtigt und den Download drosselt, wenn andere Dienste die Bandbreite benötigen.

Der Download kann sowohl Synchron also auch mit dem Schalter "-Asynchronous" in den Hintergrund gelegt werden. Mit Start-BITSTransfer, u.a.

PS C:\> get-command *-bits*

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Cmdlet          Add-BitsFile                                       BitsTransfer
Cmdlet          Complete-BitsTransfer                              BitsTransfer
Cmdlet          Get-BitsTransfer                                   BitsTransfer
Cmdlet          Remove-BitsTransfer                                BitsTransfer
Cmdlet          Resume-BitsTransfer                                BitsTransfer
Cmdlet          Set-BitsTransfer                                   BitsTransfer
Cmdlet          Start-BitsTransfer                                 BitsTransfer
Cmdlet          Suspend-BitsTransfer                               BitsTransfer

Entsprechend kann Auch per PowerShell die BITS-Funktion genutzt werden.

$sourceurl = "http://www.msxfaq.de"
$Zieldatei = "c:\temp\msxfaq.html"

Import-Module BitsTransfer

# Synchrone Uebertragung
Start-BitsTransfer -Source $sourceurl -Destination $Zieldatei

# ASyncrhjone übertragung
Start-BitsTransfer -Source $sourceurl -Destination $Zieldatei -Asynchronous

Bei der asynchronen Nutzung sehen Sie natürlich keinen Indikator, wie weit der Download schon ist und sie müssen mit Get-BitsTransfer bei Gelegenheit nachschauen, ob ihr Download schon abgeschlossen ist.

Wenn Sie die PowerShell als Administrator starten, können Sie mit "Get-BITSTransfer -AllUsers" auch sehen, was andere Prozesse (z.B. WSUS, Windows Update, Outlook etc) gerade so im Hintergrund per BITS übertragen.

Einschränkung: Error-Handing

Sowohl die seit PowerShell 3.0 verfügbaren Commandlets "Invoke-WebRequest" und "Invoke-RESTMethod" als auch der System.Net.WebClient machen es einfach per HTTP mit einem Server zu kommunizieren. Aber Sie haben eine Einschränkungen gemeinsam, die Sie können sollten: Die Fehlerbehandlung:

Solange ein Abruf fehlerfrei funktioniert, d.h. der Webserver erreichbar ist und man mit einer 2xx/3xx-Meldung quittiert wird, dann erhalten Sie auch ein Objekt mit der Antwort zurück. Warum auch immer passiert die so nicht, wenn Sie auf einen Fehler (4xx/5xx) laufen. Dann wird nämlich nicht ein Antwortobjekt zurück gegeben, welches als Statuscode dann den entsprechenden Fehlercode hat, sondern es wird eine Exception geworfen. Hier am Beispiel eines 401:

Ich hätte es anders logischer angesehen, denn auch ein 401 o.ä. stellt zwar ein Fehler innerhalb der Aktion dar, aber der "Abruf" als solches war schon erfolgreich. Auch die 401-Fehlerseite enthält Informationen, Header etc., der z.B. bei UCWA - Unified Communication Web API erforderlich ist. Laut MSDN kann eigentlich nur eine "WebException" kommen.

Wir benötigen also eine "Try/Catch"-Anweisung, die solche Fehler abfängt. Ich habe mir das so aufbereitet, dass ich auf jeden Fall einen eigenen Status an das Objekt anfüge, welche ich später dann einfach abfragen kann.

Try {
   $result = Invoke-WebRequest -uri $test
   Add-member -InputObject $result -Name Status -MemberType noteproperty -Value $result.Statuscode
} 
Catch {
   [System.Net.HttpWebResponse]$result = [System.Net.HttpWebResponse] $_.Exception.Response
   Add-member -InputObject $result -Name Status -MemberType noteproperty -Value $result.StatusCode.Value__
   $error.clear()
}

So landet in "$Result" entweder die erfolgreiche Antwort oder eben die partielle Fehlerantwort. Allerdings sind es zwei unterschiedliche Objekte, die zwar beide das Property "Statuscode" haben, aber einmal als Objekt und einmal als Enumeration. Daher habe ich hier den Staus als eigenes Feld addiert. Folgende Fälle habe ich mal getestet.

Hinweis: Die Variable $data wird nur belegt, wenn die Antwort ein 200/300-Code ist. Bricht die Anforderung mit einem Fehler ab, dann wird die Variable nicht verändert.

Fehlerfall Verhalten $webclient Exception
StatusMessage
Exception
StatusCode
$data

200 OK

Daten wurden abgerufen

Enthält Response Header

OK

Kein

HTML-Content

302 Redirect

Daten wurden vom Redirect-Ziel abgerufen !

Enthält Response Header

OK

kein 

HTML-Content

401 Error
403 Forbidden

Exception wird geworfen

Null$

ProtocolError

7

$null

Keine Verbindung

Abbruch nach 20Sek

unverändert!

ConnectFailure

2

 

Kein DNS

Abbruch nach 3 Sek

unverändert!

NameResolutionFailure

1

 

HTTPS auf ungültiges Cert

Exception wird geworfen 

$null 

TrustFailure 

9

$null 

Eine komplette Liste der verschiedenen Statuscodes bekommt man mit

PS C:\> [System.Enum]::GetValues([System.Net.WebExceptionStatus])
Success
NameResolutionFailure
ConnectFailure
ReceiveFailure
SendFailure
PipelineFailure
RequestCanceled
ProtocolError
ConnectionClosed
TrustFailure
SecureChannelFailure
ServerProtocolViolation
KeepAliveFailure
Pending
Timeout
ProxyNameResolutionFailure unknownError
MessageLengthLimitExceeded
CacheEntryNotFound
RequestProhibitedByCachePolicy
RequestProhibitedByProxy

Allerdings konnte ich den "Success" noch nicht sehen, da beim einem Erfolg ja keine Exception geworfen wird.

Aus meiner Sicht eine sehr unglückliche Definition der WebClient-Klasse. Ich hätte mir gewünscht, dass Sie immer ein Objekt zurück gibt, welcher die Antwort des Servers enthält. Eine Exception wäre sicher möglich, wenn der Server nicht erreichbar ist aber auch ein 4xx oder 5xx Fehler des Servers sollte dennoch als "erfolgreiche Antwort" angesehen werden. Es würde das weitere Programmieren schon einfacher machen.

Aber es ist so, wie es ist und ich habe noch keinen Weg gefunden den vom Webserver bei einem Fehler mitgelieferten Content zu erhalten. Zumindest ist es mir aber schon gelungen z.B. die Header der Fehlermeldung zu sammeln.

Einschränkung: Restricted Header

Manchmal ist es erforderlich im HTTP-Request auch Felder im Header zu setzen. Sowohl "Invoke-Webrequest" als auch "Invoke-RESTMethod" erlauben über den Parameter "-headers" auch die Übergabe eines Dictionary mit eigenen Headern.

$data = Invoke-WebRequest  `
               -uri $URL   `
               -Method GET  `
               -Header @{Referer = "http://www.msxfaq.de"} `

Allerdings gibt es auch hier Beschränkungen, so dass Sie nicht alle Felder frei setzen können. Mich hat es bei Experimenten mit UCWA - Unified Communication Web API erwischt, bei denen ich das Feld "Referer" setzen wollte, was aber mit folgendem Fehler abgelehnt wurde:

Der Wert "System.ArgumentException: Der 'Referer'-Header muss mit der
entsprechenden Eigenschaft oder Methode geändert werden.
Parametername: name
   bei System.Net.WebHeaderCollection.ThrowOnRestrictedHeader(String headerName)

Wer also z.B. den "Referer" setzen will, kann den Parameter "Headers" beiden Commandlets getrost vergessen. Aber beide Commandlets erlauben die Angabe einer "WebSession" beim Aufruf. Sie wird vom Commandlet beim Aufruf gefüllt so dass man sie nachfolgenden Aufrufen mitgeben kann. Sie kann aber auch vorher instanziert und gefüllt werden.

PS C:\> $websession= new-object Microsoft.PowerShell.Commands.WebRequestSession
PS C:\> $websession

Headers               : {}
Cookies               : System.Net.CookieContainer useDefaultCredentials : False
Credentials           :
Certificates          : UserAgent             : Mozilla/5.0 (Windows NT; Windows NT 6.1; de-DE) WindowsPowerShell/3.0
Proxy                 :
MaximumRedirection    : -1

Und entsprechend können die Parameter erweitert oder geändert werden. Parameter, die Sie dann aber per Commandlet angeben, überschreiben diese Angaben wieder.

$websession= new-object Microsoft.PowerShell.Commands.WebRequestSession
$websession.UserAgent = "MSXFAQ Test""
$websession.Headers.Add("Referer","http://www.msxfaq.de")
$data = Invoke-WebRequest  `
               -uri $URL `
               -Method GET `
               -WebSession websession

Aber auch hier kommen Sie nicht weiter. Auch hier wird dieser Versuch einen "Referer" mit zu übergeben mit einem Fehler quittiert

Es gibt noch einige weitere Felder wie z.B.

  • Accept Connection
  • Content-Length
  • Content-Type
  • Date Expect Host
  • If-Modified-Since
  • Range
  • Referer
  • Transfer-Encoding
  • User-Agent
  • Proxy-Connection

Ein Teil davon kann man aber schon über Parameter der PowerShell Commandlets setzen. Andere nicht. Den vollen Zugriff auf die Felder erhalten Sie nur über die NET-API. Der WebClient liefert als Stream aber nur den Inhalte, also den Body der Abfrage. Die Header der Abfrage landen als Dictionary im Feld "ResponseHeaders". Ein Zugriff auf den Statuscode (2xx/3xx) habe ich so nicht nicht erhalten.

Einschränkung: Response Header und 401

Um per UCMA auf Dienste von Lync zuzugreifen, muss ich aber nicht nur bestimmte Header setzen, sondern auch "Response Header" auslesen. Das ist insbesondere knifflig mit einer provozierten 401-Meldungen, die im "WWW-Authenticate"-Header eine erforderliche URL als Antwort enthält.

  • WebClient
    Das Feld "ResponseHeader" ist leider leer, wenn die Gegenseite mit einem 401 quittiert. Allerdings enthält die Exception die gewünschten Informationen in $_.Exception.InnerException.Response.Headers
  • Invoke-Webrequest
    Die Antwort mit allen Daten ist leider "$null", wenn die Gegenseite mit einem 401 quittiert. Allerdings enthält die Exception die gewünschten  Informationen im Feld $_.Exception.Response.Headers
  • System.Net.HTTPWebrequest
    Liefert bei einem 401 keine Daten und damit auch kein ResponseHeader mit aus.

Es ist nur unschön, dass wir die Daten nicht aus der Antwort zum Request sondern über die Exception abfangen müssen.

Einschränkung Session Reusing

Bei der Nutzung von PowerShell für End2End-HTTP ist mir aufgefallen, dass die Antwortzeiten sehr schnell waren. Ich habe dann mit Wireshark nachgeschaut und gesehen, dass eine TCP-Verbindung nur beim ersten mal aufgebaut wird.

Das ist beim Einsatz von HTTPS noch deutlicher zu merken, da der TLS-Handshake bei den Folgeverbindungen ebenfalls unterbleibt. Die PowerShell instanziert da wohl im Hintergrund ein Objekt und lässt es noch weiter laufen für folgende Requests an die gleichen Ziele. Erst nach ca. 80-100 Sekunden später baut das Objekt die Verbindung eigenständig ab. Die Verbindung wird sofort beendet, wenn Sie die PowerShell selbst beenden.

Also habe ich die verschiedenen Optionen verglichen:

Die Ergebnisse stimmen nur in dem Maße, wie der Server auch das Keep-Alive unterstützt und die Verbindung nichts selbst beendet. Das ist bei 4xx-Fehlerseiten oder bei einem Redirect sehr oft der Fall. Hier geht der Web-Server oft davon aus dass der Client nicht wiederkommt.

Methode Wiederverwendung der Connection

Invoke-Webrequest

Ja, kann aber mit dem Parameter "-DisableKeepAlive" verhindert werden.

Invoke-RestMethod

Ja, kann aber mit dem Parameter "-DisableKeepAlive" verhindert werden.

System.Net.WebClient

Ja. Auch ein Aufruf der Dispose-Methode gibt die TCP-Connection nicht frei.

System.Net.HttpWebRequest

Ja, Selbst wenn ich die Variable neu instanziere bleibt die TCP-Connection erhalten

System.Net.Http.HttpClient

Nein, wenn ich jedes mal das Objekt neu instanzieren

Wenn ich also wirklich mit jedem Aufruf eine neue Verbindung nutzen möchte um z.B.: die Ports von Proxy-Server zu verbrauchen, dann muss ich das mit der "System.Net.Http.HttpClient "-Klasse machen. Wer also möglich viele IP-Ports konsumieren möchte kann einen kleinen DoS gegen den NAT-Router fahren:

Add-Type -AssemblyName System.Net.Http;
write-host " Start TCP Connections $((get-nettcpconnection).count)"
1..1000|% {
   write-progress "Loop $($_) of 50000"
   $winhttpclient = new-object System.Net.Http.HttpClient;
   $winhttpclient.DefaultRequestHeaders.add("User-Agent", "Mozilla/5.0");
   $null = $winhttpclient.GetStringAsync("https://outlook.office365.com/favicon.ico");
}
write-host " End TCP Connections $((get-nettcpconnection).count)"

In Wireshark sieht man schön, dass wirklich viele Connections genutzt werden:

Und auch auf dem Client sehe ich den "Portverbrauch":

Doe 65535 Ports kann ich so aber kaum erreichen, da ein Idle-Verbindung nach 2 Min wieder abgebaut wird und so ein PowerShell-Script ist nicht schnell genug. Natürlich sollte man die Rückgaben auch "prüfen", ob denn noch was zurück kommt. Auf dem WebServer gibt es hier abgesehen von der Last kein Problem. Es könnte aber ihren Proxy oder NAT-Router eher überlasten als dass ihrem lokalen PC die Ports ausgehen. Allerdings müssen Sie bei den meisten WebServern noch einen User-Agent mitliefern, damit sie keinen "400 bad Request" sendet.

Bsp: Einfacher anonymer Download einer URL

Der einfachste Schritt ist der Download einer anonym erreichbaren Quelle per HTTP. Es muss sich dabei nicht zwingend um eine "HTML-Datei handelt.

$webclient = new-object System.Net.WebClient
$webpage = $webclient.DownloadString("http://www.msxfaq.de")

Dieser Zweizeiler lädt die angegebene URL herunter und speichert Sie in der Variable "$webpage". Interessant ist dieser Prozess, wenn Sie z.B. eine XML-Datei per HTTP herunter laden und die Variable gleich mit einem "XML" versehen.

#Variante 1
[xml]$configxml = $webclient.DownloadString("http://autodiscover.netatwork.de/autodiscover.xml")

#Variante 2
$configxml = [xml]($webclient.DownloadString("http://autodiscover.netatwork.de/autodiscover.xml")

Dann können Sie direkt auf die XML-Datei zugreifen.

Bsp: Authentifizierung

Die nächste Steigerung ist natürlich die Authentifizierung, da die meisten interessanten Dienste nicht immer anonym erreichbar sind. Die entsprechenden Anmeldedaten muss man natürlich entweder mitgeben oder Windows nutzt die integrierte Authentifizierung (NTLM/Kerberos)

$webclient = new-object System.Net.WebClient
$webclient.Credentials = New-Object System.Net.NetworkCredential("frank","meinkennwort")
$webpage = $webclient.DownloadString("http://sicher.msxfaq.net")

So wird die Anfrage dann authentifiziert versendet, zumindest wenn es sich um eine HTTP-Authentifizierung handelt. Aber auch mit dem neuen PowerShell 3 Befehl "Invoke-Webrequest" kann die Anmeldung über den Parameter "Credential" gesetzt werden. Wenn Sie die Anmeldedaten aber nicht mit "Get-Credential" abfragen wollen, müssen Sie einen kleinen Umweg gehen:

[string] $strUser = 'domain\Username'
[System.Security.SecureString]$strPass = ConvertTo-SecureString -String "password" -AsPlainText -Force
$Cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($strUser, $strPass)

Invoke-Webrequest -uri $URL -credential $cred

Der Vorteil gegenüber WebPage ist, dass das Ergebnis kein "String" sondern ein "HtmlWebResponseObject (Microsoft.PowerShell.Commands.WebResponseObject )" ist, die einfacher als Objekt angesprochen werden kann.

Die Anmeldung über die Commandlets funktionieren nach meinem Wissen aber immer erst, wenn die Gegenseite auch eine Authentifizierung anfordert. Die PowerShell scheint dazu nämlich zuerst einmal einen anonymen Request zu senden, um sich dann einen 401 einzufangen. In den 401 steht aber dann auch drin, welche Authentifizierungsverfahren die Gegenseite unterstützt. Hier einmal ein IIS/Exchange, der auf eine anonyme Anfrage einen 401 mit "WWW-Authenticate:" liefert, während BMW dies nicht tut.

Hier müssen Sie ggfls. "blind" die Anmeldedaten senden, wenn die PowerShell Module das richtige Authentifizierungsverfahren nicht ermitteln können. Die meisten WebServer liefern im 401 die möglichen Anmeldeverfahren mit aber gerade in der IoT-Welt oder mit Kleinst-Prozessoren, z.B. ESP8266, ist dies nicht immer möglich. Dann muss man sich selbst um den "Authentication Header" kümmern:

# Rest-Aufruf mit vorgegebener Basic Auth
$User = "Username"
$pass = "kennwort"
$basicauth=[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($User+":"+$pass))
$headers=@{}
$headers.Add("Authorization","Basic $basicauth")
$result= Invoke-RestMethod `
   -Method GET `
   -Headers $headers `
   -Uri https://test.msxfaqnet `
   -Headers $headers"

Webseiten mit formularbasierter Anmeldung müssen natürlich anders abgewickelt werden. Das ist dann ein mehrstufiger Prozess, bei dem Sie Formulardaten senden und z.B. den Cookie oder das Webticket in der Antwort bei folgenden Requests mit senden. Aber auch das geht.

$body = @{ Username = 'Username'
   password = 'kennwort'
}

$authrequest = invoke-webrequest `
   -method POST `
   -usebasicparsing `
   -URI "https://test.msfaq.de/logon.aspx" `
   -Body $body

Hier muss man eben die Anmeldedaten als Formularfeld "blind" mitgeben. Das ist quasi bei allen "Form Based"-Anmeldungen der Fall. 

Bsp: Dateidownload

Wenn es nicht darum geht eine HTML-Datei als String für weitere Verarbeitungen zu nutzen, sondern eine auch größere Datei herunter zu laden, dann geht das auch über den System.Net.Webclient. Allerdings nutzen Sie dann besser die "DownloadFile"-Methode

$webclient = new-object System.Net.WebClient
$webclient.DownloadFile($sourceURL,$targetfile)

Die herunter geladene Datei landet als Datei mit dem angegeben Namen (hier $targetfile).  Die "DownloadFile"-Methode liefert keinen Ergebniscode zurück. Ein Fehler wird als "Error" gemeldet,

Entsprechend sollten Sie so einen Download in einem "Try/Catch"-Block einschließen und den Fehler abhandeln.

Bsp: HTTPProxy nutzen

In den meisten Firmen dürfen Clients nicht direkt das Internet erreichen, sondern müssen einen Proxy verwenden. Auch dies ist mit dem System.Net.WebClient möglich.

Die meisten PowerShell Module bedienen sich den WinHTTP-Einstellungen

Die "Proxy"-Eigenschaft ist der passenden Platz.

Entsprechend muss der WebClient erweitert werden:

$WebClient = New-Object System.Net.WebClient
$WebProxy = New-Object System.Net.WebProxy("http://proxy:8080",$true)
$Credentials = New-Object Net.NetworkCredential("Username,"Password","domain")
$WebProxy.Credentials = $Credentials
$WebClient.Proxy = $WebProxy
$result = $WebClient.DownloadString("http://www.msxfaq.net")

Sie können aber auch in der Shell einfach einen Proxy hinterlegen. Das kann erforderlich sein, wenn Sie in einer PowerShell-Shell weitere Commandlets nutzen.

[system.net.webrequest]::defaultwebproxy = new-object system.net.webproxy('http://proxyserver:port') 
[system.net.webrequest]::defaultwebproxy.credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials 
[system.net.webrequest]::defaultwebproxy.BypassProxyOnLocal = $true

Alternativ geht es auch in einer Admin CMD-Shell zum setzen des Systemproxy anhand der Browser/WinHTTP-Einstellungen

netsh winhttp import proxy source=ie

Achtung: Der Proxy muss die Anfragen anonym oder mittels integrierter Anmeldung (NTLM/Kerberos) erlauben. Die PowerShell startet keinen "Anmeldedialog", wenn der Proxy mit einem "407 Proxy Authentication Required" antwortet.

Damit lassen sich dann auch Proxy-Server nutzen.

Bsp: GET und POST

Das HTTP-Protokoll kennt verschiedene Methoden des Zugriffs. Die beiden wichtigsten sind GET und POST. Die meisten Anfragen an Webseiten sind einfache "GET-Befehle, bei denen der Pfad und ggfls. Parameter mit angegeben werden. Dieses Verfahren ist sehr einfach zu implementieren, aber eignet sich natürlich nicht für das Übertragen größerer Datenmengen vom Client zum Server. In den Anfangszeiten des Internet konnten im Browser z.B. Formulare ausgefüllt werden, die dann per POST hochgeladen werden. Mit system.net.webclient ist dann die Methode uploadData z.B. nutzbar

Der zweite Parameter kann auf "POST" gesetzt, werden.

Net.Webclient mit UserAgent und Serverantwort

Bei der Entwicklung zu End2End-HTTP habe ich versucht sehr oft und schnell immer wieder die gleiche HTTP_URL zu laden und beim Test gegen https://outlook.office365.com/favicon.ico ist mir ein unterschiedliches Verhalten des Servers je nach Client aufgefallen. per Fiddler habe ich die HTTPS-Anfragen analysiert:

$url = "https://outlook.office365.com/favicon.ico
$webclient = New-Object Net.WebClient 
$webclient.Downloadstring($url)
Invoke-WebRequest $url

Der Mitschnitt ist aber interessant und überraschend.

  • Beide Methoden unterstützen Proxy
    Auch ohne explizite Konfiguration nutzt die PowerShell den "System Proxy"
  • Beide Methoden verstehen einen 302 redirect
    Die abgefragte URL wird vom Server auf /owa/favicon.ico umgeleitet. Beide Aufrufen folgen alleine der Umleitung und fragen eigenständig in einem zweiten Request die neue URL ab.. Mein Skript sieht den 302 gar nicht
  • Connection Pooling
    Obwohl es unterschiedliche Commandlets sind, wird nur einmal ein SSL-Handshake aufgebaut. Das könnte nun aber auch in der Nutzung eines Proxy und Fiddler begründet sein.

Aber obwohl sich bis dahin beide Abrufe gleich verhalten, bekommt nur der "Invoke-Webrequest" auch das Bild. Der einfache "net.webclient" wird vom Office 365 Service nicht bedient. Also habe ich mir den Request genauer angeschaut

Der einzig sichtbare Unterscheid ist hier der User-Agent. Sollte Office 36 einen Abruf ohne diesen Header ablehnen?. Das kann ich ja schnell ändern

$url = "https://outlook.office365.com/favicon.ico
$webclient = New-Object Net.WebClient;
$webclient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT; Windows NT 10.0; de-DE) "); 
$webclient.Downloadstring($url);

Und schon gelingt auch der Abruf. Achten Sie also darauf, dass Sie beim Einsatz von Net.WebClient auch einen UserAgent setzen, da Webserver sich dann unterschiedlich verhalten.

Weitere Links