Cookies in C#

Hallöchen.
Nach etwas längerer Pause melde ich mich mal wieder aus dem „Ruhestand“:
Wie versprochen, gibt es nun eine grundlegende Einführung in das Benutzen
von Cookies.
Wozu Cookies? Abgesehen davon, dass sie ziemlich lecker sind (haha),
bilden Cookies die Möglichkeit, eine Session im Webbrowser zu speichern.
Im Klartext: Anhand eine Session kann ich mich z.B. gegenüber einem Webserver
als eindeutiger Benutzer identifizieren. Ohne bei JEDEM Aufruf, immer wieder
meine Logindaten zu übertragen.
Eine Loginfunktion auf einer Webseite funktioniert etwa so:

Irgend ein Besucher ruft die Seite auf.
Der Besucher authentifiziert sich mit Login-daten, in der Regel Benutzername + Passwort
Ist diese Kombination richtig, gibt der Server sein OK, dass er den Benutzer eindeutig identifiziert hat
-> Der Server legt eine eindeutig identifizierbare Session für den Benutzer an, und sendet sie ihm
Mit dieser Session kann sich der Benutzer, solange sie der Server anerkennt,
immer wieder – auch ohne Benutzername+Passwort – eindeutig identifizieren (einloggen)

Dem Sicherheitsversierten Besucher wird sicher schon etwas aufgefallen sein:
Diese Sessions sind eigentlich ein Sicherheitsrisiko, nicht?
Jaein, besonders gefährlich ist, dass die Session „geklaut“ werden kann, wenn sie
unverschlüsselt übermittelt wird. Die Session ist eine ID, ein (mehr oder wenig) langer
String. Kennt jemand diesen String, kann er sich damit beim Server Authentifizieren.
Das ganze wird Session-Hijacking genannt.
Allerdings sind die Sessions nur temporär gültig, durch einen Logout des Benutzers werden
sie ungültig, und die zufällig generierten Strings sind relativ schwer zu erraten.
Solange der Server dicht hält (die Sessions geheim hält) und die Verbindung verschlüsselt
ist (HTTPS verbreitet sich immer weiter), sollte alles OK sein.

Zurück zum eigentlichen Thema: Wir wissen also, alles was wir brauchen um einen
Webservice zu nutzen, der ein Benutzerkonto verlangt, ist ein Cookie.
Die Einsatzbereiche sind vielfältig, von Automation (Bots) bis hin zum eigenen
Client für bestimmte Webanwendungen. Ich nehme das Beispiel: Forenbrowser

In Foren braucht man einen Account um Beiträge zu verfassen, manchmal auch um überhaupt
Beiträge ansehen zu dürfen, also das perfekte Einsatzgebiet für uns. Wir bauen einen
Client, der uns in MyBB-Foren einloggen kann, mit dem wir Beiträge verfassen können, etc.
(Nicht wirklich, wer Lust hat, kann gerne das Beispielprogramm übernehmen und weiterführen.
Dazu muss man die HTML-Seiten richtig parsen und ausgeben lassen – wir begnügen uns mit
dem Einloggen und dem herunterladen einer Seite, die nur für Angemeldete Nutzer sichtbar ist.)

Aus der obrigen erklärung, wie Sessions funktionieren, geht schon einigermaßen hervor,
wie wir unsere Kekse kriegen:
Wir senden unsere Logindaten an den Server, dieser antwortet mit der Session / dem Cookie.
Danach müssen wir nur den Cookie bei folgenden Seitenaufrufen mitsenden, damit wir
„eingeloggt“ bleiben. Um das genau zu betrachten, zeichnen wir die HTTP-Header auf, die
gesendet und empfangen werden. Ich benutze das Firefox-Addon Live HTTP Headers.

http://your.domain.de/forum/member.php
POST /forum/member.php HTTP/1.1
Host: your.domain.de
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:11.0) Gecko/20100101 Firefox/11.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://your.domain.de/forum/member.php?action=login
Content-Type: application/x-www-form-urlencoded
Content-Length: 80
username=NoMad&password=123456&remember=yes&submit=Anmelden&action=do_login&url=

Hier sehen wir sämtliche Daten, die für das Login notwendig sind. Dabei ist eigentlich
alles statisch, außer Nutzername, Passwort und Content-Length. Unser Programm muss nur
diese 3 Werte dynamisch in einen HTTP request einfügen, und den Antwortheader empfangen:

HTTP/1.1 200 OK
Date: Wed, 04 Apr 2012 22:13:26 GMT
Server: Apache
Set-Cookie: mybb[lastvisit]=1333361623; expires=Thu, 04-Apr-2013 22:13:26 GMT; path=/forum/; domain=.your.domain.de
Set-Cookie: mybb[lastactive]=1333577606; expires=Thu, 04-Apr-2013 22:13:26 GMT; path=/forum/; domain=.your.domain.de
Set-Cookie: loginattempts=1; expires=Thu, 04-Apr-2013 22:13:26 GMT; path=/forum/; domain=.your.domain.de
Set-Cookie: mybbuser=2_UawaXGNUZrkEPhewt7F5rekWaw54vKPbwIC6ds5pMMJIvbysoW; expires=Thu, 04-Apr-2013 22:13:26 GMT; path=/forum/; domain=.your.domain.de; HttpOnly
Set-Cookie: sid=60ea84a3ed5ab5796f9c1c5a772a7512; path=/forum/; domain=.your.domain.de; HttpOnly
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 1050
Connection: close
Content-Type: text/html; charset=UTF-8

Hier ist unser Cookie gespeichert, und darin die Session-ID (sid) die wir brauchen.
Achtung, hier kommt der Code:

public CookieCollection Authenticate()
{
 //This is the login-data
 string Content = "username="+USER+"&password="+PASS+"&remember=yes&action=do_login";
 HttpWebResponse response = null;
 try
 {
  //Form a request
  HttpWebRequest request = HttpWebRequest.Create(ADDR + "/member.php?action=login") as HttpWebRequest;
  request.CookieContainer = new CookieContainer();
  request.Method = "POST";
  request.ContentLength = ASCIIEncoding.ASCII.GetByteCount(Content);
  request.ContentType = "application/x-www-form-urlencoded";
  //Send the login data
  request.GetRequestStream().Write(ASCIIEncoding.ASCII.GetBytes(Content), 0, ASCIIEncoding.ASCII.GetByteCount(Content));
  //Get a response
  response = request.GetResponse() as HttpWebResponse;
 }
 catch(Exception ex)
 {
  return null;
 }
return CookieJar = response.Cookies;
}

Glücklicherweise sind HTTP-Anfragen in C# ziemlich einfach. Man formt erst die Anfrage,
sendet gegebenenfalls seine Daten in einem Stream, und holt sich die Antwort ab.
Was man wissen sollte: Damit wir am Ende nicht die Header durchparsen und manuell
Kekse backen, pardon, Cookies erstellen müssen, muss dem Request vor dem Senden
ein CookieContainer hinzugefügt werden.
Auf die ganzen Header welchen User-Agent wir verwenden, welches Encoding wir erwarten etc.
verzichten wir, da uns nur die Cookies interessieren, und die sind im Header. Wer das
Programm also noch weiter ausbauen will, der sollte noch fehlende Header ergänzen,
den Antwortstream auslesen und, falls dieser gzip/deflate komprimiert ist,
dekomprimieren. Anschließend kann dann ausgewertet werden.

Wir fügen aber nur noch die errechnete Länge der Daten die wir senden hinzu, schreiben
diese Daten in den requeststream und warten auf eine Antwort.
Haben wir die Antwort erhalten, wird der vorher hinzugefügte CookieContainer automatisch
mit den erhaltenen Cookies gefüllt, und wir erhalten eine CookieCollection in der Antwort.
Hier wird es etwas kompliziert, da wir es einmal mit CookieContainern und
andererseits mit CookieCollections zu tun haben. Zur besseren Verständnis:

CookieContainer können/sollen/dürfen die Cookies von mehreren Requests beinhalten.
Also Cookies von .tacticalcode.blogspot.com, .google.de usw.
CookieCollections sind für einzelne Requests zuständig, dort sind nur die Cookies drin,
die für die aktuelle Konversation relevant sind, also nur Cookies einer Domain.
Und hier stoßen wir auf ein großes Problem: Ein Request MUSS einen CookieContainer haben,
damit die Cookies darin mitgesandt werden. Der CookieContainer speichert wie bekannt Cookies
vieler Domains, also muss eine Domain angegeben werden, für die die Cookies mitgesandt werden sollen.
.NET < 4.0 hat einen Bug: Domains, die mit einem Punkt beginnen (bedeutet: alle Subdomains eingeschlossen), werden nicht RFC-Standardgemäß richtig ausgewertet, .NET < 4.0 schickt diese Cookies nur mit, wenn die Domain genau übereinstimmt. Was natürlich nicht der Fall ist, Domains dürfen nicht mit einem Punkt beginnen. Also können die .NET Versionen vor 4.0Beta2 nur richtig mit Cookies umgehen, die auf eine ganz bestimmte Domain festgelegt sind, und nicht mit der Punkt-Wildcard auch für alle Subdomains gelten. Daher: Unbedingt .NET 4.0 als Zielframework wählen! Dann können wir folgenden Code ohne Sorgen nutzen: [sourcecode language="csharp"]public void DownloadSite(string url, CookieCollection CC) { try { //Request HttpWebRequest request = HttpWebRequest.Create(new Uri(url)) as HttpWebRequest; request.CookieContainer = new CookieContainer(); request.Method = "GET"; //Add cookies if we have some if (CC != null && CC.Count > 0)
request.CookieContainer.Add(CC);

//Get the server’s response
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
//Responsestream and stream for a local file
Stream responsestr = response.GetResponseStream();
if (File.Exists(„data.html“))
File.Delete(„data.html“);
FileStream file = new FileStream(„data.html“, FileMode.CreateNew, FileAccess.Write);
byte[] buffer = new byte[1024];
int read = 0;
int total = 0;

//Read the responsestream, write that into the local-file stream
while ((read = responsestr.Read(buffer, 0, buffer.Length)) > 0)
{
file.Write(buffer, 0, read);
total += read;
}

//Close both streams
responsestr.Close();
file.Close();
}
catch (Exception ex)
{
Environment.Exit(1);
}
}[/sourcecode]

Die Methode nutzt die angegebenen Cookies um sich zu authentifizieren, dadurch erhalten wir
z.B. Zugang zu Bereichen, die für Gäste gesperrt sind.
Et voilá, wir wissen nun, wie man Cookies in C# verwendet. Soweit ist alles intuitiv, nur
folgendes muss man sich merken:

  • CookieContainer muss zum Request hinzugefügt werden, damit Cookies empfangen werden
  • .NET <4.0 kann mit Domains die mit einem "." beginnen nicht richtig umgehen

Hier noch ein bisschen Lesematerial:
Shai Raiten – Cookie Collection and CookieContainer
CookieContainer domain handling issue

Die Codes weichen dieses mal etwas vom original ab, den Debug-Kram habe ich ausgelassen.
Den vollen Sourcecode gibt es wie immer zum DOWNLOAD bei mediafire.
PS: Wenn die MPAA wirklich mediafire abschießen will, gibts Krieg!
Ich nutze Mediafire sehr gern als Hoster für meine Projekte, besonders da man keine nervigen
Wartezeiten hat, um 60kb herunterzuladen. Liebe MPAA, ich habe auch geistiges Eigentum,
ich bin auch ein Urheber, und wenn euch die Urheber so am Herzen liegen, solltet ihr nicht
deren Vermarktungsplatformen kaputt machen! Nicht jeder ist so großnäsig und will nur GeldGeldGeld.

MfG
NoMad

Dieser Eintrag wurde veröffentlicht in C#
Bookmarken: Permanent-Link Schreibe einen Kommentar oder hinterlasse einen Trackback: Trackback-URL.
Achtung: Wordpress interpretiert bestimmte Zeichenfolgen als Markup und verändert diese. Nutzt für Programmcode lieber Gist oder PasteBin-Services und verlinkt die Code-Schnipsel.

Post a Comment

Sie müssen angemeldet sein, um kommentieren zu können.