Einfachster Proxyserver in C#

yurij

Lt. Commander
Registriert
Jan. 2008
Beiträge
1.055
Brauchte für ein Browsergame hack einen Proxyserver, irgendwie nichts passendes im Netz gefunden. Die funktionieren entweder nicht oder sind zu komplex. Alle haben gemeinsam, dass sie versuchen den Datenstream zu entschlüsseln und zu verarbeiten.

Bin also mit eigener socket-basierten Lösung gekommen:
Das ist wahrscheinlich der einfachste HTTP Proxy Server der Welt, funktioniert aber dennoch mit allen HTTP Streams. Genau genommen versuche ich den Stream nicht zu entschlüsseln, sondern leite es augenblicklich weiter. Der Proxy funktioniert wie der primitivste Stream-Switch.

Es werden Sockets verwendet - aus sicht der HTTP Anwendung die einfachste mögliche Schicht. Proxy-Switch verwendet Multithreading und Socket-Wiederverwendung.
Programmiert als Windows-Forms Anwendung mit Visual Studio 2010 C#

Im Anhand nur der Proxy-Grundgerüst.
Als nächstes will ich bestimmte GET requests im Proxy/Switch abfangen und statt den angeforderten Flash-Dateien, eigene modifizierte zustellen (Spoofing).
Zudem ist das Browsergame viel zu langsam, wahrscheinlich würde ich HTML BODY Quality="High" in Quality="Low" umschreiben. All das funktionierte schon, habe zwei andere Proxy-Server Varianten programmiert, diese aber wegen diversen Problemen verworfen. Dieser hier ist so einfach und zuverlässig, dass ich mich entschloss es zu veröffentlichen. Vielleicht wird's jemand brauchen.

http://home.arcor.de/yurij/exProxy.zip

das ist die Hauptprozedur des Switches:
Code:
        public void SocketProcessor(object SocketObject)
        {
            ThreadDelta(1);

            // ex("Socket communication processing ...");
            Socket ClientSocket = (Socket)SocketObject;
            String ServerHost = null;
            Socket ServerSocket = new Socket(ClientSocket.AddressFamily, ClientSocket.SocketType, ClientSocket.ProtocolType);

            byte[] SendBuffer = new byte[8 * 1024];
            int SendSize = 0;

            byte[] ReceiveBuffer = new byte[8 * 1024];
            int ReceiveSize = 0;

            // CROSS-SOCKET TRAFFIC EXCHANGE
            try
            {

                while (true) {

                    if (!ClientSocket.Connected) break;

                    // PROCESS CLIENT REQUEST STREAM
                    if (ClientSocket.Available > 0)
                    {
                        SendSize = ClientSocket.Receive(SendBuffer);

                        // TRY TO GET HOST
                        String Method = Encoding.ASCII.GetString(SendBuffer, 0, 3);
                        if (Method == "GET") {
                            String RequestString = Encoding.ASCII.GetString(SendBuffer);
                            int P1 = RequestString.IndexOf(' ') + 1;
                            int P2 = RequestString.IndexOf(' ', P1);
                            String URL = RequestString.Substring(P1, P2 - P1);
                            Uri URI = new System.Uri(URL);
                            String NewHost = URI.Host;
                            ex("Request: " + URL);

                            // (RE)CONNECT TO SERVER
                            if (NewHost != ServerHost)
                            {
                                if (ServerSocket.Connected) ServerSocket.Close();
                                ServerSocket = new Socket(ClientSocket.AddressFamily, ClientSocket.SocketType, ClientSocket.ProtocolType);
                                ServerHost = NewHost;
                                ServerSocket.Connect(ServerHost, 80);
                                ex("Connected to " + ServerHost);
                            }
                        }

                        if (ServerSocket.Connected) {
                            if (SendSize > 0)
                                ServerSocket.Send(SendBuffer, SendSize, SocketFlags.None);
                        }
                    }
                    
                    // REDIRECT REPLY FROM SERVER TO HOST
                    if (ServerSocket.Connected)
                    {
                        if (ServerSocket.Available > 0)
                        {
                            ReceiveSize = ServerSocket.Receive(ReceiveBuffer);
                            if (ReceiveSize > 0)
                                ClientSocket.Send(ReceiveBuffer, ReceiveSize, SocketFlags.None);
                        }
                    }
                    else break;

                    if (ClientSocket.Available == 0 && ServerSocket.Available == 0)
                        Thread.Sleep(100);
                }
            }
            catch (Exception e)
            {
                ex("EXCEPTION: " + e.Message);
            }

            if (ServerSocket.Connected) ServerSocket.Close();
            if (ClientSocket.Connected) ClientSocket.Close();

            ThreadDelta(-1);
        }
 
Du wirst hier keine Antworten bekommen da das illegal ist

PC FREAKY
 
@PC FREAKY
Er will keine Antworten, sondern hat selbst eine Lösung für Suchende geschrieben.
 
wo steht dass es illegal ist? bitte § oder link.
bitte nicht mischen das was illegal ist mit dem was dir persönlich nicht gefällt.

und selbst wenn, habe ich hier kein cheat gepostet, sondern einen generischen proxy-server.
Kannst es nutzen für was auch immer du willst: caching, logging, werbe-blocking.
es ist absolut transparent.

Edit: es werden GET und POST anfragen vollständig unterstützt ;)
CONNECT (stichwort SSL) wird nicht unterstützt.

mein browser (firefox) scheint die sockets nicht schließen zu wollen, selbst wenn ich firefox beende, bleiben die socket verbindungen irgendwie offen.
werde die sockets wohl softwaregesteuert durch den proxy nach einem timeout inaktivität schließen müssen.

ausserdem habe ich die korrekte schließung der threads beim beenden des programms noch nicht implementiert.

es ist eben eine kleine baustelle.

aber wenn's fertig ist poste ich vielleicht noch mal ein update.
 
Zuletzt bearbeitet:
Wieso dieser ganze Aufwand? Nimm doch einfach Opera und binde eine User-JS für die betreffende Seite ein. Dann kannst du direkt innerhalb des Browsers manipulieren. Ist deutlich einfacher!
 
nein

mit javascript kannst du kein FLASH scripten, nur an den vorgesehenen schnittstellen.
das flash hauptfile lädt weitere flash files, diese wiederum andere usw.

proxy is der einfachste weg, statt diesen files eigene zu laden.
man könnte da noch ein browser plugin programmieren, hab mich aber für proxy entschieden, da es für alle browser transparent funktioniert.
 
Zuletzt bearbeitet:
Ich glaube es geht ihm um einen Proxy bei der Arbeit :D

Wenn es diese Sache ist, dann lass lieber Finger davon, wenn du dein Job behalten willst! ;)
 
Wußte gar nicht, dass das zur Verfügung stellen des Quellcodes eines einfachen Proxy Server/Services a) illegal ist und b) den Job des Urhebers riskieren könnte

zu a) Einen Proxy zu programmieren wird wohl kaum illegal sein, desweiteren Danke Yurij das du den Code allen Interessierten zur Verfügung stellst. Hoffe nur das es kein Code aus einem Projekt/Produkt einer (deiner?) Firma ist, da dann vermutlich doch so einige Rechte verletzt wurden.

zu b) Wenn der Code von Yurij in der Freizeit geschrieben wurde, warum sollte er dann seinen Job riskieren?

Und im übrigen, so wie ich die Anfangsnachricht verstanden habe, will Yurij nur seine Arbeit mit uns teilen, falls jemand das mal selber benötigen sollte und nicht allzu lange googeln will. Also wo ist da das Problem, Leute?!?

Danke Yurij und weiter so... Meine Stimme hast du!

Rossibaer
 
@Rossibaer,

Nicht das programmieren könnte seinen Job gefährden sondern das benutzen des Proxy's um auf Seiten wie Facebook während der Arbeit zuzugreifen, die sonst gesperrt wären.. Aber das kann man natürlich mit anderen proxy's (wie squid ? ) auch.. Ausserdem weiss das der Threadersteller auch, denn für doof halte ich ihn nicht.. :)

@Yurij, ich finde es auch gut dass du den Code mit uns teilst.. Das finde ich super.. Nur wieso in C# ? In Java oder C++ (allenfalls nat. auch C) hätte ich was lernen können... ;)
 
@Rosibär
Ja ich meinte das benutzen des Programms bei der Arbeit könnte sein Job kosten. Ich denke, dass ein strenger Arbeitgeber wohl alle PC regelmäßig auf Proxi-Software untersuchen lässt, deswegen kann man ja auf eine fertige Software von vorne herein verzichten.

Bin eigentlich gar nicht gegen den Code. Muss man nur aufpassen wie man es benutzt :D
 
Eine Software kann erstmal keinem Verwendungszweck zugeordnet werden, genausowenig wie ein Messer einem Verwendungszweck zugeordnet werden kann. Beim Beispiel des Messer will ich sagen, eine 20 cm lange geriffelte feststehende Klinge könnte für das Schneiden von Brot verwendet werden. Genausogut könnte es aber auch zum Zerstückeln einer Leiche verwendet werden. Diese Aussagen sind aber anhand der Betrachtung der Klinge nicht möglich. Sicher drängt sich einem schnell eine Vermutung auf, wofür sie gebraucht werden kann. Jedoch ist es falsch zu sagen, das sie nur für diesen einen Zweck eingesetzt wird. Am Ende entscheidet der Benutzer, für was und wie er dieses Messer einsetzt.

Bei Software ist es genauso. Einerseits nennt der Programmierer einen plausiblen Einsatzzweck seiner Codes, der für meine Begriffe durchaus legal sein könnte. Was aber tatsächlich mit dieser Software gemacht wird und wofür sie gebraucht oder mißbraucht wird, dass liegt voll und ganz in der Hand des Anwenders oder 2. Entwicklers der diese Codes in seinem Programm verwendet. Also bitte versucht nicht zu vorschnell über eine Sache zu urteilen und schaut mal ob es nicht noch eine andere Sichtweise außer schwarz und weiß gibt. Die Realität hat nun mal viele Facetten und ist nicht in die Kategorien gut oder böse einteilbar. Besonders nicht wenn der eigentliche Urheber nach der Veröffentlichung seines Werkes keinerlei Kontrolle über die Einsatzszenarien der Codes hat. Es ist nun mit der Veröffentlichung Open Source ohne mit einer restriktiven Lizenz versehen. Selbst der Hinweis auf den Urheber, wenn man den geposteten Code verwendet, wird noch nicht einmal vom Urheber verlangt. Also insgesamt ist dies ein sehr großes Geschenk, denn es ist die Arbeit eines vermutlich Einzelnen, der dafür seine Freizeit, Strom aber auch sein Fachwissen, Kreativität gab um uns das Leben etwas leichter zu machen. Man hat nun hier die Basis für ein eigenes kleines oder auch großes Projekt. Wie gesagt, wofür es eingesetzt wird, liegt im Ermessen des Benutzers.

Aber ich verstehe eure Bedenken hinsichtlich des Arbeitsplatzes, gehe aber auch davon aus, dass bis auf ein paar Skriptkiddies und Tutorialkopierer eigentlich jeder Arbeitnehmer wissen müsste, was passiert, wenn er sich über die Firmenpolicies hinweg setzt und Schutzmechanismen untergräbt/überwindet um seinen eigenen privaten Interessen während(!) der Arbeitszeit nachzukommen. Jemand der dies macht, hat aus meiner Sicht auch nichts besseres verdient als die Abmahnung oder den Rauswurf durch seinen Arbeitgeber. Vielleicht lernt er dadurch etwas und ist beim nächsten Arbeitgeber auch endlich bereit für seinen Lohn die entsprechende Leistung zu liefern. So oft ich auch den Beitrag von Yurij lese, kann ich keinerlei solcher Absichten erkennen. Also lasst bitte die Kirche im Dorf und freut euch über sein Geschenk.

@Boeby: Wenn du etwas lernen möchtest, dann kannst du auch versuchen den Quellcode von Yurij in eine deiner gewünschten Sprachen (Java, C / C++) zu übersetzen. Glaub mir, der Lerneffekt ist dabei weitaus größer, fast unbezahlbar, als wenn du nur den fertigen Code in der Zielsprache vor dir liegen hast. Versuche das mal. Das trainiert ungemein! So lernst du z.B. das Auge fürs Wesentliche eines Quellcodes, das Verfahren zur Lösung des Problems, die Möglichkeit dein Wissen aus jeder Sprache zu vervollständigen, neue Sprachen in angemessener Zeit zu erlernen, die Verwandschaften von Sprachen auf unbekannte Sprachen anzuwenden, sowie die Stärken und Schwächen der einzelnen Sprachen kennen.


So, genug geschrieben. Bis zum nächsten Mal...
Rossibaer
 
Hallo wieder, erstmal danke für den Lob,
aber mit meiner Arbeit hat es nichts zu tun.

Habe mich mit dem Thema weiter beschäftigt und musste feststellen,
dass weder ich noch die "Authoren" aus dem Netz, von dennen ich versucht habe zu lernen,
selbst nicht das nötige Hintergrundwissen haben, und alle Code-Snippets großteils Müll sind.

Also besorgte ich mir gestern das nötige Hintergrundwissen aus erster Hand:
RFC Spezifikationen für HTTP/1.0 und HTTP/1.1 gelesen.

So, die Sache sieht so aus:
Es gibt das alte HTTP/1.0 vom 1996, die erste Fassung des Protokolls
ist extrem einfach. Pro HTTP Anfrage gibt es eine Verbindung, welche,
nachdem die Antwort zurückgeschickt ist, vom Server geschlossen wird.
Alles super easy, der Proxy kann anhand des Content-Length Headers oder
der geschlossenen Verbindung sofort erkennen, dass der Antwort-Stream zu Ende ist.

HTTP/1.0:
Proxy akzeptiert Socket -> Liest Sendepuffer -> Liest erste Zeile -> dekodiert sie -> erkennt Zielhost
baut Verbindung mit Zielhost auf -> leitet Anfrage weiter -> ließt Antwort-Header ->
dekodiert -> erkennt anhand von Content-Length wie lang die Antwort-Body ist
-> liest Antwort zu Ende (Content-Length kennt er ja schon) -> leitet es zum client -> schließt beide Sockets.

Wie gesagt alles sehr einfach. Es gibt aber ein riesen Problem und das ist der Hauptgrund warum man
bald HTTP/1.1 Spezifikation brauchte. Früher hatte Homepages ganz wenige Dateien zum übertragen.
Moderne Seites erfordern den Browser hunderte von Dateien zu laden. Das würde mit dem alten Protokoll
bedeuten: hunderte von Socket-Verbindungen pro Seitenaufruf! Modernen Web-Server würden mit HTTP/1.0
wegen dem Kommunikationsoverhead zusammenbrechen.

HTTP/1.1
Hier werden die Dinge viel viel komplizierter!
Wer sich mit HTTP/1.1 Proxy beschäftigen will passt jetzt bitte gut auf...
Erstens ich empfehle jedem der sich damit auseindander setzen will, nicht versuchen weiter zu Googeln,
die Informationen im Netz sind unvollständig oder irreführend und deren Authoren haben meist selbst wenig Ahnung.
Ließt unbedingt HTTP/1.1 RFC Spezifikation! Dort steht ALLES was man wissen muss,
Ich weiss, das Dokument ist umfangreich, man muss aber nicht alles lesen, für Proxy-Server sind nur
einige wenige aber wichtige Informationen relevant. Ich versuche zusammenzufassen was wichtig ist:

Das wichtigste: HTTP/1.1 schreibt PERSISTENTE VERBINDUNGEN vor.
Das heißt der Client (Browser) und der Server schließen Socketverbindungen NICHT, nachdem
die Anfrage und Antwort verarbeitet sind. Das kann man mit dem Header Connection: close erzwingen,
ist aber ineffizient und nicht das Standardverhalten von HTTP/1.1
In der regel bauen moderne Browser mehrere (bis zu 8) Socketverbindungen mit dem Proxy auf.
Die Anfragen werden auf diese Sockets dynamisch verteilt.

Erst in der RFC habe ich Information über PIPELINING gefunden.
PIPILINING zusammen mit PERSISTENTEN VERBINDUNGEN ist der Grundbaustein um HTTP/1.1 Anfragen richtig zu verarbeiten,
mann muss diese Konzepte unbedingt verstanden haben.

Pipelining mit persistenten Verbindungen bedeutet, dass der Client gleich mehrere Anfragen auf den Socket
schickt ohne auf die Antwort auf die erste Anfrage überhaupt zu warten. Wichtig zu wissen ist,
dass diese Anfragen alle zu verschiedenen Hosts führen können.
Der Client erwartet dabei, dass die Antworten (welche auch immer) genau in der REIHENFOLGE DER ANFRAGEN zurückkommen.
Dafür MUSS der Proxy sowohl Anfragen als auch Antworten GENAU SEPARIEREN KÖNNEN. Die Sockets
als Transportschicht bieten nur Streams an und Streams wissen nichts von Anfragen und Atworten, es sind eben
(endlose) Bytestreams. Der Proxy muss aber erkennen wo eine Anfrage beginnt und wo sie endet.
Die Antwort ist in der Anfrage selbst zu finden! Der Proxy muss also die Anfrage DEKODIEREN. Ohne das geht garnichts.

Wenn wir das alles wissen können wir jetzt endlich unsere Strategie für Proxyserver bauen:

1) Der HTTP/1.1 Proxyserver muss mehrere Sockets gleichzeitig verarbeiten (am besten mit Threading). Das macht mein Proxy schon.
2) Der Proxyserver MUSS sowohl Anfragen als auch die Antworten DEKODIEREN. Das heisst erstmal Headers dekodieren und
in z.B. Arrays speichern um Sie zu verarbeiten. Genau das macht mein Proxy (noch) nicht, deswegen ist es unbrauchbar.
3) - Die GRÖßTE HERAUSFORDERUNG für Proxyserver überhaupt ist die genaue Länge der Anfragen und Antworten zu erkennen
um diese separat behandeln zu können! Liest dazu RFC. Grobe zusammenfassung:
- Ende der Header kann man mit doppelten CRLF CRLF erkennen, ab hier beginnt Body.
Wenn Header Content-Length oder Transfer-Encoding Header beinhelten, gibt es ein Message-Body, sonst nicht und die Nachricht ist zu ende.
- Content-Length gibt ganau die Länge des Body, und wird für anfragen mit statischen Inhalt verwendet.
- Transfer-Encoding: chunked wird für dynamischen Inhalt verwendet, wo der Server noch am generieren der Inhalte ist,
und deswegen die endgültige Länge der Antwort nicht kenn und somit dem Client nicht mitteilen kann.
Computerbase Forum verwendet übrigens chunked encoding. Über chunked encoding sollte man RFC lesen oder Googeln.
- der Proxy mus in der Lage sein alle diese Coding-Arten verarbeiten zu können um genau feststellen zu können,
wann eine Anfrage oder Antwort zu Ende ist. Wenn der Server falsche Länge signalisiert, schreibt RFC vor,
diesen Server zu "bestrafen" und die Verbindung mit Ihme mit "Bad Request" oder einem ähnlichen Statuscode zu signalisieren.
4) - An einem Socket können Anfragen zu verschiedenen Zielhosts in einer Pipeline liegen.
Das heisst der Proxy muss Socketverbindungen zu Zielhosts nicht lokal für sich, sondern threadübergreifend in
einem Array oder Liste dynamisch verwalten: nennen wir das mal "Server-Socket-Cache"
Das heisst der Thread, welcher eine Anfrage vom Client erhält muss erstmal schauen ob für den Zielhost
im Server-Socket-Cache bereits eine Verbindung existiert und ob diese frei (nicht von anderen Socket-Threads belegt) ist.
Falls es eine freie Verbindung gibt, muss der Thread exklusiven Zugriff darauf anfordern,
anderfalls muss eine neue (im Server-Socket-Cache) aufgebaut werden, oder der Thread wartet bis bestehende
Socket-Verbindung im Cache frei wird.

Wer obere 4 Punkte verstanden hat ist zusammen mit dem restlichen Kleinwissen (Programmiersprache, über HTTP header)
in der lage einen HTTP/1.1 Proxy Server zu programmieren.

Fazit:
Mein Proxy-Switch (obwohl es meist funktioniert) ist MÜLL. Mein Ansatz ist FALSCH: ein Proxy (egal ob 1.0 oder 1.1) MUSS Nachrichten dekodieren!
Es gibt nur zwei Lösungen:
1) Entweder ich versuche alles einfach zu halten, und mein Proxy tut so als ob er ein HTTP/1.0 Proxy wäre,
und stellt sich bei HTTP/1.1 Anfragen dumm, was meist dazu führt, dass Clients auf HTTP/1.0 Protokoll ausweichen, wo alles schön einfach ist.
Zu erinnerung: HTTP/1.0 Anfragen verwenden kein Pipelinging, keine Persistente verbindungen, und kein "chunked" Transfer-Coding.
Eine anfrage = eine Verbindung.
2) Ich implementiere das korrekte Verhalten der oberen 4 Haupt-Punkte für einen HTTP/1.1 Proxy.

Da es im Internet kaum quelloffene einfache Proxyserver für HTTP/1.1 gibt, ist die verlockung groß, selbst einen
zu schreiben und zu veröffentlichen.... Order ich gehe den einfachen Weg mit 1.0... Mal sehen.... ;)
 
Die vierte Version ist online. Genau genommen ist das eine v0.4 also ne Alpha.

Link ist der selbe:
Visual C# Projektmappe (http://home.arcor.de/yurij/exProxy.zip)
exPproxy zum sofortstarten und ausprobieren (http://home.arcor.de/yurij/exProxy.exe)
Danach im Browser als Proxyserver die IP 127.0.0.1 mit Port 8888 einstellen.

Habe den Proxy komplett neu geschrieben. Habe mich entschlossen HTTP/1.1 zu implementierenm da alles andere in heutiger zeit käse ist.
Es dekodiert nun die HTTP nachrichten und bestimmt korrekt deren länge. Auch chunked encoding ist implementiert. Auf einen dynamischen server-socket-cache verzichte ich, versuche alles weiterhin einfach zu halten. Ich bin (noch) kein C# Guru, deswegen verzichte ich auch auf komplexere klassen-einkapselungen, threadsichere zugriffe und den ganzen kram.
Es gibt nur zwei Funktionen, die äußere regelt Socketverbindungen und Nachrichtenaustausch, die innere übernimmt korrekte Nachrichtendekodierung, und stellt sicher dass vom Stream GENAU EINE http nachrict ausgelesen und weitergereicht wird.

Es ist kein produktives Proxy, es kann nicht mal Ansatzweise HTTP Standard vollständig abdecken, also nur für Lernzwecke! Der Quellcode ist noch nicht ganz perfekt, aber ich arbeite dran.

Kann jetzt endlich mit dem Spoofing teil anfangen.

P.S.: Ich schreibe diese Nachricht bereits über den neuen Proxy ;)
Werde ab jetzt über den Proxy sürfen, um nach möglichen Problemen Ausschau zu halten.
 
Zuletzt bearbeitet:
Hallo yurij,

mir ist bei dem echt gelungenen Proxy-Server ein "kleiner" Fehler aufgefallen undzwar wenn man z.b auf

http://duxworld.de

geht kommt komischerweiße ein 301-er Http-Status und er leitet dann auf "http://duxworld.dehttp://duxworld.de" um obwohl da ne 200-er kommen sollte. Geht man ohne deinen Proxy drauf dann klappt das ganze problemlos.

Ich freu mich schon auf die nächste Version! :D
 
Hallo Yurij,

ich habe mal deinen neuen Code vom Proxyserver geladen und angeschaut. Was mir sofort ins Auge fiel war folgende Defintion:
Code:
static string CRLF = Environment.NewLine;

Was mich stört ist die Tatsache, dass du ein Carriage Return + Line Feed haben möchtest mit dem du arbeitest, aber Environment.NewLine gibt nur in Abhängigkeit vom OS die entsprechende Übersetzung für "neue Zeile" zurück. Bei Windows dürftest du damit kein Problem haben und RFC konform sein, da es CR LF ist. Bei Unix unter Mono dürfte es aber sehr wahrscheinlich Probleme machen, da nach meinem Verständnis ein einfaches Carriage Return ohne Line Feed (oder war es anders rum?) eine neue Zeile erzeugt. Gem. RFC würde ich dann besser CRLF als das definieren, was es ist:

Code:
static string CRLF = "\r\n";

Oder um auf Nummer sicher zu gehen das CRLF als Byte Array erstellen und damit arbeiten.

Code:
static byte[] CRLF = new byte[]{13, 10};

Die 2. Variante würde ich deswegen bevorzugen, weil hier keine Annahme bzw. Abhängigkeit vom Encoding ist und somit die Bytefolge immer als CRLF interpretiert werden kann. Bei 1. Variante ist man erstmal in der Unicode Schiene gefangen und muss explizit mit ASCII Encoding das Ganze dann bearbeiten, denn am Ende sendest/empfängst du immer nur Bytes über die Leitungen.

Ok, zu viele Worte... Ansonsten find ich es klasse und mach weiter so!

Rossibaer
 
Zurück
Oben