Lifehack: zet de digitale versie van je krant op je eigen FTP

Ingediend door Dirk Hornstra op 09-jul-2019 22:20

Als abonnee van de krant vind ik het heerlijk om 's morgens aan het ontbijt in de "ouderwetse" papieren versie het nieuws te lezen, nog even een puzzeltje te maken, de strips te lezen. Maar als abonnee heb je ook toegang tot de web-editie. En omdat ik IT-er ben vind ik dat ik dat ik ook via de sociale media-kanalen mezelf moet profileren. Dus als ik dan een artikel in de krant tegenkom wat werk-gerelateerd is (of misschien over TRES zelf gaat) of bijvoorbeeld betrekking heeft op mijn woonplaats (en als webmaster van die website ook het nieuws met de mede-bewoners wil delen) is het handig dat ik die artikelen "snel" beschikbaar heb.

En snel is dus niet dat je eerst moet inloggen, vervolgens moet gaan bladeren, daarna het artikel moet gaan opslaan en dat je dan pas wat met het artikel kunt doen (en ja, natuurlijk voeg ik bronvermelding toe!). Dus ik heb in C# een script gemaakt wat eigenlijk hetzelfde doet als ikzelf. Als je inlogt krijg je een cookie. Met die cookie heb je toegang tot de krant, de URL's worden opgebouwd op basis van de datum, dus op een redelijk simpele manier kan ik de bestanden automatisch downloaden en op een plek zetten waar ik er zelf snel bij kan. Helemaal top.

Hieronder in het kort de flow de inlog-flow:


CookieContainer ck = new CookieContainer();
HttpWebRequest wr = null;
HttpWebResponse wresp = null;
StreamReader reader = null;
StreamWriter writer = null;

try {
    wr = (HttpWebRequest)WebRequest.Create("inlog-url");
    wr.CookieContainer = ck;
    wr.AllowAutoRedirect = true;
    wr.KeepAlive = true;
    wr.ContentType = "application/x-www-form-urlencoded";
    wr.Method = "POST";
    wr.Expect = "";
    wr.ProtocolVersion = HttpVersion.Version10;
    wr.Accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg";
    wr.Referer = "referrer-url";
    wr.UserAgent = ".....";
    wr.Headers.Add("Accept-Language", "en-us");
    wr.Headers.Add("Accept-Encoding", "gzip, deflate");
    wr.Headers.Add("Cache-Control", "no-cache");
// post values
    string un = ConfigurationManager.AppSettings["configurationfile-username"];
    string pw = ConfigurationManager.AppSettings["configurationfile-password"];
    writer = new StreamWriter(wr.GetRequestStream());
    writer.Write("act=login&email=" + un + "&password=" + pw);
    writer.Close();
    wresp = (HttpWebResponse)wr.GetResponse();
    wresp.GetResponseStream();
    ck = wr.CookieContainer;
    try { wresp.Close(); } catch (Exception) { }
}
catch (Exception x) { }  

Je ziet in bovenstaande dat je na het inloggen een cookie terugkrijgt, daarmee ben je in de volgende pagina's geautoriseerd en kun je dus door de pagina's "bladeren". Deze zit in de cookiecontainer, dus deze moet je in alle opvolgende aanvragen meesturen. De pagina's van de kranten zijn onderverdeeld in JPEGs per artikel, dus je hebt ze allemaal los beschikbaar.

Omdat er binnenkort onderhoud op de server gepleegd wordt ben ik bezig de code over te zetten naar een VPS-omgeving. Daar kwam ik erachter dat de bovenstaande code nog wel werkt, maar dat als je zelf naar de inlogpagina gaat er een redirect inzit naar een nieuw inlogdeel. En ook naar een heel nieuwe weergave van de krant... Op die pagina waar de redirect uitgevoerd wordt staat nog steeds het inlogformulier en ook de krant is daar op de oude manier beschikbaar en te downloaden, maar op een bepaald moment zal dat vervallen. Ik moet mijn code dus ombouwen. En dat had nog wel even "wat voeten in de aarde".

In de nieuwe situatie post je een stuk JSON code, een aantal waardes in de header en krijg je een JSON-waarde (als je correct inlogt) terug met waardes die je verder in je code moet gebruiken;


// code is bijna gelijk aan bovenstaande. toevoeging van enkele waardes in de header en dit:
wr.ContentType = "application/json";

string mainsession_sessionid = "";
string mainsession_expiry = "";
bool mainsession_sessioncookie = false;

string publicid = "";
string lc_guid = "";

//... overige code

writer = new StreamWriter(wr.GetRequestStream());
writer.Write($"{{\"realmName\":\"default_realm\",\"authenticationSchemeName\":\"emailAddress_password\",\"rememberMe\":false,\"identifiers\":[{{\"name\":\"emailAddress\",\"value\":\"{un}\"}}],\"validators\":[{{\"name\":\"password\",\"value\":\"{pw}\"}}]}}");
writer.Close();
wresp = (HttpWebResponse)wr.GetResponse();
reader = new StreamReader(wresp.GetResponseStream());
string responseData = reader.ReadToEnd();
JObject jsonData = JsonConvert.DeserializeObject<JObject>(responseData);
foreach (JProperty child in jsonData.Children())
{
    switch(child.Name.ToLower())
    {
        case "mainsession":
            JObject mainsessionItems = JsonConvert.DeserializeObject<JObject>(child.Value.ToString());
            foreach (JProperty mainchild in mainsessionItems.Children())
            {
                switch (mainchild.Name.ToLower())
                {
                    case "sessionid":
                        mainsession_sessionid = mainchild.Value.ToString();
                        break;
                    case "expiry":
                        mainsession_expiry = mainchild.Value.ToString();
                        break;
                    case "sessioncookie":
                        bool.TryParse(mainchild.Value.ToString(), out mainsession_sessioncookie);
                        break;
                }
            }
            break;
        case "publicid":
            publicid = child.Value.ToString();
            break;
        case "guid":
            lc_guid = child.Value.ToString();
            break;
    }
}
reader.Close();

De waardes die je terug krijg zijn sessionid, expiry (wanneer je token niet meer geldig is), sessioncookie is een true of false (lijkt bij mij altijd false te zijn), een publicid en een guid-waarde.

In Chrome (en Firefox ook) kun je via het netwerkverkeer zien welke URL's aangeroepen worden, wat voor headers er mee gestuurd worden, dus ik ga proberen dit stap voor stap te reproduceren.

Als je zelf inlogt kom je in een volgende pagina waarin je 2 knoppen ziet, of je naar de editie Noord wilt of naar de editie Zuid. Die pagina is trouwens ook op te vragen zonder ingelogd te zijn. Binnen die flow wordt een bepaalde URL aangeroepen die je JSON teruggeeft, daar zit een key-property in (wat later in de URL terugkomt), dus die bewaar je in een variabele.

Inloggen, stap 1. Deze key opvragen is stap 2.
Stap 3 is dat je een bepaalde URL aanroept waar deze "key" onderdeel van uit maakt. Ook moet je in de querystring een filter op datum meegeven. Die pagina die je oproept geeft je een stuk JSON terug met onder andere de waarde van de folder, de waarde van het publicatie-ID en het aantal pagina's.

In tegenstelling tot mijn oude downloadfunctie haal je niet de artikelen stuk voor stuk binnen, maar zijn de afbeeldingen een volledige pagina van de krant.

Stap 4 is het opvragen van een pagina om een token te krijgen. Daar liep ik er tegenaan dat ik eerst een cookie-wall voor mijn kiezen kreeg. Die moet je dus ontwijken. Dat doe ik op basis van onderstaande code:

 
                       string tokenPageUrl = " .... ";
                       string cookieStart = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.000Z");
                        string cookieEnd = DateTime.Now.AddHours(1).ToString("yyyy-MM-ddTHH:mm:ss.000Z");
                        string cookieConsentValue = HttpUtility.UrlEncode("{\"createdAt\":\"" + cookieStart + "\",\"validTill\":\"" + cookieEnd + "\",\"ID\":\"" + publicid + "\",\"templateID\":\"lc\",\"userID\":\"" + mainsession_sessionid + "\",\"permissions\":{\"analytics\":false,\"personalization\":false,\"functional\":true,\"advertising\":false,\"socialMedia\":false}}");
                        ck.Add(new Cookie() { Name = "cookiename", Domain = "domain of the paper", Path = "/", Value = cookieConsentValue });

                        wr = (HttpWebRequest)WebRequest.Create(tokenPageUrl);
                        wr.CookieContainer = ck;
                        wr.AllowAutoRedirect = false;
                        wr.KeepAlive = true;
                        wr.ContentType = "application/json";

                        wr.Method = "GET";
                        wr.Expect = "";
                        wr.ProtocolVersion = HttpVersion.Version10;
                        wr.Accept = "application/json, text/javascript, */*; q=0.01";
                        wr.Referer = detailPage;
                        wr.UserAgent = ".....";
                        wr.Headers.Add("Accept-Language", "nl,en-US;q=0.7,en;q=0.3");
                        wr.Headers.Add("Accept-Encoding", "gzip, deflate, br");
                        wr.Headers.Add("Cache-Control", "no-cache");
                        wr.Headers.Add("Origin", "original URL");

                        wresp = (HttpWebResponse)wr.GetResponse();
                        reader = new StreamReader(wresp.GetResponseStream());
                        responseData = reader.ReadToEnd();

                        string tokenWeNeed = wresp.Headers["Location"].ToString();
                        if (string.IsNullOrEmpty(redirectLocation) == false)
                        {
                            tokenWeNeed = tokenWeNeed.Replace("/pre-part-of-the-url-before-the-token/", "");
                        }

                        ck = wr.CookieContainer;
                        reader.Close();
                        try
                        {
                            wresp.Close();
                        }
                        catch (Exception) { }

Je ziet hierboven dat je een redirect-URL krijgt, in die URL zit het token wat je nodig hebt.

Stap 5 is dat je een stukje JSON moet posten naar een URL. Daar zitten twee variabelen in, token, dat is hierboven tokenWeNeed. Maar ook nog een waarde udid. En ik had geen idee waar dat vandaan kwam. Ik kon het nergens in de broncode vinden. Dus dan is er maar één locatie, het moet in het JavaScript bepaald worden. Dus daarin gezocht op udid en ja, daar kwam ik dit tegen:


// ....this._getUDID()

{key:"_getUDID",value:function(){var a=this.$.preferences.getPreference("udid");return a||(a=md5(navigator.userAgent+new Date().getTime()),this.$.preferences.savePreference("udid",a)),a}}

Gelukkig was dit "leesbaar javascript", je ziet dus dat je de userAgent die je in de headers meestuurt moet plakken aan een unix-timestamp en daar de MD5 waarde van berekent. Ik gebruik deze waardes en ga ze posten, je krijgt dan een stuk JSON met een access-token terug. En dat werkt!


                        writer = new StreamWriter(wr.GetRequestStream());
                        string t_token = tokenWeNeed;

                        Int64 retval = 0;
                        var st = new DateTime(1970, 1, 1);
                        TimeSpan t = (DateTime.Now.ToUniversalTime() - st);
                        retval = (Int64)(t.TotalMilliseconds + 0.5);
                        string timeStamp = retval.ToString();

                        MD5 md5 = System.Security.Cryptography.MD5.Create();
                        byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(wr.UserAgent + timeStamp);
                        byte[] hash = md5.ComputeHash(inputBytes);

                        StringBuilder sb = new StringBuilder();
                        for (int i = 0; i < hash.Length; i++)
                        {
                            sb.Append(hash[i].ToString("X2"));
                        }

                        string t_uid = sb.ToString();
                        writer.Write($"{{\"token\":\"{t_token}\",\"udid\":\"{t_uid}\"}}");
                        writer.Close();

                        wresp = (HttpWebResponse)wr.GetResponse();
                        reader = new StreamReader(wresp.GetResponseStream());
                        responseData = reader.ReadToEnd();

                        string accessTokenUrl = "";

                        jsonData = JsonConvert.DeserializeObject<JObject>(responseData);
                        foreach (JProperty child in jsonData.Children())
                        {
                            switch (child.Name.ToLower())
                            {
                                case "access_url":
                                    accessTokenUrl = child.Value.ToString();
                                    break;
                            }
                        }

                        ck = wr.CookieContainer;
                        reader.Close();
                        try
                        {
                            wresp.Close();
                        }
                        catch (Exception) { }

Stap 6 is dat we deze URL aanroepen. Maar als die "terug komt" krijg je een cookie met een sessie-waarde van PHP terug (PHPSESSID=...), maar ook een redirect. Die redirect moet je niet uitvoeren, want we moeten die cookie bewaren, zodat we die bij onze volgende aanroepen mee kunnen sturen:


                        wr = (HttpWebRequest)WebRequest.Create(accessTokenUrl);

                        wr.AllowAutoRedirect = false;
                        wr.KeepAlive = true;
                        wr.ContentType = "application/json";

                        wr.Method = "GET";
                        wr.Expect = "";
                        wr.ProtocolVersion = HttpVersion.Version10;
                        wr.Accept = "application/json, text/javascript, */*; q=0.01";
                        wr.Referer = "https://login.e-pages.dk/lc/portal/prevalidated/" + t_token + "/covers";
                        wr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0";
                        wr.Headers.Add("Accept-Language", "nl,en-US;q=0.7,en;q=0.3");
                        wr.Headers.Add("Accept-Encoding", "gzip, deflate, br");
                        wr.Headers.Add("Cache-Control", "no-cache");
                   
                        wresp = (HttpWebResponse)wr.GetResponse();
                        reader = new StreamReader(wresp.GetResponseStream());
                        responseData = reader.ReadToEnd();

                        string phpSession = wresp.Headers["Set-Cookie"];
                        phpSession = phpSession.Substring(0, phpSession.IndexOf(";"));

                        ck = wr.CookieContainer;
                        reader.Close();
                        try
                        {
                            wresp.Close();
                        }
                        catch (Exception) { }

Stap 7 is dat we de pagina van de krant van vandaag opvragen, in die pagina staat een stuk javascript, waarin het unieke deel van de URL naar alle afbeeldingen staat. Die hebben we nog nodig om de downloads uit te voeren:


                        wr = (HttpWebRequest)WebRequest.Create($"...../{publicationID}/");
                        

                        wr.Host = "host of the pages";
                        wr.UserAgent = ".....";
                        wr.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
                        wr.Headers.Add("Accept-Language", "nl,en-US;q=0.7,en;q=0.3");
                        wr.Headers.Add("Accept-Encoding", "gzip, deflate, br");
                        wr.Referer = "login page of the paper";
                        wr.KeepAlive = true;
                        wr.Headers.Add("Cookie", phpSession);
                        wr.Headers.Add("Upgrade-Insecure-Requests", "1");

                        wresp = (HttpWebResponse)wr.GetResponse();
                        reader = new StreamReader(wresp.GetResponseStream());
                        responseData = reader.ReadToEnd();

                        string uniquePart = Regex.Match(responseData, "var key = (.*)").Value;
                        uniquePart = Regex.Match(uniquePart, "\"(.*)\"").Value.TrimStart(new char[] { '"' }).TrimEnd((new char[] { '"' }));
   
                        ck = wr.CookieContainer;
                        reader.Close();
                        try
                        {
                            wresp.Close();
                        }
                        catch (Exception) { }

                        using (WebClient wc = new WebClient())
                        {
                            for (int k = 1; k < pageCount; k++)
                            {
                                string url = $"..../{uniquePart}/..../{publicationID}/vector/t{k}.jpg";
                                wc.DownloadFile(url, $"local-file-system\\{k.ToString().PadLeft(3, '0')}.jpg");
                            }
                        }

Halverwege het proces (het deel wat uit het javascript kwam) vroeg ik me af of het me wel zou lukken om het werkend te krijgen. Een heel gepuzzel. Maar als het dan werkt, dan geeft dat een kick. Nog even de disclaimer dat de bovenstaande code geen clean code is, zit allemaal in één grote functie, wat in een normale situatie uitgesplitst moet gaan worden, het ging nu eerst om het punt "krijg ik het werkend of niet". Wel dus :)