ToDecimal und GetBytes für BitConverter in F#

Die Klasse BitConverter hat keine Methoden die den Typ decimal unterstützen. In der Community Content Sektion auf den MSDN Seiten gibt es ein paar Lösungen in C#. Im Folgenden kommt die Lösung von Nick Darnell umgeschrieben in F#:

module BitConverterExtensions
 
type global.System.BitConverter with
 
  static member GetBytes (value : decimal) =
    let bits = global.System.Decimal.GetBits(value)
    [| for bit in bits do for i in 0 .. 8 .. 24 -> byte(bit >>> i) |]
 
  static member ToDecimal (value : byte[], startIndex : int) =
    let s = seq { startIndex .. 4 .. startIndex + 12 }
    let bits = [| for i in s -> Array.sub value i 4
                             |> Array.mapi (fun (ix : int) (b : byte) -> (int(b) <<< (ix <<< 3)))
                             |> Array.fold (fun acc x -> acc ||| x) 0 |]
    new global.System.Decimal(bits)
 

XAML-Grafiken in ASP.NET

Lang, lang ist’s her, dass hier was passiert ist, aber es geschehen doch noch Wunder. Zur Zeit hab ich immer mehr mit WPF/XAML zu tun, aber auch mit normaler Web-Entwicklung (keine RIAs). Wie passen diese beiden Dinge nun zusammen? Richtig, eigentlich gar nicht. Da meine Begeisterung was WPF/XAML angeht aber täglich wächst und ich diese Möglichkeiten auch in einer Standard-ASP.NET-Anwendung ohne Silverlight nutzen möchte, habe ich in den letzten paar Stunden eine recht nette Möglichkeit dafür entwickelt.

Grob gesehen ging es mir nur darum, mit WPF erzeugte grafische Effekte irgendwie darstellen zu können – am besten als einfaches Bild (JPEG, PNG, etc.). Gesagt, getan, entstanden ist ein HttpHandler, der die Aufgabe hat, eine Eingangs-XAML-Datei in eine Ausgangs-PNG-Datei umzuwandeln.

Für die Entwicklung hatte ich mir vorgenommen, den Header von http://visitmix.com/2008 in leicht modifizierter Form in XAML nach zu bauen. Ausgerüstet mit dem XAMLPad ging es also an die Header-Grafik inklusive MIX08-Logo. Herausgekommen ist das:

images\header.xaml

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:sys="clr-namespace:System;assembly=mscorlib"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Width="1000" Height="140">
  <Page.Resources>
    <PathGeometry x:Key="rect" Figures="M 0,0 L 15,0 L 15,15 L 0,15" />
    <PathGeometry x:Key="rect_1r" Figures="M 15,0 L 15,15 L 0,15 L 0,7.5 Q 0,0 7.5,0" />
    <PathGeometry x:Key="rect_2r" Figures="M 15,0 L 15,7.5 Q 15,15 7.5,15 L 0,15 L 0,8 Q 0,0 8,0" />
  </Page.Resources>
  <Grid>
    <Path Stroke="#333" StrokeThickness="2" Data="M -5,139 L 200,139 Q 220,139 250,100 Q 280,60 300,60
L 800,60 Q 820,60 850,100 Q 880,139 900,139 L 1005,139
L 1005,-5 L -5,-5"> <Path.Fill> <LinearGradientBrush StartPoint="0,1"> <GradientStop Offset="0" Color="#FFF" /> <GradientStop Offset="1" Color="#EEE" /> </LinearGradientBrush> </Path.Fill> </Path> <Canvas> <Canvas Canvas.Top="20" Canvas.Left="20"> <Canvas.RenderTransform> <ScaleTransform ScaleX="1.4"
ScaleY="{Binding RelativeSource={RelativeSource self}, Path=ScaleX}" /> </Canvas.RenderTransform> <Canvas.BitmapEffect> <OuterGlowBitmapEffect GlowColor="Black" /> </Canvas.BitmapEffect> <Canvas.Resources> <Style TargetType="{x:Type Path}"> <Setter Property="Fill" Value="#F93366" /> </Style> </Canvas.Resources> <Path Canvas.Top="47" Canvas.Left="0" Data="{StaticResource rect_1r}"> <Path.RenderTransform> <RotateTransform Angle="270" /> </Path.RenderTransform> </Path> <Path Canvas.Top="16" Canvas.Left="0" Data="{StaticResource rect}" /> <Path Canvas.Top="0" Canvas.Left="0" Data="{StaticResource rect_1r}" /> <Path Canvas.Top="0" Canvas.Left="31" Data="{StaticResource rect_2r}"> <Path.RenderTransform> <RotateTransform Angle="90" /> </Path.RenderTransform> </Path> <Path Canvas.Top="0" Canvas.Left="32" Data="{StaticResource rect_2r}"> <Path.RenderTransform> <RotateTransform Angle="0" /> </Path.RenderTransform> </Path> <Path Canvas.Top="0" Canvas.Left="63" Data="{StaticResource rect_1r}"> <Path.RenderTransform> <RotateTransform Angle="90" /> </Path.RenderTransform> </Path> <Path Canvas.Top="16" Canvas.Left="48" Data="{StaticResource rect}" /> <Path Canvas.Top="47" Canvas.Left="63" Data="{StaticResource rect_1r}"> <Path.RenderTransform> <RotateTransform Angle="180" /> </Path.RenderTransform> </Path> <Path Canvas.Top="15" Canvas.Left="69" Data="M 15,0 L 15,7.5 Q 15,15 7.5,15 Q 3.25,15 1.25,10.25
Q 0,0 8,0
"> <Path.RenderTransform> <RotateTransform Angle="270" /> </Path.RenderTransform> </Path> <Path Canvas.Top="16" Canvas.Left="69" Data="{StaticResource rect}" /> <Path Canvas.Top="47" Canvas.Left="84" Data="{StaticResource rect_1r}"> <Path.RenderTransform> <RotateTransform Angle="180" /> </Path.RenderTransform> </Path> <Path Canvas.Top="0" Canvas.Left="104" Data="{StaticResource rect_2r}"> <Path.RenderTransform> <RotateTransform Angle="90" /> </Path.RenderTransform> </Path> <Path Canvas.Top="16" Canvas.Left="104" Data="{StaticResource rect}" /> <Path Canvas.Top="32" Canvas.Left="135" Data="{StaticResource rect_2r}"> <Path.RenderTransform> <RotateTransform Angle="90" /> </Path.RenderTransform> </Path> <Path Canvas.Top="47" Canvas.Left="104" Data="{StaticResource rect_2r}"> <Path.RenderTransform> <RotateTransform Angle="180" /> </Path.RenderTransform> </Path> <Path Canvas.Top="15" Canvas.Left="135" Data="{StaticResource rect_2r}"> <Path.RenderTransform> <RotateTransform Angle="180" /> </Path.RenderTransform> </Path> </Canvas> <Path Stroke="Black" StrokeThickness="2" Canvas.Top="47" Canvas.Left="193"
Data="M 0,0 L 10,0 L 10,11 L 0,11 L 0,0" /> <Path Stroke="Black" StrokeThickness="2" Canvas.Top="47" Canvas.Left="207"
Data="M 10,5 L 0,5 L 0,0 L 10,0 L 10,11 L 0,11 L 0,0" /> <TextBlock Canvas.Top="95" Canvas.Left="20" FontFamily="Segeo UI" FontSize="22" Foreground="#666"
Text="The Next Web Now" /> </Canvas> </Grid> </Page>

Um nun in der ASP.NET-Anwendung auf XAML Funktionalitäten zugreifen zu können, müssen in der web.config noch 3 Assembly-Referenzen hinzugefügt werden:

web.config

<system.web>
  <compilation debug="false">
    <assemblies>
...
<add assembly="WindowsBase, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <add assembly="PresentationCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> <add assembly="PresentationFramework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> </assemblies> </compilation>
...

Nun geht es an den eigentlichen Kern dieser Beispielanwendung – den HttpHandler. Ein Httphandler wird auf bestimmt Dateitypen angewendet. Da ich es für nicht sinnvoll halte, ihn komplett auf alle *.xaml Dateien anzusetzen, hab ich mich für die Erweiterung .xpng entschieden. Diese wird im Handler dann für den Zugriff auf die XAML-Datei umgeschrieben:

App_Code\XamlImageHandler.cs

using System;
using System.IO;
using System.Threading;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Web;

public class XamlImageHandler : IHttpHandler
{
    #region IHttpHandler Members

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "image/png";

        Thread t = new Thread(new ParameterizedThreadStart(InternalProcessRequest));
        t.SetApartmentState(ApartmentState.STA);
        t.Start(context);
        t.Join();
    }

    #endregion

    private void InternalProcessRequest(object context)
    {
        HttpContext ctx = context as HttpContext;

        string xamlFilename = Path.ChangeExtension(
ctx.Server.MapPath(ctx.Request.AppRelativeCurrentExecutionFilePath), ".xaml"); System.Windows.Controls.Page page = (System.Windows.Controls.Page)XamlReader.Load( new FileStream(xamlFilename, FileMode.Open)); int width = (int)page.Width; int height = (int)page.Height; page.Arrange(new Rect(new Point(0, 0), new Point(width, height))); page.UpdateLayout(); RenderTargetBitmap bmp = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Default); bmp.Render(page); PngBitmapEncoder png = new PngBitmapEncoder(); png.Frames.Add(BitmapFrame.Create(bmp)); using (MemoryStream stream = new MemoryStream()) { png.Save(stream); stream.WriteTo(ctx.Response.OutputStream); } } }

…und nicht vergessen, den Handler in der web.config zu registrieren:

web.config

<httpHandlers>
...
<add verb="GET,HEAD" path="*.xpng" type="XamlImageHandler" validate="false"/> </httpHandlers>

Und das war’s auch schon. Für’s Copy/Paste noch meine statische HTML-Seite und das dazu gehörige CSS:

css\2008.css

body { background-color: #666; font-family: 'Segoe UI', Tahoma, Helvetica, sans-serif;
font-size: 0.8em; margin: 0; padding: 0; }




a
{ color: #39C; text-decoration: none; } #container { background: #FFF url(http://visitmix.com/2008/images/container_bg.jpg) no-repeat scroll 0 0;
margin: 0 auto; width: 1000px; height: 500px; } #header { background-image: url('../images/header.xpng'); height: 140px;
position: fixed; top: 0; width: 1000px; } #topNav { float: right; font-size: 10px; margin: 0 10px; padding-bottom: 10px;
text-align: right; text-transform: uppercase; } #topNav ul { list-style-type: none; margin: 10px 0 0; } #topNav li { border-right: 1px solid #999; height: 11px; margin: 0 6px 1px 0;
padding-right: 6px; width: 200px; } #topNav li a { color: #333; text-transform: uppercase; }

Default.htm

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>MIX08 Conference - March 5-7, 2008</title> <link href="css/2008.css" type="text/css" rel="stylesheet" /> </head> <body> <div id="container"> <div id="header"> <div id="topNav"> <ul> <li><a href="http://visitmix.com/2008/default.aspx">Home</a></li> <li><a href="http://www.visitmix.com/2008/worldwide">Worldwide</a></li> <li><a href="#">About the Conference</a></li> <li><a href="#">the Agenda</a></li> <li><a href="#">Speaker Bios</a></li> <li><a href="#">Mixtify</a></li> <li><a href="http://content.visitmix.com/public/sessions.aspx">Sessions</a></li> <li><a href="#">Sponsors</a></li> <li><a href="http://visitmix.com">MIX Online</a></li> </ul> </div> </div> </div> </body> </html>


Screenshot der Beispielanwendung

Eindeutige Zufallszahlen mit LINQ

Die Standardfunktionalität der Klasse Random stellt nicht sicher, dass die erzeugten Zufallszahlen eindeutig sind. Wer eindeutige Zahlen benötigt, kann das ganze recht einfach mit LINQ realisieren. Sieht auf den ersten Blick vielleicht viel aus, aber die größte Arbeit macht hier nur das Einfügen der Return-Statements in den Interface-Methoden.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace UniqueRandomNumbers
{
    class RandomSequence : System.Collections.Generic.IEnumerable<int>
    {
        public System.Collections.Generic.IEnumerator<int> GetEnumerator()
        {
            return new RandomEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return new RandomEnumerator() as System.Collections.IEnumerator;
        }
    }

    class RandomEnumerator : IEnumerator<int>
    {
        private Random rnd = new Random();

        public int Current
        {
            get { return rnd.Next(); }
        }

        public void Dispose()
        {
        }

        object System.Collections.IEnumerator.Current
        {
            get { return (object)rnd.Next(); }
        }

        public bool MoveNext()
        {
            return true;
        }

        public void Reset()
        {
            throw new NotImplementedException();
        }
    }

    class Program
    {
        static void Main()
        {
            // 10 eindeutige Zufallszahlen erzeugen und ausgeben
            foreach (int i in (from int value in new RandomSequence()
                               select value).Distinct().Take(10))
            {
                Console.WriteLine(i);
            }

            // Zeitmessung für die Erzeugung von 500.000
            // eindeutigen Zufallszahlen
            Stopwatch sw = new Stopwatch();

            sw.Start();
            var numbers = (from int value in new RandomSequence()
                           select value).Distinct().Take(500000);
            int count = numbers.Count();
            sw.Stop();

            Console.WriteLine(
                "Elapsed time for {0} unique numbers: {1} ms",
                count, sw.ElapsedMilliseconds);
        }
    }
}

Das Ergebnis kann sich meiner Meinung nach sehen lassen:

Elapsed time for 500000 unique numbers: 351 ms

So oder so ähnlich kann die Ausgabe bei einem Intel(R) Pentium(R) 4 CPU 3,00GHz aussehen.

Google Attack #10 – Download

Wie versprochen gibt’s das Projekt auch noch als Download [86,0 KB].

Ich find’s zur Zeit übrigens sehr spannend, wie sich die Zugriffsstatistiken meines Blogs entwickeln. Nur schade, dass die ganzen neuen Besucher über Google kommen. Die Live Suche nach den gleichen Begriffen sieht entschieden besser aus.

Höchstwahrscheinlich kehrt jetzt wieder für eine ganze Weile lang Ruhe im Blog ein und ich werd‘ mich wieder spannenderen Dingen widmen. Wer allerdings Fragen oder Anregungen zum Google Attack Projekt hat, jederzeit gern. Einzige Ausnahme: Du kommst von Google und hast die Anregung, dass ich die 10 Beiträge entferne ;-).

Google Attack #9 – Let’s click

Es ist vollbracht. Das AutomatedProxyWebBrowser-Control wartet nur noch auf seinen Host. Im neu erstellten Projekt TestApplication2 sollte ja immernoch Form1 existieren. Diese wird nur das Control hosten, mehr nicht. Im Load-Event werden sowohl der WebsiteManager als auch der ProxyManager erzeugt und initialisiert. Ein Timer, der eine Komponente der Form ist, überprüft jede Sekunde, ob der ProxyManager bereits mindestens einen Proxy zur Verfügung stellen kann. Sobald das der Fall ist, wird das AutomatedProxyWebBrowser-Control gestartet und der Timer beendet.

TestApplication2 (Windows Forms Anwendung)

Form1.vb

Imports System.IO
Imports System.Xml.Serialization

Imports Configuration
Imports Contracts
Imports Manager
Imports ProxyScraper

Public Class Form1

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Me.AutomatedProxyWebBrowser1.WebsiteManager = New WebsiteManager()
        Me.AutomatedProxyWebBrowser1.ProxyManager = New ProxyManager()

        AddHandler Me.AutomatedProxyWebBrowser1.ProxyManager.LastProxyRemoved, AddressOf LoadProxyList

        If Not Me.DesignMode Then
            LoadWebsiteList()
            LoadProxyList()
        End If
    End Sub

    Private Sub LoadWebsiteList()
        Dim websiteSources As WebsiteSources = Nothing
        Dim serializer As New XmlSerializer(GetType(WebsiteSources))

        Using stream As New FileStream(My.Settings.WebsiteListConfigurationFile, FileMode.Open)
            websiteSources = CType(serializer.Deserialize(stream), WebsiteSources)
        End Using

        If websiteSources.Sources IsNot Nothing Then
            For Each website As String In websiteSources.Sources
                Me.AutomatedProxyWebBrowser1.WebsiteManager.AddWebsite(New Uri(website))
            Next
        End If
    End Sub

    Private Sub LoadProxyList()
        Dim proxySources As ProxySources = Nothing
        Dim serializer As New XmlSerializer(GetType(ProxySources))

        Using stream As New FileStream(My.Settings.ProxyListConfigurationFile, FileMode.Open)
            proxySources = CType(serializer.Deserialize(stream), ProxySources)
        End Using

        Dim proxyList As New List(Of Proxy)(100)
        If proxySources IsNot Nothing Then
            Dim scraper As IProxyScraper = New SimpleWebScraper()

            For Each proxySource As String In proxySources.Sources
                Dim proxies As IEnumerable(Of Proxy) = scraper.Scrape(proxySource)

                If proxies IsNot Nothing Then
                    proxyList.AddRange(proxies)
                End If
            Next
        End If

        Dim thread = New Threading.Thread(AddressOf PopulateProxyList)
        thread.IsBackground = True
        thread.Start(proxyList)
    End Sub

    Private Sub PopulateProxyList(ByVal obj As Object)
        Dim proxyList = CType(obj, IEnumerable(Of Proxy))
        For Each proxy As Proxy In proxyList
            Me.AutomatedProxyWebBrowser1.ProxyManager.AddProxy(proxy)
        Next
    End Sub

    Private Sub CheckProxyAvailability(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles proxyCheckTimer.Tick
If Me.AutomatedProxyWebBrowser1.ProxyManager IsNot Nothing AndAlso _ Me.AutomatedProxyWebBrowser1.ProxyManager.GetRandomProxy() IsNot Nothing Then proxyCheckTimer.Stop() Me.AutomatedProxyWebBrowser1.Start() End If End Sub End Class

Hin und wieder hängt die Anwendung ein wenig. Ich geh mal davon aus, dass es dann an den Proxies liegt, die wahrscheinlich eine zu hohe Antwortzeit haben.

Noch ein Hinweis am Rande: Die Anwendung sollte, so wie sie jetzt ist, nicht auf die eigene Seite losgelassen werden, denn dann ist die Sperrung des Accounts vorprogrammiert. Grund: Die Anzahl der produzierten Views ist nahezu identisch mit den generierten Klicks. Wer das eigene Konto auffülen möchte, sollte also nach jedem generierten Klick mindestens 40 Views ohne Klick folgen lassen. Das wäre dann ein Klickrate von 2,5%, was völlig ausreichend ist. Mit dem aktuellen Stand lässt sich also lediglich die Konkurrenz auschalten.

Fazit: Das Ganze hätte ich vor Monaten direkt nach der Eröffnung meines AdSense Account machen sollen, hätte nur besser werden können. Zumindest hätte ich mich dann nicht über einen Klickbetrug-Vorwurf aufregen müssen.

Google Attack #8 – Konfiguration

Damit das neue WebBrowser-Control auch etwas zu tun hat, müssen nun die Seiten, die es besuchen soll, konfiguriert werden. Hierzu wird eine XML Datei erstellt, die nach dem zuvor erstellten Schema aufgebaut ist.

TestApplication2 (Windows Forms Anwendung)

Websites.xml

<?xml version="1.0" encoding="utf-8"?>
<WebsiteSources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Sources>http://.../</Sources>
  <Sources>http://.../</Sources>
  <Sources>http://.../</Sources>
</WebsiteSources>

Für die Seiten selbst werde ich hier keine Beispiele geben, aber man sollte für Testzwecke genug Seiten finden, auf denen AdSense Werbung platziert ist.

Das Gleiche muss nun noch für die Proxies gemacht werden.

Proxies.xml

<?xml version="1.0" encoding="utf-8"?>
<ProxySources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Sources>http://www.steganos.com/?area=updateproxylist</Sources>
  <Sources>http://www.proxybase.de/?l=de&amp;c=proxylist&amp;intSitePos=0&amp;listLimit=500&amp;listMethod=all&amp;listSort=lastcheck%20DESC</Sources>
  <Sources>http://www.proxy-list.biz</Sources>
  <Sources>http://www.proxy-list.net/anonymous-proxy-lists.shtml</Sources>
  <Sources>http://proxy.6te.net/proxy.php?id=latest</Sources>
  <Sources>http://www.proxy-servers.org/anonymous-proxy-list.php?list=224</Sources>
  <Sources>http://www.multiproxy.org/txt_anon/proxy.txt</Sources>
  <Sources>http://www.digitalcybersoft.com/ProxyList/fresh-proxy-list.shtml</Sources>
</ProxySources>

Google Attack #7 – Der Browser

So, das Ende ist nah. Diesmal gibt ein erweitertes WebBrowser-Control, welches

  • eigenständig navigiert und
  • eigenständig den Proxy wechselt.

Ich muss zugeben, das Control hätte man auch besser bauen können, aber ich hab so langsam keine Lust mehr auf das Tool (bringt mir ja eh nix mehr).

Noch ein paar Anmerkungen: Die wichtigste Methode ist wohl Start. Diese sollte aufgerufen werden, sobald dem Control sowohl ein WebsiteManager als auch ein ProxyManager bekannt sind. Desweiteren hab ich etwas Logging-Funktionalität eingebaut, die nur im Debug-Modus verwendet wird.

Das Projekt selbst heißt einfach nur TestApplication2 und wird später auch die finale Anwendung (also das Hauptfenster) beinhalten.

TestApplication2 (Windows Forms Anwendung)

AutomatedProxyWebBrowser.vb

Imports System.Collections.ObjectModel
Imports System.Xml

Imports Contracts

Public Class AutomatedProxyWebBrowser
    Inherits WebBrowser

    Private ReadOnly random As New Random()

Private ReadOnly adSenseHost As New Uri("http://pagead2.googlesyndication.com") Private adClicked As Boolean Private adSenseClicks As Integer
Private currentProxy As Proxy Private currentUri As Uri

Private _websiteManager As IWebsiteManager Private _proxyManager As IProxyManager
Private noActionTimer As Timer Private globalNoActionTimer As Timer #If DEBUG Then Private debugConsole As New TextBox() #End If Public Sub New() InitializeComponent() ScriptErrorsSuppressed = True End Sub Public Property ProxyManager() As IProxyManager Get Return Me._proxyManager End Get Set(ByVal value As IProxyManager) Me._proxyManager = value End Set End Property Public Property WebsiteManager() As IWebsiteManager Get Return Me._websiteManager End Get Set(ByVal value As IWebsiteManager) Me._websiteManager = value End Set End Property Private Function FindAdSenseFrames() As Collection(Of HtmlElement) Dim adSenseFrames As New Collection(Of HtmlElement) For Each iframe As HtmlElement In Document.GetElementsByTagName("iframe") Dim srcAttribute = iframe.GetAttribute("src") If srcAttribute IsNot Nothing Then If New Uri(srcAttribute).Host = adSenseHost.Host Then adSenseFrames.Add(iframe) End If End If Next Return adSenseFrames End Function Private Sub ChangeProxyAndReloadSite() If Not Me.DesignMode Then currentUri = _websiteManager.GetRandomWebsite() currentProxy = _proxyManager.GetRandomProxy() If currentProxy Is Nothing Then noActionTimer.Start() Return End If If currentUri Is Nothing Then Throw New Exception("No websites available.") End If #If DEBUG Then debugConsole.AppendText("Using site ") debugConsole.AppendText(currentUri.ToString()) debugConsole.AppendText(Environment.NewLine) debugConsole.AppendText("Using proxy ") debugConsole.AppendText(currentProxy.ToString()) debugConsole.AppendText(Environment.NewLine) #End If ManagedWinApi.InternetSettings.ChangeProxy(currentProxy.ToString()) Navigate(currentUri) noActionTimer.Start() End If End Sub Private Sub AutomatedProxyWebBrowser_DocumentCompleted(ByVal sender As Object, ByVal e As System.Windows.Forms.WebBrowserDocumentCompletedEventArgs) Handles Me.DocumentCompleted globalNoActionTimer.Stop() globalNoActionTimer.Start() If e.Url.Host = currentUri.Host Then Dim adSenseFrames As Collection(Of HtmlElement) = FindAdSenseFrames() If adSenseFrames.Count > 0 Then #If DEBUG Then debugConsole.AppendText("Clicking AdSense #") debugConsole.AppendText((adSenseClicks + 1).ToString()) debugConsole.AppendText(Environment.NewLine) #End If CType(Me.Parent, Form).Activate() adClicked = True adSenseFrames(random.Next(adSenseFrames.Count - 1)).Focus() SendKeys.SendWait("{TAB}{TAB}{ENTER}") noActionTimer.Start() Else ChangeProxyAndReloadSite() End If ElseIf adClicked Then adClicked = False adSenseClicks += 1 ChangeProxyAndReloadSite() End If End Sub Dim started As Boolean Public Sub Start() If started Then Return End If started = True #If DEBUG Then Dim debugWindow As New Form() debugWindow.Text = "Debug Window" debugWindow.TopMost = True debugWindow.ShowInTaskbar = False debugWindow.FormBorderStyle = FormBorderStyle.SizableToolWindow debugWindow.Width = 400 debugWindow.Height = 200 debugWindow.Controls.Add(debugConsole) debugConsole.Dock = DockStyle.Fill debugConsole.Multiline = True debugConsole.ScrollBars = ScrollBars.Vertical Me.components.Add(debugWindow) debugWindow.Show() #End If noActionTimer = New Timer(Me.components) noActionTimer.Interval = 1000 AddHandler noActionTimer.Tick, AddressOf NoActionTimer_Tick globalNoActionTimer = New Timer(Me.components) globalNoActionTimer.Interval = 10000 AddHandler globalNoActionTimer.Tick, AddressOf NoActionTimer_Tick ChangeProxyAndReloadSite() End Sub Private Sub NoActionTimer_Tick(ByVal sender As Object, ByVal e As EventArgs) ProxyManager.RemoveProxy(currentProxy) ProxyManager.RemoveProxy(currentProxy) ChangeProxyAndReloadSite() End Sub Private lastProgess As Long Private progressChanges As Integer Private Sub AutomatedProxyWebBrowser_ProgressChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.WebBrowserProgressChangedEventArgs) Handles Me.ProgressChanged #If DEBUG Then 'debugConsole.AppendText(String.Format(Globalization.CultureInfo.InvariantCulture, _ ' "Progress: {0} of {1}", _ ' e.CurrentProgress, _ ' e.MaximumProgress)) 'debugConsole.AppendText(Environment.NewLine) #End If noActionTimer.Stop() If lastProgess <> e.CurrentProgress Then globalNoActionTimer.Stop() globalNoActionTimer.Start() End If lastProgess = e.CurrentProgress If e.CurrentProgress = 100 Then progressChanges = 0 Else progressChanges += 1 End If If progressChanges > 50 Then Me.Stop() End If End Sub #If DEBUG Then Private Sub AutomatedProxyWebBrowser_StatusTextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.StatusTextChanged debugConsole.AppendText(Me.StatusText) debugConsole.AppendText(Environment.NewLine) End Sub #End If End Class

Da war nochwas, was bisher nur am Anfang erwähnt wurde: Was ist mit den verräterischen Cookies? Nichts. Das WebBrowser-Control speichert und/oder sendet standardmäßig keine Cookies.

Google Attack #6 – Proxies nutzen

Für das Ansteuern der Webseiten eignet sich wohl das WebBrowser-Control am besten. Die Klassen aus dem Namespace System.Net helfen hier gar nicht mehr, da diese nur den Seitenquelltext holen können; Google AdSense basiert allerdings auf Javscript und das wird glücklicherweise ohne jegliches Zutun automatisch vom WebBrowser-Control ausgeführt.

Nun wird man aber schnell merken, was diesem Control fehlt – eine Eigenschaft namens Proxy. Man kann dem WebBrowser-Control keinen eigenen Proxy zuweisen, es verwendet immer die IE-Einstellung. Aber gut, je weniger Möglichkeiten wir haben, desto einfacher fällt die Entscheidung was getan wird. Es muss also die IE-Proxy-Einstellung geändert werden. Dieses kleine Feature hat mich den ganzen Sonntag Nachmittag gekostet. Das .NET Framework bietet hier definitiv nichts, man muss also auf die WinAPI zurückgreifen. Hierfür gibt es wieder ein neues Projekt ManagedWinApi, was im Gegensatz zu seinen Vorgängern ausnahmesweise mal in C# implementiert ist (in VB.NET hab ich’s ehrlich gesagt nicht hinbekommen, da half auch kein C# to VB.NET Converter mehr).

ManagedWinApi (Klassenbibliothek)

InternetSettings.cs

using System;
using System.Runtime.InteropServices;

namespace ManagedWinApi
{
    public class InternetSettings
    {
        private const int INTERNET_OPTION_PROXY = 38;
        private const int INTERNET_OPEN_TYPE_PROXY = 3;

        struct INTERNET_PROXY_INFO
        {
            public int dwAccessType;
            public IntPtr lpszProxy;
            public IntPtr lpszProxyBypass;
        }

        [DllImport("wininet.dll", SetLastError = true)]
        private static extern bool InternetSetOption(
            IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);

        public static void ChangeProxy(string proxy)
        {
            INTERNET_PROXY_INFO ipi;

            ipi.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
            ipi.lpszProxy = Marshal.StringToHGlobalAnsi(proxy);
            ipi.lpszProxyBypass = Marshal.StringToHGlobalAnsi("local");

            IntPtr ipiPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(ipi));
            Marshal.StructureToPtr(ipi, ipiPtr, true);

            InternetSetOption(
                IntPtr.Zero, INTERNET_OPTION_PROXY, ipiPtr, Marshal.SizeOf(ipi));
        }
    }
}

Die Werte der Konstanten kann man aus wininet.h entnehmen, den Aufbau der Struktur und die Methodensignatur aus der Online-Hilfe.

Google Attack #5 – ProxyManager-Optimierung (Teil 2)

Eine Sache fehlte doch noch. Jetzt werden Proxies zwar erst zur internen Liste hinzugefügt, wenn sie auch nutzbar sind (meist nach 10 Sekunden), aber sie werden danach nicht zwingend gleich wieder verwendet. Ich weiss nicht nach welchem Zeitraum die Willkommensseite wieder erscheinen würde, aber wenn wir uns bei allen Proxies zwischendurch einfach mal wieder kurz melden, sollte die Willkommensseite kein Problem mehr darstellen.

Dazu Folgendes: Im Konstruktor der Klasse ProxyManager starte ich jetzt einen Thread, der permanent alle Proxies kurz anspricht. Der Thread läuft mit unterster Priorität und wartet nach jeder Web-Anfrage 100 Millisekunden, so das andere Threads absoluten Vorrang haben. Damit der Thread auch sauber wieder beendet wird, habe ich zusätzlich noch das Interface IDisposable implementiert.

Manager (Klassenbibliothek)

ProxyManager.vb (Erweiterungen)

Public Class ProxyManager
    Implements IProxyManager, IDisposable
    Private proxyRefreshThread As Threading.Thread

    Public Sub New()
        proxyRefreshThread = New Threading.Thread(AddressOf RefreshProxies)
        proxyRefreshThread.IsBackground = True
        proxyRefreshThread.Priority = Threading.ThreadPriority.Lowest
        proxyRefreshThread.Start()
    End Sub

    Private Sub RefreshProxies()
        While True
            Dim proxyQueue As New Queue(Of Proxy)

            SyncLock proxiesLock
                For Each proxy As Proxy In proxies
                    proxyQueue.Enqueue(proxy)
                Next
            End SyncLock

            While proxyQueue.Count > 0
                Using wc As New WebClient()
                    Dim proxy As Proxy = proxyQueue.Dequeue()
                    wc.Proxy = New WebProxy(proxy.Server, proxy.Port)
                    Try
                        wc.OpenRead(testUri).Close()
                    Catch ex As WebException
                        RemoveProxy(proxy)
                    End Try
                End Using

                Threading.Thread.Sleep(100)
            End While
        End While
    End Sub



Private disposedValue As Boolean = False Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not Me.disposedValue Then If disposing Then If proxyRefreshThread IsNot Nothing AndAlso proxyRefreshThread.IsAlive Then proxyRefreshThread.Abort() proxyRefreshThread.Join() End If End If End If Me.disposedValue = True End Sub #Region " IDisposable Support " Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub #End Region End Class

Google Attack #4 – ProxyManager-Optimierung

Nach ein paar Tests mit dem ProxyManager war ich unzufrieden mit der Implementierung. Die meisten Proxies in den Listen scheinen tot zu sein und ich finde es macht keinen Sinn, dass der ProxyManager diese speichert (die Wahrscheinlichkeit, dass sie nur vorübergehend nicht erreichbar sind, ist wohl eher gering). Also sollte beim Hinzufügen eines Proxy schon geprüft werden, ob dieser erreichbar ist. Einfach gesagt, aber der Teufel steckt wie immer im Detail.

Wie findet man raus, ob ein Proxy erreichbar ist? Man nutzt ihn einfach. Man versucht über den Proxy einfach irgendeine Seite zu erreichen – entweder klappt es, dann funktioniert er wohl oder es klappt nicht, dann ist entweder der Proxy oder die Seite nicht erreichbar. Also nehmen wir doch einfach eine Seite, die immer erreichbar sein sollte – Google zum Beispiel.

Das nächste Problem: Wenn ein Proxy nicht erreichbar ist, dauert es ewig bis man das auch weiss – meistens wartet man halt einfach nur auf das Timeout vom Verbindungsversuch. Einzig gute Lösung: Die Liste der Proxies darf nicht synchron geprüft werden, sondern asynchron.

Gibt es noch einen Haken? Ja. Es gibt scheinbar verdammt viele Proxies, die einen beim ersten Aufruf mit einer netten Willkommensseite empfangen und erst nach 10 Sekunden auf die eigentlich angeforderte Seite weiterleiten. Diese Proxies sollten dann bestenfalls auch erst nach 10 Sekunden zur Liste der verfügbaren Proxies hinzugefügt werden.

Nochwas? Ja. Mit einigen Proxies können wir, obwohl sie erreichbar sind, nichts anfangen. Die einen machen einen Turing-Test, andere stehen in China und lassen erst gar keine Anfragen aus China raus und wieder andere melden Fehler, wenn man sich selbst nicht in einem vorgegebenen IP Bereich befindet. Ob die angezeigte Seite nun die ist, die wir haben wollen oder irgendeine Fehlerseite, lässt sich am einfachsten durch das Überprüfen des Seitentitels herausfinden.

Na dann, schreiten wir zur Lösung. Die Klasse ProxyManager hat ein neue Methoden und eine verschachtelte Klasse bekommen und ein Großteil der vorhandenen Methoden wurde geändert. Bedingt durch die Menge der Anpassungen, stell ich die Klasse nochmal komplett ein.

Manager (Klassenbibliothek)

ProxyManager.vb

Imports System.Collections.ObjectModel
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Net

Imports Contracts

Public Class ProxyManager
    Implements IProxyManager

    Private Shared ReadOnly testUri As New Uri("http://www.google.com")
    Private Const testUriTitle As String = "Google"

    Private ReadOnly random As New Random()

    Private proxies As New Collection(Of Proxy)
    Private proxiesLock As New Object()

    Public Sub AddProxy(ByVal proxy As Contracts.Proxy) Implements Contracts.IProxyManager.AddProxy
        AsyncAddProxy(proxy)
    End Sub

    Private Sub AsyncAddProxy(ByVal proxy As Proxy)
        Dim wc As New WebClient()

        With wc
            AddHandler .OpenReadCompleted, AddressOf WebClient_OpenReadCompleted

            Try
                .Proxy = New WebProxy(proxy.Server, proxy.Port)
                .OpenReadAsync(testUri, New OpenReadAsyncInfo(wc, proxy))
            Catch ex As Exception
                .Dispose()
            End Try
        End With
    End Sub

    Private Shared ReadOnly titleRegex As New Regex(Regex.Escape("<title>" + _
                                                                 testUriTitle + _
                                                                 "</title>"), _
                                                    RegexOptions.Compiled)

    Private Sub WebClient_OpenReadCompleted(ByVal sender As Object, ByVal e As OpenReadCompletedEventArgs)
        Dim info As OpenReadAsyncInfo = CType(e.UserState, OpenReadAsyncInfo)

        If Not e.Cancelled Then
            If e.Error Is Nothing Then
                Dim refreshTime As Integer = 0
                Dim websiteContent As String = String.Empty

                Using e.Result
                    Using reader As New StreamReader(e.Result)
                        Try
                            websiteContent = reader.ReadToEnd()
                        Catch ex As WebException
                        Finally
                            reader.Close()
                        End Try
                    End Using
                End Using

                If Not titleRegex.IsMatch(websiteContent) Then
                    refreshTime = ScrapeRefreshTime(websiteContent)
                End If

                If refreshTime <> Threading.Timeout.Infinite Then
                    Dim timer As New Threading.Timer(AddressOf AddProxyToList, _
                                                     info.Proxy, refreshTime, _
                                                     Threading.Timeout.Infinite)
                End If
            End If
        End If
    End Sub

    Private Sub AddProxyToList(ByVal proxy As Proxy)
        SyncLock proxiesLock
            If Not proxies.Contains(proxy) Then
                proxies.Add(proxy)
            End If
        End SyncLock
    End Sub

    Private Shared ReadOnly metaRefreshStringPart1 As String = _
        Regex.Escape("<meta http-equiv=""refresh"" content=""")

    Private Shared ReadOnly metaRefreshStringPart2 As String = _
        ";\s*url=(?<Url>[^""]+)"

    Private Shared ReadOnly refreshRegex As New Regex(metaRefreshStringPart1 + _
                                                      "(?<Time>\d+)" + _
                                                      metaRefreshStringPart2, _
                                                      RegexOptions.Compiled Or _
                                                      RegexOptions.IgnoreCase)

    Private Shared Function ScrapeRefreshTime(ByVal str As String) As Integer
        Dim m As Match = refreshRegex.Match(str)

        If m.Success Then
            Dim tempUri As New Uri(m.Groups("Url").Value)
            Dim builder As New UriBuilder()

            With builder
                .Scheme = tempUri.Scheme
                .Host = tempUri.Host.TrimEnd("."c)
                .Path = tempUri.AbsolutePath
                .Port = tempUri.Port
                .Query = tempUri.Query
            End With

            If testUri.Equals(builder.Uri) Then
                Return Integer.Parse(m.Groups("Time").Value) * 1000
            End If
        End If

        Return Threading.Timeout.Infinite
    End Function

    Public Function GetRandomProxy() As Contracts.Proxy Implements Contracts.IProxyManager.GetRandomProxy
        Dim result As Proxy = Nothing

        SyncLock proxiesLock
            If proxies.Count <> 0 Then
                result = proxies(random.Next(proxies.Count - 1))
            End If
        End SyncLock

        Return result
    End Function

    Public Event LastProxyRemoved() Implements Contracts.IProxyManager.LastProxyRemoved

    Public Sub RemoveProxy(ByVal proxy As Contracts.Proxy) Implements Contracts.IProxyManager.RemoveProxy
        SyncLock proxiesLock
            If proxies.Contains(proxy) Then
                proxies.Remove(proxy)

                If proxies.Count = 0 Then
                    RaiseEvent LastProxyRemoved()
                End If
            End If
        End SyncLock
    End Sub

    Class OpenReadAsyncInfo
        Public WebClient As WebClient
        Public Proxy As Proxy

        Public Sub New(ByVal client As WebClient, ByVal proxy As Proxy)
            Me.WebClient = client
            Me.Proxy = proxy
        End Sub
    End Class
End Class