Für (z.B.) den Anwendungsfall, dass man automatisiert Inhalte einer Webseite herunterladen möchte, sich vorher aber anmelden muss (per HTTP-Post). Oder einfach so zum screen scraping.
Wurde zwar schon vielfach nachgefragt und gelöst, aber hier nun eine schöne API dafür Kümmert sich auch um Anmeldecookies und kann invalide SSL-Zertifikate ignorieren.
Beispiel:
ExtendedWebClient extendedWebClient = new ExtendedWebClient();
// Anmelden per POST; anonymer Typ als Parameterobjekt
extendedWebClient.Post("http://www.example.com/login.php",new
{
username = "carnivore",
password = "secret_password"
});
// Parametrisierte GET-Anfrage ausführen; Dictionary für Parameter
string content = extendedWebClient.Get("http://www.example.com/search.php",new Dictionary<string,string>
{
{ "searchstring", "http post application/x-www-form-urlencoded" },
{ "maxresults" , 25 }
});
spezielle Features:
- einfaches POST und GET
- Nachbearbeiten der WebRequests möglich (z.B. User-Agent setzen)
- Parameter einfach als Objekte möglich
- Cookies für aufeinanderfolgende Requests berücksichtigen
- auch vom Betriebssystem als unsicher betrachtete (SSL-)Zertifikate erlauben (ExtendedWebClient.IgnoreInvalidCertificates = true)
- ggf. Cookies für gesamte Domain gültig machen (ForceApplyCookiesToDirectories)
- MultiPart-Post mit Dateien als Inhalt (FormFile als Parametertyp verwenden)
- tatsächlichen Dateinamen für Dateidownloads ermitteln (GetFilenameFromWebServer)
Code:
Hinweis: Assembly "System.Web" muss referenziert sein (Projekt -> Verweis hinzufügen).
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Text;
using System.Web;
/// <summary>
/// Erweitert den System.Net.WebClient um Funktionen zum expliziten Ausführen von HTTP-GET- und HTTP-POST-Anfragen, bei denen Cookies für eine Session bestehen bleiben.
/// </summary>
//[System.Diagnostics.DebuggerStepThrough]
public class ExtendedWebClient : WebClient
{
private readonly CookieContainer cookieContainer = new CookieContainer(); // um bei Login-Szenarien auch eingeloggt zu bleiben
/// <summary>
/// Tritt ein, wenn der HttpWebRequest für eine Anfrage erstellt wurde.
/// </summary>
public event Action<HttpWebRequest> HttpWebRequestCreated;
/// <summary>
/// Ruft einen Wert ab, der bestimmt, ob die Gültigkeit von Cookies immer auf Verzeichnisebene gesetzt wird, oder legt diesen fest.
/// </summary>
public bool ForceApplyCookiesToDirectories { get; set; }
/// <summary>
/// Ruft einen Wert ab, der bestimmt, ob auch vom Betriebssystem als unsicher betrachtete (SSL-)Zertifikate erlaubt werden, oder legt diesen fest,
/// </summary>
public static bool IgnoreInvalidCertificates
{
get => (ServicePointManager.ServerCertificateValidationCallback==ExtendedWebClient.ignoreInvalidCertificateValidationCallback);
set => ServicePointManager.ServerCertificateValidationCallback = (value) ? ExtendedWebClient.ignoreInvalidCertificateValidationCallback : null;
}
private static readonly RemoteCertificateValidationCallback ignoreInvalidCertificateValidationCallback = delegate { return true; }; // akzeptiert alle Zertifikate
/// <summary>
/// Erstellt eine neue Instanz der ExtendedWebClient-Klasse
/// </summary>
public ExtendedWebClient()
=> this.Encoding = Encoding.UTF8;
/// <summary>
/// Gibt ein WebRequest-Objekt für die angegebene Ressource zurück.
/// </summary>
/// <param name="address">Ein URI, der die anzufordernde Ressource identifiziert.</param>
/// <returns>Ein neues WebRequest-Objekt für die angegebene Ressource.</returns>
protected override WebRequest GetWebRequest(Uri address)
{
WebRequest webRequest = base.GetWebRequest(address);
// CookieConainer immer mit einhängen
if (webRequest is HttpWebRequest httpWebRequest)
lock (this)
{
httpWebRequest.CookieContainer = this.cookieContainer;
this.HttpWebRequestCreated?.Invoke(httpWebRequest);
}
return webRequest;
}
/// <summary>
/// Gibt die WebResponse für die angegebene WebRequest zurück.
/// </summary>
/// <param name="webRequest">Eine WebRequest, mit der die Antwort abgerufen wird. </param>
/// <returns>Eine WebResponse mit der Antwort auf die angegebene WebRequest. </returns>
protected override WebResponse GetWebResponse(WebRequest webRequest)
{
WebResponse webResponse = base.GetWebResponse(webRequest);
if (webResponse is HttpWebResponse httpWebResponse)
lock (this)
{
Debug.WriteLine(httpWebResponse.StatusCode+"\t"+webRequest.RequestUri);
// Bug umgehen, bei dem der CookieContainer keine Cookies speichert, die eine Beschränkung auf einen Host mit voranstehendem "." hat
foreach (Cookie cookie in httpWebResponse.Cookies)
this.cookieContainer.Add(cookie);
// anderen Bug umgehen, bei dem nicht alle nötigen Cookies übertragen werden
if (this.ForceApplyCookiesToDirectories)
foreach (Cookie cookie in this.cookieContainer.GetCookies(webRequest.RequestUri))
{
if (cookie.Path.Length>1 && cookie.Path.Contains("/") && !cookie.Path.EndsWith("/"))
cookie.Path = cookie.Path.Remove(cookie.Path.LastIndexOf('/') + 1);
this.cookieContainer.Add(cookie);
}
}
return webResponse;
}
/// <summary>
/// Liest den Inhalt einer WebResponse als Zeichenkettenrepräsentation aus.
/// </summary>
/// <param name="webResponse">Eine WebResponse.</param>
/// <returns>Den Inhalt einer WebResponse als Zeichenkettenrepräsentation.</returns>
public string ReadWebResponse(WebResponse webResponse)
{
using (StreamReader streamReader = new StreamReader(webResponse.GetResponseStream(),this.Encoding))
return streamReader.ReadToEnd();
}
/// <summary>
/// Gibt das Ergebnis einer HTTP-POST-Anfrage an die angegebene Ressource zurück.
/// </summary>
/// <param name="url">Ein URI, der die anzufordernde Ressource identifiziert.</param>
/// <param name="parameters">Ein Objekt, dessen Properties als POST-Parameter benutzt werden.</param>
/// <returns>Das Ergebnis einer HTTP-POST-Anfrage an die angegebene Ressource (als Zeichenkettenrepräsentation).</returns>
public string Post(string url,object parameters)
=> this.Post(url,ExtendedWebClient.ObjectToDictionary(parameters));
/// <summary>
/// Gibt das Ergebnis einer HTTP-POST-Anfrage an die angegebene Ressource zurück.
/// </summary>
/// <param name="url">Ein URI, der die anzufordernde Ressource identifiziert.</param>
/// <param name="parameters">Eine Auflistung der POST-Parameter.</param>
/// <returns>Das Ergebnis einer HTTP-POST-Anfrage an die angegebene Ressource (als Zeichenkettenrepräsentation).</returns>
public string Post(string url,IDictionary<string,object> parameters)
{
using (WebResponse response = this.GetWebResponse(this.Post(new Uri(url),parameters)))
return this.ReadWebResponse(response);
}
/// <summary>
/// Gibt das Ergebnis einer HTTP-POST-Anfrage an die angegebene Ressource zurück.
/// </summary>
/// <param name="uri">Ein URI, der die anzufordernde Ressource identifiziert.</param>
/// <param name="parameters">Eine Auflistung der POST-Parameter.</param>
/// <returns>Das Ergebnis einer HTTP-POST-Anfrage an die angegebene Ressource (als Zeichenkettenrepräsentation).</returns>
public WebRequest Post(Uri uri,IDictionary<string,object> parameters)
{
WebRequest webRequest = this.GetWebRequest(uri);
webRequest.Method = "POST";
// "einfaches" POST
if (!parameters.Values.Any(p => p is FormFile))
{
string paramsString = ExtendedWebClient.CreateParamString(parameters);
byte[] content = this.Encoding.GetBytes(paramsString);
webRequest.ContentType = "application/x-www-form-urlencoded";
webRequest.ContentLength = content.Length;
using (Stream requestStream = webRequest.GetRequestStream())
requestStream.Write(content,0,content.Length);
return webRequest;
}
// Multipart-POST
else
{
string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
byte[] boundaryBytes = this.Encoding.GetBytes("\r\n--" + boundary + "\r\n");
webRequest.ContentType = "multipart/form-data; boundary=" + boundary;
using (Stream requestStream = webRequest.GetRequestStream())
{
foreach (KeyValuePair<string,object> parameter in parameters)
{
requestStream.Write(boundaryBytes,0,boundaryBytes.Length);
if (parameter.Value is FormFile formFile)
{
string header = "Content-Disposition: form-data; name=\"" + parameter.Key + "\"; filename=\"" + formFile.Name + "\"\r\nContent-Type: " + formFile.ContentType + "\r\n\r\n";
byte[] bytes = this.Encoding.GetBytes(header);
requestStream.Write(bytes,0,bytes.Length);
int bytesRead;
if (formFile.Stream==null)
{
bytes = File.ReadAllBytes(formFile.FilePath);
requestStream.Write(bytes,0,bytes.Length);
}
else
{
byte[] buffer = new byte[32768];
while ((bytesRead = formFile.Stream.Read(buffer,0,buffer.Length)) != 0)
requestStream.Write(buffer,0,bytesRead);
}
}
else
{
string data = "Content-Disposition: form-data; name=\"" + parameter.Key + "\"\r\n\r\n" + parameter.Value;
byte[] bytes = this.Encoding.GetBytes(data);
requestStream.Write(bytes,0,bytes.Length);
}
}
byte[] trailer = this.Encoding.GetBytes("\r\n--" + boundary + "--\r\n");
requestStream.Write(trailer,0,trailer.Length);
requestStream.Close();
}
return webRequest;
}
}
/// <summary>
/// Gibt das Ergebnis einer HTTP-GET-Anfrage an die angegebene Ressource zurück.
/// </summary>
/// <param name="url">Ein URI, der die anzufordernde Ressource identifiziert.</param>
/// <param name="parameters">Ein Objekt, dessen Properties als GET-Parameter benutzt werden.</param>
/// <returns>Das Ergebnis einer HTTP-GET-Anfrage an die angegebene Ressource (als Zeichenkettenrepräsentation).</returns>
public string Get(string url,object parameters = null)
=> this.Get(url,ExtendedWebClient.ObjectToDictionary(parameters));
/// <summary>
/// Gibt das Ergebnis einer HTTP-GET-Anfrage an die angegebene Ressource zurück.
/// </summary>
/// <param name="url">Ein URI, der die anzufordernde Ressource identifiziert.</param>
/// <param name="parameters">Eine Auflistung der GET-Parameter.</param>
/// <returns>Das Ergebnis einer HTTP-GET-Anfrage an die angegebene Ressource (als Zeichenkettenrepräsentation).</returns>
public string Get(string url,IDictionary<string,object> parameters)
{
using (WebResponse response = this.GetWebResponse(this.Get(new Uri(url),parameters)))
return this.ReadWebResponse(response);
}
/// <summary>
/// Gibt ein WebRequest-Objekt für eine HTTP-GET-Anfrage an die angegebene Ressource zurück.
/// </summary>
/// <param name="uri">Ein URI, der die anzufordernde Ressource identifiziert.</param>
/// <param name="parameters">Eine Zeichenkettenrepräsentation aller Parameter-Wertepaare.</param>
/// <returns>Ein neues WebRequest-Objekt für die angegebene Ressource.</returns>
private WebRequest Get(Uri uri,IDictionary<string,object> parameters)
{
if (parameters.Any())
uri = new Uri(uri.OriginalString + "?" + ExtendedWebClient.CreateParamString(parameters));
WebRequest webRequest = this.GetWebRequest(uri);
webRequest.Method = "GET";
return webRequest;
}
/// <summary>
/// Ruft den konkreten Namen einer Datei ab
/// </summary>
/// <param name="url">die URL, unter der die Datei verfügbar ist</param>
public string GetFilenameFromWebServer(string url)
{
WebRequest webRequest = this.GetWebRequest(new Uri(url));
webRequest.Method = "HEAD";
using (WebResponse webResponse = webRequest.GetResponse())
if (!String.IsNullOrEmpty(webResponse.Headers["Content-Disposition"]))
return webResponse.Headers["Content-Disposition"].Substring(webResponse.Headers["Content-Disposition"].IndexOf("filename=") + 9).Replace("\"","");
return null;
}
/// <summary>
/// Erstellt eine Auflistung, die alle öffentlichen Eigenschaften eines Objekts als Schlüssel/Wertpaare enthält
/// </summary>
/// <param name="value">Ein Objekt, für das die Auflistung erstellt werden soll.</param>
/// <returns>Eine Auflistung, die alle öffentlichen Eigenschaften eines Objekts als Schlüssel/Wertpaare enthält.</returns>
private static Dictionary<string,object> ObjectToDictionary(object value)
{
Dictionary<string,object> result = new Dictionary<string,object>();
if (value!=null)
foreach (PropertyInfo propertyInfo in value.GetType().GetProperties())
if (propertyInfo.CanRead && propertyInfo.GetIndexParameters().Length==0)
result.Add(propertyInfo.Name,propertyInfo.GetValue(value,null));
return result;
}
/// <summary>
/// Erstellt die Zeichenkettenrepräsentation für Parameter-Wertepaare.
/// </summary>
/// <param name="parameters">Eine Auflistung der Parameter-Wertepaare.</param>
/// <returns>Die Zeichenkettenrepräsentation für Parameter-Wertepaare.</returns>
private static string CreateParamString(IDictionary<string,object> parameters)
=> String.Join("&",parameters.Select(kvp => HttpUtility.UrlEncode(kvp.Key) + "=" + HttpUtility.UrlEncode(kvp.Value+String.Empty)));
/// <summary>
/// Stellt Informationen über eine File-Objekt, das als Multipart-Post übertragen wird, bereit
/// </summary>
public class FormFile
{
/// <summary> Ruft den Namen der Datei ab oder legt diesen fest </summary>
public string Name { get; set; }
/// <summary> Ruft den Content-Type der Datei ab oder legt diesen fest </summary>
public string ContentType { get; set; }
/// <summary> Ruft den Pfad der Datei ab oder legt diesen fest </summary>
public string FilePath { get; set; }
/// <summary> Ruft den Stream der Datei ab oder legt diesen fest </summary>
public Stream Stream { get; set; }
}
}
Schlagwörter: GET, POST, WebRequest, HttpWebRequest, Login, Cookies, Scraping