C# – prihlásenie do administračného rozhrania WordPress

Pri vývoji programu WordPress Publisher som narazil na istý problém – WordPress API neumožňuje získať štatistiky návštevnosti. Jediné miesto, kde sa tieto štatistiky dajú získať, je priamo administračné rozhranie WordPress-u. Tak sa teda priamo v programe prihlásim do tohto rozhrania a štatistiky vyčítam odtiaľ. To sa ale ukázalo ako značne neľahká úloha.

 

Tento článok je trochu technický a na plné pochopenie je potrebná aspoň základná znalosť problematiky HTTP requestov (pojem hlavičky a pod.).

 

Celý problém je v tom, že proces prihlasovania je postavený na cookies, ktoré sa dajú získať len odoslaním prihlasovacích údajov cez spojenie zašifrované pomocou SSL. V jazyku C# (a v .NET všeobecne) sa na prácu s týmito požiadavkami používa trieda HttpWebRequest, no pri pokusoch s touto triedou som narazil na jej veľký nedostatok.

 

Vytvoril som nový HttpWebRequest, nastavil som všetky potrebné parametre a odoslal som ho – server poslal správnu odpoveď so správnymi HTTP hlavičkami Set-Cookie. Človek by čakal, že tým je celý problém vyriešený, vytvoria sa príslušné cookies a pomocou nich sa mi podarí prihlásiť. No také jednoduché to bohužiaľ nie je. Zostal som zaskočený, no po prijatí odpovede sa dané cookies z nejakého dôvodu nevytvorili.

 

Po troche pátrania (a hraní sa s programom Fiddler) som zistil, že odpoveď servera je v poriadku, no blok HTTP hlavičiek obsahuje okrem iného aj hlavičku Location, teda príkaz presmerovania (konkrétne na adresu administračného rozhrania). Problém nastal pri spracovaní prijatej odpovede triedou HttpWebRequest (alebo možno HttpWebResponse). V momente, keď HTTP response obsahovala presmerovanie, celá odpoveď servera sa zahodila a došlo k presmerovaniu na stránku uvedenú v hlavičke Location (bez vytvorenia príslušných cookies). V tomto prípade išlo o stránku administračného rozhrania (http://nazovblogu.wordpress.com/wp-admin/).

 

To je ale problém, pretože administračné rozhranie si overí existenciu prihlasovacích cookies. Ak sa nenájdu, opäť sa odošle príkaz na presmerovanie, tento raz na prihlasovaciu stránku, odkiaľ som začínal. HttpWebRequest jednoducho odignoruje všetky hlavičky, až kým nenarazí na koniec reťaze presmerovaní. Výsledkom je, že aj keď dôjde k úspešnému odoslaniu prihlasovacích údajov, nie je možné programovo prezerať obsah adminsitračného rozhrania.

 

Jediné riešenie tohto problému tak spočíva v použití triedy Socket na prácu so sieťou na tej najnižšej úrovni (HttpWebRequest je nadstavbou nad socketmi). Tam ale nastáva iný problém – keďže prihlasovanie je chránené pomocou SSL, nie je možné odosielať plain-text HTTP requesty. Nad socketom tak treba  implementovať SSL, čo by bola dosť problematická úloha. Našťastie však .NET Framework obsahuje dve triedy, ktoré väčšinu práce urobia za mňa – NetworkStream a SslStream.

 

S týmito nástrojmi už mám k dispozícii všetko, čo budem potrebovať na úspešné prihlásenie: vytvorím nový socket a pripojím sa na príslušnú adresu. Nad týmto socketom vytvorím nový NetworkStream. Nad NetwokStream-om vytvorím inštanciu triedy SslStream a autentifikujem sa – tým inicializujem SSL spojenie. Nakoniec do SslStreamu zapíšem dáta (plain-text) obsahujúce definíciu potrebného HTTP requestu – SslStream dáta zašifruje a pošle cez NetworkStream až na zadanú adresu. Potom mi už len stačí z SslStream-u vyčítať dáta obsahujúce odpoveď – HTTP response obsahujúci všetky potrebné Set-Cookie hlavičky. Z týchto hlavičiek môžem zostaviť potrebné objekty Cookie (trieda .NET).

 

Tým zložitá práca končí a na získavanie dát z administračného rozhrania WordPress-u už môžem použiť HttpWebRequest ako zvyčajne – stačí mi použiť práve vytvorené Cookies.

 

A tu je celé riašenie pokope a s komentármi (a snáď aj prehľadne 😉 ):

private void LogIn(string BlogAddress)
{
    // napríklad: BlogAddress = mar3ek.wordpress.com
    string username = HttpUtility.UrlEncode("username"); //
    string password = HttpUtility.UrlEncode("password");

    // URL kódované prihlasovacie údaje - simulácia vyplnenia prihlasovacieho formulára
    string form = "log={0}&pwd={1}&wp-submit=Prihl%C3%A1si%C5%A5+sa&redirect_to=http%3A%2F%2F{2}%2Fwp-admin%2F&testcookie=1";
    form = string.Format(form, username, password, BlogAddress);
    IPHostEntry entry = Dns.GetHostEntry(BlogAddress); // potrebné na získanie IP adresy
    // teoreticky nie je potrebné zisťovať IP, socket je možné vytvoriť aj pomocou volania Socket.Connect(fulladdress)

    Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); // vytvorenie socketu
    s.Connect(entry.AddressList[0], 443); // pripojenie socketu na IP adresu cez SSL port (443)

    NetworkStream NetStream = new NetworkStream(s); // stream na odosielanie/prijímanie dát

    System.Net.Security.SslStream SSLStream = new System.Net.Security.SslStream(NetStream); // SslStream implementuje SSL nad NetworkStreamom - automaticky naviaže SSL spojenie bez nutnosti implementovať niečo ručne
   
    /* vytvorenie HTTP requestu */
    StringBuilder RequestBuilder = new StringBuilder();
    RequestBuilder.AppendLine("POST https://{0}/wp-login.php HTTP/1.1");
    RequestBuilder.AppendLine("Host: {0}");
    RequestBuilder.AppendLine("User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6 (.NET CLR 3.5.30729)");
    RequestBuilder.AppendLine("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
    RequestBuilder.AppendLine("Accept-Language: en-us,en;q=0.5");
    RequestBuilder.AppendLine("Accept-Encoding: gzip,deflate");
    RequestBuilder.AppendLine("Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7");
    RequestBuilder.AppendLine("Keep-Alive: 115");
    RequestBuilder.AppendLine("Connection: keep-alive");
    RequestBuilder.AppendLine("Referer: {0}/wp-login.php");
    RequestBuilder.AppendLine("Cookie: wordpress_test_cookie=WP+Cookie+check");
    RequestBuilder.AppendLine("Content-Type: application/x-www-form-urlencoded");
    RequestBuilder.AppendLine("Content-Length: {1}");
    RequestBuilder.AppendLine();
    RequestBuilder.AppendLine("log={2}&pwd={3}&wp-submit=Prihl%C3%A1si%C5%A5+sa&redirect_to=http%3A%2F%2F{0}%2Fwp-admin%2F&testcookie=1");

    byte[] data = Encoding.UTF8.GetBytes(String.Format(RequestBuilder.ToString(), BlogAddress, form.Length, username, password)); // dáta na odoslanie

    SSLStream.AuthenticateAsClient(BlogAddress); // SSL handshake
    SSLStream.Write(data, 0, data.Length); // odoslanie requestu

    int BufferSize = 1024; // veľkosť zásobníka prijatých dát

    StringBuilder ResponseBuilder = new StringBuilder();
    byte[] ResponseFrame = new byte[BufferSize]; // buffer na prijaté dáta
    int Read = SSLStream.Read(ResponseFrame, 0, BufferSize); // počet prečítaných bytov
    while (Read > 0)
    {
        ResponseBuilder.Append(Encoding.UTF8.GetString(ResponseFrame, 0, Read)); // pole bytov -> text
        Read = SSLStream.Read(ResponseFrame, 0, BufferSize);
    }

    CookieContainer CookieJar = new CookieContainer();

    foreach (string HeaderLine in ResponseBuilder.ToString().Split("\n\r".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
    {
        if (HeaderLine.StartsWith("Set-Cookie")) // hlavička Set-Cookie
        {
            string CookieDefinition = HeaderLine.Replace("Set-Cookie: ", ""); // definícia cookie
            string CookieName, CookieValue, CookiePath, CookieDomain;
            CookieName = CookieValue = CookiePath = CookieDomain = "";

            string[] CookieDefinitionParts = CookieDefinition.Replace(" ","").Split(';');

            foreach (string DefinitionPart in CookieDefinitionParts)
            {
                if (!DefinitionPart.StartsWith("path=") && !DefinitionPart.StartsWith("domain=") && DefinitionPart.Contains("=")) // cookie name + cookie value
                {
                    CookieName = DefinitionPart.Split('=')[0];
                    CookieValue = DefinitionPart.Split('=')[1];
                    continue;
                }
                if (DefinitionPart.StartsWith("path=")) // cookie path
                {
                    CookiePath = DefinitionPart.Replace("path=", "");
                    continue;
                }
                if (DefinitionPart.StartsWith("domain=")) // cookie domain
                {
                    CookieDomain = DefinitionPart.Replace("domain=", "");
                    continue;
                }
            }

            CookieJar.Add(new Cookie(CookieName, CookieValue, CookiePath, CookieDomain)); // vytvorenie cookie
        }
    }

    /* odoslanie requestu s nastavenými cookies - prebehne bez problémov */
    HttpWebRequest req = (HttpWebRequest)WebRequest.Create("https://" + BlogAddress + "/wp-admin/index.php");
    req.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3";
    req.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
    req.KeepAlive = false;
    req.AllowAutoRedirect = false;
    req.Referer = "https://" + BlogAddress + "/wp-login.php";
    req.ContentType = "application/x-www-form-urlencoded";
    req.CookieContainer = CookieJar;
    req.AllowAutoRedirect = true;
    req.AutomaticDecompression = DecompressionMethods.GZip;
    req.Method = "GET";
    req.Timeout = 30000;

    HttpWebResponse WebResponse = (HttpWebResponse)req.GetResponse();

    using (System.IO.StreamReader sr = new System.IO.StreamReader(WebResponse.GetResponseStream(), Encoding.UTF8))
    {
        MessageBox.Show(sr.ReadToEnd());
    }
}

Jeden by nepovedal, že .NET Framework môže obsahovať takúto nepresnosť. Ale možno to nie je nepresnosť, možno tak HttpWebRequest pracuje zámerne. Ak je to tak, pointa mi asi uniká. V každom prípade mám kód, ktorý funguje a robí presne to, čo chcem. A použitie socketov zaručuje väčšiu flexibilitu riešenia.

Reklamy

Pridaj komentár

Zadajte svoje údaje, alebo kliknite na ikonu pre prihlásenie:

WordPress.com Logo

Na komentovanie používate váš WordPress.com účet. Odhlásiť sa / Zmeniť )

Twitter picture

Na komentovanie používate váš Twitter účet. Odhlásiť sa / Zmeniť )

Facebook photo

Na komentovanie používate váš Facebook účet. Odhlásiť sa / Zmeniť )

Google+ photo

Na komentovanie používate váš Google+ účet. Odhlásiť sa / Zmeniť )

Connecting to %s