πΎ Archived View for sylvaindurand.org βΊ rss.xml captured on 2022-04-28 at 17:56:08.
View Raw
More Information
-=-=-=-=-=-=-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Sylvain Durand</title>
<link>https://sylvaindurand.org/</link>
<atom:link href="https://sylvaindurand.org/rss.xml" rel="self" type="application/rss+xml" />
<item>
<title>Personal finance with Beancount</title>
<link>https://sylvaindurand.org/personal-finance-with-beancount/</link>
<pubDate>Sun, 20 Mar 2022 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/personal-finance-with-beancount/</guid>
<description><p>When I was 7 years old, my parents gave me pocket money for the first time: 10 F per week (about β¬2 today) accompanied by a small notebook in which I had to do my accounting. I suppose it was a way to monitor what I was going to do with that fortune, but I kept track of my finance.</p>
<p>Since my first debit card 15 years ago, I&rsquo;ve tracked every expense I&rsquo;ve made. The goal is pretty simple: to know where my money comes from and where it goes, and how my lifestyle is changing. Over the years, these accounts have also become a very special form of diary: they note each of my outings, each activity, bar, restaurant, cinema or theater.</p>
<h2 id="how-ive-tracked-my-finances-in-the-past">How I&rsquo;ve tracked my finances in the past</h2>
<p>Despite a few attempts with software like HomeBank, I used a simple Excel sheet for almost twelve years. One line per transaction, with a column for the date, a category, a label, and the amount (one column per account).</p>
<p>This approach has many flaws:</p>
<ul>
<li>
<p>each account requires a different column, which makes things hard to read and change after a few years;
the whole is based on formulas that must be maintained, with a risk of errors;</p>
</li>
<li>
<p>an excel file only tracks cash flow and does not allow you to keep track of your debts (&ldquo;how much do I owe Bob?&rdquo;), the money you are owed (&ldquo;how much money does Alice owe me?&rdquo;), or your scheduled payments (&ldquo;how much do I still have to pay for my car?&rdquo;);</p>
</li>
<li>
<p>unless specific formulas are created, it does not allow you to create reports to answer questions like &ldquo;how have my restaurant expenses evolved over the last few years?&rdquo;, &ldquo;how much did I spend on my last vacation?&rdquo; or &ldquo;how much is left in my shopping budget this month?&rdquo;;</p>
</li>
<li>
<p>this system is not easy to evolve, and involves more and more formulas and sheets that are difficult to maintain over time;</p>
</li>
<li>
<p>let&rsquo;s not talk about the follow-up of investments or expenses sharing;</p>
</li>
<li>
<p>an excel sheet is not easy to version and modify from several devices;</p>
</li>
<li>
<p>I don&rsquo;t like to depend on proprietary software.</p>
</li>
</ul>
<h2 id="plain-text-accounting-with-beancount">Plain text accounting with Beancount</h2>
<p>In 2019, I discovered a concept that has changed the way I track my personal finances: plain text accounting. Basically, it&rsquo;s very simple: all transactions are written on a simple text file, with a particular syntax. The software reads them and is able to make all kinds of reports, presentation, either from the command line or in a nice web interface.</p>
<p>Above all, the whole thing is based on a logic of double-entry bookkeeping: I didn&rsquo;t know what it brought, but I couldn&rsquo;t do without it today!</p>
<p>Several softwares exist, notably <code>ledger</code> who invented the concept, <code>hledger</code>, and <code>beancount</code> which is the one I use to take advantage of the excellent <code>fava</code> web interface.</p>
<h3 id="why-double-entry-bookkeeping">Why double-entry bookkeeping?</h3>
<p>For a long time, the interest of double-entry bookkeeping seemed to me to be limited to the corporate world, and at best useless for an individual. That was before I tried it!</p>
<p>There are two types of things you want to track:</p>
<ul>
<li>
<p>Accounts: assets (owned) or liability (owed);</p>
</li>
<li>
<p>Transactions: income (inflows) and expenses (outflows).</p>
</li>
</ul>
<p>Income and expenses affect your accounts as follows:</p>
<pre tabindex="0"><code>Revenues (+) βΎ (Assets - Liabilities) βΎ Expenses (-)
</code></pre><p>Each transaction requires at least two balanced transactions involving assets (or liabilities), revenues and expenses. Let&rsquo;s say I want to buy a good video game. The syntax is as follows:</p>
<pre tabindex="0"><code>2022-03-21 * &#34;Steam&#34; &#34;Buying Stardew Valley&#34;
Assets:Checking -13.99 EUR
Expenses:Games 13.99 EUR
</code></pre><p>Here I buy using my checkings account (which is an asset), which will therefore decrease by β¬13.99. I&rsquo;m also increasing my expenses, in a subaccount named &ldquo;games&rdquo; where, in the future, I will be able to find in it all the games I bought and for how much. The sum of all operations must necessarily be zero (nothing is lost, nothing is created)!</p>
<p>Now let&rsquo;s imagine that I invite Bob at the restaurant, pay partly with cash, and that I complete it with my bank card:</p>
<pre tabindex="0"><code>2022-03-21 * &#34;Susama Sushi&#34; &#34;Lunch with Bob&#34;
Assets:Cash -20.00 EUR
Assets:Checking -15.50 EUR
Expenses:Restaurant 35.00 EUR
</code></pre><p>What if Bob promised to pay me back for his meal?</p>
<pre tabindex="0"><code>2022-03-21 * &#34;Susama Sushi&#34; &#34;Lunch with Bob&#34;
Assets:Cash -20.00 EUR
Assets:Checking -15.50 EUR
Assets:Debts:Bob 17.50 EUR
Expenses:Restaurant 17.50 EUR
</code></pre><p>And when Bob pays us back with a bank transfer:</p>
<pre tabindex="0"><code>2022-03-23 * &#34;Susama Sushi&#34; &#34;Bob refund&#34;
Assets:Checking 17.50 EUR
Assets:Debts:Bob -17.50 EUR
</code></pre><p>Want to know how much you spend on restaurants? Check out the <code>Expenses:Restaurant</code> account balance. Need to know how much you have left on your checking account? This is the balance of <code>Assets:Checking</code>. Does Bob still owe you money? Just look at <code>Assets:Debts:Bob</code> balance.</p>
<p>There are no constraints on the accounts you can create, and you can have longer depths (<code>Expenses:Home:Phone</code> and <code>Expenses:Home:Internet</code> for example, allowing you to simply see a <code>Expenses:Home</code> total).</p>
<h2 id="other-instructions">Other instructions</h2>
<p>Beancount file read by beancount consists essentially of a series of transactions as indicated above. You can also open or close accounts, indicate a balance (allowing Beancount to return an error if something does not match), comments, attachments&hellip;</p>
<p>You can also make currency conversion, stock market transactions, allowing you to track costs, prices, realized and unrealized capital gains.</p>
<p>For example, I buy here 3 Apple shares at a $163.98 price per unit, with 0.5% fees, so a $164.80 cost per unit:</p>
<pre tabindex="0"><code>2022-03-17 * &#34;Buying 3 AAPL&#34;
Assets:Stocks:Cash -494.40 USD
Assets:Stocks:AAPL 3 AAPL {164.80 USD} @ 163.98 USD
</code></pre><p>Basically, you can count something else than financial values: days off, worked hours&hellip;</p>
<h3 id="accounts">Accounts</h3>
<p>Through the years, my income and expense accounts have become the following (of course, this hierarchy works well for me, but will depend on your needs):</p>
<pre tabindex="0"><code class="language-raw" data-lang="raw">Incomes
β
ββββ Scholarships
ββββ Gifts
ββββ Classes
ββββ Pensions
ββββ Salaries
ββββ Gross
ββββ Taxes
Expenses
β
ββββ Subscriptions
β ββββ Banks
β ββββ Internet
β ββββ Telephone
β ββββ Newspapers
β
ββββ Housing
β ββββ Rent
β ββββ Insurances
β ββββ Electricity
β ββββ Gas
β
ββββ Purchases
β ββββ Books
β ββββ Clothing
β ββββ Cash
β ββββ Gifs
β ββββ Electronics
β ββββ Games
β ββββ Misc
β
ββββ Needs
β ββββ Groceries
β ββββ Health
β ββββ Care
β
ββββ Leisure
β ββββ Bars
β ββββ Coffee
β ββββ Cinemas
β ββββ Concerts
β ββββ Outings
β ββββ Sports
β ββββ Theaters
β
ββββ Meals
β ββββ Bakeries
β ββββ Deliveries
β ββββ Restaurants
β
ββββ Transportation
β ββββ Subway
β ββββ Cabs
β ββββ Trains
β
ββββ Travel
ββββ Exceptional
Assets
β
ββββ Checkings
β ββββ Bank1
β ββββ Bank2
β ββββ Bank3
β
ββββ Deposits
β
ββββ Savings
ββββ LDDS
ββββ PEL
ββββ PEA
ββββ Cash
ββββ Unit1
ββββ Unit2
</code></pre><h2 id="command-line-usage">Command line usage</h2>
<p>Beancount offers several commands to generate reports on your accounts:</p>
<ul>
<li>
<p><code>bean-report</code> extract reports to the console;</p>
</li>
<li>
<p><code>bean-query</code> allows to execute precise queries very similar to SQL;</p>
</li>
<li>
<p><code>bean-check</code> verify that your syntax and transactions work correctly;</p>
</li>
<li>
<p><code>bean-format</code> reformat the bean file to right-align all the numbers;</p>
</li>
<li>
<p><code>bean-web</code> provide a small web interface.</p>
</li>
</ul>
<p>Beancount can easily be augmented with plugins written in Python. These allow you to easily modify or add transactions or accounts automatically. It becomes easy to make automatic depreciations, to split transactions, to share expenses or to follow a budget.</p>
<p>The Beancount documentation is particularly complete and allows you to discover the concepts, the most common use cases, but also the most advanced needs:</p>
<p><a href="https://beancount.github.io/docs/index.html">Beancount documentation</a></p>
<h2 id="web-interface-with-fava">Web interface with Fava</h2>
<p>However, one of the strengths of Beancount is also the Fava web interface. Fava proposes to display simply all the accounts, expenses, incomes, assets, with different graphs which allow easily to change views, to filter or to observe only different time steps.</p>
<p>Fava also offers a system of plugins that allows you to build your own personalized report pages, in a very efficient way.</p>
<p><a href="https://beancount.github.io/fava/">Fava homepage</a></p>
<p><a href="https://fava.pythonanywhere.com/example-beancount-file/">Fava demo</a></p>
<h2 id="stats">Stats</h2>
<p>As of today, my file has 30.058 lines, 7259 directives (15.622 postings in 6902 transactions) and includes all my transactions since mid 2007!</p>
<p>The report of the balances is executed in 0.3 seconds. Even if these performances suit me perfectly, the next version of Beancount, planned in C++ against Python today, should largely improve them.</p>
</description>
</item>
<item>
<title>Setting up a VPN with Wireguard</title>
<link>https://sylvaindurand.org/setting-up-a-vpn-with-wireguard/</link>
<pubDate>Sun, 20 Mar 2022 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/setting-up-a-vpn-with-wireguard/</guid>
<description><p>VPNs are very practical tools that allow you to secure the connection between two machines, to secure your connection to the Internet when you connect in public places or to bypass geographical access restrictions. When you have a personal server and you don&rsquo;t want to expose it to the public, a VPN is ideal to reduce the attack surface and access your tools.</p>
<p>For a long time, I used OpenVPN, which was quite heavy to configure and set up, and not always easy to maintain&hellip; until the arrival of Wireguard.</p>
<p>Wireguard is a more recent tool, completely open source, very light, simple to set up, which claims to comply with the latest encryption standards and is particularly efficient. It has recently been integrated into the Linux kernel and has clients for Windows, Mac, iOS or Android.</p>
<p>For this example, we will connect a &ldquo;server&rdquo; with a &ldquo;client&rdquo;. In reality, all the devices connected through Wireguard are perfectly equivalent, and it is possible to configure the data transfers as you wish.</p>
<h2 id="installation">Installation</h2>
<p>Under Arch Linux, Wireguard is already integrated in the Linux kernel, but installing <code>wireguard-tools</code> allows to get simple configuration tools.</p>
<h2 id="keys-creation">Keys creation</h2>
<p>To communicate, each device will need a public key and an associated private key. The public key must be shared with each machine with which we wish to communicate, and we must benefit in return from their public keys.</p>
<p>Thus, for this example, we will create a private key <code>wg.key</code> with the right permissions, and the corresponding public key <code>wg.pub</code>:</p>
<pre tabindex="0"><code>cd .ssh
(umask 0077; wg genkey &gt; server.key)
wg pubkey &lt; server.key &gt; server.pub
</code></pre><p>We do the same by creating a set of keys for the client:</p>
<pre tabindex="0"><code>cd .ssh
(umask 0077; wg genkey &gt; client.key)
wg pubkey &lt; client.key &gt; client.pub
</code></pre><p>Finally, although it is not mandatory, you can create pre-shared keys that will be used on both devices to strengthen security:</p>
<pre tabindex="0"><code>(umask 0077; wg genpsk &gt; preshared.pub)
</code></pre><h2 id="server-configuration">Server configuration</h2>
<p>The configuration is done in the <code>/etc/wireguard/wg0.conf</code> file where we will start by declaring our interface. Specify the contents of the private key <code>server.key</code> in <code>PrivateKey</code> and the port in <code>ListenPort</code>. The <code>PostUp</code> and <code>PostDown</code> lines designate the commands to be run when activating or deactivating the Wireguard network respectively. These indicate that we have to reroute everything that is requested from or to our client. Your server will have the address <code>10.0.0.1</code> on the Wireguard network.</p>
<pre tabindex="0"><code>[Interface]
PrivateKey = ...
Address = 10.0.0.1/24
ListenPort = 51871
PostUp = sysctl -w net.ipv4.ip_forward=1 ; iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o enp3s0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o enp3s0 -j MASQUERADE
</code></pre><p>The next step is to specify which clients can connect to the VPN. For each of them, we add in the same file a peer section. Specify the contents of the client&rsquo;s public key <code>client.pub</code> in <code>PublicKey</code>, and always the pre-shared key in <code>PreSharedKey</code>. Your client will have the address <code>10.0.0.2</code> on the Wireguard network.</p>
<pre tabindex="0"><code>[Peer]
PublicKey = ...
PreSharedKey = ...
AllowedIPs = 10.0.0.2/32
</code></pre><h2 id="client-configuration">Client configuration</h2>
<p>On the client side, the configuration will also written in <code>/etc/wireguard/wg0.conf</code>. This time we give it the address <code>10.0.0.2</code> and we indicate the private key contained in <code>client.key</code>:</p>
<pre tabindex="0"><code>[Interface]
PrivateKey = ...
Address = 10.0.0.2/24
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o enp3s0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o enp3s0 -j MASQUERADE
</code></pre><p>It is also given the server as peer, the server&rsquo;s public key contained in <code>server.pub</code>, and the pre-shared key. The <code>EndPoint</code> field must indicate the domain name or external IP where the server will be reachable, as well as the port (which must, of course, be externally accessible and routed if necessary). The <code>PersistentKeepalive</code> field is not mandatory but allows to solve connectivity problems when connecting through a NAT router.</p>
<pre tabindex="0"><code>[Peer]
PublicKey = ...
PreSharedKey = ...
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.domain.tld:51871
PersistentKeepalive = 25
</code></pre><p>The <code>AllowedIPs</code> field indicates which IPs will be passed through the VPN. With <code>0.0.0.0/0</code>, all client IPs will be passed through the VPN. If you want to limit the use of the VPN to local services on the server (e.g. self-hosted apps), you can use <code>10.0.0.0/24</code> for example.</p>
<h2 id="activation">Activation</h2>
<p>To start the VPN, you can run <code>wg-quick up wg0</code> with root rights, both on the client and on the server. To turn it off, a simple <code>wg-quick down wg0</code> is enough. It is also possible to use a service to start the VPN automatically at startup:</p>
<pre tabindex="0"><code>sudo systemctl enable --now wg-quick@wg0
</code></pre><h2 id="wireguard-on-your-phone">Wireguard on your phone</h2>
<p>Wireguard has the advantage of being easily available on both iOS and Android, allowing you to access your local network and applications from any wifi network or with cellular data.</p>
<p>In both cases, the application allows you to enter all the fields in the same way as <code>wg0.conf</code>, but of course, entering the certificates by hand would be very tedious.</p>
<p>Fortunately, it is also possible to proceed with a QR code!</p>
<p>Create a public key and a shared key for your phone, as described above, as well as a pre-shared key if necessary. Declare them as a peer on your server, and create a <code>wg0-phone.conf</code> file on your computer.</p>
<p>Once this is done, we can then use <code>qrencode</code> to transform this configuration file into a QR code directly readable (and importable) by the phone, which will be displayed in the terminal:</p>
<pre tabindex="0"><code>qrencode -t ansiutf8 &lt; wg0-iphone.conf
</code></pre><pre tabindex="0"><code class="language-raw" data-lang="raw">βββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββ βββββ β ββββββ ββ βββ β ββββ ββββββ βββββ ββββ
ββββ β β β ββ ββββββββ β β βββββββ ββ β β ββββ
ββββ βββββ ββ βββ ββββ β βββ β βββββββββββ βββββ ββββ
ββββββββββββββ βββββββ β βββ βββββββ β ββββββββββββββ
ββββ βββ βββ βββ ββββββ βββ ββ β βββββ ββ ββββββ
ββββ ββββββββββ ββ β βββ βββββ β βββ ββββββββββββββ
βββββββ βββ ββββββββββββ ββββββββ ββββ β βββ ββββββ
βββββββββββ ββ ββ ββββ ββββ β ββ ββββ βββββ ββββ
ββββ βββββββ ββ βββββ βββ βββ β ββ β βββ βββββββββ
βββββββββββββ β βββββββ β ββ β ββββββββββ β βββββ
ββββ βββ βββ ββββ βββββ βββ β β ββ ββ βββ ββ βββββ
βββββ β βββ β βββ ββ βββ ββ ββββ βββ ββββββββ
ββββββ ββ β βββββ βββ β β ββββββ βββ ββ ββ ββββ
ββββ βββββββββββ βββ ββββββββ βββ ββββββββββββ βββββ
ββββββ β βββ βββ ββ βββ βββ βββ β βββββ βββ βββββ
ββββ ββ ββββββ βββ βββββ βββββ β βββ βββ βββββββββββ
ββββ ββ ββββββ ββββββββββ β ββββ ββββ ββββ ββββββββ
ββββββββ ββββ ββββ βββββββββββββ β β βββ ββββββββββββ
βββββββββββββ ββββ ββββ βββ βββ βββ ββ βββ β βββββ
ββββ βββββ βββββββββ βββ βββ ββββββββ βββ ββββββββ
ββββ β β βββ βββ βββ β ββ βββββ β βββ β ββββββββ
ββββ βββββ βββ β ββ β β β ββ β ββββ ββββ ββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
</code></pre></description>
</item>
<item>
<title>Remote backups with Borg and Rclone</title>
<link>https://sylvaindurand.org/remote-backups-with-borg-and-rclone/</link>
<pubDate>Sat, 19 Mar 2022 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/remote-backups-with-borg-and-rclone/</guid>
<description><p>For a long time, my backup methods were very&hellip; questionable. A few times a year, I would manually copy the files I wanted to download to an external hard drive. Of course, this raised a lot of problems: the backups were irregular and rare (up to six months between two backups, so it was not unusual for me to lose the previous day&rsquo;s work because of a mistake). Moreover, they were done by hand, which was not only painful, but also risked forgetting files or making mistakes. Lastly, the hard disk was stored at my place, which ran the risk of losing everything in case of theft or fire.</p>
<p>So it was time to use a much more serious backup method. My goal is to create a script to automatically back up the files of several folders in an archive, and to synchronize it on a remote server. This archive should :</p>
<ul>
<li>deduplicate the files, i.e. store only the changes from one date to another (but have the possibility to extract the complete backup from any date) so as not to take up a considerable amount of space;</li>
<li>compress the files, to limit the space taken up as well;</li>
<li>encrypt the backup, so that the data cannot be decoded by someone and store it safely in the cloud.</li>
</ul>
<h2 id="archiving-data-with-borg">Archiving data with Borg</h2>
<p>Borg is a deduplicating backup program which supports compression and authenticated encryption. After several tries, I ended up preferring it for its simplicity and performance over its many competitors, such as Restic, Duplicacy or Duplicati.</p>
<h3 id="initialize-the-archive">Initialize the archive</h3>
<p>First, a Borg directory must be initialized. To do this, we use the following command:</p>
<pre tabindex="0"><code>borg init --encryption=repokey /path/to/repo
</code></pre><p>A password is required to encrypt the archive. To avoid having to type it (to be used in a script for example), it is possible to store it in a <code>~/.borg-passphrase</code> file and pass it as an environment variable:</p>
<pre tabindex="0"><code>export BORG_PASSCOMMAND=&#34;cat $HOME/.borg-passphrase&#34;
</code></pre><h3 id="adding-files">Adding files</h3>
<p>Let&rsquo;s imagine that we want to create a &ldquo;Monday&rdquo; archive containing two folders:</p>
<pre tabindex="0"><code>borg create /path/to/repo::Monday ~/folder1 ~/folder2
</code></pre><p>Since the files stored by Borg are <em>compressed</em>, the archive will be smaller than the original files.</p>
<p>The next day, you can create a new &ldquo;Tuesday&rdquo; archive with the same files:</p>
<pre tabindex="0"><code>borg create /path/to/repo::Tuesday ~/folder1 ~/folder2
</code></pre><p>Thanks to deduplication, Borg will only store new data: files that have not been modified are not added a second time, which greatly limits the size of the archive.</p>
<p>Of course, in the case of a script that automates the backup, for example on a daily basis, it is easiest to enter the date as the name of the archive:</p>
<pre tabindex="0"><code>DATE=$(date +%Y-%m-%d)
borg create /path/to/repo::$DATE
</code></pre><h3 id="manipulate-data">Manipulate data</h3>
<p>The manipulation of the data is relatively simple. <code>borg list repo</code> allows to list the existing archives (dates). <code>borg list repo::Monday</code> allows you to list the existing files in an archive, and <code>borg extract repo::Monday</code> allows you to extract it into a directory. It is even possible to use <code>borg mount repo::Monday mnt</code> to mount an image and directly browse the archive.</p>
<h2 id="cloud-storage-with-rclone-and-scaleway">Cloud storage with Rclone and Scaleway</h2>
<p>A good backup should be replicated to a remote site. I decided to use a cloud service, which I don&rsquo;t mind since the data is encrypted.</p>
<p>My choice was Scaleway which offers a Glacier type service: C14 Cold Storage. Its price is really low: β¬0.002 per GB per month with 75 GB of free storage. Incoming and outgoing transfer, archiving and restoration are free.</p>
<p>For 350 GB, it costs me β¬0.5 per month, against β¬3.5 on OVH or Amazon Glacier which in addition charge for the outgoing transfer or extraction.</p>
<p>Last but not least, it is a French service and allows me to store the files in France, without having to trust foreign laws.</p>
<h3 id="configuring-rclone-with-scaleway">Configuring Rclone with Scaleway</h3>
<p>Rclone is, like Rsync with SSH servers, a command line program that allows you to manage or synchronize files in the cloud. It can be used with a lot of cloud providers.</p>
<p>The configuration is simple, and is done through the file <code>.config/rclone/rclone.conf</code> (where <code>access_key_id</code>, <code>secret_access_key</code>, <code>region</code> et <code>endpoint</code> are given by Scaleway):</p>
<pre tabindex="0"><code>[scaleway]
type = s3
provider = Scaleway
access_key_id = xxxxx
secret_access_key = xxxxx
region = fr-par
endpoint = s3.fr-par.scw.cloud
acl = private
storage_class = GLACIER
</code></pre><p>Once your bucket is created, you can then simply synchronize your Borg archive (or, really, any folder) with it:</p>
<pre tabindex="0"><code>rclone sync -v /path/to/repo scaleway:my-bucket
</code></pre></description>
</item>
<item>
<title>Access the logs of a shared OVH server</title>
<link>https://sylvaindurand.org/access-the-logs-of-a-shared-ovh-server/</link>
<pubDate>Sun, 24 Oct 2021 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/access-the-logs-of-a-shared-ovh-server/</guid>
<description><p>As it only weighs a few megabytes, does not generate any income and only has a few hundred visitors each day, I wanted to host this site on a free platform.</p>
<p>The advantage of static sites is that a lot of platforms actually offer free hosting: GitHub Pages, Netlify, Cloudflare, Render all offer a continuous integration that allows to generate and host a static site.</p>
<p>But I had another constraint : to be able to access the server logs, to be able to follow the number of visitors directly from the dedicated script of Matomo, without imposing a tracker to the visitors.</p>
<p><a href="https://github.com/matomo-org/">Matomo Log Analytics script</a></p>
<p>I chose OVH, where I register my domain names, which &ldquo;offers&rdquo; with them a free shared hosting including a very generous storage space of&hellip; 10 megabytes. In my case, minimalism pays: it&rsquo;s more than enough for me!</p>
<p>However, I found that the automated access to the logs was particularly badly documented, hence this short article!</p>
<h3 id="creating-an-account-to-access-the-logs">Creating an account to access the logs</h3>
<p>First of all, OVH requires the creation of a &ldquo;user/password&rdquo; pair to allow access to the logs. To do this, from the customer area, in <code>Web Cloud</code>, <code>Hostings</code>, select your domain name, then go to the <code>Statistics and logs</code> tab. In the <code>User Administration</code> section, create a username and password to access the logs.</p>
<p>Take the opportunity to note the log server access address, which is indicated in the &ldquo;View logs&rdquo; link, such as <code>logs.clusterxxx.hosting.ovh.net</code>.</p>
<h3 id="accessing-the-logs">Accessing the logs</h3>
<p>To access the logs, a simple wget is enough with the right parameters. The following code retrieves the logs of the day before (to be then analyzed by Matomo for example):</p>
<pre tabindex="0"><code>LOG_SERVER=&#34;https://logs.clusterxxx.hosting.ovh.net/&#34;
USER=&#34;youruser&#34;
PASS=&#34;yourpassword&#34;
SITE=&#34;domain.tdl&#34;
MONTH=`date -d &#39;yesterday 13:00&#39; &#39;+%m-%Y&#39;`
DATE=`date -d &#39;yesterday 13:00&#39; &#39;+%d-%m-%Y&#39;`
wget --http-user=$USER --http-password=$PASS \
$LOG_SERVER$SITE&#34;/logs/logs-$MONTH/$SITE-$DATE.gz&#34;
</code></pre></description>
</item>
<item>
<title>Deploy a static website with FTP</title>
<link>https://sylvaindurand.org/deploying-a-static-website-with-ftp/</link>
<pubDate>Sun, 24 Oct 2021 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/deploying-a-static-website-with-ftp/</guid>
<description><p>Until now, I was used to exporting static sites with a simple <code>rsync</code>, which synchronizes the locally generated directory with the files on the server. However, I had to deal with a shared server only accessible through FTP. For me, it was a tool I hadn&rsquo;t used since the 2000s, with CuteFTP and then FileZilla, dragging and dropping files&hellip; Fortunately, the command line tool <code>lftp</code> simply allows to maintain a remote copy &ldquo;in mirror&rdquo; with a local directory.</p>
<h3 id="connection">Connection</h3>
<p>To avoid typing your login credentials in the terminal or logging in automatically, let&rsquo;s create a <code>~/.netrc</code> file containing :</p>
<pre tabindex="0"><code># ~/.netrc
machine yourserver login youruser password yourpassword
</code></pre><p>We can then connect via FTP with :</p>
<pre tabindex="0"><code>lftp user@domain.tld
</code></pre><p>It is also possible to connect with SFTP, which is more secure, by specifying the protocol and port with the following command:</p>
<pre tabindex="0"><code>lftp sftp://user@domain.tld:22
</code></pre><p>It is possible that the first time you type a command, you will get the error <code>Fatal error: Host key verification failed</code>. This is because the RSA key fingerprint is missing from the list of known servers. To fix this, a simple <code>ssh user@domain.tld</code> followed by the <code>yes</code> response when asked if you accept the server.</p>
<h3 id="commands">Commands</h3>
<p>To get a remote copy in a <code>www</code> directory that is exactly the same as a local <code>public</code> directory, use :</p>
<pre tabindex="0"><code>mirror -R public www --delete
</code></pre><p>I also use the options <code>--ignore-time</code> (to send only the necessary sites), <code>--parallel 5</code> to have several simultaneous connections, which speeds up the sending substantially (the possible number depends on your connection and your host), and <code>-v</code> to see the actions performed.</p>
<h3 id="automated-script">Automated script</h3>
<p>To automatically execute these commands in a script, just use <code>EOF</code> and the <code>quit 0</code> command at the end. For example, with FTP (without a secure connection):</p>
<pre tabindex="0"><code>lftp user@domain.tld &lt;&lt; EOF
mirror -R public www --delete --ignore-time --parallel 5 -v
quit 0
EOF
</code></pre><p>And with SFTP (connection via SSH):</p>
<pre tabindex="0"><code>lftp sftp://user@domain.tld:22 &lt;&lt; EOF
mirror -R public www --delete --ignore-time --parallel 5 -v
quit 0
EOF
</code></pre></description>
</item>
<item>
<title>Decrypt multiple drives at boot</title>
<link>https://sylvaindurand.org/decrypt-several-drives-at-boot/</link>
<pubDate>Sat, 23 Oct 2021 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/decrypt-several-drives-at-boot/</guid>
<description><p>The previous articles showed how to use a fully encrypted system which could be remotely unlocked if necessary. In any case, a simple password is enough to decrypt the main disk and start the system:</p>
<p><a href="https://sylvaindurand.org/arch-linux-with-full-encryption/">Arch Linux with full encryption</a></p>
<p><a href="https://sylvaindurand.org/remotely-unlock-an-encrypted-system/">Remotely unlock an encrypted system</a></p>
<p>In my case, however, several other hard disks are also encrypted, not necessarily with the same passwords: here we will see how to decrypt them all at once, with a single password.</p>
<p>To do this, I create a random key, which will be stored on my main (encrypted) disk:</p>
<pre tabindex="0"><code>head -c 64 /dev/urandom &gt; /root/.data.key
chmod 600 /root/.data.key
</code></pre><p>Assuming that the disk to be decrypted is <code>/dev/sda1</code>, I can then tell <code>cryptsetup</code> to add this file to it as the encryption key (the current password will be retained):</p>
<pre tabindex="0"><code>cryptsetup -v luksAddKey -i 1 /dev/sda1 /root/.data.key
</code></pre><p>In order for the disk to be decrypted at boot time, I edit <code>/etc/crypttab</code> to add :</p>
<pre tabindex="0"><code># /etc/crypttab
data UUID=$(blkid /dev/sda1 -o value -s UUID) /root/.data.key
</code></pre><p>And <code>/etc/fstab</code> :</p>
<pre tabindex="0"><code># /etc/fstab
/dev/mapper/data /media/data ext4 rw,noatime 0 2
</code></pre><p>At boot time, as soon as the system is decrypted and started, <code>/etc/fstab</code> and <code>/etc/crypttab</code> will then automatically mount the disk and decrypt it using the newly created file.</p>
</description>
</item>
<item>
<title>Remotely unlock an encrypted system</title>
<link>https://sylvaindurand.org/remotely-unlock-an-encrypted-system/</link>
<pubDate>Tue, 05 Oct 2021 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/remotely-unlock-an-encrypted-system/</guid>
<description><p>I self-host a server under Arch Linux on which are stored various personal data: documents, photos, music, videos&hellip; Its data are fully encrypted, including the main system, to avoid any risk &ndash; in case of burglary for example.</p>
<p><a href="https://sylvaindurand.org/arch-linux-with-full-encryption/">Arch Linux with full encryption</a></p>
<p>However, encrypting the whole system raises a difficulty: a password is needed at every reboot. This means that it is necessary to connect a keyboard and a screen to the server, which is not necessarily practical if it is only intended to be accessed only by SSH.</p>
<p>Above all, this server must remain constantly available: in case of power failure or malfunction, it must be able to be restarted without me necessarily being on site.</p>
<p>The solution is simple: when the password prompt appears, launch a minimal SSH session that allows to enter the password.</p>
<p>This article is directly inspired by the dm-crypt/Specialties page from the Arch Linux wiki, which shows different ways to do this. It assumes that you already have a fully functioning encrypted system.</p>
<p><a href="https://wiki.archlinux.org/title/Dm-crypt/Specialties">Arch Wiki: dm-crypt/Specialties</a></p>
<h3 id="packages">Packages</h3>
<p>We will use <code>mkinitcpio-netconf</code>, which allows network access during the early boot phase, <code>mkinitcpio-tinyssh</code> and <code>tinyssh-convert</code> to initiate SSH access, and <code>mkinitcpio-utils</code> to get a session:</p>
<pre tabindex="0"><code>sudo pacman -Syu mkinitcpio-netconf \
mkinitcpio-tinyssh \
tinyssh-convert \
mkinitcpio-utils
</code></pre><h3 id="network">Network</h3>
<p>To get network access, it is necessary to pass connection information with the <code>ip</code> option to the kernel at boot time. My server connects directly with DHCP on the eth0 interface, so I use :</p>
<pre tabindex="0"><code>ip=:::::eth0:dhcp
</code></pre><p>If your router connects without DHCP to a static IP, we can use :</p>
<pre tabindex="0"><code>ip=192.168.1.1:::::eth0:none
</code></pre><p>If you need to connect via wifi, the AUR package <code>mkinitcpio_wifi</code>: the documentation is detailed here:</p>
<p><a href="https://wiki.archlinux.org/title/Dm-crypt/Specialties#Remote_unlock_via_wifi">Arch Wiki: Remote unlock via wifi</a>.</p>
<h3 id="key">Key</h3>
<p>To connect at startup, it is necessary to send your public key. TinySSH only accepts Ed25519 or ECDSA keys; I use the first type with <code>ssh-keygen -t ed25519 -a 100</code>.</p>
<p>This public key must be placed in the <code>/etc/tinyssh/root_key</code> file.</p>
<p>To use the same key that you already use to SSH into the server, just copy it:</p>
<pre tabindex="0"><code>cp ~/.ssh/authorized_keys /etc/tinyssh/root_key
</code></pre><h3 id="launching-in-the-boot-sequence">Launching in the boot sequence</h3>
<p>Finally, we modify the <code>/etc/mkinitcpio.conf</code> file to replace <code>encrypt</code> with <code>netconf tinyssh encryptssh</code> in the line that starts with <code>HOOKS</code>.</p>
<p>Finally, we incorporate the changes with <code>sudo mkinitcpio -P</code>.</p>
<h3 id="locally">Locally</h3>
<p>All that remains is to create a simple configuration, locally, to unlock your server. We modify <code>~/.ssh/config</code> with :</p>
<pre tabindex="0"><code>Host unlock
Hostname domain.tld
User root
IdentityFile ~/.ssh/key
</code></pre><p>Then, when the machine is waiting for the password at startup, you just have to run <code>ssh unlock</code> to be able to type the password!</p>
</description>
</item>
<item>
<title>Setting up a dynamic DNS with OVH</title>
<link>https://sylvaindurand.org/setting-up-a-dynamic-dns-with-ovh/</link>
<pubDate>Mon, 06 Sep 2021 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/setting-up-a-dynamic-dns-with-ovh/</guid>
<description><p>My ISP, like most of the others in France, only assigns dynamic IP addresses to its customers: this means that the IP address is likely to change at any time, especially when my box is updated or rebooted. Most of the time, the possibility to have a fixed IP address is an option, reserved for expensive offers.</p>
<p>For most users, this does not present any practical problems. However, the situation becomes more complicated when you want to self-host sites or services that are intended to be accessible from the outside: how can you associate a domain name with a machine whose IP address can change at any time?</p>
<p>This is the principle of dynamic DNS: you install on your machine a small software that regularly checks the current IP address, and as soon as it changes, sends it to your provider to update the DNS zone accordingly.</p>
<p>This article shows how to do this with OVH and a Raspberry Pi running Raspbian.</p>
<h2 id="configuration-on-the-ovh-side">Configuration on the OVH side</h2>
<p>This part assumes that the domain name is managed by OVH, and that no record exists in the DNS zone for the domain name or subdomain that will point to our machine.</p>
<h3 id="creation-of-a-dynhost-identifier">Creation of a DynHost identifier.</h3>
<p>The first step is to create a DynHost ID, which will automatically update the DNS record.</p>
<p>To do this, go to <code>Web Cloud</code>, <code>Domain name</code>, select your domain name (we will use <code>domain.tld</code> for the rest of this article), then the <code>DynHost</code> tab. Then select <code>Manage accesses</code> and <code>Create a login</code>.</p>
<p>We indicate the login suffix (in my case, <code>domain.tld-pi</code> for a Raspberry Pi), the subdomain concerned (<code>*</code> to manage all domains with this login) and a password. This account will allow us to identify ourselves to update the zone.</p>
<p>Once the DNS zone is propagated, your server should now be accessible from the chosen domain name.</p>
<h3 id="setting-up-the-dynamic-recording">Setting up the dynamic recording</h3>
<p>Once the identifier has been created, we can now create the DNS record. Still in the <code>DynHost</code> tab, we select <code>Add a DynHost</code>, then we indicate the domain name concerned, and the current IP address. It is this last one which will be updated automatically.</p>
<h2 id="automate-the-dns-change">Automate the DNS change</h2>
<p>Our server must now be able to update the DNS zone when it detects an IP change. We will use <code>ddclient</code> for this:</p>
<pre tabindex="0"><code>sudo apt install ddclient
</code></pre><p>Let&rsquo;s skip the questions that are asked. Once finished, we directly modify the configuration file as follows (with, of course, your login, your password and your domain):</p>
<pre tabindex="0"><code># /etc/ddclient.conf
protocol=dyndns2
use=web, web=checkip.dyndns.com, web-skip=&#39;Current IP Address&#39;
server=www.ovh.com
login=domain.tld-user
password=&#39;password&#39;
subdomain.domain.tld
</code></pre><p>Then, we restart the service:</p>
<pre tabindex="0"><code>sudo systemctl restart ddclient
</code></pre><p>Your server will now communicate to OVH the new IP address when it changes!</p>
</description>
</item>
<item>
<title>Arch Linux with full encryption</title>
<link>https://sylvaindurand.org/arch-linux-with-full-encryption/</link>
<pubDate>Tue, 16 Mar 2021 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/arch-linux-with-full-encryption/</guid>
<description><p>Over the past years, I have installed Arch Linux several dozen times on different devices. This article allows me to easily find the steps to install the distribution on a Dell XPS 9300, with UEFI and full encryption of the whole computer.</p>
<p>Warning: the Arch Linux installation guide should remain your reference if you try to install on your own machine. Not only can the instructions evolve over time, but it alone will allow you to adapt the steps to your needs. It is, finally, something very instructive!</p>
<h3 id="creating-the-installation-disk">Creating the installation disk</h3>
<p>Download the installation image from the Arch Linux website.</p>
<p>Then, insert a USB stick or micro SD card, find its address with <code>fdisk -l</code>. You can then copy the image of the key (every data on it will be lost) with :</p>
<pre tabindex="0"><code>sudo dd bs=4M if=&lt;image.iso&gt; of=/dev/&lt;sdx&gt; oflag=sync
</code></pre><h3 id="booting-from-the-installation-disk">Booting from the installation disk</h3>
<p>After making sure you have disabled Secure Boot from the BIOS for the most recent computers, you turn on the computer from the USB key: once launched, the installer consists of a simple terminal.</p>
<h3 id="keyboard-layout">Keyboard layout</h3>
<p>The keyboard uses an American layout by default. Being French, I use:</p>
<pre tabindex="0"><code>loadkeys fr-latin9
</code></pre><h3 id="network">Network</h3>
<p>For the rest of the installation, we will need the Internet to retrieve the packages. To connect to a wifi network, we use (where [ssid] is the name of your access point):</p>
<pre tabindex="0"><code>iwctl station wlan0 connect [ssid]
</code></pre><h3 id="clock">Clock</h3>
<p>To avoid any synchronization problems, we will also update the computer&rsquo;s clock with it:</p>
<pre tabindex="0"><code>timedatectl set-ntp true
</code></pre><h3 id="partitioning">Partitioning</h3>
<p>I choose to format the computer to create two partitions: a 100 MB boot partition, and a second one containing the system filling the rest of the space. Of course, all data will be deleted!</p>
<p>You can see which disks and partitions exist with <code>parted -l</code>.</p>
<p>First of all, in my case, I delete the previous UEFI entry:</p>
<pre tabindex="0"><code>efibootmgr -b 0 -B
</code></pre><p>In my case, the name of the main disk is <code>/dev/nvme0n1</code>. To partition it, I run:</p>
<pre tabindex="0"><code>wipefs -af /dev/nvme0n1
parted -s /dev/nvme0n1 mklabel gpt
parted -s /dev/nvme0n1 mkpart primary fat32 1MiB 100MiB
parted -s /dev/nvme0n1 set 1 esp on
parted -s /dev/nvme0n1 mkpart primary ext4 100MiB 100%
</code></pre><h3 id="formatting">Formatting</h3>
<p>These two partitions are then formatted, creating an encrypted space on the second one:</p>
<pre tabindex="0"><code>mkfs.fat -F32 /dev/nvme0n1p1
cryptsetup -y -v luksFormat --iter-time 100 /dev/nvme0n1p2
cryptsetup open /dev/nvme0n1p2 cryptroot
mkfs.ext4 /dev/mapper/cryptroot
</code></pre><p>Finally, we mount these partitions with :</p>
<pre tabindex="0"><code>mount /dev/mapper/cryptroot /mnt
mkdir /mnt/boot
mount /dev/nvme0n1p1 /mnt/boot
</code></pre><h3 id="installation">Installation</h3>
<p>We use <code>pacstrap</code> to install the minimal system on our partition, and create the <code>fstab</code> file. I add <code>iwd</code> to have wifi on reboot, and <code>intel-ucode</code> for Intel processors:</p>
<pre tabindex="0"><code>pacstrap /mnt base linux linux-firmware iwd intel-ucode
genfstab -U /mnt &gt;&gt; /mnt/etc/fstab
</code></pre><p>You then enter the newly created system with:</p>
<pre tabindex="0"><code>arch-chroot /mnt
</code></pre><h3 id="localization">Localization</h3>
<p>You choose your time zone, language, and keyboard with commands like this:</p>
<pre tabindex="0"><code>ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime
hwclock --systohc
echo &#34;LANG=fr_FR.UTF-8&#34; &gt; /etc/locale.conf
echo &#34;KEYMAP=fr-latin9&#34; &gt; /etc/vconsole.conf
echo &#34;en_US.UTF-8 UTF-8
fr_FR.UTF-8 UTF-8&#34; &gt;&gt; /etc/locale.gen
locale-gen
</code></pre><h3 id="network-1">Network</h3>
<p>We then activate the different services to benefit from wifi and DNS at startup:</p>
<pre tabindex="0"><code>systemctl enable systemd-networkd systemd-resolved iwd
echo &#34;[General]
EnableNetworkConfiguration=True&#34; &gt;&gt; /etc/iwd/main.conf
</code></pre><p>We also need to specify our hostname:</p>
<pre tabindex="0"><code>echo &#34;xps&#34; &gt; /etc/hostname
echo &#34;127.0.0.1 localhost
::1 localhost
127.0.1.1 xps.localdomain&gt;xps&#34; &gt;&gt; /etc/hosts
</code></pre><h3 id="initramfs">Initramfs</h3>
<p>The following instructions are added to request the password at startup:</p>
<pre tabindex="0"><code>sed -i &#39;s/keyboard/keyboard keymap encrypt/&#39; /etc/mkinitcpio.conf
mkinitcpio -P
</code></pre><h3 id="root-password">Root password</h3>
<p>Create a root password with:</p>
<pre tabindex="0"><code>passwd
</code></pre><h3 id="bootloader">Bootloader</h3>
<p>We simply use <code>efibootmgr</code> to create a simple UEFI boot entry without any bootloader:</p>
<pre tabindex="0"><code>UUID1=$(blkid /dev/nvme0n1p2 -o value -s UUID)
UUID2=$(blkid /dev/mapper/cryptroot -o value -s UUID)
efibootmgr -d /dev/nvme0n1 -p 1 -c -L &#34;Arch Linux&#34; \
-l /vmlinuz-linux -u &#34;cryptdevice=UUID=${UUID1}:cryptroot \
root=UUID=${UUID2} rw quiet \
initrd=\intel-ucode.img initrd=\initramfs-linux.img&#34;
</code></pre><h3 id="the-end-">The end !</h3>
<p>You can leave the installation and reboot with:</p>
<pre tabindex="0"><code>exit
reboot
</code></pre></description>
</item>
<item>
<title>From cron tasks to systemd timers</title>
<link>https://sylvaindurand.org/from-cron-tasks-to-systemd-timers/</link>
<pubDate>Tue, 08 Dec 2020 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/from-cron-tasks-to-systemd-timers/</guid>
<description><p>Like the vast majority of users of Linux systems, I have long used cron tasks when it came to running tasks at regular intervals.</p>
<p>The tasks to be performed are to be entered in a text file, accessible from <code>crontab -e</code>:</p>
<pre tabindex="0"><code># m h dom mon dow command
- * * * * /path/to/script.sh # each minutes
@reboot /path/to/script.sh # on startup
</code></pre><p>The disadvantages are multiple. The smallest unit of time is the minute. It is not possible to easily activate, deactivate, start or stop a command. Finally, it is very hard to see or simulate what is really happening.</p>
<p>Nowadays, many Linux distributions embed <code>systemd</code>, which makes it easy to perform this kind of operations.</p>
<h2 id="creating-a-service">Creating a service</h2>
<p>With <code>systemd</code>, it&rsquo;s all about services. Here is an example to create a very simple service:</p>
<pre tabindex="0"><code># /etc/systemd/system/myservice.service
[Service]
Type=oneshot
ExecStart=path/to/script.sh
# WorkingDirectory=/home/user/path/
# User=user
</code></pre><p>It indicates that the script must be started only once, and its address. But it is also possible to indicate the working directory, the user who launches it&hellip; And even more if other services are needed, when to launch it, etc.</p>
<p>You can check that everything works fine with <code>systemctl start myservice</code>.</p>
<h2 id="creating-the-timer">Creating the timer</h2>
<p>The timer will have the same name, but will end with <code>.timer</code>:</p>
<pre tabindex="0"><code># /etc/systemd/system/myservice.timer
[Timer]
OnCalendar=*:*:0/10
[Install]
WantedBy=timers.target
</code></pre><p>The <code>OnCalendar</code> line allows you to specify the frequency, or the activation date. The most frequent synthesis is <code>*-*-* *:*:*</code>, with the date and time. In the example above, the script is launched every 10 seconds.</p>
<p>In order for our changes to be taken into account, we need to run the following command, the files being cached:</p>
<pre tabindex="0"><code>systemctl daemon-reload
</code></pre><p>Finally, the timer is launched, making sure that it restarts at startup:</p>
<pre tabindex="0"><code>systemctl enable --now myservice.timer
</code></pre></description>
</item>
<item>
<title>Real-time log analytics with Matomo</title>
<link>https://sylvaindurand.org/real-time-log-analytics-with-matomo/</link>
<pubDate>Sun, 06 Dec 2020 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/real-time-log-analytics-with-matomo/</guid>
<description><p>I must admit, I like to know how many people visit my site. However, I hate trackers, and I won&rsquo;t consider using Google Analytics which would retrieve much more private information about my users for its own account.</p>
<p>I really appreciate Matomo (formerly Piwik), which is open source, offers a quality interface and plenty options.</p>
<p>By default, Matomo uses a javascript tracker, but it also has a script to import server logs. This makes it possible to have statistics without any tracker, impossible to block and therefore reliable, with a very low load for the server.</p>
<p>Most of the time, these scripts work once a day, but it is possible to implement it almost in real time without the need of a rsyslog or syslog-ng setup. We will see here how to read Nginx logs in real time under Matomo.</p>
<h2 id="nginx-configuration">Nginx configuration</h2>
<p>First of all, we&rsquo;re going to use a more readable and parsable json log format in nginx. In <code>/etc/nginx.conf</code>, we declare a new log format named matomo:</p>
<pre tabindex="0"><code># /etc/nginx.conf
log_format matomo &#39;{&#39;
&#39;&#34;ip&#34;: &#34;$remote_addr&#34;,&#39;
&#39;&#34;host&#34;: &#34;$host&#34;,&#39;
&#39;&#34;path&#34;: &#34;$request_uri&#34;,&#39;
&#39;&#34;status&#34;: &#34;$status&#34;,&#39;
&#39;&#34;referrer&#34;: &#34;$http_referer&#34;,&#39;
&#39;&#34;user_agent&#34;: &#34;$http_user_agent&#34;,&#39;
&#39;&#34;length&#34;: $bytes_sent,&#39;
&#39;&#34;generation_time_milli&#34;: $request_time,&#39;
&#39;&#34;date&#34;: &#34;$time_iso8601&#34;}&#39;;
</code></pre><p>Finally, in the <code>server { }</code> tag which contains the configuration of your site, indicate the location and format where your logs will be stored:</p>
<pre tabindex="0"><code>access_log /var/log/nginx/access.log matomo;
</code></pre><p>Finally, after restarting nginx, the logs should then start in the specified file.</p>
<h2 id="matomo-log-importation">Matomo log importation</h2>
<p>Matomo comes with a small python script, very efficient, to read the logs and import them: <code>log-analytics/import_logs.py</code>.</p>
<p>We will use <code>logtail</code>, which allows, at each use, to read only the new lines added to the logs since the last time it has been launched. This avoids double counting.</p>
<p>We can then pass the result to the Python script:</p>
<pre tabindex="0"><code>/usr/sbin/logtail /var/log/nginx/access.log \
| /usr/bin/python \
path/to/matomo/misc/log-analytics/import_logs.py
--url=https://&lt;your-matomo-url&gt; \
--enable-http-errors \
--enable-http-redirects \
--log-format-name=nginx_json -
</code></pre><p>To be recognized, the host must be correctly filled in the Matomo parameters. If you have several sites, it allows to distribute them automatically.</p>
<p>This code can be launched with a <code>timer systemd</code> or <code>cron</code>, for example every minute. This makes it possible to update in real time the last visits and the visitor map on Matomo.</p>
<p>However, in order to update the graphs and store long-term data, it is necessary to archive them.</p>
<p>By default, this is done automatically by browsing the site from the browser.</p>
<p>However, it is also possible to disable this possibility, and launch it from a cron task. The command is:</p>
<pre tabindex="0"><code>php path/to/matomo/console core:archive \
--url=&lt;your-matomo-url&gt; \
--force-all-websites \
--force-date-last-n=2
</code></pre></description>
</item>
<item>
<title>Gemini and Hugo</title>
<link>https://sylvaindurand.org/gemini-and-hugo/</link>
<pubDate>Fri, 04 Dec 2020 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/gemini-and-hugo/</guid>
<description><p>For a few years, I have been using Hugo, a static site generator, to produce these pages. At the same time very fast and corresponding perfectly to my needs, it is above all very modular.</p>
<p>I was therefore not surprised to see that it was quite easy to convert, with little effort, my site for the Gemini protocol. This was not done without some tricks. Let&rsquo;s see how!</p>
<h2 id="declaring-gemini-as-an-output-format">Declaring Gemini as an output format</h2>
<p>Hugo can output content in multiple formats: most of them are already predefined, but it is also possible to create your own. This is what we are going to do for Gemini.</p>
<p>First, in the configuration file <code>config.yml</code> we will declare a new type <code>text/gemini</code> with the file suffix <code>.gmi</code>:</p>
<pre tabindex="0"><code>mediaTypes:
text/gemini:
suffixes:
- &#34;gmi&#34;
</code></pre><p>Once this is done, we declare a new output format, which uses this type, which is given the name <code>GEMINI</code>.</p>
<pre tabindex="0"><code>outputFormats:
GEMINI:
name: GEMINI
isPlainText: true
isHTML: false
mediaType: text/gemini
protocol: &#34;gemini://&#34;
permalinkable: true
</code></pre><p>Finally, it only remains to ask Hugo to generate pages for the different contents. For example, in my case:</p>
<pre tabindex="0"><code>outputs:
home: [&#34;HTML&#34;, &#34;RSS&#34;, &#34;GEMINI&#34;]
page: [&#34;HTML&#34;, &#34;GEMINI&#34;]
</code></pre><p>To be able to generate the files, it is now necessary to create layouts to see how to display them!</p>
<h2 id="index-page">Index page</h2>
<p>To start with the index, we can start with <code>layout/index.gmi</code>. For example, here is a simple text, followed by a list of posts:</p>
<pre tabindex="0"><code>## List of posts
{{ range .RegularPages }}
=&gt; {{ .RelPermalink }} {{ .Title }}
{{- end }}
</code></pre><p>Here, I sort the articles in descending chronological order, grouping them by date. This gives the following code:</p>
<pre tabindex="0"><code>## Posts grouped by year
{{ range .RegularPages.GroupByDate &#34;2006&#34; }}
### {{ .Key }}
{{ range .Pages.ByDate.Reverse }}
=&gt; {{ .RelPermalink }} {{ .Title }}
{{- end }}
{{ end }}
</code></pre><h2 id="posts">Posts</h2>
<p>For posts, we can create a <code>layout/_default/single.gmi</code>. Basically, it would suffice to display the title and content:</p>
<pre tabindex="0"><code># {{ .Title }}
{{ .RawContent }}
</code></pre><h3 id="images">Images</h3>
<p>For images, I extract them with a simple regex and show them as a link:</p>
<pre tabindex="0"><code>{{- $content := .RawContent -}}
{{- $content = $content | replaceRE `\!\[(.+?)\]\((.+?)\)` &#34;=&gt; $2 Image: $1&#34; }}
{{ $content }}
</code></pre><h3 id="links">Links</h3>
<p>For the links, I decided to simply not use inline links on the site, but only put the links on a single paragraph. This allows me, as before, a very simple regex:</p>
<pre tabindex="0"><code>{{- range findRE `\[.+?\]\(.+?\)` $content }}
{{- $content = $content | replaceRE `\[(.+?)\]\((.+?)\)(.+)` &#34;$1$3\n\n=&gt; $2 $1 &#34; }}
{{- end }}
</code></pre><p>However, this is not a very satisfactory method when you have a site that has a lot of links online. A solution, proposed by the site Brain Baking, allows you to reference each link with a number ([1], [2]&hellip;) and then to put the links underneath, automatically, thanks to a clever code:</p>
<p><a href="https://brainbaking.com/post/2021/04/using-hugo-to-launch-a-gemini-capsule/">Using Hugo to Launch a Gemini Capsule</a></p>
<h3 id="navigation-to-other-pages">Navigation to other pages</h3>
<p>If you want to add links for previous and next articles with:</p>
<pre tabindex="0"><code>{{ if .Next }}=&gt; {{ .Next.RelPermalink }} β Newer: {{ .Next.Title }}{{ end }}
{{ if .Prev -}}=&gt; {{ .Prev.RelPermalink }} β Older: {{ .Prev.Title }}{{- end }}
</code></pre><h2 id="feeds">Feeds</h2>
<p>To create RSS feeds, we can create a new output format, then define its layout.</p>
<h3 id="rss">RSS</h3>
<p>We will do the same here! In <code>config.yml</code>, we define:</p>
<pre tabindex="0"><code>outputFormats:
GEMINI_RSS:
baseName: &#34;feed&#34;
mediaType: &#34;application/rss+xml&#34;
isPlainText: false
outputs:
home: [&#34;HTML&#34;, &#34;GEMINI&#34;, ..., &#34;GEMINI_RSS&#34;]
</code></pre><p>Then, we create <code>layouts/index.gemini_rss.xml</code> with the following content:</p>
<pre tabindex="0"><code>&lt;rss version=&#34;2.0&#34; xmlns:atom=&#34;http://www.w3.org/2005/Atom&#34;&gt;
&lt;channel&gt;
&lt;title&gt;{{ .Site.Title }}&lt;/title&gt;
&lt;description&gt;{{ i18n &#34;description&#34; }}&lt;/description&gt;
&lt;link&gt;{{ (replace .Permalink &#34;https://&#34; &#34;gemini://&#34;) | safeURL }}&lt;/link&gt;
&lt;atom:link href=&#34;{{ .Permalink | safeURL }}feed.xml&#34; rel=&#34;self&#34; type=&#34;application/rss+xml&#34; /&gt;
{{- range .RegularPages }}
&lt;item&gt;
&lt;title&gt;{{ .Title }}&lt;/title&gt;
&lt;link&gt;{{ (replace .Permalink &#34;https://&#34; &#34;gemini://&#34;) | safeURL }}&lt;/link&gt;
&lt;pubDate&gt;{{ .Date.Format &#34;Mon, 02 Jan 2006 15:04:05 -0700&#34; | safeHTML }}&lt;/pubDate&gt;
&lt;guid&gt;{{ (replace .Permalink &#34;https://&#34; &#34;gemini://&#34;) | safeURL }}&lt;/guid&gt;
&lt;/item&gt;
{{ end }}
&lt;/channel&gt;
&lt;/rss&gt;
</code></pre><p>The RSS feed is now available on <code>/feed.xml</code>.</p>
<h2 id="export">Export</h2>
<p>I use <code>rsync</code> to easily export my files to the server:</p>
<pre tabindex="0"><code>hugo
rsync -avz --no-perms --no-owner --no-group \
--no-times --delete public/ vps:/var/gemini
rm -rf public
</code></pre><p>This last folder is then read by a gemini server, as explained in the previous article:</p>
<p><a href="https://sylvaindurand.org/discovering-the-gemini-protocol/">Discovering the Gemini protocol</a></p>
</description>
</item>
<item>
<title>Discovering the Gemini protocol</title>
<link>https://sylvaindurand.org/discovering-the-gemini-protocol/</link>
<pubDate>Thu, 03 Dec 2020 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/discovering-the-gemini-protocol/</guid>
<description><p>Recently, I made the discovery of the Gemini protocol, developed earlier this year, and I must say I was immediately quite excited about it.</p>
<p>I miss the web of 20 years ago: simpler, open, where you used to browse directories to find some small websites, often made by passionate amateurs. All the content was available, there were no barriers, no social networks, no tracking. The pages did not exceed a few tens of kilobytes.</p>
<p>Above all, we used to meet people there: small personal sites, forums and IRC chats were meeting places where one could share and exchange a lot of things.</p>
<p>Internet has been privatized. The small web still exists, but is buried and hardly appears in search engines anymore. There is more content and people than ever before, and yet one quickly locks oneself into the same routines and circles. Web languages have become so complex that building a new browser has become impossible. We have built an oligopoly.</p>
<p>Gemini takes the daring step of starting from scratch: rather than starting from scratch with HTTP and HTML, it reinvents them, in a deliberately minimalist and non-extensible way.</p>
<h2 id="gemini-server-with-agate">Gemini server with Agate</h2>
<p>It is said that Gemini is designed so that a server or client can be designed in less than 24 hours. In fact, there are already a large number of alternatives! I finally chose Agate.</p>
<p>On my server under Debian, I directly install the binaries from Github.</p>
<pre tabindex="0"><code>gunzip agate.x86_64-unknown-linux-gnu.gz
mv agate.x86_64-unknown-linux-gnu agate
chmod +x agate
sudo mv agate /usr/local/bin/agate
</code></pre><p>I gives the correct writes to <code>/var/gemini</code> (assuming the user is part of <code>www-data</code> group):</p>
<pre tabindex="0"><code>mkdir /var/gemini
sudo chown -R www-data:www-data /var/gemini
sudo chmod -R g+rwX /var/gemini
</code></pre><p>The server is then simply started with:</p>
<pre tabindex="0"><code>agate --content /var/gemini/ --hostname domain.tld
</code></pre><p>The space will then be available at <code>gemini://domain.tld</code>. To start it at boot, I use <code>systemd</code>:</p>
<pre tabindex="0"><code># /etc/systemd/system/agate.service
[Unit]
Description=Agate
[Service]
Type=simple
Restart=always
RestartSec=5
Environment=&#34;PYTHONUNBUFFERED=1&#34;
ExecStart=agate --content /var/gemini/ --hostname domain.tld
StandardOutput=file:/var/log/gemini.log
StandardError=file:/var/log/gemini_error.log
[Install]
WantedBy=default.target
</code></pre><p>We can then start it with:</p>
<pre tabindex="0"><code>sudo systemctl start agate
</code></pre><p>To activate it on reboot:</p>
<pre tabindex="0"><code>sudo systemctl enable agate
</code></pre><h2 id="first-page">First page</h2>
<p>Gemini uses simple text files, with the extension <code>.gmi</code>.</p>
<p>The specification is particularly simple! Text, without any formating. Possibility to make headings, codes and quotations with a markdown-like language. The links have original syntax with &ldquo;=&gt;&rdquo;.</p>
<p>An <code>index.gmi</code> located in <code>/var/gemini</code> should now be available with a Gemini client.</p>
<h2 id="gemini-client">Gemini client</h2>
<p>As with servers, there are many possible clients. I chose Amfora, which I find very pleasant to use, and which has the taste of being directly available with <code>pacman</code>.</p>
<pre tabindex="0"><code>sudo pacman -Syu amfora
</code></pre><h2 id="first-impressions">First impressions</h2>
<p>Of course, this minimalist tool will only be of interest to a very few. The world on Gemini is very small. A few pages allow to list the servers, and a small search engine is active. A few hundred sites exist, not all of them are active.</p>
<p>On the other hand, browsing these pages allowed me to discover many individuals, with their blogs (or rather their gemlog), and I took the time to read artisanal content that I hadn&rsquo;t chosen (or that hadn&rsquo;t been chosen for me), which hadn&rsquo;t happened for a long time!</p>
<p>Obviously, this protocol will not supplant the web, and it does not pretend to do so. I don&rsquo;t know if it will gather a sufficient community to live by itself (even if some interesting names, including one of the creators of Sway are there!). But I wish it to find his niche.</p>
</description>
</item>
<item>
<title>Dynamic wallpapers with Sway</title>
<link>https://sylvaindurand.org/dynamic-wallpapers-with-sway/</link>
<pubDate>Wed, 02 Dec 2020 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/dynamic-wallpapers-with-sway/</guid>
<description><p>During the last few months, I fell in love with Sway, a tiling window manager modeled on i3 that runs natively under Wayland. I found there all the efficiency and all my habits acquired under i3, but with a much smoother and more responsive display.</p>
<p>All the applications I use run very smoothly, and I am discovering the joy of a responsive display without tearing. Firefox in particular has an incredibly smooth scroll with a touchpad, which until now only existed under macOS.</p>
<p>On a daily basis, I use <code>waybar</code> for displaying workspaces and status bar, <code>kitty</code> for the terminal emulator and <code>wofi</code> as an launcher. All the applications I use every day are Wayland native, with the exception of <code>steam</code> and <code>gimp</code>: for these, <code>xwayland</code> makes the gateway with unfortunately a lack of HiDPI support for the moment which results in pixelized applications.</p>
<p>The only thing I regretted however, although useless and fancy, is to be able to automate a wallpaper change. It is managed by <code>swaybg</code> which, when restarted, displays a grey screen for a moment.</p>
<h2 id="reloading-the-wallpaper-without-a-grey-blink">Reloading the wallpaper without a grey blink</h2>
<p>The trick is not to relaunch <code>swaybg</code>, but to create a new instance and then kill the old one.</p>
<p>Let&rsquo;s say you launch your wallpaper with:</p>
<pre tabindex="0"><code>swaybg -i image1.png -m fill &amp;
</code></pre><p>The process identifier (PID) of the last command is automatically stored in <code>$!</code>. We record it in a variable:</p>
<pre tabindex="0"><code>PID=$!
</code></pre><p>When desired, we can launch a second instance with a second wallpaper. This one will not appear at the moment because the first instance is still running:</p>
<pre tabindex="0"><code>swaybg -i image2.png -m fill &amp;
</code></pre><p>The wallpaper will then refresh, without a gray screen, as soon as the first instance is killed:</p>
<pre tabindex="0"><code>kill $PID
</code></pre><h2 id="random-wallpaper-change">Random wallpaper change</h2>
<p>Suppose we have a folder <code>img/</code> containing several images. It is possible to select one of them randomly with:</p>
<pre tabindex="0"><code>find img/. -type f | shuf -n1
</code></pre><p>To randomly change the wallpaper, you can then use:</p>
<pre tabindex="0"><code>swaybg -i $(find img/. -type f | shuf -n1) -m fill &amp;
</code></pre><p>One can then create, on this basis, a small script that changes the wallpaper, for example every ten minutes (600 seconds):</p>
<pre tabindex="0"><code>#!/bin/sh
swaybg -i $(find img/. -type f | shuf -n1) -m fill &amp;
OLD_PID=$!
while true; do
sleep 600
swaybg -i $(find img/. -type f | shuf -n1) -m fill &amp;
NEXT_PID=$!
sleep 5
kill $OLD_PID
OLD_PID=$NEXT_PID
done
</code></pre><p>Make it executable with <code>chmod +x</code>, then you can load it automatically with Sway by adding, in your sway config:</p>
<pre tabindex="0"><code>exec &#34;cd your_script_folder &amp;&amp; ./your_script.sh&#34;
</code></pre><p>A better way would be to use <code>pidof swaybg</code>, then <code>kill</code>: it is illustrated in the next section.</p>
<h2 id="dynamic-wallpaper-depending-of-the-hour-of-the-day">Dynamic wallpaper depending of the hour of the day</h2>
<p>I recently made a dynamic wallpaper, based on the following animation:</p>
<p><a href="https://louie.co.nz/25th_hour/">Louis Coyle&rsquo;s 25th hour</a></p>
<p>The animation was recorded and transformed into 144 images, 6 images per hour, making it possible to obtain a wallpaper that changes every 10 minutes.</p>
<p>The script uses this command to round the current time to ten minutes:</p>
<pre tabindex="0"><code>#!/bin/sh
#!/bin/sh
cd projets/lakeside
while true; do
PID=`pidof swaybg`
swaybg -i ./img/$(date -u -d @$((($(date -u +%s) / 600) * 600)) &#34;+%Hh%M&#34;).png -m fill &amp;
sleep 1
kill $PID
sleep 599
done
</code></pre></description>
</item>
<item>
<title>Update notifications with libnotify</title>
<link>https://sylvaindurand.org/update-notifications-with-libnotify/</link>
<pubDate>Sun, 18 Oct 2020 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/update-notifications-with-libnotify/</guid>
<description><p>The libnotify library allows you to send desktop notifications to a notification daemon commonly used under Linux. Here we will see how to successfully update notifications already displayed.</p>
<h2 id="with-notify-send">With notify-send?</h2>
<p>This library offers the <code>notify-send</code> command, which displays a notification directly on the desktop:</p>
<pre tabindex="0"><code>notify-send &#34;Notification title&#34; &#34;Notification description&#34;
</code></pre><p>But what if you want to update or delete a notification already displayed? If this command is executed several times, the notifications pile up rather than updating the one that has just been sent.</p>
<p>The manual (<code>man notify-send</code>) shows us that the options are rather short and do not allow us to expect much:</p>
<pre tabindex="0"><code>-?, --help
-u, --urgency=LEVEL
-t, --expire-time=TIME
-i, --icon=ICON[,ICON...]
-c, --category=TYPE[,TYPE...]
-h, --hint=TYPE:NAME:VALUE
</code></pre><p>We will therefore have to move away from it and return to the fundamentals.</p>
<h2 id="using-gdbus">Using gdbus</h2>
<p><code>notify-send</code> simply sends information as defined in the &ldquo;Desktop Notifications&rdquo; specification, which we will be able to do ourselves with <code>gdbus</code>.</p>
<p>The following function will allow us to obtain the same result as before:</p>
<pre tabindex="0"><code>gdbus call \
--session \
--dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.Notify \
&#34;identifier&#34; \
&#34;1&#34; \
&#34;&#34; \
&#34;Notification title&#34; \
&#34;Notification description&#34; \
&#34;[]&#34; \
&#34;{}&#34; \
&#34;2000&#34;
</code></pre><p>The second identifier (here <code>1</code>) concerns the id of the notification we wish to replace, we will come back to this shortly. The next identifier allows you to define an icon. <code>&quot;[]&quot;</code> allows you to define actions, <code>&quot;{}&quot;</code> to define hints, and finally the last argument <code>2000</code> presents the time during which the notification must remain visible (in milliseconds).</p>
<p>Once this command is executed, the system returns a response that looks like :</p>
<pre tabindex="0"><code>(uint32 13,)
</code></pre><p>This number, here <code>13</code>, is the id of the notification that we will be able to replace.</p>
<p>This means that the following command will not create a new notification, but will replace the one we just created:</p>
<pre tabindex="0"><code>gdbus call \
--session \
--dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.Notify \
&#34;identifier&#34; \
&#34;13&#34; \
&#34;&#34; \
&#34;My updated title&#34; \
&#34;My updated description&#34; \
&#34;[]&#34; \
&#34;{}&#34; \
&#34;2000&#34;
</code></pre><h2 id="getting-notification-id">Getting notification id</h2>
<p>All that remains is to automatically retrieve this number, and to reinject it as an argument in our function.</p>
<p>To retrieve it, filter the previous function with :</p>
<pre tabindex="0"><code>| sed &#39;s/[^ ]* //; s/,.//&#39;
</code></pre><p>We could then store the number in a temporary file, for example with <code>&gt; /tmp/.notif</code>. However, to avoid using the hard disk or the SSD, let&rsquo;s go directly to the RAM :</p>
<pre tabindex="0"><code>&gt; ${XDG_RUNTIME_DIR}/.notif
</code></pre><p>This number can be initiated at startup with :</p>
<pre tabindex="0"><code>echo &#39;1&#39; &gt; ${XDG_RUNTIME_DIR}/.notif
</code></pre><p>It is then easily retrieved with:</p>
<pre tabindex="0"><code>notif=`cat ${XDG_RUNTIME_DIR}/.notif`
</code></pre><h2 id="result">Result</h2>
<p>The following code, executed several times, will then update the notification :</p>
<pre tabindex="0"><code>notif=`cat ${XDG_RUNTIME_DIR}/.notif`
gdbus call \
--session \
--dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.Notify \
&#34;identifier&#34; \
&#34;`echo $notif`&#34; \
&#34;&#34; \
&#34;My updated title&#34; \
&#34;My updated description&#34; \
&#34;[]&#34; \
&#34;{}&#34; \
&#34;2000&#34; \
| sed &#39;s/[^ ]* //; s/,.//&#39; &gt; ${XDG_RUNTIME_DIR}/.notif
</code></pre></description>
</item>
<item>
<title>Arch Linux on Macbook Pro late 2013</title>
<link>https://sylvaindurand.org/installing-arch-linux-on-macbook-pro-late-2013/</link>
<pubDate>Sun, 12 Jan 2020 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/installing-arch-linux-on-macbook-pro-late-2013/</guid>
<description><p>Arch Linux is a distribution whose installation procedure may seem demanding, but which in return offers complete control over what is installed, but above all allows you to experiment with and understand many of the basic elements of how a Linux system works.</p>
<p>This article details the specific steps that allow me to get a minimum functional installation on a 13&quot; MacBook Pro, late 2013 (11.x). This one is characterized by a few specificities, notably a recalcitrant Broadcom wifi card.</p>
<p>It is strictly discouraged to follow these commands blindly, because Arch Linux is a distribution that is very regularly updated and because each system is different.</p>
<p>Start by opening and following the installation guide and the specific page on the Arch Linux Wiki, which are very detailed. This article can serve as a complement of the intallation guide.</p>
<p><a href="https://wiki.archlinux.org/title/installation_guide">Arch Wiki: Installation guide</a></p>
<p><a href="https://wiki.archlinux.org/index.php/MacBookPro11,x">Arch Wiki: Macbook Pro 11.x</a></p>
<h3 id="activating-wifi-during-the-installation-process">Activating Wifi during the installation process</h3>
<p>This is the most blocking part compared to the usual computers. The MacBook Pro for late 2013 usually comes with a Broadcom wifi card, which is known to be difficult to use with regular drivers.</p>
<p>The purpose of this part is only to have a network connection to do the installation, we are not yet configuring the wifi for our final system. If your system is already installed and you want to have a working wifi, you can go further down on this page.</p>
<p>To check which card is on board, use the command:</p>
<pre tabindex="0"><code>lspci -k
</code></pre><p>In my case, the lines concerned are as follows:</p>
<pre tabindex="0"><code>Network controller: Broadcom Inc. and subsidiaries BCM4360 802.11ac Wireless Network Adapter (rev 03)
Subsystem: Apple Inc. BCM3460 802.11ac Wireless Network AdapterKernel driver in use: bcma-pci-bridge
Kernel modules: bcma, wl
</code></pre><p>If the <code>iw dev</code> command doesn&rsquo;t return anything, it is because the driver (<code>broadcom-wl</code> in our case), although present in the installer, is not loaded. We start by disabling the concurrent modules, which could be run:</p>
<pre tabindex="0"><code>rmmod b43 bcma ssb wl
</code></pre><p>Then, we activate:</p>
<pre tabindex="0"><code>modprobe wl
</code></pre><p>If all goes well, <code>iw dev</code> now displays something:</p>
<pre tabindex="0"><code>phy#1
Interface wlan0
ifindexwdev 0x100000001
addr 00:00:00:00:00:00
type managedtxpower 200.00 dBm
</code></pre><p>You can then activate the connection and then scan the surrounding networks (if the interface has, in the result of the previous command, a name other than <code>wlan0</code>, you have to adapt it):</p>
<pre tabindex="0"><code>ifconfig wlan0 up
iwlist wlan0 scan
</code></pre><p>If you can&rsquo;t connect, you can still use a small USB wifi adapter to continue the installation, or use the &ldquo;modem&rdquo; mode of your Android phone.</p>
<p>To connect to the wifi, use the command (where <code>ssid</code> and <code>password</code> are respectively the name and password of your access point):</p>
<pre tabindex="0"><code>wpa_supplicant -B -i wlan0 -c &lt;(wpa_passphrase &#34;ssid&#34; &#34;password&#34;)
</code></pre><p>Be careful not to put a space between the <code>&lt;</code> and the <code>(</code>, to avoid getting the error <code>zsh: number expected</code>. Also, depending on the situation, it may not work on some channels in the 5 GHz band. In this case, the simplest way is to reconfigure your wifi on the 2.4 GHz band.</p>
<p>We get an IP address with the following command (which should display a succession of communications):</p>
<pre tabindex="0"><code>dhcpcd
</code></pre><p>If you get the message <code>no interface have a carrier</code>, you can try running <code>dhcpcd --release</code> and then <code>dhcpcd</code> again.</p>
<p>We&rsquo;re checking that we&rsquo;re connected with:</p>
<pre tabindex="0"><code>ping archlinux.org
</code></pre><h3 id="installing-wifi-for-the-first-launch">Installing Wifi for the first launch</h3>
<p>We still have to install our driver on our system to have a network connection on reboot. Let&rsquo;s start by retrieving the tools to be able to connect, while <code>arch-chroot</code>:</p>
<pre tabindex="0"><code>pacman -Syu networkmanager
</code></pre><p>It will allow us, after the reboot, to simply connect with <code>nmtui</code>.</p>
<p>With a Broadcom BCM4360 (rev 03) card, we will install the <code>broadcom-wl</code> package in its DKMS version. This will allow it to be upgraded along with the Linux kernels.</p>
<pre tabindex="0"><code>pacman -Syu linux-headers broadcom-wl-dkms
</code></pre><h3 id="red-light-in-the-jack-audio-output">Red light in the jack audio output</h3>
<p>I had a lot of trouble to get rid of the red light in the jack output. I manage to do it with:</p>
<pre tabindex="0"><code>sudo pacman -S alsa-utils
sudo hda-verb /dev/snd/hwC1D0 0x21 SET_PIN_WID 0x00
</code></pre></description>
</item>
<item>
<title>Outlook emails analytics with Python</title>
<link>https://sylvaindurand.org/outlook-email-analytics-with-python/</link>
<pubDate>Sun, 15 Dec 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/outlook-email-analytics-with-python/</guid>
<description><p>Microsoft Outlook uses a proprietary data format, &ldquo;PST&rdquo; (Personal Storage Table), to store emails, appointments or tasks. It was not until 2010 that the specifications of this format were made public, which probably explains why few tools are available to open it and use its data.</p>
<p>Under Python, however, there is the libpff library that allows most metadata to be exported. The following lines show how to use it to graph incoming emails.</p>
<h2 id="installation-of-libpff">Installation of libpff</h2>
<p>Like all Python libraries, libpff can be installed with <code>pip</code> :</p>
<pre tabindex="0"><code>pip install libpff-python
</code></pre><p>However, at the time of writing, the version proposed by pip (20161119) does not allow to retrieve the time of the messages. Corrections have been made in a more recent version (20190725), available with :</p>
<pre tabindex="0"><code>pip install libpff-python-ratom
</code></pre><h2 id="retrieving-emails">Retrieving emails</h2>
<h3 id="file-opening">File opening</h3>
<p>First we load the library:</p>
<pre tabindex="0"><code>import pypff
</code></pre><p>Then we open our file: the opening can nevertheless be quite long depending on the size of your archive.</p>
<pre tabindex="0"><code>pst = pypff.file()
pst.open(&#34;mails.pst&#34;)
</code></pre><h3 id="metadata-extraction">Metadata extraction</h3>
<p>It is possible to navigate through the structure using the functions offered by the library, from the root:</p>
<pre tabindex="0"><code>root = pst.get_root_folder()
</code></pre><p>To extract the data, a recursive function can then be used:</p>
<pre tabindex="0"><code>def parse_folder(base):
messages = []
for folder in base.sub_folders:
if folder.number_of_sub_folders:
messages += parse_folder(folder)
print(folder.name)
for message in folder.sub_messages:
messages.append({
&#34;subject&#34;: message.subject,
&#34;sender&#34;: message.sender_name,
&#34;datetime&#34;: message.client_submit_time
})
return messages
messages = parse_folder(root)
</code></pre><p>This function can be quite slow depending on the number of messages (about 300 messages are processed per second on my computer).</p>
<p>Once this is done, can then import this file into Pandas:</p>
<pre tabindex="0"><code>import pandas as pd
df = pd.DataFrame(messages)
</code></pre><h3 id="time-conversion">Time conversion</h3>
<p>The hours extracted from the file are stored in UTC format, which means that you will have to reprocess the correct time zone. First, the time zone is declared UTC, then converted to the desired time zone:</p>
<pre tabindex="0"><code>df[&#39;datetime&#39;] = df[&#39;datetime&#39;].dt.tz_localize(tz=&#39;UTC&#39;)
df[&#39;datetime&#39;] = df[&#39;datetime&#39;].dt.tz_convert(tz=&#39;Europe/Paris&#39;)
</code></pre><h3 id="plot-example">Plot example</h3>
<p>We will plot a point cloud showing the arrival time of emails by date. To do this, two columns are created with the coordinates of the points to be traced:</p>
<pre tabindex="0"><code>df[&#39;hour&#39;] = df[&#39;datetime&#39;].dt.hour + df[&#39;datetime&#39;].dt.minute / 60
df[&#39;date&#39;] = df[&#39;datetime&#39;].dt.year + df[&#39;datetime&#39;].dt.dayofyear / 365
</code></pre><p>Then we trace:</p>
<pre tabindex="0"><code>import matplotlib.pyplot as plt
import seaborn as sns
plt.clf()
ax = sns.scatterplot(x=&#34;date&#34;, y=&#34;hour&#34;, s=3, alpha=.3, linewidth=0, marker=&#34;.&#34;, data=df)
ax.set(xlim=(2014.5,2020), ylim=(7,25))
ax.invert_yaxis()
sns.despine()
ax.get_figure().savefig(&#34;plot.png&#34;, dpi=400)
</code></pre><p>This gives us:</p>
<p><img src="mails.png" alt="Scatter plot of emails depending of the hour and date"></p>
</description>
</item>
<item>
<title>Send emails with msmtp</title>
<link>https://sylvaindurand.org/send-emails-with-msmtp/</link>
<pubDate>Sat, 03 Aug 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/send-emails-with-msmtp/</guid>
<description><p>There are many situations in which sending emails from a server can be useful, either because applications require it, or to do monitoring, such as being notified of security updates available on your system or the proper execution of cron tasks.</p>
<p>However, configuring a mail server with <code>postfix</code>, <code>exim</code> or <code>sendmail</code> requires a large number of steps to be performed and a careful configuration, due to all the devices set up to fight spam (including DKIM, SPF, DMARC, static IP and Reverse DNS, white list&hellip;), which makes the task very tedious for a simple personal server.</p>
<p>Nevertheless, it is possible to simply use an existing email account, and send via SMTP emails as a traditional email client would.</p>
<p>Until now, I used to use ssmtp for this, but this one is no longer maintained, and it is no longer possible to install it from Debian 10 Buster. We will use msmtp, which is just as easy to use and efficient.</p>
<h3 id="installation-of-msmtp">Installation of msmtp</h3>
<p>From a Debian based system, you can simply install the following packages :</p>
<pre tabindex="0"><code>sudo apt-get install msmtp msmtp-mta
</code></pre><p>The configuration file is as follows:</p>
<pre tabindex="0"><code>sudo nano /etc/msmtprc
</code></pre><p>Then use the following settings with the IDs and settings of your email provider:</p>
<pre tabindex="0"><code>defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.msmtp.log
account gmail
host smtp.gmail.com
port 587
from username@gmail.com
user username
password password
account default : gmail
</code></pre><p>Or, with OVH :</p>
<pre tabindex="0"><code>defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.msmtp.log
account ovh
host ssl0.ovh.net
port 587
from username@domain.tld
user username@domain.tld
password password
account default : ovh
</code></pre><p>Once the file has been saved, we can try to send our first emails.</p>
<h3 id="test-sending-emails">Test sending emails</h3>
<p>It is possible to use <code>msmtp</code> directly to send your email:</p>
<pre tabindex="0"><code>echo &#34;Message&#34; | mail -s &#34;Title&#34; &lt;email-adress&gt;
</code></pre><h3 id="change-the-senders-name">Change the sender&rsquo;s name</h3>
<p>It is possible to define, for each user of your server, a personalized sender name when sending emails:</p>
<pre tabindex="0"><code>sudo chfn -f &#39;Custom name for root&#39; root
sudo chfn -f &#39;Custom name for user&#39; &lt;user&gt;
</code></pre><h3 id="example-of-use-apticron">Example of use: apticron</h3>
<p>If you have a server, you should always be informed of the latest updates available for your system.</p>
<p>If you use a system based on Debian, and you use <code>apt</code> to install or update your packages, the little <code>apticron</code> utility automatically sends you an email as soon as an update is available.</p>
<p>It is installed with:</p>
<pre tabindex="0"><code>sudo apt-get install apticron
</code></pre><p>To indicate your email address, simply change it:</p>
<pre tabindex="0"><code>sudo nano /etc/apticron/apticron.conf
</code></pre><p>Finally, indicate the address at which you wish to receive the notifications:</p>
<pre tabindex="0"><code>EMAIL=&#34;&lt;your-email&gt;&#34;
</code></pre></description>
</item>
<item>
<title>Overlay image in pure CSS</title>
<link>https://sylvaindurand.org/overlay-image-in-pure-css/</link>
<pubDate>Fri, 10 May 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/overlay-image-in-pure-css/</guid>
<description><p>There is no native solution within browsers to display, on click, a full screen image. Several Javascript libraries have come to fill this gap: Lightbox (which was a precursor, to the point of giving its name to the concept), Fancybox, PhotoSwipe&hellip;</p>
<p>These solutions are sometimes heavy, whereas if you are only looking for a simple behavior, a few lines of CSS are enough, and no Javascript code is required.</p>
<h2 id="principle">Principle</h2>
<h3 id="target-selector">:target selector</h3>
<p>We will use the CSS selector <code>:target</code>, which allows to apply a style to an element when its identifier (<code>id=&quot;&quot;</code>) is the one of the anchor (<code>#</code>) in the page URL. To take an example:</p>
<pre tabindex="0"><code>&lt;a href=&#34;#myid&#34;&gt;Click to color in red!&lt;/a&gt;
&lt;div id=&#34;myid&#34;&gt;Hello World!&lt;/div&gt;
&lt;style&gt;
div:target {
color: red;
}
&lt;/style&gt;
</code></pre><p>Note that this selector actually allows you to add a lot of interactive elements to a page without Javascript, such as showing and hiding an element or a navigation bar.</p>
<h3 id="compatibility">Compatibility</h3>
<p>According to developer.mozilla.org and caniuse.com, the <code>:target</code> selector is supported by all browsers released since 2008, and especially since Internet Explorer 9.</p>
<h2 id="html">HTML</h2>
<p>For each image, the HTML markup will be as simple as possible:</p>
<pre tabindex="0"><code>&lt;!-- The link that, when clicked, will display the image in full screen --&gt;
&lt;a href=&#34;#img-id&#34;&gt;
&lt;img src=&#34;image-thumbnail.png&#34; alt=&#34;Thumbnail&#34;&gt;
&lt;/a&gt;
&lt;!-- The full screen image, hidden by default --&gt;
&lt;a href=&#34;#_&#34; class=&#34;overlay&#34; id=&#34;img-id&#34;&gt;
&lt;img src=&#34;image-fullscreen.png&#34; alt=&#34;Fullscreen&#34;&gt;
&lt;/a&gt;
</code></pre><p>The first line, customizable at will, will display the image in full screen thanks to the link to <code>#img-id</code>. The second, hidden by default, contains the corresponding identifier.</p>
<p>If several images are present on the page, it will obviously be necessary to give them a unique identifier for each one, and to match the link.</p>
<p>The link to <code>#_</code> has no particular meaning, and could have been replaced by <code>#anything</code>. It simply aims to cause <code>#img-id</code> to lose the status <code>:target</code> and therefore to close the full screen when clicking.</p>
<h2 id="css">CSS</h2>
<h3 id="display">Display</h3>
<p>The overlay is nothing more than a <code>div</code> with a fixed position, located at the top left and covering the entire page. We add a <code>flex</code> property to it to place the image in the center of it:</p>
<pre tabindex="0"><code>
.overlay {
/* Display over the entire page */
position: fixed;
z-index: 99;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
/* Horizontal and vertical centering of the image */
display: flex;
align-items: center;
text-align: center;
/* We hide all this by default */
visibility: hidden;
}
.overlay img{
/* Maximum image size */
max-width: 90%;
max-height: 90%;
/* We keep the ratio of the image */
width: auto;
height: auto;
}
</code></pre><h3 id="activation-on-click">Activation on click</h3>
<p>As we have seen previously, we now have to add a <code>visibility: visible</code> with the selector <code>:target</code> to display all this when you click on the trigger link.</p>
<pre tabindex="0"><code>.overlay:target {
visibility: visible;
outline: none;
cursor: default;
}
</code></pre><p>We add an <code>outline: none</code> to avoid the border that is displayed, by default, on several browsers. In addition, <code>cursor: default</code>, optional, avoids having a &ldquo;hand&rdquo; in place of the cursor on the entire page.</p>
<h3 id="animation">Animation</h3>
<p>The Javascript libraries mentioned above allowed to have a nice fade effect when opening. Never mind: CSS allows almost any kind of madness in terms of animation.</p>
<p>We&rsquo;re going to stay sober here, with a slight bland effect, with a very small zoom effect on the image.</p>
<pre tabindex="0"><code>.overlay {
opacity: 0;
transition: opacity .3s;
}
.overlay img {
transform: scale(0.95);
transition: transform .3s;
}
.overlay:target img {
transform: scale(1);
}
</code></pre><h3 id="full-code">Full code:</h3>
<p>Finally, our CSS code became:</p>
<pre tabindex="0"><code>
.overlay {
position: fixed;
z-index: 99;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
display: flex;
align-items: center;
text-align: center;
visibility: hidden;
opacity: 0;
transition: opacity .3s;
}
.overlay img{
max-width: 90%;
max-height: 90%;
width: auto;
height: auto;
transform: scale(0.95);
transition: transform .3s;
}
.overlay:target {
visibility: visible;
outline: none;
cursor: default;
}
.overlay:target img {
transform: scale(1);
}
</code></pre><h2 id="bonus-shortcode-for-hugo">Bonus: shortcode for Hugo</h2>
<p>With the static website generator Hugo, you can easily automate the process with a shortcode. To do so, we create a <code>img.html</code> file in <code>layouts/shortcodes/</code> :</p>
<pre tabindex="0"><code>&lt;a href=&#34;#{{ anchorize (.Get &#34;src&#34;) }}&#34;&gt;
&lt;img src=&#34;{{.Get &#34;src&#34; }}&#34; alt=&#34;{{.Get &#34;alt&#34; }}&#34; /&gt;
&lt;/a&gt;
&lt;a href=&#34;#_&#34; class=&#34;overlay&#34; id=&#34;{{ anchorize (.Get &#34;src&#34;) }}&#34;&gt;
&lt;img src=&#34;{{.Get &#34;src&#34; }}&#34; alt=&#34;{{.Get &#34;alt&#34; }}&#34;&gt;
&lt;/a&gt;
{{ .Inner }}
</code></pre><p>Then, you just have to add, in your articles:</p>
<pre tabindex="0"><code>{{&lt; img src=&#34;image.jpg&#34; alt=&#34;Description&#34; /&gt;}}
</code></pre></description>
</item>
<item>
<title>GPS data from photos with Python</title>
<link>https://sylvaindurand.org/gps-data-from-photos-with-python/</link>
<pubDate>Sat, 04 May 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/gps-data-from-photos-with-python/</guid>
<description><p>Most recent cameras and mobile phones record photographs with geographical data (longitude, latitude, but also altitude). If this data is readable with most photo and file explorer software, it is also possible to access it with Python.</p>
<h2 id="reading-metadata">Reading metadata</h2>
<p>The Python Imaging Library (PIL) provides easy access to EXIF data with the function <code>_getexif()</code>.</p>
<p>It is easily installed, under Python3, with the fork Pillow. We use the following command (depending on your configuration, <code>pip3</code> can be directly accessible with <code>pip</code>):</p>
<pre tabindex="0"><code>pip3 install pillow
</code></pre><p>However, this results in an indexed dictionary with numerical identifiers. To get the corresponding names, we use <code>ExifTags</code> and rename the keys of the dictionary:</p>
<pre tabindex="0"><code>from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
def get_exif(filename):
exif = Image.open(filename)._getexif()
if exif is not None:
for key, value in exif.items():
name = TAGS.get(key, key)
exif[name] = exif.pop(key)
if &#39;GPSInfo&#39; in exif:
for key in exif[&#39;GPSInfo&#39;].keys():
name = GPSTAGS.get(key,key)
exif[&#39;GPSInfo&#39;][name] = exif[&#39;GPSInfo&#39;].pop(key)
return exif
exif = get_exif(&#39;image.jpg&#39;)
</code></pre><p>The <code>exif</code> variable then contains all the metadata of the image: objective, aperture, speed, author&hellip; Our GPS data is stored in <code>exif['GPSInfo']</code>, in the following form:</p>
<pre tabindex="0"><code>{&#39;GPSLongitude&#39;: ((19, 1), (31, 1), (5139, 100)),
&#39;GPSAltitudeRef&#39;: b&#39;\x00&#39;,
&#39;GPSAltitude&#39;: (92709, 191),
&#39;GPSTimeStamp&#39;: ((13, 1), (18, 1), (42, 1)),
&#39;GPSHPositioningError&#39;: (10, 1),
&#39;GPSLatitudeRef&#39;: &#39;N&#39;,
&#39;GPSLatitude&#39;: ((63, 1), (40, 1), (5908, 100)),
&#39;GPSLongitudeRef&#39;: &#39;W&#39;}
</code></pre><h2 id="getting-gps-data">Getting GPS data</h2>
<h3 id="sexagesimal-form">Sexagesimal form</h3>
<p>Geographic coordinates are usually expressed in the sexagesimal system, or DMS for degrees (Β°), minutes (β²) and seconds (β³). The unit is the degree of angle (1 turn = 360Β°), then the minute of angle (1Β° = 60β²), then the second of angle (1Β° = 3 600β³).</p>
<p>Compared to the equatorial plane, the latitude is completed by a letter <code>N</code> (hemisphere) or <code>S</code> depending on whether one is located in the northern or southern hemisphere. Compared to the Greenwich meridian, the longitude is completed by a letter <code>W</code> or <code>E</code> depending on whether you are located in the west or east.</p>
<p>The data is directly accessible in <code>exif['GPSInfo']</code> to be able to output this format:</p>
<pre tabindex="0"><code>def get_coordinates(info):
for key in [&#39;Latitude&#39;, &#39;Longitude&#39;]:
if &#39;GPS&#39;+key in info and &#39;GPS&#39;+key+&#39;Ref&#39; in info:
e = info[&#39;GPS&#39;+key]
ref = info[&#39;GPS&#39;+key+&#39;Ref&#39;]
info[key] = ( str(e[0][0]/e[0][1]) + &#39;Β°&#39; +
str(e[1][0]/e[1][1]) + &#39;β²&#39; +
str(e[2][0]/e[2][1]) + &#39;β³ &#39; +
ref )
if &#39;Latitude&#39; in info and &#39;Longitude&#39; in info:
return [info[&#39;Latitude&#39;], info[&#39;Longitude&#39;]]
get_coordinates(exif[&#39;GPSInfo&#39;])
</code></pre><p>We get:</p>
<pre tabindex="0"><code>[&#39;63.0Β°40.0β²59.08β³ N&#39;, &#39;19.0Β°31.0β²51.39β³ W&#39;]
</code></pre><h3 id="decimal-form">Decimal form</h3>
<p>To obtain automated geographic data processing, a decimal format is often more convenient: minutes are divided by 60 and seconds by 3600 and added together. Latitude is negative in the Southern Hemisphere (S), and west of the Greenwich Meridian (W). The calculation is then very simple:</p>
<pre tabindex="0"><code>def get_decimal_coordinates(info):
for key in [&#39;Latitude&#39;, &#39;Longitude&#39;]:
if &#39;GPS&#39;+key in info and &#39;GPS&#39;+key+&#39;Ref&#39; in info:
e = info[&#39;GPS&#39;+key]
ref = info[&#39;GPS&#39;+key+&#39;Ref&#39;]
info[key] = ( e[0][0]/e[0][1] +
e[1][0]/e[1][1] / 60 +
e[2][0]/e[2][1] / 3600
) * (-1 if ref in [&#39;S&#39;,&#39;W&#39;] else 1)
if &#39;Latitude&#39; in info and &#39;Longitude&#39; in info:
return [info[&#39;Latitude&#39;], info[&#39;Longitude&#39;]]
get_decimal_coordinates(exif[&#39;GPSInfo&#39;])
</code></pre><p>We get:</p>
<pre tabindex="0"><code>[63.683077777777775, -19.530941666666667]
</code></pre><h2 id="with-gpsphoto">With GPSPhoto</h2>
<p>Since the end of 2018, the Python library <code>GPSPhoto</code> allows to obtain directly the geographical coordinates of a photo.</p>
<p>It is installed with:</p>
<pre tabindex="0"><code>pip3 install GPSPhoto
</code></pre><p>We can now use:</p>
<pre tabindex="0"><code>from GPSPhoto import gpsphoto
gpsphoto.getGPSData(&#39;image.jpg&#39;)
</code></pre><p>We get:</p>
<pre tabindex="0"><code>{&#39;Latitude&#39;: 63.683077777777775,
&#39;Longitude&#39;: -19.530941666666667,
&#39;Altitude&#39;: 485.3874345549738,
&#39;UTC-Time&#39;: &#39;13:18:42&#39;,
&#39;Date&#39;: &#39;09/03/2018&#39;}
</code></pre><p>Nevertheless, according to the file, GPSPhoto tends to easily return errors that, in my case, make it impossible to automate a large number of photos:</p>
<pre tabindex="0"><code>ValueError: malformed node or string: &lt;_ast.BinOp object at 0x10ea16320&gt;
</code></pre><h2 id="automation">Automation</h2>
<p>If you want to recover metadata from a set of photos, you can iterate to a folder (and its subfolders).</p>
<pre tabindex="0"><code>import os
points = []
for r, d, f in os.walk(path):
for file in f:
if file.lower().endswith((&#39;.png&#39;, &#39;.jpg&#39;, &#39;.jpeg&#39;)):
filepath = os.path.join(r, file)
exif = get_exif(filepath)
if exif is not None and &#39;GPSInfo&#39; in exif:
latlong = get_decimal_coordinates(exif[&#39;GPSInfo&#39;])
if latlong is not None:
points.append(latlong)
</code></pre><p>You can then export <code>points</code> to display it on a map, for example with Leaflet.</p>
</description>
</item>
<item>
<title>Surveillance camera with Raspberry Pi</title>
<link>https://sylvaindurand.org/surveillance-camera-with-raspberry-pi/</link>
<pubDate>Wed, 24 Apr 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/surveillance-camera-with-raspberry-pi/</guid>
<description><p>While connected surveillance cameras can be useful, they are also likely to have serious security breaches. They are thus at the origin of one of the most important DDOS attacks, or are sometimes publicly visible. In fact, it is very difficult to identify the origin of the cameras; no guarantee can be given on the security level or the durability of the manufacturer&rsquo;s servers.</p>
<p>However, it is very simple β and very economical β to be able to make a simple monitoring system yourself using a Raspberry Pi. The Raspberry Pi Foundation offers a small camera module). Many other brands exist, and the following article will also work with a simple USB webcam.</p>
<h2 id="camera-activation">Camera activation</h2>
<p>First connect the camera to the Raspberry Pi using the correct interface. Apply yourself to this connection: it took me a long time to try to operate this camera before I realized that the web was not inserted to the bottom of the connector.</p>
<p>It is then necessary to activate the camera with:</p>
<pre tabindex="0"><code>sudo raspi-config
</code></pre><p>Select <code>Interfacing Options</code> Β» <code>P1 Camera</code> Β» <code>Enable</code>, before restarting the Raspberry Pi.</p>
<p>The camera uses the <code>v4l2</code> driver, which is integrated by default in the kernel provided with Raspbian. To test if the camera is working properly, you can take a picture with :</p>
<pre tabindex="0"><code>raspistill -v -o test.jpg
</code></pre><p>You can also record a video (here for five seconds):</p>
<pre tabindex="0"><code>raspivid -o test.h264 -t 5000
</code></pre><p>If your camera is upside down for connection reasons, you can return it with :</p>
<pre tabindex="0"><code>v4l2-ctl --set-ctrl vertical_flip=1
v4l2-ctl --set-ctrl horizontal_flip=1
</code></pre><h2 id="first-try-motioneye">First try: MotionEye</h2>
<p>I first tried to use the excellent MotionEye software, which offers a web interface to view the webcam, but also to make recordings and detect movements.</p>
<p>However, I couldn&rsquo;t get a suitable framerate: whatever the settings used (including with minimal resolution and quality), I never managed to exceed 1.0Β fps, which makes the result quite disappointing&hellip;</p>
<h2 id="second-try-v4l2rtspserver">Second try: v4l2rtspserver</h2>
<p>The well named RTSP, for Real Time Streaming Protocol, allows to set up a particularly fluid streaming.</p>
<h3 id="installation">Installation</h3>
<p>The v4l2rtspserver software allows you to easily use this protocol with our camera. You can install it with:</p>
<pre tabindex="0"><code>sudo apt-get install cmake liblog4cpp5-dev libv4l-dev
git clone https://github.com/mpromonet/v4l2rtspserver.git
cd v4l2rtspserver/
cmake .
make
sudo make install
</code></pre><h3 id="configuration">Configuration</h3>
<p>You can then start a video stream with:</p>
<pre tabindex="0"><code>v4l2rtspserver -H 972 -W 1296 -F 15 -P 8555 /dev/video0
</code></pre><p>The previous command allows you to delimit an image in 1296x972, with 15 fps (which allows a suitable brightness in a dark room).</p>
<p>The video stream then becomes available from the following address from any software that can play it, such as VLC (on your computer or phone):</p>
<pre tabindex="0"><code>rtsp://&lt;raspberry-pi-ip&gt;:8555/unicast
</code></pre><p>The image becomes perfectly smooth and, as soon as the connection is sufficient, the quality is very suitable. However, there is still a latency time of about one second.</p>
<p>It should be noted that v4l2rtspserver also allows you to specify a user name and password, for greater security.</p>
<h3 id="launch-on-startup">Launch on startup</h3>
<p>To launch v4l2rtspserver at boot, let&rsquo;s start by entering the command with the above parameters in the <code>ExecStart</code> section of the following file:</p>
<pre tabindex="0"><code>sudo nano /lib/systemd/system/v4l2rtspserver.service
</code></pre><p>We then activate v4l2rtspserver at boot:</p>
<pre tabindex="0"><code>sudo systemctl enable v4l2rtspserver
</code></pre></description>
</item>
<item>
<title>Installing Pi-hole with PiVPN</title>
<link>https://sylvaindurand.org/installing-pi-hole-with-pivpn/</link>
<pubDate>Wed, 20 Mar 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/installing-pi-hole-with-pivpn/</guid>
<description><p>While ad blockers remain highly effective within browsers, advertisements and trackers remain largely intact outside, whether it is computers, phones, televisions and other connected objects.</p>
<p>Pi-hole is an application that allows you to block ads and trackers by acting directly at the DNS resolution level. In general, it provides relatively comprehensive information on a large number of connections that pass through your network.</p>
<p>Combined with PiVPN, it becomes possible to use Pi-hole when roaming, from your phone or on a public network, while encrypting all your connections.</p>
<h2 id="installation-of-pi-hole">Installation of Pi-hole</h2>
<p>It is also important that your router assigns a fixed IP address to the Raspberry Pi, so that it can be declared as a DNS host from your devices.</p>
<p>The installation of Pi-hole is done simply with a single command:</p>
<pre tabindex="0"><code>curl -sSL https://install.pi-hole.net | bash
</code></pre><p>On a personal note, I chose Quad9&rsquo;s DNS servers (<code>9.9.9.9</code>).</p>
<p>Once the installation is complete, you can change the password:</p>
<pre tabindex="0"><code>sudo pihole -a -p
</code></pre><p>There are three ways to ensure that your computer is using your Raspberry Pi to resolve DNS.</p>
<p>The easiest way is to activate the Pi-Hole DHCP server: from the web server, you go to <code>Settings</code>, then <code>DHCP</code>, before selecting <code>DHCP server enabled</code>; then you have to go to the administration interface of your router or your box to disable DHCP.</p>
<p>Another solution is, in the configuration interface of your router or internet box, to declare the IP of your Raspberry Pi as a DNS server field.</p>
<p>If none of these solutions are possible, it is necessary to declare your Raspberry Pi as a DNS server in the network settings of all your devices.</p>
<p>Restart your network connection: the Pi-hole administration interface should now be available from <code>http://pi.hole/</code>.</p>
<p>You can then declare the blocking lists there to start blocking ads.</p>
<h2 id="installation-of-pivpn">Installation of PiVPN</h2>
<p>The installation is done again with a single command line:</p>
<pre tabindex="0"><code>curl -L https://install.pivpn.io | bash
</code></pre><p>When asked which DNS server to choose, select <code>Custom</code>, then indicate the IP of the Raspberry Pi.</p>
<p>Once the installation is done, let&rsquo;s go back to the administration interface of Pi-hole, to go to <code>Settings</code>, <code>DNS</code>, and <code>Interface listening behavior</code>: we use <code>Listen on all interfaces</code> to make sure that PiVPN is communicating with Pi-hole.</p>
<p>You can simply create a new VPN profile with:</p>
<pre tabindex="0"><code>pivpn add
</code></pre><p>To retrieve the profile, we use locally:</p>
<pre tabindex="0"><code>scp pi:/home/pi/ovpns/&lt;your-profile&gt;.ovpn .
</code></pre><p>It is now possible to connect, from an OpenVPN client, to a computer or a phone, to benefit from the filtering of Pi-hole.</p>
<p>Make sure your client does not have an option to use other DNS (the client for iOS, for example, automatically switches to Google&rsquo;s DNS servers).</p>
</description>
</item>
<item>
<title>Raspbian Lite on Raspberry Pi</title>
<link>https://sylvaindurand.org/raspbian-lite-on-raspberry-pi/</link>
<pubDate>Tue, 12 Mar 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/raspbian-lite-on-raspberry-pi/</guid>
<description><p>More than four years ago, I wrote an article explaining how to install Arch Linux on a Raspberry Pi:</p>
<p><a href="https://sylvaindurand.org/installing-archlinux-on-raspberry-pi/">Installing Arch Linux on Raspberry Pi</a></p>
<p>I have since migrated to the Raspbian distribution, based on Debian and made specifically for the Raspberry Pi.</p>
<p>Historically very large, because it has a graphic interface and several specific software, Raspbian now exists in a Light version that allows you to limit the packages to the essentials, to manage your Raspberry Pi from the command line.</p>
<p>This article is a small personal memo on the steps to install and secure Raspbian to get a Raspberry Pi ready to use, usable via command line with SSH.</p>
<h2 id="downloading">Downloading</h2>
<p>First we download the image of Raspbian. Select &ldquo;Raspbian Stretch Lite&rdquo;. The official site offers user manuals for Linux, MacOS and Windows.</p>
<p>On MacOS, we start by looking at the address of the SD card:</p>
<pre tabindex="0"><code>diskutil list
</code></pre><p>Once you know the SD card number, which appears as <code>/dev/disk&lt;number&gt;</code>, you unmount the volume, then write the image on it :</p>
<pre tabindex="0"><code>diskutil unmountDisk /dev/disk&lt;number&gt;
sudo dd bs=1m if=image.img of=/dev/rdisk&lt;number&gt; conv=sync
</code></pre><p>To be able to connect with SSH, we must create a file named <code>ssh</code> at the root of our SD card:</p>
<pre tabindex="0"><code>touch /Volumes/boot/ssh
</code></pre><p>If you need to connect on a Wifi network at boot:</p>
<pre tabindex="0"><code>nano /Volumes/boot/wpa_supplicant.conf
</code></pre><p>We put:</p>
<pre tabindex="0"><code>ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
network={
ssid=&#34;&lt;your-network-ssid&gt;&#34;
psk=&#34;&lt;your-network-password&gt;&#34;
key_mgmt=WPA-PSK
}
</code></pre><p>Finally, we eject the card:</p>
<pre tabindex="0"><code>sudo diskutil eject /dev/rdisk&lt;number&gt;
</code></pre><h2 id="first-connection">First connection</h2>
<p>Once the Raspberry is powered up with the SD card, you can connect with SSH:</p>
<pre tabindex="0"><code>ssh pi@&lt;raspberry-pi-ip&gt;
</code></pre><p>The default password is <code>raspberry</code>. Let&rsquo;s start by changing it:</p>
<pre tabindex="0"><code>passwd
</code></pre><h3 id="updating">Updating</h3>
<p>After setting up a new server, we start by updating all the packages:</p>
<pre tabindex="0"><code>sudo apt-get update &amp;&amp; sudo apt-get upgrade
sudo apt-get dist-upgrade
sudo apt autoremove
</code></pre><h3 id="language-settings">Language settings</h3>
<p>In order to set the language and time zone, launch:</p>
<pre tabindex="0"><code>sudo dpkg-reconfigure locales
sudo dpkg-reconfigure tzdata
</code></pre><h3 id="security">Security</h3>
<p>We will prevent the `root&rsquo; connection and change ports, in order to prevent a large number of attack attempts. To do this, we edit:</p>
<pre tabindex="0"><code>sudo nano /etc/ssh/sshd_config
</code></pre><p>We modify the following parameters with:</p>
<pre tabindex="0"><code>Port &lt;your-custom-port&gt;
LoginGraceTime 2m
PermitRootLogin no
StrictModes yes
PermitEmptyPasswords no
</code></pre><p>We finally restart the Raspberry Pi:</p>
<pre tabindex="0"><code>sudo reboot
</code></pre><h3 id="connection-via-ssh">Connection via SSH</h3>
<p>Locally, we create a new private key:</p>
<pre tabindex="0"><code>ssh-keygen -t ed25519 -a 100
</code></pre><p>Then we edit <code>.ssh/config</code> to connect easily:</p>
<pre tabindex="0"><code>Host pi
Hostname &lt;raspberry-pi-ip&gt;
Port &lt;your-custom-port&gt;
User pi
IdentityFile ~/.ssh/&lt;your-private-key&gt;
</code></pre><p>Then we send the public key to the server:</p>
<pre tabindex="0"><code>ssh-copy-id -i ~/.ssh/&lt;your-private-key&gt; pi
</code></pre><p>You can then connect directly with <code>ssh vps</code>.</p>
<p>The system is now fully operational!</p>
</description>
</item>
<item>
<title>Emulate a browser with Selenium</title>
<link>https://sylvaindurand.org/emulate-a-browser-with-selenium/</link>
<pubDate>Sun, 10 Mar 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/emulate-a-browser-with-selenium/</guid>
<description><p>Python is particularly effective when you want to automatically browse or perform actions on web pages. If it is enough to use libraries like Beautiful Soup when you just scrape web pages, it is sometimes necessary to perform actions on pages that require Javascript, or even to imitate human behavior. To do this, it is possible to fully emulate a web browser with Python.</p>
<h2 id="installation">Installation</h2>
<p>We will use Selenium, which is easily installed with :</p>
<pre tabindex="0"><code>pip install selenium
</code></pre><p>To be able to use it, it is necessary to install a rendering engine. We will use Gecko, the Firefox rendering engine.</p>
<p>To do this, download <code>geckodriver</code>, decompress it, and place it in <code>/usr/local/bin</code>.</p>
<pre tabindex="0"><code>tar xvfz geckodriver-version-plateform.tar.gz
mv geckodriver /usr/local/bin
</code></pre><h2 id="usage-in-python">Usage in Python</h2>
<p>To use Selenium, simply import at the beginning of the file:</p>
<pre tabindex="0"><code>from selenium import webdriver
</code></pre><h3 id="simple-usage">Simple usage</h3>
<p>You can start Selenium with:</p>
<pre tabindex="0"><code>driver = webdriver.Firefox(executable_path=r&#39;/usr/local/bin/geckodriver&#39;)
</code></pre><p>Thus, to click on an element, we use for example:</p>
<pre tabindex="0"><code>driver.find_element_by_class_name(&#39;my_class&#39;).click()
</code></pre><p>To free the memory, you can exit Selenium with :</p>
<pre tabindex="0"><code>driver.quit()
</code></pre><h3 id="use-without-graphical-interface">Use without graphical interface</h3>
<p>If you want to launch it without a graphical user interface, you can use:</p>
<pre tabindex="0"><code>options = webdriver.FirefoxOptions()
options.add_argument(&#39;-headless&#39;)
driver = webdriver.Firefox(options=options, executable_path=r&#39;/usr/local/bin/geckodriver&#39;)
</code></pre><h3 id="using-with-a-proxy-or-tor">Using with a proxy or Tor</h3>
<p>If you use a proxy, or Tor (which is used as a proxy, with local IP <code>127.0.0.0.1</code> and port <code>9050</code>), it is possible to connect to it with Selenium using the following options:</p>
<pre tabindex="0"><code>profile = webdriver.FirefoxProfile()
profile.set_preference(&#34;network.proxy.type&#34;, 1)
profile.set_preference(&#34;network.proxy.socks&#34;, &#39;127.0.0.1&#39;)
profile.set_preference(&#34;network.proxy.socks_port&#34;, 9050)
profile.set_preference(&#34;network.proxy.socks_remote_dns&#34;, False)
profile.update_preferences()
</code></pre><p>You can then use:</p>
<pre tabindex="0"><code>driver = webdriver.Firefox(firefox_profile=profile, executable_path=r&#39;/usr/local/bin/geckodriver&#39;)
</code></pre><h3 id="cache-and-cookies">Cache and cookies</h3>
<p>Other options are available, for example to disable the cache:</p>
<pre tabindex="0"><code>profile.set_preference(&#34;browser.cache.disk.enable&#34;, False)
profile.set_preference(&#34;browser.cache.memory.enable&#34;, False)
profile.set_preference(&#34;browser.cache.offline.enable&#34;, False)
profile.set_preference(&#34;network.http.use-cache&#34;, False)
</code></pre><p>It is also possible to clear cookies with:</p>
<pre tabindex="0"><code>driver.delete_all_cookies()
</code></pre></description>
</item>
<item>
<title>Wildcard certificates with Letβs Encrypt</title>
<link>https://sylvaindurand.org/wildcard-certificates-with-letsencrypt/</link>
<pubDate>Sat, 16 Feb 2019 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/wildcard-certificates-with-letsencrypt/</guid>
<description><p>Launched in late 2015, Letβs Encrypt is a public benefit organization which democratized the use of HTTPS by providing free SSL certificates with an automated validation system.</p>
<p>After several months of testing, Let&rsquo;s Encrypt has launched the second version of its client (ACME v2) with a highly anticipated feature : you can obtain wildcard certificates, valid for all subdomains of one domain.</p>
<p>This possibility is particularly interesting when using many subdomains: so far, it was necessary to issue a new certificate to add a new subdomain, or to delete an old one. Here, a simple <code>*.domain.tld</code> is enough!</p>
<h3 id="installation">Installation</h3>
<p>To get our certificates, we will use the <code>certbot</code> client. The site offers several installation methods depending on your platform. On Debian, you can simply use:</p>
<pre tabindex="0"><code>sudo apt-get install certbot
</code></pre><h3 id="issuing-a-certificate-manually">Issuing a certificate manually</h3>
<p>Be careful, if you want a certificate for both the domain root (<code>domain.tld</code>) and its subdomains (<code>*.domain.tld</code>), both must be specified. With the <code>-d</code> parameter, it is possible to list the desired domains and subdomains:</p>
<pre tabindex="0"><code>sudo letsencrypt certonly --manual --preferred-challenges dns --register -d domain.tld -d *.domain.tld
</code></pre><p>Let&rsquo;s encrypt will now have to ask us to prove that we have control over the domain names requested. It will request the creation of a specific TXT record in the DNS zone of the domain name, which can be done from your registar:</p>
<pre tabindex="0"><code>----------------------------------------------------
Please deploy a DNS TXT record under the name
_acme-challenge.domain.tld with the following value:
c81US66r6JVk1LwyFHbzINQvIU_m5gJWXgcUm8Qj2
Before continuing, verify the record is deployed.
----------------------------------------------------
Press Enter to Continue
</code></pre><p>Two TXT records will be requested for the same domain, it is completely normal (for both <code>domain.tld</code> and <code>*.domain.tld</code>).</p>
<p>Once created, the certificate is located in <code>/etc/letsencrypt/live/domain.tld</code>.</p>
<p>This certificate cannot be renewed automatically: it is necessary, as it approaches its expiration, to renew the step.</p>
<h3 id="automated-request-example-with-ovh">Automated request: example with OVH</h3>
<p>Fortunately, there are plugins to <code>certbot</code> allowing to automatically request certificates, which take care of modifying the DNS themselves to proceed with the validation. For example, under Debian, the following packages are provided:</p>
<pre tabindex="0"><code># apt-cache search certbot-dns
python3-certbot-dns-cloudflare
python3-certbot-dns-digitalocean
python3-certbot-dns-dnsimple
python3-certbot-dns-gandi
python3-certbot-dns-gehirn
python3-certbot-dns-google
python3-certbot-dns-linode
python3-certbot-dns-ovh
python3-certbot-dns-rfc2136
python3-certbot-dns-route53
python3-certbot-dns-sakuracloud
</code></pre><p>Depending on your registrar, you can find documentation on their API and how to set up renewal. I am an OVH customer:</p>
<pre tabindex="0"><code>sudo apt-get install python3-certbot-dns-ovh
</code></pre><p>Then we go on <code>https://eu.api.ovh.com/createToken/</code> to create a token in link with his account (be careful, we have to indicate his login of type <code>xx00000-ovh</code> and not his email address) with the following rules:</p>
<pre tabindex="0"><code>GET /domain/zone/*
PUT /domain/zone/*
POST /domain/zone/*
DELETE /domain/zone/*
</code></pre><p>We get the generated data to create the file :</p>
<pre tabindex="0"><code>#/root/.ovh.ini
dns_ovh_endpoint = ovh-eu
dns_ovh_application_key =
dns_ovh_application_secret =
dns_ovh_consumer_key =
</code></pre><p>We give it restricted rights, and then we can create a certificate:</p>
<pre tabindex="0"><code>sudo chmod 600 /root/.ovh.ini
sudo certbot certonly --dns-ovh --dns-ovh-credentials /root/.ovh.ini -d &#34;domain.tld&#34; -d &#34;*.domain.tld&#34;
</code></pre></description>
</item>
<item>
<title>Use Tor with Python</title>
<link>https://sylvaindurand.org/use-tor-with-python/</link>
<pubDate>Fri, 21 Dec 2018 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/use-tor-with-python/</guid>
<description><p>This page will show you how to use Tor to anonymously access data with a Python script. This can be particularly useful if you want to create a scrapper without being banned by the server concerned.</p>
<h2 id="tor-installation">Tor installation</h2>
<p>The installation of Tor depends on your system, and is detailed on the official website. On a Debian or Raspbian, we use:</p>
<pre tabindex="0"><code>sudo apt-get install tor
</code></pre><p>To launch Tor, just run:</p>
<pre tabindex="0"><code>sudo service tor start
</code></pre><p>To check if it works, simply run the following command from a terminal:</p>
<pre tabindex="0"><code>curl --socks5 localhost:9050 --socks5-hostname localhost:9050 -s https://check.torproject.org/ | cat | grep -m 1 Congratulations | xargs
</code></pre><p>This command will display:</p>
<pre tabindex="0"><code>Congratulations. This browser is configured to use Tor.
</code></pre><h2 id="usage-with-python">Usage with Python</h2>
<h3 id="with-requests-library">With requests library</h3>
<p>To request a page, use the <code>requests</code> library. If you do not have it, just install it:</p>
<pre tabindex="0"><code>pip install requests
pip install requests[socks]
pip install requests[security]
</code></pre><p>If there is an error for the last command, try to install <code>cryptography</code> requirements:</p>
<pre tabindex="0"><code>sudo apt-get install build-essential libssl-dev libffi-dev python-dev
</code></pre><p>We then use, in Python:</p>
<pre tabindex="0"><code>import requests
</code></pre><p>You can check your IP address without Tor with the command:</p>
<pre tabindex="0"><code>requests.get(&#39;https://ident.me&#39;).text
</code></pre><p>To use Tor, we tell it to use a proxy:</p>
<pre tabindex="0"><code>proxies = {
&#39;http&#39;: &#39;socks5://127.0.0.1:9050&#39;,
&#39;https&#39;: &#39;socks5://127.0.0.1:9050&#39;
}
requests.get(url, proxies=proxies).text
</code></pre><p>So, you should have a new IP address with:</p>
<pre tabindex="0"><code>requests.get(&#39;https://ident.me&#39;, proxies=proxies).text
</code></pre><h3 id="obtaining-a-new-identity">Obtaining a new identity</h3>
<p>If you need a new identity, and change your IP address, you need to install stem:</p>
<pre tabindex="0"><code>pip install stem
</code></pre><p>The Tor controller must also be configured to request identity renewal:</p>
<pre tabindex="0"><code>sudo nano /etc/tor/torrc
</code></pre><p>We use the parameters:</p>
<pre tabindex="0"><code>ControlPort 9051
CookieAuthentication 1
</code></pre><p>Then we restart Tor to take into account these modifications:</p>
<pre tabindex="0"><code>sudo service tor restart
</code></pre><p>With Python, we now use the following command:</p>
<pre tabindex="0"><code>from stem import Signal
from stem.control import Controller
with Controller.from_port(port = 9051) as c:
c.authenticate()
c.signal(Signal.NEWNYM)
</code></pre><p>To check it, we look if we get a new IP with:</p>
<pre tabindex="0"><code>requests.get(&#39;https://api.ipify.org&#39;, proxies=proxies).text
</code></pre><h3 id="strengthen-anonymity-by-changing-the-user-agent">Strengthen anonymity by changing the User-Agent</h3>
<p>If anonymity is required, it may be useful to change the user-agent , which betrays our identity to the server. To do this, install <code>fake_useragent</code>:</p>
<pre tabindex="0"><code>pip install fake_useragent
</code></pre><p>We can then use, in Python:</p>
<pre tabindex="0"><code>from fake_useragent import UserAgent
headers = { &#39;User-Agent&#39;: UserAgent().random }
requests.get(url, proxies=proxies, headers=headers).text
</code></pre><h3 id="automation-with-cron">Automation with Cron</h3>
<p>If your Python script is to be used regularly using a Cron job, it may be useful to add a random delay to prevent the access time from being too regular:</p>
<pre tabindex="0"><code>import random, time
wait = random.uniform(0, 2*60*60)
time.sleep(wait)
</code></pre></description>
</item>
<item>
<title>Launch Chromium in kiosk mode</title>
<link>https://sylvaindurand.org/launch-chromium-in-kiosk-mode/</link>
<pubDate>Thu, 16 Aug 2018 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/launch-chromium-in-kiosk-mode/</guid>
<description><p>Chromium makes it easy to be started in &ldquo;kiosk&rdquo; mode, that is to say to launch in full screen, without any window border, toolbar or notification (surprisingly, this feature is not offered by Mozilla Firefox).</p>
<p>Our goal will be to get:</p>
<ul>
<li>a minimal installation of the graphical server, without desktop environment or window manager;</li>
<li>an automatic launch on startup, in full screen;</li>
<li>no toolbar or notification.</li>
</ul>
<h2 id="display-server">Display server</h2>
<h3 id="installation">Installation</h3>
<p>To display the browser, we will have to install an X server. There is no need to install a desktop environment or window manager, unnecessarily large, since the browser will be launched directly in full screen.</p>
<pre tabindex="0"><code>sudo apt-get install xserver-xorg-video-all xserver-xorg-input-all xserver-xorg-core xinit x11-xserver-utils
</code></pre><h3 id="launch-at-startup">Launch at startup</h3>
<p>To start the server automatically at startup, edit the <code>~/.bash_profile</code> file, which is executed when the user logs in, to put the following content (the server starts with <code>startx</code>, but it is also necessary to check that a screen is available to avoid an error, for example, with SSH):</p>
<pre tabindex="0"><code>if ( -z $DISPLAY ) &amp;&amp; ( $(tty) = /dev/tty1 ); then
startx
fi
</code></pre><p>Your system must also be configured so that the user is automatically logged in at startup. How to proceed depends on your configuration.</p>
<h2 id="chromium">Chromium</h2>
<h3 id="installation-1">Installation</h3>
<p>We will install, of course, Chromium, but also <code>unclutter</code>, which will allow us to hide the pointer of the mouse:</p>
<pre tabindex="0"><code>sudo apt-get install chromium-browser
sudo apt-get install unclutter
</code></pre><h3 id="launch-at-startup-1">Launch at startup</h3>
<p>To start them automatically at startup, we create a file<code>~/.xinitrc</code> (this file is executed when the X server starts) which contains the following commands (take care to choose your URL and indicate the screen resolution) :</p>
<pre tabindex="0"><code>#!/bin/sh
xset -dpms
xset s off
xset s noblank
unclutter &amp;
chromium-browser /path/to/your/file.html --window-size=1920,1080 --start-fullscreen --kiosk --incognito --noerrdialogs --disable-translate --no-first-run --fast --fast-start --disable-infobars --disable-features=TranslateUI --disk-cache-dir=/dev/null --password-store=basic
</code></pre><p>The <code>xset</code> commands are used to avoid the automatic standby of the system, which will otherwise interrupt the display after a specified time.</p>
<p>The option <code>--window-size=</code> is essential, otherwise Chromium will only be displayed on half of the screen, despite the other instructions, in the absence of a window manager.</p>
</description>
</item>
<item>
<title>Setting up a Nginx server on macOS</title>
<link>https://sylvaindurand.org/setting-up-a-nginx-web-server-on-macos/</link>
<pubDate>Fri, 15 Dec 2017 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/setting-up-a-nginx-web-server-on-macos/</guid>
<description><p>Seeking a satisfactory solution to create a local web server for programming in macOS with PHP and MySQL, I was disappointed that the turnkey solutions were far from equaling the WAMP that may exist on Windows.</p>
<p>But I forgot macOS is a Unix system, and unlike Windows, it&rsquo;s perfectly possible to create a customized local server with some packages. We will see how to install Nginx, PHP-FPM and MariaDB (MySQL) on macOS thanks to Homebrew package manager.</p>
<h2 id="homebrew">Homebrew</h2>
<p>HomeBrew is a package manager for macOS, that allows to easily install various Unix applications. To install, simply execute the command shown on the official website:</p>
<p><a href="https://brew.sh/">Homebrew officiel website</a></p>
<h2 id="nginx">Nginx</h2>
<p>Although Apache is natively included with macOS, we propose here to install Nginx, particularly lightweight and easily configurable. To install and launch Nginx on startup, we use:</p>
<pre tabindex="0"><code>brew install nginx
brew services start nginx
</code></pre><p>Now, <code>localhost:8080</code> should show <code>Welcome to nginx!</code>.</p>
<p>The configuration file is located at <code>/opt/homebrew/etc/nginx/nginx.conf</code>. Here is a minimal working example showing a website in your home folder:</p>
<pre tabindex="0"><code>worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
server {
listen 8080;
server_name localhost;
location / {
root /Users/&lt;user&gt;/my_website;
index index.html;
}
}
}
</code></pre><p>We then restart Nginx in order to take this changes into account:</p>
<pre tabindex="0"><code>brew services restart nginx
</code></pre><h2 id="php">PHP</h2>
<p>In order to use PHP with Nginx we will use PHP-FPM:</p>
<pre tabindex="0"><code>brew install php
</code></pre><p>Then, we re-edit the configuration file to use <code>index.php</code> instead of <code>index.html</code>:</p>
<pre tabindex="0"><code>index index.php;
</code></pre><p>Finally, add in the section <code>server</code> the following lines to run PHP for all files with the extension<code> .php</code>:</p>
<pre tabindex="0"><code>location ~ \.php {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
</code></pre><p>Then, we take these changes into account and start PHP:</p>
<pre tabindex="0"><code>brew services restart nginx
brew services start php
</code></pre><h2 id="mysql">MySQL</h2>
<p>We will now install and launch MariaDB:</p>
<pre tabindex="0"><code>brew install mariadb
brew services start mariadb
</code></pre><p>Here is the perfect MAMP installation !</p>
</description>
</item>
<item>
<title>PHP 5.2 on Debian 7 Wheezy</title>
<link>https://sylvaindurand.org/php-5-2-on-debian-7-wheezy/</link>
<pubDate>Fri, 02 Dec 2016 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/php-5-2-on-debian-7-wheezy/</guid>
<description><p>Sometimes you need to bring back to life old lines of code which will only works with old PHP versions. We will here see how to install PHP 5.2 on Debian 7 Wheezy (because I couldn&rsquo;t get it work on Debian 8 Jessie&hellip;).</p>
<p>PHP 5.2 end of life occured in January 2011; thus it is obviously a very bad idea to install it on a production environment, for obvious security reasons.</p>
<h2 id="sources">Sources</h2>
<p>We will use the Debian 6 Lenny sources. To do so, let&rsquo;s edit <code>/etc/apt/sources.list</code> in order to add the repo:</p>
<pre tabindex="0"><code>deb http://archive.debian.org/debian lenny main contrib non-free
</code></pre><p>We will also edit <code>/etc/apt/preferences</code> in order to tell <code>apt</code> we want to use PHP older versions. We add:</p>
<pre tabindex="0"><code>Package: *
Pin: release n=lenny*
Pin-Priority: 100
Package: php5 php5-common php5-cli php5-curl php5-gd php5-mcrypt php5-mysql php5-mhash php5-xsl php5-xmlrpc php5-sqlite libapache2-mod-php5 phpmyadmin
Pin: release n=lenny*
Pin-Priority: 999
</code></pre><h2 id="installation">Installation</h2>
<p>The installation isn&rsquo;t straightforward due to a conflict with the dependency <code>libkrb53</code>. We will first download the packages:</p>
<pre tabindex="0"><code>cd ~
apt-get update
apt-get download php5-common php5-cli php5-curl php5-gd \
php5-mcrypt php5-mysql php5-mhash php5-xsl php5-xmlrpc \
php5-sqlite libapache2-mod-php5
</code></pre><p>Then, we install them, while specifying we want to ignore the missing <code>libkrb53</code>:</p>
<pre tabindex="0"><code>dpkg --ignore-depends=libkrb53 -i *.deb
</code></pre><h2 id="after-the-installation">After the installation</h2>
<p>Now, PHP 5.2 should work, but now <code>apt</code> doesn&rsquo;t accept to install new packages anymore. It will ask to run <code>apt-get -f install</code>, wich will delete PHP:</p>
<pre tabindex="0"><code>You might want to run &#39;apt-get -f install&#39; to correct these.
The following packages have unmet dependencies:
libapache2-mod-php5 : Depends: libkrb53 (&gt;= 1.6.dfsg.2) but it is not installed
php5-cli : Depends: libkrb53 (&gt;= 1.6.dfsg.2) but it is not installed
E: Unmet dependencies. Try using -f.
</code></pre><p>So, each time you need to install or upgrade a package, you will have to run <code>apt-get -f install</code> before doing your stuff, then to install PHP 5.2 again with <code>dpkg --ignore-depends=libkrb53 -i *.deb</code>.</p>
</description>
</item>
<item>
<title>Making Jekyll multilingual</title>
<link>https://sylvaindurand.org/making-jekyll-multilingual/</link>
<pubDate>Sat, 01 Oct 2016 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/making-jekyll-multilingual/</guid>
<description><p>Jekyll has a very flexible design that allows a great freedom of choice, allowing the user to simply introduce features that are not integrated into its engine. This is particularly the case when one wants to create a multilingual website: while CMS remain very rigid and often require plugins, few filters are sufficient to achieve it with <em>Jekyll</em>.</p>
<p>This article aims to present a way to create a multilingual site with <em>Jekyll</em>. It has to be installed on your computer and you should be able to know how to generate a simple website.</p>
<p><a href="https://sylvaindurand.org/static-website-with-jekyll/">Static website with Jekyll</a></p>
<h2 id="goals">Goals</h2>
<p>The method will provide a fully multilingual website, but will remain very flexible to allow everyone to do what he wants.</p>
<h3 id="flexibility">Flexibility</h3>
<p>Our website can be translated in as many languages as wanted. Each page may be translated or not in the various languages (regardless of the version, they may be pages which are not translated in each language). All pages and posts can be placed as desired.</p>
<h3 id="full-translation">Full translation</h3>
<p>On each page, all content must be in the same language : title, article, but also menus, dates, feeds or typography.</p>
<h3 id="language-selector">Language selector</h3>
<p>A language selector, as the one on the top right of this website, will show the actual language and the different translations available (the selector musn&rsquo;t lead to the translated homepage, but to the translation of the current page).</p>
<h3 id="search-engine-optimization">Search engine optimization</h3>
<p>The links between the different versions of the same content must be known search engines, which can directly provide users with content in their language.</p>
<h3 id="no-plugin">No plugin</h3>
<p>Everything will work without any plugin, in order to get a good compatibility with the future versions of Jekyll and to be able to host it on GitHub Pages.</p>
<h2 id="principle">Principle</h2>
<p>The method is very simple: we will indicate, for each post and page, its language (<code>lang</code>) and an unique identifier (<code>ref</code>) to link the different translations. Jekyll will do the rest!</p>
<p>To do so, we will use <code>lang</code> et <code>ref</code> in the frontmatter of each post and page. For instance, in English:</p>
<pre tabindex="0"><code>---
title: Hello world!
lang: en
ref: hello
---
</code></pre><p>Then, in French:</p>
<pre tabindex="0"><code>---
title: Bonjour le monde !
lang: fr
ref: hello
---
</code></pre><p>Or, in Chinese:</p>
<pre tabindex="0"><code>---
title: δ½ ε₯½οΌδΈηοΌ
lang: zh
ref: hello
---
</code></pre><h2 id="links-between-the-two-translations">Links between the two translations</h2>
<h3 id="list-of-articles-of-the-same-language">List of articles of the same language</h3>
<p>The page listing the articles has to display only the one with the good translation. That is easy to do, thanks to the <code>lang</code> metadata.</p>
<p>The following code provides every article with the same language than the actual page:</p>
<pre tabindex="0"><code>{% assign posts=site.posts | where:&#34;lang&#34;, page.lang %}
&lt;ul&gt;
{% for post in posts %}
&lt;li&gt;
&lt;a href=&#34;{{ post.url }}&#34;&gt;{{ post.title }}&lt;/a&gt;
&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;
</code></pre><p>Unfortunately, there is currently no way to paginate the articles, without any plugin or Javascript.</p>
<h3 id="language-selector-1">Language selector</h3>
<p>To create a language selector, like the one at the top right of this page, the process is very similar. We show the language of each translation available, including the actual page, sorting by path in order to always get the same order:</p>
<pre tabindex="0"><code>&lt;ul&gt;
{% assign posts=site.posts | where:&#34;ref&#34;, page.ref | sort: &#39;lang&#39; %}
{% for post in posts %}
&lt;li&gt;
&lt;a href=&#34;{{ post.url }}&#34; class=&#34;{{ post.lang }}&#34;&gt;{{ post.lang }}&lt;/a&gt;
&lt;/li&gt;
{% endfor %}
{% assign pages=site.pages | where:&#34;ref&#34;, page.ref | sort: &#39;lang&#39; %}
{% for page in pages %}
&lt;li&gt;
&lt;a href=&#34;{{ page.url }}&#34; class=&#34;{{ page.lang }}&#34;&gt;{{ page.lang }}&lt;/a&gt;
&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;
</code></pre><p>As you can see, we need to repeat the code for the pages (<code>site.page</code>) and the posts (<code>site.posts</code>). It will be possible to delete this redundancy when Jekyll will use Liquid 4.</p>
<p>Then, in order to emphase the actual version, just use CSS (you need to declare the <code>lang</code> attribute on the <code>html</code>, with <code>&lt;html lang=&quot;{{ page.lang }}&quot;&gt;</code> in the layout). For instance, if you want to bold it:</p>
<pre tabindex="0"><code>.en:lang(en), .fr:lang(fr), .zh:lang(zh){
font-weight: bold;
}
</code></pre><h3 id="links-to-previous-and-next-posts">Links to previous and next posts</h3>
<p>Using loops can also allow us to show the previous and the next posts:</p>
<pre tabindex="0"><code>{% for post in site.posts %}
{% if post.lang == page.lang %}
{% if prev %}
&lt;a href=&#34;{{ post.url }}&#34;&gt;Previous&lt;/a&gt;
{% endif %}
{% assign prev = false %}
{% if post.id == page.id %}
{% assign prev = true %}
{% endif %}
{% endif %}
{% endfor %}
</code></pre><p>And:</p>
<pre tabindex="0"><code>{% for post in site.posts reversed %}
{% if post.lang == page.lang %}
{% if next %}
&lt;a href=&#34;{{ post.url }}&#34;&gt;Next&lt;/a&gt;
{% break %}
{% endif %}
{% assign next = false %}
{% if post.id == page.id %}
{% assign next = true %}
{% endif %}
{% endif %}
{% endfor %}
</code></pre><h2 id="tweaking">Tweaking</h2>
<h3 id="translation-of-website-elements">Translation of website elements</h3>
<p>Around the articles, it is also necessary to translate the various elements like menus, header, footer, some titles&hellip;</p>
<p>To do so, we can provide translations into <code>_config.yml</code> (since Jekyll 2.0, it is also possible to put the translations in the <code>_data</code> folder). Then, in the following example, <code>{{ site.t[page.lang].home }}</code> will generate <code>Home</code>, <code>Accueil</code> or <code>ι¦ι‘΅</code> depending of the page language:</p>
<pre tabindex="0"><code>t:
en:
home: &#34;Home&#34;
fr:
home: &#34;Accueil&#34;
zh:
home: &#34;ι¦ι‘΅&#34;
</code></pre><p>It is possible to do the same in order to generate the menu of each version. For example, if you want to provide a two elements menu, you just have to provide in <code>_config.yml</code> :</p>
<pre tabindex="0"><code>t:
en:
home:
name: &#34;Home&#34;
url: &#34;/&#34;
about:
name: &#34;About&#34;
url: &#34;/about/&#34;
fr:
home:
name: &#34;Accueil&#34;
url: &#34;/accueil/&#34;
about:
name: &#34;Γ propos&#34;
url: &#34;/a-propos/&#34;
zh:
home:
name: &#34;ι¦ι‘΅&#34;
url: &#34;/ι¦ι‘΅/&#34;
about:
name: &#34;ε
³δΊ&#34;
url: &#34;/ε
³δΊ/&#34;
</code></pre><p>Then, you can generate the menu with a simple loop:</p>
<pre tabindex="0"><code>&lt;ul&gt;
{% for menu in site.t[page.lang] %}
&lt;li&gt;&lt;a href=&#34;{{ menu[1].url }}&#34;&gt;{{ menu[1].name }}&lt;/a&gt;&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;
</code></pre><h3 id="translation-of-dates">Translation of dates</h3>
<p>At this point, everything can be translated on the site except the dates automatically generated by Jekyll. Short formats, consisting only of numbers, can be adapted without difficulty. Depending of the language, we want to get:</p>
<ul>
<li>in English : &ldquo;2014/09/01&rdquo; ;</li>
<li>in French : &ldquo;01/09/2014&rdquo; ;</li>
<li>in Chinese : &ldquo;2014εΉ΄9ζ1ε·&rdquo;.</li>
</ul>
<p>To do so, we just have to use the following code, which we may then put in the <code>_includes</code> folder in order to use it when needed:</p>
<pre tabindex="0"><code>{% if page.lang == &#39;en&#39; %}
{{ page.date | date: &#34;%d/%m/%Y&#34; }}
{% endif %}
{% if page.lang == &#39;fr&#39; %}
{{ page.date | date: &#34;%Y-%m-%d&#34; }}
{% endif %}
{% if page.lang == &#39;zh&#39; %}
{{ page.date | date: &#34;%YεΉ΄%-mζ%-dε·&#34; }}
{% endif %}
</code></pre><p>For the long format dates, it is possible to use date filters and replacements for any format. For example, we want to get:</p>
<ul>
<li>in English : &ldquo;1<!-- raw HTML omitted -->st<!-- raw HTML omitted --> March 2016&rdquo; ;</li>
<li>in French : &ldquo;1<!-- raw HTML omitted -->er<!-- raw HTML omitted --> mars 2016&rdquo;.</li>
</ul>
<p>To do so, we just have to put the following code in a file named <code>date.html</code> stored in the <code>_includes</code>:</p>
<pre tabindex="0"><code>{% assign day = include.date | date: &#34;%-d&#34; %}
{% if page.lang != &#39;fr&#39; %}
{% case day %}
{% when &#39;1&#39; or &#39;21&#39; or &#39;31&#39; %} {{ day }}&lt;sup&gt;st&lt;/sup&gt;
{% when &#39;2&#39; or &#39;22&#39; %} {{ day }}&lt;sup&gt;nd&lt;/sup&gt;
{% when &#39;3&#39; or &#39;23&#39; %} {{ day }}&lt;sup&gt;rd&lt;/sup&gt;
{% else %} {{ day }}&lt;sup&gt;th&lt;/sup&gt;
{% endcase %}
{% else %}
{% if day == &#34;1&#34; %}
{{ day }}&lt;sup&gt;er&lt;/sup&gt;
{% else %} {{ day }}
{% endif %}
{% endif %} {% if page.lang != &#39;fr&#39; %}
{{ include.date | date: &#34;%B&#34; }}
{% else %}
{% assign m = include.date | date: &#34;%-m&#34; %}
{% case m %}
{% when &#39;1&#39; %}janvier
{% when &#39;2&#39; %}fΓ©vrier
{% when &#39;3&#39; %}mars
{% when &#39;4&#39; %}avril
{% when &#39;5&#39; %}mai
{% when &#39;6&#39; %}juin
{% when &#39;7&#39; %}juillet
{% when &#39;8&#39; %}aoΓ»t
{% when &#39;9&#39; %}septembre
{% when &#39;10&#39; %}octobre
{% when &#39;11&#39; %}novembre
{% when &#39;12&#39; %}dΓ©cembre
{% endcase %}
{% endif %} {{ include.date | date: &#34;%Y&#34; }}
</code></pre><p>Then, we just have to call:</p>
<pre tabindex="0"><code>{% include date.html date=page.date %}
</code></pre><h2 id="website-access-and-search-engine">Website access and search engine</h2>
<p>The website is completely static, so it is difficult to know the language of our visitors, either by detecting the headers sent by the browser or on the basis of their geographical location. Nevertheless, it is possible to indicating the search engines which pages are translations of the same content (thus, users finding our website through a search engine should be offered the good translation).</p>
<p>To do so, two ways are possible: use <code>&lt;link&gt;</code> or create a <code>sitemaps.xml</code> file.</p>
<h3 id="with-a-link-tag">With a link tag</h3>
<p>You only have to provide in the <code>&lt;head&gt;</code> part of the page, every translation available of the actual version (you need to be careful to use the good country codes). To do so, we can use the following code, similar to those used previously:</p>
<pre tabindex="0"><code>{% assign posts=site.posts | where:&#34;ref&#34;, page.ref | sort: &#39;lang&#39; %}
{% for post in posts %}
&lt;link rel=&#34;alternate&#34; hreflang=&#34;{{ post.lang }}&#34; href=&#34;{{ post.url }}&#34; /&gt;
{% endfor %}
{% assign pages=site.pages | where:&#34;ref&#34;, page.ref | sort: &#39;lang&#39; %}
{% for page in pages %}
&lt;link rel=&#34;alternate&#34; hreflang=&#34;{{ page.lang }}&#34; href=&#34;{{ page.url }}&#34; /&gt;
{% endfor %}
</code></pre><h3 id="with-a-sitemaps-file">With a sitemaps file</h3>
<p>The <code>sitemaps.xml</code> file, which allows search engines to know the pages and the structure of your website, also helps tell the search engines which pages are different translations of the same content.</p>
<p>For this, just indicate all pages of the site (regardless of language) in <code>&lt;url&gt;</code> elements, and for each of them all the versions that exist, including the one we are now describing.</p>
<p>This file can be generated automatically by Jekyll with the following code, which will create a <code>sitemaps.xml</code> file on the root of the website:</p>
<pre tabindex="0"><code>---
layout:
permalink: /sitemaps.xml
---
&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;
&lt;urlset xmlns=&#34;http://www.sitemaps.org/schemas/sitemap/0.9&#34; xmlns:xhtml=&#34;http://www.w3.org/1999/xhtml&#34;&gt;
{% for post in site.posts %}
{% if post.id contains &#34;404&#34; %}{% else %}
&lt;url&gt;
&lt;loc&gt;{{site.base}}{{ post.url }}&lt;/loc&gt;
{% assign versions=site.posts | where:&#34;ref&#34;, post.ref %}
{% for version in versions %}
&lt;xhtml:link rel=&#34;alternate&#34; hreflang=&#34;{{ version.lang }}&#34; href=&#34;{{site.base}}{{ version.url }}&#34; /&gt;
{% endfor %}
&lt;lastmod&gt;{{ post.date | date_to_xmlschema }}&lt;/lastmod&gt;
&lt;changefreq&gt;weekly&lt;/changefreq&gt;
&lt;/url&gt;
{% endif %}
{% endfor %}
{% for page in site.pages %}
{% if page.id contains &#34;404&#34; %}{% else %}
&lt;url&gt;
&lt;loc&gt;{{site.base}}{{ page.url }}&lt;/loc&gt;
{% assign versions=site.pages | where:&#34;ref&#34;, page.ref %}
{% for version in versions %}
&lt;xhtml:link rel=&#34;alternate&#34; hreflang=&#34;{{ version.lang }}&#34; href=&#34;{{site.base}}{{ version.url }}&#34; /&gt;
{% endfor %}
&lt;changefreq&gt;weekly&lt;/changefreq&gt;
&lt;/url&gt;
{% endif %}
{% endfor %}
&lt;/urlset&gt;
</code></pre></description>
</item>
<item>
<title>Getting a git server with Gitolite</title>
<link>https://sylvaindurand.org/getting-a-git-server-with-gitolite/</link>
<pubDate>Fri, 19 Aug 2016 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/getting-a-git-server-with-gitolite/</guid>
<description><p>Github helped popularize git and its concepts to many. Nevertheless, it involves making public all the codes you want to host, unless you buy a fairly expensive subscription, and for a limited number of projects. Moreover, Github is not open, and it is therefore necessary to give it some trust to store its projects.</p>
<p>Several solutions exist to host a git server on his personal server. For example, Gitlab aims to become equal to Github, with a graphical interface and many tools; thereby, it is very large and relatively heavy for a small server.</p>
<p>Here we will see how to install Gitolite, which provides a very light and functional git server, in order to synchronizing repositories between different working stations.</p>
<h2 id="installation">Installation</h2>
<h3 id="creating-git-user-on-the-server">Creating git user on the server</h3>
<p>First, we will create on the server a <code>git</code> user, which will be needed:</p>
<pre tabindex="0"><code>sudo adduser git
</code></pre><p>Once connected to <code>git</code> on the server, we create a <code>bin</code> directory that will contain binaries, that we then add to the <code>path</code>:</p>
<pre tabindex="0"><code>cd ~
mkdir bin
PATH=$PATH:~/bin
PATH=~/bin:$PATH
</code></pre><h3 id="creating-an-authentication-key">Creating an authentication key</h3>
<p>In order to connect to Gitolite, we will used SSH key-based authentication, both simpler and more secure than HTTP. If you do not already have a key, it is necessary to create one. For this, we will used <code>ssh-keygen</code> locally:</p>
<pre tabindex="0"><code>cd ~/.ssh
ssh-keygen -t ed25519 -a 100
</code></pre><p>When the path is requested, enter your username <code>&lt;user&gt;</code>. The password is optional: if you choose one, it will be asked each time a git command is requesting the server.</p>
<p>We then send the public key on the server:</p>
<pre tabindex="0"><code>cat ~/.ssh/&lt;user&gt;.pub | ssh git@&lt;hostname&gt; -p &lt;port&gt; &#39;umask 0077; mkdir -p .ssh; cat &gt;&gt; &lt;user&gt;.pub&#39;
</code></pre><p>Finally, we create a <code>~/.ssh/config</code> file that contains the following information:</p>
<pre tabindex="0"><code>Host git
HostName &lt;hostname&gt;
Port &lt;port&gt;
User git
IdentityFile ~/.ssh/&lt;user&gt;
</code></pre><h3 id="installing-gitolite">Installing Gitolite</h3>
<p>Back to the user <code>git</code> on the server, we can now install Gitolite:</p>
<pre tabindex="0"><code>cd ~
git clone git://github.com/sitaramc/gitolite
gitolite/install -ln
gitolite setup -pk ~/&lt;user&gt;.pub
</code></pre><p>Locally, you can now check that everything works well with <code>ssh git</code>. This should return something like:</p>
<pre tabindex="0"><code>PTY allocation request failed on channel 0
hello &lt;user&gt;, this is git@&lt;hostname&gt; running
gitolite3 v3.6.5-9-g490b540 on git 2.1.4
R W gitolite-admin
R W testing
</code></pre><p>That&rsquo;s all ! Now, if you want to clone a <code>repo</code> directory, simply run the command <code>git clone git:repo</code>. The <code>git push</code>,<code> pull</code>, <code>fetch</code> &hellip; will operate without password.</p>
<h2 id="configuration">Configuration</h2>
<p>The main originality of Gitolite is that its configuration system uses a specific git repository. To configure Gitolite, simply clone the <code>gitolite-admin</code> repository:</p>
<pre tabindex="0"><code>git clone git:gitolite-admin
</code></pre><p>This repository contains a <code>conf/gitolite.conf</code> file that lists the directories and permissions, and a <code>keydir</code> folder containing the users public keys (your <code>&lt;user&gt;.pub</code> key, provided during the installation, is already there).</p>
<p>For example, to add a <code>project</code> repository, just edit <code>conf/gitolite.conf</code> to add:</p>
<pre tabindex="0"><code>repo project
RW+ = &lt;user&gt;
</code></pre><p>To apply all changes, <code>commit</code> then <code>push</code> those files on the server.</p>
</description>
</item>
<item>
<title>SSH key-based authentication</title>
<link>https://sylvaindurand.org/ssh-key-based-authentication/</link>
<pubDate>Wed, 17 Aug 2016 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/ssh-key-based-authentication/</guid>
<description><p>When you want to connect to an SSH server, default authentication is based on username and password. However, passwords are quite insecure, difficult to remember and hard to write; they are effective against computers if they are very restrictive, even unusable, for humans.</p>
<p>The key-based authentication can fill these two requirements: it provides a very high safety and makes connection fast and easy. This authentication is based on three components:</p>
<ul>
<li>a public key, which is previously provided to the server;</li>
<li>a private key, used to prove its identity while connecting;</li>
<li>a passphrase (optional), which will be requested every time the private key is provided.</li>
</ul>
<h2 id="key-generation">Key generation</h2>
<p>First, we will locally create an SSH private key, and the associated public key:</p>
<pre tabindex="0"><code>cd ~/.ssh
</code></pre><p>The algorithm <code>ed25519</code> appears so far to be one of the most secure, while remaining very fast:</p>
<pre tabindex="0"><code>ssh-keygen -t ed25519 -a 100
</code></pre><p>However, it is still new and is not supported on all systems. In this case, it is possible to use RSA instead:</p>
<pre tabindex="0"><code>ssh-keygen -t rsa -b 4096
</code></pre><p>When the path is asked, simply write the username <code>&lt;user&gt;</code>.</p>
<p>It is then proposed to enter a password, which will be requested every time the private key is provided. It is not necessary. Of course, in any case, the private key should never be transmitted.</p>
<p>Once this has been accomplished, we now have:</p>
<ul>
<li><code>~/.ssh/&lt;user&gt;</code>, your private key;</li>
<li><code>~/.ssh/&lt;user&gt;.pub</code>, the associated public key.</li>
</ul>
<h3 id="transfer-on-the-server">Transfer on the server</h3>
<p>We need to transmit the public key we just generated to the server. Its content must be stored on the server in <code>~/.ssh/authorized_keys</code>.</p>
<p>Locally, this can be done in one command line:</p>
<pre tabindex="0"><code>cat ~/.ssh/&lt;user&gt;.pub | ssh &lt;user&gt;@&lt;hostname&gt; -p &lt;port&gt; &#39;umask 0077; mkdir -p .ssh; cat &gt;&gt; ~/.ssh/authorized_keys&#39;
</code></pre><h2 id="configuration">Configuration</h2>
<p>In order to be able to quickly connect to the server, we create a local configuration file:</p>
<pre tabindex="0"><code>nano ~/.ssh/config
</code></pre><p>It will contain the following data:</p>
<pre tabindex="0"><code>Host &lt;shortcut&gt;
HostName &lt;hostname&gt;
Port &lt;port&gt;
User &lt;user&gt;
IdentityFile ~/.ssh/&lt;user&gt;
</code></pre><p>In order to connect to the server, we will now just have to use:</p>
<pre tabindex="0"><code>ssh &lt;shortcut&gt;
</code></pre><p>If you associated a password to the private key, it will then be asked. Otherwise, you will be connected directly.</p>
</description>
</item>
<item>
<title>Improving typography on Jekyll</title>
<link>https://sylvaindurand.org/improving-typography-on-jekyll/</link>
<pubDate>Mon, 29 Feb 2016 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/improving-typography-on-jekyll/</guid>
<description><p>Observing typographical rules on the Internet is not always easy: although Unicode reserves many areas of characters for typographic symbols, many punctuation marks and spaces are most of the time unused.</p>
<p>With Jekyll, the articles are written very simply with Markdown before being generated in HTML by the engine: we can add automatic rules to improve typography on our site without carrying about it when writing articles.</p>
<h2 id="apply-filters-to-the-content">Apply filters to the content</h2>
<h3 id="modify-paragraphs-leave-the-code">Modify paragraphs, leave the code</h3>
<p>Care must be taken to apply the changes only in paragraphs, not in code blocks that could become unusable for users that will copy them.</p>
<p>To do so, we will replace <code>{{ content }}</code>, in the <code>_layout/default.html</code> file (or equivalent), with :</p>
<pre tabindex="0"><code>{% assign content = content | split: &#39;&lt;pre&#39; %}
{% for parts in content %}
{% assign part = parts | split: &#39;&lt;/pre&gt;&#39; %}
{% assign c = part.first %}
{% assign t = part.last %}
{% if part.size == 2 %}
{% capture output %}{{ output }}&lt;pre{{ c }}&lt;/pre&gt;{% endcapture %}
{% endif %}
{% capture output %}{{ output }}{{ t }}{% endcapture %}
{% endfor %}
{{ output }}
</code></pre><p>We just have to act on the variable <code>t</code>, which corresponds to the texts, by applying different filters, just after <code>{% assign t = part.last %}</code>.</p>
<p>For example, if you want to replace all a&rsquo;s in b&rsquo;s in the texts, but not in the code blocks, juste use:</p>
<pre tabindex="0"><code>{% assign t = t | replace: &#39;a&#39; , &#39;b&#39; %}
</code></pre><h2 id="french-typography">French typography</h2>
<p>We will show here how to make Jekyll automatically generates quotes French and non-breaking spaces to the right length before the various punctuation marks.</p>
<h3 id="using-french-guillemets">Using French guillemets</h3>
<p>For quotes French <code>Β«</code> and <code>Β»</code>, we simply replace English quotation marks, as seen in the previous section, with: (we add a normal non-breaking space before the closing quotation mark, and after entering quote with <code>&amp;#160;</code>)</p>
<pre tabindex="0"><code>{% assign t = t | replace: &#39;β&#39;, &#39;Β«&amp;#160;&#39;
| replace: &#39;β&#39;, &#39;&amp;#160;Β»&#39; %}
</code></pre><h3 id="using-the-right-spaces-before-punctuation">Using the right spaces before punctuation</h3>
<p>In French, the colons (<code>:</code>) and percent (<code>%</code>) must be preceded by a normal non-breaking space, which is obtained with <code>&amp;#160;</code>. We use:</p>
<pre tabindex="0"><code>{% assign t = t | replace: &#39; :&#39;, &#39;&amp;#160;:&#39;
| replace: &#39; %&#39;, &#39;&amp;#160;%&#39; %}
</code></pre><p>However, the semicolon (<code>;</code>), the exclamation point (<code>!</code>) and the question mark (<code>?</code>) are preceded by a thin non-breaking space. Thereof is obtained using <code>&amp;thinsp;</code>. However, it is preferable to use <code>&lt;span style=&quot;white-space:nowrap&quot;&gt;&amp;thinsp;&lt;/span&gt;;</code> because some browsers that do not treat the space as indivisible.</p>
<p>We then use the following code to get the desired result:</p>
<pre tabindex="0"><code>{% assign t = t | replace: &#39; ;&#39;, &#39;&lt;span style=&#34;white-space:nowrap&#34;&gt;&amp;thinsp;&lt;/span&gt;;&#39;
| replace: &#39; !&#39;, &#39;&lt;span style=&#34;white-space:nowrap&#34;&gt;&amp;thinsp;&lt;/span&gt;!&#39;
| replace: &#39; ?&#39;, &#39;&lt;span style=&#34;white-space:nowrap&#34;&gt;&amp;thinsp;&lt;/span&gt;?&#39; %}
</code></pre><h3 id="final-result">Final result</h3>
<p>Finally, the following code provides all the typographical improvements presented above:</p>
<pre tabindex="0"><code>{% assign content = content | split: &#39;&lt;pre&#39; %}
{% for parts in content %}
{% assign part = parts | split: &#39;&lt;/pre&gt;&#39; %}
{% assign c = part.first %}
{% assign t = part.last %}
{% assign t = t | replace: &#39;β&#39;, &#39;Β«&amp;#160;&#39;
| replace: &#39;β&#39;, &#39;&amp;#160;Β»&#39;
| replace: &#39; :&#39;, &#39;&amp;#160;:&#39;
| replace: &#39; %&#39;, &#39;&amp;#160;%&#39;
| replace: &#39; ;&#39;, &#39;&lt;span style=&#34;white-space:nowrap&#34;&gt;&amp;thinsp;&lt;/span&gt;;&#39;
| replace: &#39; !&#39;, &#39;&lt;span style=&#34;white-space:nowrap&#34;&gt;&amp;thinsp;&lt;/span&gt;!&#39;
| replace: &#39; ?&#39;, &#39;&lt;span style=&#34;white-space:nowrap&#34;&gt;&amp;thinsp;&lt;/span&gt;?&#39; %}
{% if part.size == 2 %}
{% capture output %}{{ output }}&lt;pre{{ c }}&lt;/pre&gt;{% endcapture %}
{% endif %}
{% capture output %}{{ output }}{{ t }}{% endcapture %}
{% endfor %}
{{ output }}
</code></pre><p>If your website is multilingual, you can restrict the previous modifications to the French articles, by adding:</p>
<pre tabindex="0"><code>{% if page.lang == &#39;fr&#39; %}
...
{% endif %}
</code></pre><p>Finally, to lighten the code, it is possible to include these codes in a specific page, for example <code>_includes/typography.html</code>. Then, we simply use:</p>
<pre tabindex="0"><code>{% if page.lang != &#39;en&#39; %}
{% include typography.html %}
{% else %}
{{ content }}
{% endif %}
</code></pre></description>
</item>
<item>
<title>Compressing Liquid generated code</title>
<link>https://sylvaindurand.org/compressing-liquid-generated-html/</link>
<pubDate>Sun, 28 Feb 2016 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/compressing-liquid-generated-html/</guid>
<description><p>The Liquid syntax, which is used by Jekyll, has an unpleasant default: it generates significant spaces and unneccessary line breaks in the generated pages source code.</p>
<p>This is especially true when you want to indent your Liquid code, or want to use loops.</p>
<p>For instance, the following example (which show each multiplication which gives &ldquo;12&rdquo;) will generate almost 500 lignes of codes, almost empty:</p>
<pre tabindex="0"><code>&lt;ul&gt;
{% for i in (1..12) %}
{% for j in (1..12) %}
{% assign result = i | times: j %}
{% if result == 12 %}
&lt;li&gt; {{ i }} β¨ {{ j }} = 12 &lt;/li&gt;
{% endif %}
{% endfor %}
{% endfor %}
&lt;/ul&gt;
</code></pre><p>What happens? Liquid reads all the characters in the loop, including spaces and line breaks, and restores them without asking any questions. Thus, this double loop will generate hundred of useless breaklines and spaces.</p>
<p>However, in order to get a readable HTML code, we would like to get:</p>
<pre tabindex="0"><code>&lt;ul&gt;
&lt;li&gt; 1 β¨ 12 = 12 &lt;/li&gt;
&lt;li&gt; 2 β¨ 6 = 12 &lt;/li&gt;
&lt;li&gt; 3 β¨ 4 = 12 &lt;/li&gt;
&lt;li&gt; 4 β¨ 3 = 12 &lt;/li&gt;
&lt;li&gt; 6 β¨ 2 = 12 &lt;/li&gt;
&lt;li&gt; 12 β¨ 1 = 12 &lt;/li&gt;
&lt;/ul&gt;
</code></pre><h2 id="first-try--unindent-your-code">First try : unindent your code</h2>
<p>A first answer, which is also the less satisfying, would be to delete all spaces and linebreak which shoudn&rsquo;t be shown. In the previous example, it would give:</p>
<pre tabindex="0"><code>&lt;ul&gt;{% for i in (1..12) %}{% for j in (1..12) %}{% assign result = i | times: j %}{% if result == 12 %}
&lt;li&gt; {{ i }} β¨ {{ j }} = 12 &lt;/li&gt;{% endif %}{% endfor %}{% endfor %}
&lt;/ul&gt;
</code></pre><p>The HTML output is perfect, but our Liquid code is unreadable&hellip;</p>
<h2 id="second-try--using-capture">Second try : using capture</h2>
<p>The <code>capture</code> tag will store, as a variable, every interpreted code which is inside.</p>
<p>By using a <code>capture</code> tag around a Liquid code, you will hide its output, and therefore the unwanted spaces and linebreaks. Then, you just have to use a second <code>capture</code> tag around the wanted output, then to print it.</p>
<p>The previous example would become:</p>
<pre tabindex="0"><code>&lt;ul&gt;{% capture hide %}
{% for i in (1..12) %}
{% for j in (1..12) %}
{% assign result = i | times: j %}
{% if result == 12 %}
{% capture show %}{{ show }}
&lt;li&gt; {{ i }} β¨ {{ j }} = 12 &lt;/li&gt;{% endcapture %}
{% endif %}
{% endfor %}
{% endfor %}
{% endcapture %}{{ show }}
&lt;/ul&gt;
</code></pre><p>The code remains still a little verbous, and isn&rsquo;t that clean yet.</p>
<h2 id="using--the-final-answer-">Using %}{%, the final answer ?</h2>
<p>Since Jekyll 3.0.0, it is possible to put linebreaks inside Liquid tags, without any influence on the output code. So, it is possible to indent your code like:</p>
<pre tabindex="0"><code>&lt;ul&gt;{%
for i in (1..12) %}{%
for j in (1..12) %}{%
assign result = i | times: j %}{%
if result == 12 %}
&lt;li&gt; {{ i }} β¨ {{ j }} = 12 &lt;/li&gt;{%
endif %}{%
endfor %}{%
endfor %}
&lt;/ul&gt;
</code></pre><p>Despite this unusual way to write Liquid code, it remains quite readable and gives us the wanted result.</p>
<p>However, I couldn&rsquo;t find if this behaviour is wanted by Liquid developers, and thus if it will be maintain in the future.</p>
</description>
</item>
<item>
<title>Getting a web server with Nginx</title>
<link>https://sylvaindurand.org/getting-a-web-server-with-nginx/</link>
<pubDate>Fri, 26 Dec 2014 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/getting-a-web-server-with-nginx/</guid>
<description><p>Apache was once the ideal solution for creating a web server. Its great features induced a certain heaviness, and its popularity made him very attacked, and therefore frequently updated.</p>
<p>In recent years, Nginx has established itself as a high quality alternative: although comprehensive features, the software is characterized by great lightness, which allows to run on very modest configurations such as a Raspberry Pi.</p>
<p>We&rsquo;ll see how to install a Nginx able to use PHP, on a Raspberry Pi which is running Arch Linux. However, the procedure will remain very similar to another machine or other configuration (it is essentially the commands <code>systemctl</code> and <code>pacman</code> which need to be adapted).</p>
<h2 id="installing-nginx">Installing Nginx</h2>
<p>Installing Nginx is easily obtained with:</p>
<pre tabindex="0"><code>pacman -S nginx
</code></pre><p>It is then possible to start Nginx with:</p>
<pre tabindex="0"><code>systemctl start nginx
</code></pre><p>By opening the IP address (or the domain name that points to your Raspberry Pi) of your Raspberry Pi, you should see a page showing that Nginx is functional.</p>
<h2 id="static-website">Static website</h2>
<p>We will start by creating a first site entirely static. The files will be located in <code>/srv/http/mysite</code>, which will be available at <code>mysite.tld</code>.</p>
<p>For this, we edit the Nginx configuration file:</p>
<pre tabindex="0"><code>nano /etc/nginx/nginx.conf
</code></pre><p>Before the latest ending curly bracket of the file, add the following lines for declaring our new website:</p>
<pre tabindex="0"><code>server {
listen 80;
server_name mysite.tld;
root /srv/http/mysite;
index index.html;
}
</code></pre><p>The <code>listen</code> parameter specifies the listening port (we generally chose port 80 for HTTP or 443 for HTTPS), <code>server_name</code> allows to wanted URLs, <code>root</code> the file locations and <code>index</code> the file to be served by default.</p>
<p>We created this folder, and an <code>index.html</code> file with the right attributes:</p>
<pre tabindex="0"><code>mkdir -p /srv/http/mysite
nano /srv/http/index.html
</code></pre><p>Its configuration has been modified, we restart Nginx to take into account the changes:</p>
<pre tabindex="0"><code>systemctl restart nginx
</code></pre><h2 id="dynamic-website">Dynamic website</h2>
<p>Static websites are great, but being able to create dynamic sites is often helpful. This allows, for example, with a Raspberry Pi, to host web applications at home as an RSS reader or cloud. Examples are presented below.</p>
<h3 id="installing-php">Installing PHP</h3>
<p>To install PHP, we use the package <code>php-fpm</code> :</p>
<pre tabindex="0"><code>pacman -S php-fpm
</code></pre><p>The we activate it:</p>
<pre tabindex="0"><code>systemctl start php-fpm
</code></pre><h3 id="creating-a-site-with-php">Creating a site with PHP</h3>
<p>As before, we edit the Nginx configuration file:</p>
<pre tabindex="0"><code>nano /etc/nginx/nginx.conf
</code></pre><p>Before the latest ending curly bracket of the file, add the following lines for declaring how to handle PHP:</p>
<pre tabindex="0"><code>server {
listen 80;
server_name mysite.tld;
root /srv/http/mysite;
index index.php;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi.conf;
fastcgi_index index.php;
fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
}
}
</code></pre><p>Again, we restart Nginx:</p>
<pre tabindex="0"><code>systemctl restart nginx
</code></pre><h3 id="installer-sqlite">Installer sqlite</h3>
<p>A growing number of web applications use <code>sqlite</code> as a database. If you need it, install the package <code>php-sqlite</code> :</p>
<pre tabindex="0"><code>pacman -S php-sqlite
</code></pre><p>Then tells PHP to use it by adding at the end of file <code>/etc/php/php.ini</code> :</p>
<pre tabindex="0"><code>extension=pdo_sqlite.so
</code></pre><p>Then we restart PHP:</p>
<pre tabindex="0"><code>systemctl restart php-fpm
</code></pre><h3 id="installing-mysql">Installing MySQL</h3>
<p>If you want to use MySQL, install the <code>mariadb</code> package, start <code>mysqld</code> then use the installation script:</p>
<pre tabindex="0"><code>pacman -S mariadb
systemctl start mysqld
mysql_secure_installation
</code></pre><p>As before, we then tells PHP to use by adding at the end of file <code>/etc/php/php.ini</code> :</p>
<pre tabindex="0"><code>extension = mysql.so
</code></pre><h2 id="examples">Examples</h2>
<h3 id="getting-an-rss-reader-miniflux">Getting an RSS reader Miniflux</h3>
<p>Miniflux is a web application for reading RSS feeds that I particularly appreciated for its simplicity and minimalist design. It is the replacement for Google Reader I&rsquo;ve searched for a long time.</p>
<p>As previously stated, creating a dynamic site for the folder <code>/srv/http/miniflux/</code> taking care to install sqlite, needed by Miniflux.</p>
<p>Installation is simple: download Miniflux, extract it, and give write access to the folder <code>data/</code>.</p>
<pre tabindex="0"><code>cd /srv/http/
wget http://miniflux.net/miniflux-latest.zip
unzip miniflux-latest.zip
rm miniflux-latest.zip
cd miniflux
chmod 777 data/
</code></pre><p>Then, to enable monitoring RSS feeds every hour, create a <code>cron</code> task with:</p>
<pre tabindex="0"><code>crontab -e
</code></pre><p>Then enter therein:</p>
<pre tabindex="0"><code>0 */1 * * * cd /srv/http/miniflux &amp;&amp; php cronjob.php &gt;/dev/null 2&gt;&amp;1
</code></pre><h3 id="getting-a-personnal-cloud-with-owncloud">Getting a personnal cloud with OwnCloud</h3>
<p>Synchronize calendars, contacts and files between different devices - computers, tablets, phones - is very common today. Because these data can be very personal, installing a web application like OwnCloud on its Raspberry Pi.</p>
<p>Again, create a dynamic site for <code>/srv/http/owncloud/</code>. Then, we download OwnCloud:</p>
<pre tabindex="0"><code>mkdir -p /srv/http/owncloud
wget http://download.owncloud.org/community/owncloud-7.0.4.tar.bz2
tar xvf owncloud-7.0.4.tar.bz2
mv owncloud/ /srv/http/
chown -R www-data:www-data /srv/http
rm -rf owncloud owncloud-7.0.4.tar.bz2
</code></pre><p>We then add a <code>cron</code> task that will automate the update by running:</p>
<pre tabindex="0"><code>crontab -e
</code></pre><p>Then enter therein:</p>
<pre tabindex="0"><code>*/15 * * * * php -f /srv/http/owncloud/cron.php
</code></pre><p>From our local network, we can access the web interface OwnCloud by going to the address of our Raspberry Pi (the external IP or the domain name).</p>
<p>In Applications uncheck all applications you wonβt use, in order to make OwnCloud more fluid. I only keep βCalendarβ and βContactsβ.</p>
<p>In Administration, uncheck the share permissions that are not useful, and select <code>cron</code> as the update method.</p>
<p>You can start creating address books and calendars from the web interface. It provides links <code>CardDAV</code> and <code>CalDAV</code> which can then be entered, along with your username and password, on your computers and devices.</p>
</description>
</item>
<item>
<title>Installing Arch Linux on Raspberry Pi</title>
<link>https://sylvaindurand.org/installing-archlinux-on-raspberry-pi/</link>
<pubDate>Wed, 24 Dec 2014 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/installing-archlinux-on-raspberry-pi/</guid>
<description><p>Is it still necessary to present it? The Raspberry Pi is a nano-computer which, due to its small size, low power consumption and very low cost (about 35 β¬ in its most powerful version, to which must be added an SD card, ΞΌUSB and ethernet cables, less than β¬ 50 in total), makes an essential personal server or hack tool.</p>
<p>Despite its limited computing power, it is perfectly possible to make a web server, a personal cloud, a NAS, an Airplay terminal, a retro games console&hellip; or all at once. Whatever your goal, it is necessary to install a Linux distribution.</p>
<p>We will see how to install and configure Arch Linux on our Raspberry Pi, directly from the command line by the network, which means that we will have no need of keyboard or screen.</p>
<p>So for this tutorial, you&rsquo;ll only need:</p>
<ul>
<li>a <em>Raspberry Pi</em>, model B ;</li>
<li>a <em>SD card</em> with at least 2 GB to install our system;</li>
<li>an <em>Β΅USB cable</em> to put our Raspberry Pi powered;</li>
<li>an <em>ethernet cable</em> to connect our Pi to our router.</li>
</ul>
<h2 id="installing-arch-linux">Installing Arch Linux</h2>
<p>Among the many distributions available for Raspberry Pi, we will use Arch Linux, due to its lightness and comprehensive ecosystem that has been built around it.</p>
<h3 id="downloading">Downloading</h3>
<p>Start by retrieving the latest version of Arch Linux. An image is offered on the Raspberry Pi site (at the time of writing this article, the last image has been published in this format on June 2014; however, we will update the system once it is installed). Download it, and then extract it:</p>
<pre tabindex="0"><code>curl -OL https://downloads.raspberrypi.org/arch/images/arch-2014-06-22/ArchLinuxARM-2014.06-rpi.img.zip
unzip ArchLinuxARM-2014.06-rpi.img.zip
</code></pre><h3 id="installing">Installing</h3>
<p>We will write the previously downloaded image to the SD card. To do this, insert into our computer the SD card that will host the system.</p>
<p>It is necessary for us to know the path to the SD card. On macOS, you can use in the terminal the following command to get the ID of your SD card (<code>/dev/disk2</code> in the following example):</p>
<pre tabindex="0"><code>diskutil list
</code></pre><p>We now write the image to the card. Warning, the entire contents of the SD card will be lost! For this, we enter the following commands (where <code>/dev/disk2</code> (on macOS, we use <code>rdisk2</code> instead of <code>disk2</code> in order to make the copy faster) is the path to the SD card):</p>
<pre tabindex="0"><code>diskutil unmountDisk /dev/disk2
sudo dd if=ArchLinuxARM-2014.06-rpi.img of=/dev/rdisk2 bs=1m
sudo diskutil eject /dev/disk2
</code></pre><p>We can then eject the SD card and insert it into your Raspberry Pi. Connect it to our network, and then put it on.</p>
<h2 id="connecting-to-the-raspberry-pi">Connecting to the Raspberry Pi</h2>
<p>We will access our Raspberry Pi from our computer, using SSH. Users of macOS or Linux can directly launch <code>ssh</code>, while those of Windows prefer to use a program like PuTTY. Otherwise, it is possible to connect a USB keyboard and connect the Raspberry Pi to a monitor using its HDMI port.</p>
<p>Like any computer, the Raspberry Pi is identified on the network by its IP address. For us to connect, it is necessary to know it.</p>
<p>Two options are available:</p>
<ul>
<li>you want to access it from your local network, and in this case it suffices to know what IP your router assigns;</li>
<li>you want to be able to access it anywhere (for instance, this will be the case for a web server), and in this case your router must redirect external requests to your Raspberry Pi.</li>
</ul>
<h3 id="from-the-local-network">From the local network</h3>
<p>After connecting to your router and then turned our Raspberry Pi, we need to know its IP address (under macOS, use <code>arp -a</code>).</p>
<p>For more comfort, go on your router admin interface and ask to assign a fixed IP address. Thus, it will always be the same if your router or Raspberry restart.</p>
<p>Once the address of the Raspberry Pi known (we will use <code>192.168.1.1</code> in the following example), we can connect via SSH to our Raspberry Pi with the command:</p>
<pre tabindex="0"><code>ssh root@192.168.1.1
</code></pre><p>Then validate the security certificate, and enter the default password <code>root</code>. Here we are connected to our Raspberry Pi!</p>
<h3 id="from-anywhere">From anywhere</h3>
<p>In many cases, we want our Raspberry Pi accessible from outside our network. Your router must have a fixed IP; otherwise, it is necessary to use a dynamic DNS client.</p>
<p>For this, the approach depends largely on the model of your router or box: connect to the administration interface and begin to assign a fixed IP to your Raspberry Pi. Then, tell the router to always transfer to this IP address the desired ports: in particular, port 22 for SSH (if you want to configure a web server later, you can do the same with ports 80 and 443).</p>
<p>Once this is achieved, we can now connect from anywhere on internal (where <code>80.23.170.17</code> is the external IP of our network):</p>
<pre tabindex="0"><code>ssh root@80.23.170.17
</code></pre><p>We can also assign a domain name to our Raspberry (especially if it will serve as a web server). At your registar, create an <code>A</code> field for your <code>domain.tld</code> in which you specify the external IP address (<code>80.23.170.17</code> in our example). Once committed changes, we can access our Pi with:</p>
<pre tabindex="0"><code>ssh pi@domain.tld
</code></pre><p>Then validate the security certificate, and enter the default password <code>raspberry</code>. Here we are connected to our Raspberry Pi!</p>
<h2 id="configuring-arch-linux">Configuring Arch Linux</h2>
<h3 id="creating-users">Creating users</h3>
<p>First, let&rsquo;s change the administrator password:</p>
<pre tabindex="0"><code>passwd root
</code></pre><p>We can also take the opportunity to create an user account, to whom we can give <code>sudo</code> rights:</p>
<pre tabindex="0"><code>useradd -m -g users -G wheel -s /bin/bash pi
passwd pi
pacman -S sudo
</code></pre><h3 id="updating-arch">Updating Arch</h3>
<p>To update the entire (this command will update your software as well as the drivers required to Raspberry Pi) system, we simply use the following command:</p>
<pre tabindex="0"><code>pacman -Suy
</code></pre><h3 id="language">Language</h3>
<p>By default, the system is configured in English. In order to obtain an interface in another language, modify the following file:</p>
<pre tabindex="0"><code>nano /etc/locale.gen
</code></pre><p>For French, simply uncomment the line <code>fr_FR.UTF-8</code>. The we regenerate locales with:</p>
<pre tabindex="0"><code>locale-gen
</code></pre><p>Then select the default locale by editing the following file:</p>
<pre tabindex="0"><code>nano /etc/locale.conf
</code></pre><p>To this is added the following content:</p>
<pre tabindex="0"><code>LANG=&#34;fr_FR.UTF-8&#34;
LANGUAGE=&#34;fr_FR:en_US&#34;
LC_COLLATE=C
</code></pre><p>Upon restart, then the terminal will be in the right language.</p>
<h3 id="miscellaneous">Miscellaneous</h3>
<p>Arch Linux use the <code>vi</code> text editor, which I prefer <code>nano</code> for its simplicity. To change this default, we use:</p>
<pre tabindex="0"><code>pacman -Rns vi
ln -s /usr/bin/nano /usr/bin/vi
</code></pre><p>It is also possible to rename the machine name that appears in the terminal. For example:</p>
<pre tabindex="0"><code>hostname raspberry
</code></pre><h3 id="improving-safety">Improving safety</h3>
<p>Our Raspberry Pi being connected to a network, it will be subject to many attacks. To minimize the risk, it is first possible to change the default port for SSH (22) at any port. To do this, edit the following file:</p>
<pre tabindex="0"><code>nano /etc/ssh/sshd_config
</code></pre><p>Change the <code>Port 22</code> line by remplacing <code>22</code> with the wanted number (for instance, <code>50132</code>). You can then connect via SSH, indicating the parameter <code>-p 50132</code>:</p>
<pre tabindex="0"><code>ssh pi@80.23.170.17 -p 50132
</code></pre><p>Moreover, the <code>fail2ban</code> package helps prevent dictionary attacks or bruteforce reading the connection logs and blocking repeated attempts connection with a user name or bad password. Simply install the package:</p>
<pre tabindex="0"><code>pacman -S fail2ban
</code></pre><p>You can regularly monitor the logs to identify fraudulent attempts connection with the command <code>grep 'sshd' /var/log/auth.log</code>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>You now have a fully functional machine accessible from your network or from the Internet. If its computing power is limited, however, it is possible to use it in many ways:</p>
<ul>
<li>web server;</li>
<li>personal cloud;</li>
<li>RSS feed manager;</li>
<li>torrent downloads manager;</li>
<li>NAS;</li>
<li>Airplay terminal;</li>
<li>Retro games console&hellip;</li>
</ul>
</description>
</item>
<item>
<title>Using CSS to add line numbering</title>
<link>https://sylvaindurand.org/using-css-to-add-line-numbering/</link>
<pubDate>Mon, 20 Oct 2014 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/using-css-to-add-line-numbering/</guid>
<description><p>When you want to display a code listing with HTML, you use a <code>&lt;pre&gt;</code> tag in order to indicate that the text is preformatted, then inside one or multiple <code>&lt;code&gt;</code> tags to specify it is a code.</p>
<p>Showing line numbers appears may be quite useful, and the methods to achieve this vary widely: many use Javascript or tables&hellip;</p>
<p>Yet it is possible to achieve this in a simple way, using only CSS and HTML. The line numbers won&rsquo;t be selected when the user wants to copy the code.</p>
<h2 id="html">HTML</h2>
<p>The only thing to do in our HTML code is to use a <code>&lt;code&gt;</code> tag for each line of code. This will produce a perfectly valid HTML code:</p>
<pre tabindex="0"><code>&lt;pre&gt;
&lt;code&gt;line 1&lt;/code&gt;
&lt;code&gt;line 2&lt;/code&gt;
&lt;code&gt;line 3&lt;/code&gt;
&lt;code&gt;line 4&lt;/code&gt;
&lt;code&gt;line 5&lt;/code&gt;
&lt;/pre&gt;
</code></pre><h2 id="css">CSS</h2>
<p>The <code>:before</code> pseudo-element will allow us to show an element before each line, while the CSS counters will allow us to count the lines.</p>
<p>We will first define a counter that starts at one for each block, then which is incremented at each new line of code:</p>
<pre tabindex="0"><code>pre{
counter-reset: line;
}
code{
counter-increment: line;
}
</code></pre><p>Then we display the number at the beginning of each line:</p>
<pre tabindex="0"><code>code:before{
content: counter(line);
}
</code></pre><p>With Webkit browsers, selecting the code to copy it gives the impression that the numbers are selected (although fortunately they are not copied). To avoid this, simply use the property <code>user-select</code>:</p>
<pre tabindex="0"><code>code:before{
-webkit-user-select: none;
}
</code></pre><p>It only remains to improve their style as desired. That&rsquo;s it!</p>
</description>
</item>
<item>
<title>Using Github to serve Jekyll</title>
<link>https://sylvaindurand.org/using-github-to-serve-jekyll/</link>
<pubDate>Sun, 28 Sep 2014 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/using-github-to-serve-jekyll/</guid>
<description><p>GitHub has created a wonderful ecosystem built around the static websites generator Jekyll. GitHub Pages allows to generate, then to serve automatically Jekyll websites. This service, free and quite effective, brings together the best aspects of static websites β speed, reliability, security, ability to use <code>git</code> β while allowing their modification online. This article aims to show how to:</p>
<ul>
<li>synchronize your website with <code>git</code> on GitHub ;</li>
<li>generate and serve it on the fly with GitHub Pages ;</li>
<li>write articles directly on line thanks to Prose ;</li>
<li>test compilations, HTML validity and links with Travis.</li>
</ul>
<h2 id="hosting-our-website-on-github">Hosting our website on GitHub</h2>
<h3 id="account-creation">Account creation</h3>
<p>If you haven&rsquo;t already got one, create an account on GitHub by providing an username (beware, the username is important, because it will be used in the repository and the URL used by GitHub Pages), an email address and a password; otherwise, use your usual login informations.</p>
<h3 id="creating-the-repository">Creating the repository</h3>
<p>In your profile page, choose Repositories then New in order to create the repository which will host our website. The repository name has to be <code>username.github.io</code>, where <code>username</code> is the one you provided when you signed up. The website will also be available on this location.</p>
<p>Choose the Initialize this repository with a README option in order to be able to use directly <code>git clone</code>.</p>
<p>If you have got a GitHub paid plans, you can create a private repository in order to hide your codes (in the case of a simple Jekyll website, this option may not be really useful).</p>
<h3 id="synchronizing-our-local-folder">Synchronizing our local folder</h3>
<p>On your computer, create the folder where the website will be stored, then open a terminal and clone the newly created repository:</p>
<pre tabindex="0"><code>git clone https://github.com/username/username.github.io.git
cd username.github.io
</code></pre><p>Let&rsquo;s start by creating a <code>.gitignore</code> file which will allow us to ignore the <code>_site</code> repository in which the website will be generated (we musn&rsquo;t check this folder, which is only a result of the source code), the <code>Gemfile.lock</code> file we will create later, and if you use on macOS the <code>.DS_Store</code> folders created by the operating system. The <code>.gitignore</code> file will looks like:</p>
<pre tabindex="0"><code>_site
.jekyll-metadata
Gemfile.lock
.DS_Store
</code></pre><p>We can now push this file on GitHub (when you <code>push</code> for the first time, you have to provide your username and your password, but they won&rsquo;t be asked again) :</p>
<pre tabindex="0"><code>git add .gitignore
git commit -m &#34;First commit&#34;
git push
</code></pre><h3 id="synchronizing-the-website">Synchronizing the website</h3>
<p>Now, you only have to put your Jekyll website in this folder. It may be an existing website, or a newly created one thanks to <code>jekyll new</code>. It is then easy to maintain and edit the website with <code>git</code> :</p>
<ul>
<li><code>git add</code> add the changes for the next commit;</li>
<li><code>git commit</code> valids thoses changes;</li>
<li><code>git push</code> sends them on GitHub.</li>
</ul>
<p>For instance, in order to send our new website, we add every file, then we use <code>git commit</code> then <code>git push</code> :</p>
<pre tabindex="0"><code>git add --all
git commit -m &#34;First version&#34;
git push
</code></pre><p>Because the website only use simple text files, you can use git as you would do with any other project.</p>
<h2 id="serving-jekyll-on-github-pages">Serving Jekyll on GitHub Pages</h2>
<p>We will now see how to use GitHub to generate, then host and serve the website. You will just have to use <code>git push</code> in order to make GitHub generating and serving the last version of your website.</p>
<h3 id="being-able-to-reproduce-github-settings">Being able to reproduce GitHub settings</h3>
<p>Each time you will use <code>git push</code>, your website will be automatically generated on GitHub.</p>
<p>This is why it is highly important to make sure everything is going to work perfectly, if we don&rsquo;t want to break our website online. In order to ensure your computer most closely matches the GitHub Pages settings, the best way to do is to use <code>bundler</code>:</p>
<pre tabindex="0"><code>gem install bundler
</code></pre><p>Then, create on the root a <code>Gemfile</code> file which will tells to use the <code>github-pages</code> gem, which provide automatically the last versions and dependancies used by GitHub:</p>
<pre tabindex="0"><code>source &#39;https://rubygems.org&#39;
gem &#39;github-pages&#39;
</code></pre><p>You can then use <code>bundle install</code> in order to install the gem(s). The <code>bundle update</code> command will ensure you always have an up to date system.</p>
<p>The <code>bundle exec jekyll serve</code> command will now generate the website exactly as Github would do, so you can check on <code>http://localhost:4000</code> that everything is right before pushing anything.</p>
<h3 id="activating-github-pages">Activating GitHub Pages</h3>
<p>Back on GitHub, go in the <code>username.github.io</code> repository, choose <code>Settings</code>, then the <code>GitHub Pages</code> section in order to activate the website generation.</p>
<p>Within a few moments (the first time, it can take a dozen of minutes, but then the website will be generated in a couple of seconds each time you push a commit), your website will be available on <code>https://username.github.io</code>.</p>
<h3 id="using-a-custom-domain-name">Using a custom domain name</h3>
<p>If you have a custom domain name &ldquo;<code>domain.tld</code>&rdquo;, it is of course possible to use it instead of the default URL given by GitHub, which will then redirect to the new domain name. However, it won&rsquo;t be possible to use HTTPS (if you try to go to <code>https://domain.tld</code>, you will get a blank page showing <code>unknown domain: domain.tld</code>).</p>
<p>If you want to use a subdomain <code>subdomain.domain.tld</code>, tell your registar to create a <code>CNAME</code> record, with the <code>subdomain</code> name and the <code>username.github.io</code> value. Then, create a file named <code>CNAME</code> on the root of the repository, containing exactly <code>subdomain.domain.tld</code>.</p>
<h3 id="custom-404-error-page">Custom 404 error page</h3>
<p>GitHub allows you to have a custom 404 error page. When you test your website locally with <code>bundle exec jekyll serve</code>, this error page also works: you can try by providing an incorrect URL. Just tell Jekyll to create a <code>404.html</code> on the root:</p>
<pre tabindex="0"><code>---
title: Page not found
permalink: /404.html
---
This page must have been removed or had its name changed.
</code></pre><h2 id="to-go-further">To go further</h2>
<h3 id="writing-and-editing-your-articles-online-with-prose">Writing and editing your articles online with Prose</h3>
<p>One of the main disadvantages of static websites is the impossibility to edit them online without your computer.</p>
<p>Because your website is hosted on GitHub, it is possible to modify files directly online. Jekyll generates Pages each time you modify a page. You can also use the Prose.io website, which provides a nice interface, a great syntax highlighting and a preview system in order to write your articles.</p>
<p>On the Prose.io website, use your GitHub login information and allow Prose to see and change your repositories. You can then create and edit your articles, or any other file of the website.</p>
<h3 id="using-travis-to-check-your-website">Using Travis to check your website</h3>
<p>Travis allows your to generate the website each time you push something, in order to check nothing is wrong. It is also possible to add some other tests like <code>htmlproofer</code> which checks if the HTML code is valid and there are no rotten links. You will get a warning email if something is wrong.</p>
<p>To do so, use your GitHub login informations on Travis, then enable the <code>username.github.io</code> repository. Then, add <code>htmlproofer</code> in your <code>Gemfile</code> file, which now looks like:</p>
<pre tabindex="0"><code>source &#39;https://rubygems.org&#39;
gem &#39;github-pages&#39;
gem &#39;html-proofer&#39;
</code></pre><p>Finally, create a <code>.travis.yml</code> file in order to tell Travis how to build and test the website:</p>
<pre tabindex="0"><code>language: ruby
rvm:
- 2.1.1
script:
- bundle exec jekyll build &amp;&amp; bundle exec htmlproof ./_site
</code></pre><p>Now, each time you push something, Travis will send you an email if Jekyll can&rsquo;t generate your website, if the HTML code is not valid or if a link rot remains.</p>
</description>
</item>
<item>
<title>Website delivery with Cloudfront</title>
<link>https://sylvaindurand.org/website-delivery-with-cloudfront/</link>
<pubDate>Sun, 14 Sep 2014 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/website-delivery-with-cloudfront/</guid>
<description><p>The main interest of static website is to be able to be stored in the cloud, that is to say on a content delivery network able to serve our data with amazing performances, a very low cost, and multiple benefits in terms of lightness, safety and reliability.</p>
<p>This article sets out to show how to host a static website (wether built from a generator like Jekyll, Pelican or Hyde, or &ldquo;by hand&rdquo;) on Amazon Web Services, especially Amazon S3 in order to store the website files, and CloudFront in order to deliver them. The main purpose is to be able to get a website fast from anywhere, reliable, secure, highly scalable and inexpensive (with a low traffic, the hosting will cost you about one dollar a month).</p>
<p>To this purpose, we will use:</p>
<ul>
<li>S3 in order to store the website;</li>
<li>CloudFront in order to deliver our data;</li>
<li>Route 53 in order to use our domain name.</li>
<li>Awstats in order to analyse the logs.</li>
</ul>
<h2 id="hosting-on-s3">Hosting on S3</h2>
<p>After having created an AWS account, go to the management console, then to S3: this service will store the website files.</p>
<h3 id="creating-buckets">Creating buckets</h3>
<p>The website will be located on <code>www.domain.tld</code>. The domain name root, <code>domain.tld</code>, will redirect to this location. Create two buckets (with <code>Create bucket</code>) named from the expected URL: <code>domain.tld</code> and <code>www.domain.tld</code>.</p>
<h3 id="bucket-hosting-the-website">Bucket hosting the website</h3>
<p>In <code>www.domain.tld</code> properties, activate website hosting (<code>Static Website Hosting</code> then <code>Enable website hosting</code>): you can choose a home page (<code>index.html</code>) or an error page (<code>error.html</code>). The website will be available from the <code>Endpoint</code> URL: keep it, we will need it in the following section.</p>
<h3 id="buckets-redirecting">Buckets redirecting</h3>
<p>In <code>domain.tld</code> properties, choose <code>Static Website Hosting</code> in order to select <code>Redirect all requests</code>: we provide <code>domain.tld</code>. This bucket will stay empty.</p>
<p>From <code>Endpoint</code> location, we can now see the files hosted in <code>www.domain.tld</code> bucket. We can upload those files from the AWS console, but we will explain on the last part how to upload it with one bash command line.</p>
<h2 id="serving-data-with-cloudfront">Serving data with Cloudfront</h2>
<p>S3 hosts our data in one unique location. Data stored in Dublin will be provided quite fast to a visitor located in Paris (about 200 ms in order to load the home page of this website) but less in New-York (500 ms) or Shanghai (1,300 ms).</p>
<p>Amazon CloudFront is a CDN, serving content to end-users with high availability and high performance. The access time falls below 100 ms in Paris, New-York and Shanghai.</p>
<p>In return, a propagation delay exists between an upload and its update on Cloudfront. We will see in the last part how to notify any modification.</p>
<h3 id="creating-the-distribution">Creating the distribution</h3>
<p>In the AWS management console, choose CloudFront, <code>Create Distribution</code>, then <code>Web</code>.</p>
<p>In <code>Origin Domain Name</code>, we provide the address previously copied, similar to <code>www.domain.tld.s3-website-eu-west-1.amazonaws.com</code>. The field will automatically propose an other value (like <code>www.domain.tld.s3.amazonaws.com</code>); don&rsquo;t click on it: URL ending with <code>/</code> wouldn&rsquo;t lead to <code>/index.html</code>. Choose an ID in <code>Origin ID</code>.</p>
<p>Leave the other field as default, except <code>Alternate Domain Names</code> where we provide our domain name: <code>www.domain.tld</code>. Indicate the homepage in <code>Default Root Object</code>: <code>index.html</code>.</p>
<p>Our distribution is now created: we can now activate it with <code>Enable</code>. <code>InProgress</code> status means Cloudfront is currently propagating our data; when it&rsquo;s over, the status will become <code>Deployed</code>.</p>
<h2 id="domain-name-with-route-53">Domain name with Route 53</h2>
<h3 id="creating-zone">Creating zone</h3>
<p>Route 53 service will allow us to use our own domain name. In AWS console management, select Route 53 then <code>Create Hosted Zone</code>. In <code>Domain Name</code>, put your domain name without any sub-domain: <code>domain.tld</code>.</p>
<h3 id="redirecting-dns">Redirecting DNS</h3>
<p>Select the newly created zone, then the <code>NS</code> type. Its <code>Value</code> field gives 4 addresses. Tell your registrar to make the DNS pointing to them.</p>
<h3 id="domain-with-cloudfront">Domain with Cloudfront</h3>
<p>Back to Route 53, in <code>domain.tld</code>, create 3 records set with <code>Create Record Set</code>:</p>
<ul>
<li><code>domain.tld</code> (put <code>www</code> in <code>name</code>), <code>A</code> type: in <code>Alias</code>, select our Cloudfront distribution;</li>
<li><code>domain.tld</code>, <code>A</code> type: select the same name bucket.</li>
</ul>
<p>Of course, it is possible to redirect sub-domains to other services (with NS, A and CNAME records) and to use mails (MX records).</p>
<p>Now, an user going to <code>domain.tld</code> or <code>www.domain.tld</code> will target the same name buckets (thanks to Route 53) which redirect to <code>www.domain.tld</code> (thanks to S3). This address directly leads (thanks to Route 53) to the Cloudfront distribution, which provides our files stored in the bucket <code>www.domain.tld</code>. Now, we just have to send our website to Amazon S3.</p>
<h2 id="deploying-jekyll-to-the-cloud">Deploying Jekyll to the cloud</h2>
<p>We will now create a <code>sh</code> file which will build the website, compress and send to Amazon S3 files which has been updated since the previous version, and indicate it to Cloudfront.</p>
<h3 id="prerequisite">Prerequisite</h3>
<p>We will use <code>s3cmd</code> in order to sync our bucket with our local files. Install the last development version (you need to install at least the 1.5 version) which allows us to invalidate files on Cloudfront.</p>
<p>On Mac OS, with Homebrew, install <code>s3cmd</code> with <code>--devel</code> option, and <code>gnupg</code> for secured transfers:</p>
<pre tabindex="0"><code>brew install --devel s3cmd
brew install gpg
</code></pre><p>Now we have to gives <code>s3cmd</code> the ability to deal with our AWS account. In Security Credentials, go to <code>Access Key</code> and generate one access key and its secret Key. Then configure <code>s3cmd</code> with <code>s3cmd --configure</code>.</p>
<p>In order to optimize images, we will install <code>jpegoptim</code> and <code>optipng</code>:</p>
<pre tabindex="0"><code>sudo brew install jpegoptim
sudo brew install optipng
</code></pre><h3 id="building-jekyll-and-compressing-files">Building Jekyll and compressing files</h3>
<p>We first build Jekyll into the <code>_site</code> folder:</p>
<pre tabindex="0"><code>jekyll build
</code></pre><p>Using <code>jekyll-press</code> plugin will optimize HTML, CSS and JS files. If <code>jpegoptim</code> and <code>optipng</code> are installed, we can optimize images:</p>
<pre tabindex="0"><code>find _site -name &#39;*.jpg&#39; -exec jpegoptim --strip-all -m80 {} \;
find _site -name &#39;*.png&#39; -exec optipng -o5 {} \;
</code></pre><p>Then, in order to improve performances, we compress HTML, CSS and JS files with Gzip, that is to say all files out of <code>static/</code> folder:</p>
<pre tabindex="0"><code>find _site -path _site/static -prune -o -type f \
-exec gzip -n &#34;{}&#34; \; -exec mv &#34;{}.gz&#34; &#34;{}&#34; \;
</code></pre><h3 id="uploading-files-to-amazon-s3">Uploading files to Amazon S3</h3>
<p>We use <code>s3cmd</code> to upload the website; only the updated files will be sent. We use the following options:</p>
<ul>
<li><code>acl-public</code> make our files public;</li>
<li><code>cf-invalidate</code> warn Cloudfront files have to be updated;</li>
<li><code>add-header</code> defines headers (compression, cache duration&hellip;);</li>
<li><code>delete-sync</code> delete files removed locally;</li>
<li><code>M</code> defines files MIME type.</li>
</ul>
<p>We first send static files, stored in <code>static/</code>, assigning them a 10 weeks cache duration:</p>
<pre tabindex="0"><code>s3cmd --acl-public --cf-invalidate -M \
--add-header=&#34;Cache-Control: max-age=6048000&#34; \
--cf-invalidate \
sync _site/static s3://www.domain.tld/
</code></pre><p>Then we send the other files (HTML, CSS, JS&hellip;) with a 48 hours cache duration:</p>
<pre tabindex="0"><code>s3cmd --acl-public --cf-invalidate -M \
--add-header &#39;Content-Encoding:gzip&#39; \
--add-header=&#34;Cache-Control: max-age=604800&#34; \
--cf-invalidate \
--exclude=&#34;/static/*&#34; \
sync _site/ s3://www.domain.tld/
</code></pre><p>Finally we clean the bucket by deleting files which have been deleted in the local folder, and we invalidate the home page on Cloudfront (<code>cf-invalidate</code> doesn&rsquo;t do it):</p>
<pre tabindex="0"><code>s3cmd --delete-removed --cf-invalidate-default-index \
sync _site/ s3://www.domain.tld/
</code></pre><h3 id="deploy-in-one-single-command">Deploy in one single command</h3>
<p>We put those command in one single file, named <code>_deploy.sh</code>, located in Jekyll folder:</p>
<pre tabindex="0"><code>#!/bin/sh
# Building Jekyll
jekyll build
# Compressing and optimizing files
find _site -path _site/static -prune -o -type f \
-exec gzip -n &#34;{}&#34; \; -exec mv &#34;{}.gz&#34; &#34;{}&#34; \;
find _site -name &#39;*.jpg&#39; -exec jpegoptim --strip-all -m80 {} \;
find _site -name &#39;*.png&#39; -exec optipng -o5 {} \;
# Synchronisation des mΓ©dias
s3cmd --acl-public --cf-invalidate -M \
--add-header=&#34;Cache-Control: max-age=6048000&#34; \
--cf-invalidate \
sync _site/static s3://www.domain.tld/
# Sync media
s3cmd --acl-public --cf-invalidate -M \
--add-header &#39;Content-Encoding:gzip&#39; \
--add-header=&#34;Cache-Control: max-age=604800&#34; \
--cf-invalidate \
--exclude=&#34;/static/*&#34; \
sync _site/ s3://www.domain.tld/
# Delete removed files
s3cmd --delete-removed --cf-invalidate-default-index \
sync _site/ s3://www.domain.tld/
</code></pre><p>You only have to execute <code>sh _deploy.sh</code> to update the website. A few minutes may be required in order to update CloudFront data.</p>
<h2 id="stats">Stats</h2>
<p>Although our site is static and served by a CDN, it is quite possible to analyze the logs if you do not want to use a system based on a javascript code, such as Matomo or Google Analytics. Here, we will automate the task (recovery logs, processing and displaying statistics) from a server (in our example, a Raspberry Pi in Raspbian) and we will use Awstats.</p>
<h3 id="retrieving-logs">Retrieving logs</h3>
<p>Let&rsquo;s start by activating logs creation on our Cloudfront distribution. In the AWS Management Console, select the Amazon S3 service and create a &ldquo;statistics&rdquo; bucket which will store logs waiting to be retrieved. Then, in Cloudfront, select the distribution that provides our website, then <code>Distribution</code> settings, <code>Edit</code>, and select the <code>statistics</code> bucket in the field <code>Bucket for Logs</code>.</p>
<p>We create locally a folder that will retrieve these logs, then we can then retrieve the logs and then delete the bucket using <code>s3cmd</code>:</p>
<pre tabindex="0"><code>mkdir ~/awstats
mkdir ~/awstats/logs
s3cmd get --recursive s3://statistics/ ~/awstats/logs/
s3cmd del --recursive --force s3://statistics/
</code></pre><h3 id="installing-and-configuring-awstats">Installing and configuring Awstats</h3>
<p>We begin by installing and copy Awstats (where <code>www.domain.tld</code> is your domain name):</p>
<pre tabindex="0"><code>sudo apt-get install awstats
sudo cp /etc/awstats/awstats.conf \
/etc/awstats/awstats.www.domain.tld.conf
sudo nano /etc/awstats/awstats.www.domain.tld.conf
</code></pre><p>In this configuration file, change the following settings to specify how to treat Awstats logs Cloudfront (where <code>user</code> is your username):</p>
<pre tabindex="0"><code># Processing multiple gzip logs files
LogFile=&#34;/usr/share/awstats/tools/logresolvemerge.pl /home/user/awstats/logs/* |&#34;
# Formating Cloudfront generated logs
LogFormat=&#34;%time2 %cluster %bytesd %host %method %virtualname %url %code %referer %ua %query&#34;
LogSeparator=&#34;\t&#34;
# Domain names (website and Cloudfront)
SiteDomain=&#34;www.domain.tld&#34;
HostAliases=&#34;REGEX[.cloudfront\.net]&#34;
</code></pre><p>Finally, we copy the images which will be displayed in the reports:</p>
<pre tabindex="0"><code>sudo cp -r /usr/share/awstats/icon/ ~/awstats/awstats-icon/
</code></pre><h3 id="generating-stats">Generating stats</h3>
<p>Once this configuration is done, it is possible to generate statistics as a static HTML file using:</p>
<pre tabindex="0"><code>/usr/share/awstats/tools/awstats_buildstaticpages.pl \
-dir=~/awstats/ -update -config=www.domain.tld \
</code></pre><p>The statistics are now readable from the <code>awstats.www.domain.tld.html</code> file. It is then possible to publish it, send it to a server or email for example.</p>
<h3 id="regular-updating">Regular updating</h3>
<p>To automate the generation of statistics at regular intervals, creating a <code>stats.sh</code> with <code>nano ~/awstats/stats.sh</code> that retrieves logs and generates statistics:</p>
<pre tabindex="0"><code>#!/bin/sh
# Retrieving logs
s3cmd get --recursive s3://statistics/ ~/awstats/logs/
s3cmd del --recursive --force s3://statistics/
# Generating stats
/usr/share/awstats/tools/awstats_buildstaticpages.pl \
-dir=~/awstats/ -update -config=www.domain.tld \
</code></pre><p>We give the rights to this file so it can be executed, and then create a cron task:</p>
<pre tabindex="0"><code>sudo chmod 711 ~/awstats/stats.sh
sudo crontab -e
</code></pre><p>To generate statistics of every six hours for example:</p>
<pre tabindex="0"><code>0 */6 * * * ~/awstats/stats.sh
</code></pre></description>
</item>
<item>
<title>Static website with Jekyll</title>
<link>https://sylvaindurand.org/static-website-with-jekyll/</link>
<pubDate>Mon, 01 Sep 2014 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/static-website-with-jekyll/</guid>
<description><p>At the beginning of the Internet, there were static sites: each web page was written &ldquo;by hand&rdquo; using a text editor, and then put online. The disadvantages are many, especially the need to duplicate the same changes on some pages (this is why it was sometimes really hard to maintain a website), to know HTML and to have his computer available to edit pages. The advent of CSS, which allows to separate actual content from its presentation format and to share it between the pages has not changed this fact (moreover, the inexistant interoperability between browsers and the poor support of CSS with Microsoft Internet Explorer, have delayed its use).</p>
<p>It was then that appeared dynamic sites: the different programming languages, running on the server side, such as PHP, helped the rise of CMS, which made possible to create sites and change their content directly from a browser, thus allowing the emergence of sites, blogs, forums accessible to the greatest number. This is for example the case of Spip, Dotclear or WordPress. However, these systems are not without disadvantages:</p>
<ul>
<li>they are very sensitive to security gaps, which implies that you need to monitor carefully updates and logs;</li>
<li>they are server ressource intensive, and need specific hostings for large volume of visitors;</li>
<li>they badly handle significant increases in workload, so they are very sensitive to DDoS attacks or huge influx of visitors (it is not uncommon that a website become unavailable because of an important event or a link from a news website);</li>
<li>they tend to be a labyrinthine system, largely overkill for their use and needing databases.</li>
</ul>
<p>For a couple of years, static websites has come back into favor with the emergence of the static website generators. With simple text files, a program generates a website made entirely from static pages you just have to host. Security problems are thus virtually non-existent, it is possible to host your website on a very modest server or rather the opposite, to get excellent performances and handle huge increases in workload using a CDN like Cloudflare or Cloudfront. Ways to host a static website on Amazon S3 and Cloudfront are explained here:</p>
<p><a href="https://sylvaindurand.org/static-website-with-cloudfront/">Static website with Cloudfront</a></p>
<p>In addition, it is possible to follow the changes and to work collaboratively thanks to <code>git</code>, to write the articles online and to generate the website on the fly with services like GitHub and Prose.io, or to have a commenting system with Disqus.</p>
<p>This article will show how to install (I) and use (II) the Jekyll website static generator to create and modify a simple website.</p>
<h2 id="first-website-with-jekyll">First website with Jekyll</h2>
<h3 id="installation-of-jekyll">Installation of Jekyll</h3>
<p>Directly install the last stable version of Ruby, or RVM, then simply launch:</p>
<pre tabindex="0"><code>gem install jekyll
</code></pre><h3 id="creation-of-a-new-website">Creation of a new website</h3>
<p>The command <code>jekyll new mysite</code> will create the source code of a working website in the <code>mysite</code> folder. In this folder, you can generate the website with <code>jekyll build</code>. The output can then be seen in the <code>_site/</code> folder.</p>
<p>With a single command, it is possible to generate the website and create a local host in order to watch the produced website: use <code>jekyll serve</code> in order to get your website available on <code>http://localhost:4000</code>. You can also automatically regenerate the website each time you change something in the source code (however, this option doesn&rsquo;t detect the changes provided in <code>_config.yml</code>) with <code>jekyll serve -w</code>, which will be by far the most useful command when you&rsquo;ll start to play with Jekyll.</p>
<h3 id="tree-structure">Tree structure</h3>
<p>Jekyll uses several folders:</p>
<ul>
<li><code>_posts/</code> in which the articles are stored (you can freely organize your files in <code>_post</code>), with names such as <code>yyyy-mm-dd-post-name.md</code>;</li>
<li><code>_layouts/</code> which contains the layout of the website, that is to say everythin that will surround the articles;</li>
<li><code>_includes/</code>, which contains small page fragments that you wish to include in multiple places on your site (when you put a file in <code>_includes</code>, it is possible to include it anywhere with the tag <code>{{ include filename }}</code>. Il is also possible to provide it parameters).</li>
</ul>
<p>The tree structure may then looks like:</p>
<pre tabindex="0"><code>mysite/
_includes/
_layouts/
default.html
page.html
post.html
_posts/
2014-08-24-static-website-with-jekyll.md
assets/
style.sass
script.js
favicon.ico
index.html
rss.xml
_config.yml
</code></pre><p>You can add any folder or file in your website folder. If they don&rsquo;t start with an underscore, Jekyll will generate them on the same location.</p>
<h2 id="using-jekyll">Using Jekyll</h2>
<p>Now that the first website is created, we will see how to make it evolve, how to write articles and use metadata.</p>
<p>In order to create an article, juste create in <code>_posts</code> folder a file with a name with the following format: <code>yyyy-mm-dd-post-name.md</code> (it is also possible to create articles in the folder <code>_drafts</code>, without any date in the file name: thus, it will create drafts invisible in the posts list but available with their URL). This file is divided into two sections: the frontmatter where the metadata are stored, and the content of the article.</p>
<h3 id="frontmatter-and-metadata">Frontmatter and metadata</h3>
<p>The frontmatter allows us to declare metadata, which will be called or tested in the website. It is set into the top of the file, under the following format:</p>
<pre tabindex="0"><code>---
layout: default
title: My title
---
</code></pre><p>Only the <code>layout</code> variable is required: it defines which file in <code>_layouts/</code> Jekyll should use to build the page. It is also usual to define a <code>title</code> variable in order to provide a title to our article (some variables are reserved by Jekyll with a particuliar behaviour: <code>permalink</code> for exemple specifies the final URL of a file).</p>
<p>It is also possible to define default variables, declared once for all or parts of the articles. For instance, instead of declare <code>layout: default</code> in each article stored in <code>_posts/</code>, you may declare it once in <code>_config.yml</code>:</p>
<pre tabindex="0"><code>defaults:
-
scope:
path: &#34;_posts&#34;
values:
layout: &#34;default&#34;
</code></pre><h3 id="writing-articles-with-markdown">Writing articles with Markdown</h3>
<p>By default, Jekyll use Markdown. The purpose of this language is to provide a very simple syntax to replace the most commons HTML tags. It is however still quite possible to use HTML in posts.</p>
<p>From its second version, Jekyll uses Kramdown which add many features like the possibility of giving CSS classes to elements, footnotes, definition lists, tables&hellip;</p>
<h3 id="using-metadata">Using metadata</h3>
<p>Any metadata &ldquo;<code>variable</code>&rdquo;, declared in the frontmatter or as a default, can be called anywhere in the website, with the tag <code>{{ page.variable }}</code>, which returns its value.</p>
<p>It is also possible to do some tests:</p>
<pre tabindex="0"><code>{% if page.variable == &#39;value&#39; %}
banana
{% else %}
coconut
{% endif %}
</code></pre><p>We can also, for example, make loops on each article satisfying some conditions:</p>
<pre tabindex="0"><code>{% assign posts=site.posts | where: &#34;variable&#34;, &#34;value&#34; %}
{% for post in posts %}
{{ post.lang }}
{% endfor %}
</code></pre><p>Although the syntax may not be very elegant to use, the large number of available variables, plus the custom metadata, combined with the filters et commands, may become highly effective.</p>
</description>
</item>
<item>
<title>Spatial Data Analysis with R</title>
<link>https://sylvaindurand.org/spatial-data-analysis-with-r/</link>
<pubDate>Sat, 01 Feb 2014 00:00:00 +0000</pubDate>
<guid>https://sylvaindurand.org/spatial-data-analysis-with-r/</guid>
<description><p>If R language has already become a reference in statistical analysis and data processing, it may be thanks to its hability to represent and visualize data. They can be used in order to visualize spatial data in the form of cartographic representations which, combined with its other features, makes it an excellent geographic information system. This article sets out to show, through the provision of relevant example, how R can handle spatial data by creating maps.</p>
<h3 id="prerequisite">Prerequisite</h3>
<p>Once R is installed on your computer, few libraries will be used: <code>rgdal</code> allows us to import and project shapefiles, <code>plotrix</code> creates color scales, and <code>classInt</code> assigns colors to map data. Once the libraries installed with <code>install.packages</code>, load them at the beginning of the session:</p>
<pre tabindex="0"><code>library(&#39;rgdal&#39;) # Reading and projecting shapefiles
library(&#39;plotrix&#39;) # Creating color scales
library(&#39;classInt&#39;) # Assigning colors to data
</code></pre><p>Graphics will be plot using R base functions. <code>ggplot2</code> is an alternative, but it seems less relevant here: longer and less legible code, unability to plot holes inside polygones, <code>fortify</code> and ploting can last much longer.</p>
<h2 id="blank-france-map">Blank France map</h2>
<h3 id="reading-shapefiles">Reading shapefiles</h3>
<p>The <code>rgdal</code> library provides <code>readOGR()</code> in order to read shapefiles. <code>dsn</code> must contain the path where shapefiles are located, and <code>layer</code> the shapefile name, without extension. <code>readOGR</code> reads <code>.shp</code>, <code>.shx</code>, <code>.dbf</code> and <code>.prj</code> files. Departements of France are given by Geofla:</p>
<pre tabindex="0"><code># Reading departements
departements &lt;- readOGR(dsn=&#34;shp/geofla&#34;, layer=&#34;DEPARTEMENT&#34;)
# Reading departements boundaries in order to plot France boundaries
bounderies &lt;- readOGR(dsn=&#34;shp/geofla&#34;, layer=&#34;LIMITE_DEPARTEMENT&#34;)
bounderies &lt;- bounderies[bounderies$NATURE %in% c(&#39;Fronti\xe8re internationale&#39;,&#39;Limite c\xf4ti\xe8re&#39;),]
</code></pre><p>In order to show neighbouring countries, we will use data provided by Natural Earth. We will select Europe countries only:</p>
<pre tabindex="0"><code># Reading country and selecting Europe
europe &lt;- readOGR(dsn=&#34;shp/ne/cultural&#34;, layer=&#34;ne_10m_admin_0_countries&#34;)
europe &lt;- europe[europe$REGION_UN==&#34;Europe&#34;,]
</code></pre><h3 id="projection-and-plot">Projection and plot</h3>
<p>The map will use the French official projection &ldquo;Lambert 93&rdquo;, already declared in the Geofla <code>.prj</code> files. <code>spTransform</code> will be used for the European coutries.</p>
<p>Then, we will first plot French boundaries, in order to center the map on France. Borders colors are defined in <code>border</code>, their tickness in <code>lwd</code> and the filling color in <code>col</code>.</p>
<pre tabindex="0"><code># Projection
europe &lt;- spTransform(europe, CRS(&#34;+init=epsg:2154&#34;))
# Plot
pdf(&#39;france.pdf&#39;,width=6,height=4.7)
par(mar=c(0,0,0,0))
plot(bounderies, col=&#34;#FFFFFF&#34;)
plot(europe, col=&#34;#E6E6E6&#34;, border=&#34;#AAAAAA&#34;,lwd=1, add=TRUE)
plot(bounderies, col=&#34;#D8D6D4&#34;, lwd=6, add=TRUE)
plot(departements,col=&#34;#FFFFFF&#34;, border=&#34;#CCCCCC&#34;,lwd=.7, add=TRUE)
plot(bounderies, col=&#34;#666666&#34;, lwd=1, add=TRUE)
dev.off()
</code></pre><p><img src="france.webp" alt="France blank map"></p>
<h2 id="visualizing-a-data-population-density">Visualizing a data: population density</h2>
<h3 id="reading-data">Reading data</h3>
<p>The very large number of communes (the smallest administrative level in France) gives us excellent spatial data. Geofla provides us their bounderies, population and area. So we will plot population density:</p>
<pre tabindex="0"><code># Reading shapefile
communes &lt;- readOGR(dsn=&#34;shp/geofla&#34;, layer=&#34;COMMUNE&#34;)
# Calculate density
communes$DENSITY &lt;- communes$POPULATION/communes$SUPERFICIE*100000
</code></pre><h3 id="color-scale">Color scale</h3>
<p>In order to create a color scale, we will assign shades of blue to each percentile. <code>classIntervals</code> calculates percentiles, <code>smoothColors</code> create the blue scale, and <code>findColours</code> assigns blues depending on each commune depending on their population density. Then, we create a legend, with only five colors.</p>
<pre tabindex="0"><code># Color scale
col &lt;- findColours(classIntervals(
communes$DENSITY, 100, style=&#34;quantile&#34;),
smoothColors(&#34;#0C3269&#34;,98,&#34;white&#34;))
# Legend
leg &lt;- findColours(classIntervals(
round(communes$DENSITY), 5, style=&#34;quantile&#34;),
smoothColors(&#34;#0C3269&#34;,3,&#34;white&#34;),
under=&#34;less than&#34;, over=&#34;more than&#34;, between=&#34;β&#34;,
cutlabels=FALSE)
</code></pre><h3 id="plot">Plot</h3>
<p>We can now plot the map. In order to use an embedded font in the PDF, we will use <code>cairo_pdf()</code> instead of <code>pdf</code>:</p>
<pre tabindex="0"><code># Exporting in PDF
cairo_pdf(&#39;densite.pdf&#39;,width=6,height=4.7)
par(mar=c(0,0,0,0),family=&#34;Myriad Pro&#34;,ps=8)
# Ploting the map
plot(bounderies, col=&#34;#FFFFFF&#34;)
plot(europe, col=&#34;#E6E6E6&#34;, border=&#34;#AAAAAA&#34;,lwd=1, add=TRUE)
plot(bounderies, col=&#34;#D8D6D4&#34;, lwd=6, add=TRUE)
plot(communes, col=col, border=col, lwd=.1, add=TRUE)
plot(bounderies, col=&#34;#666666&#34;, lwd=1, add=TRUE)
# Ploting the legend
legend(-10000,6387500,fill=attr(leg, &#34;palette&#34;),
legend=names(attr(leg,&#34;table&#34;)),
title = &#34;Density (p/kmΒ²):&#34;)
dev.off()
</code></pre><p><img src="density.webp" alt="Population density in France"></p>
<h2 id="visualizing-an-external-data--incomes">Visualizing an external data : incomes</h2>
<p>The main value is to plot data provided by external files. We will plot the median taxable income per consumption unit.</p>
<h3 id="reading-and-supplementing-data">Reading and supplementing data</h3>
<p>Unfortunately, data is missing for more than 5 000 communes, due to tax secrecy. We can &ldquo;cheat&rdquo; in order to improve the global render by assigning to those communes the canton (a larger administrative level) median income, given in the same file.</p>
<pre tabindex="0"><code># Loading communes data
incomes &lt;- read.csv(&#39;csv/revenus.csv&#39;)
communes &lt;- merge(communes, incomes, by.x=&#34;INSEE_COM&#34;, by.y=&#34;COMMUNE&#34;)
# Loading cantons data
cantons &lt;- read.csv(&#39;csv/cantons.csv&#39;)
# Merging data
communes &lt;- merge(communes, cantons, by=&#34;CANTON&#34;, all.x=TRUE)
# Assigning canton median income to communes with missing data
communes$REVENUS[is.na(communes$REVENUS)] &lt;- communes$REVENUC[is.na(communes$REVENUS)]
</code></pre><h3 id="color-scale-1">Color scale</h3>
<p>We generate color scale and legend:</p>
<pre tabindex="0"><code>col &lt;- findColours(classIntervals(
communes$REVENUS, 100, style=&#34;quantile&#34;),
smoothColors(&#34;#FFFFD7&#34;,98,&#34;#F3674C&#34;))
leg &lt;- findColours(classIntervals(
round(communes$REVENUS,0), 4, style=&#34;quantile&#34;),
smoothColors(&#34;#FFFFD7&#34;,2,&#34;#F3674C&#34;),
under=&#34;moins de&#34;, over=&#34;plus de&#34;, between=&#34;β&#34;,
cutlabels=FALSE)
</code></pre><h3 id="plot-1">Plot</h3>
<p>And we plot:</p>
<pre tabindex="0"><code>cairo_pdf(&#39;incomes.pdf&#39;,width=6,height=4.7)
par(mar=c(0,0,0,0),family=&#34;Myriad Pro&#34;,ps=8)
plot(boundaries, col=&#34;#FFFFFF&#34;)
plot(europe, col=&#34;#F5F5F5&#34;, border=&#34;#AAAAAA&#34;,lwd=1, add=TRUE)
plot(boundaries, col=&#34;#D8D6D4&#34;, lwd=6, add=TRUE)
plot(communes, col=col, border=col, lwd=.1, add=TRUE)
plot(boundaries, col=&#34;#666666&#34;, lwd=1, add=TRUE)
legend(-10000,6337500,fill=attr(leg, &#34;palette&#34;),
legend=gsub(&#34;\\.&#34;, &#34;,&#34;, names(attr(leg,&#34;table&#34;))),
title = &#34;Median income:&#34;)
dev.off()
</code></pre><p><img src="incomes.webp" alt="Median taxable income per consumption unit in France in 2010"></p>
<h2 id="visualizing-map-data-the-road-network">Visualizing map data: the road network</h2>
<p>We can also add map data: cities, urban areas, rivers, forests&hellip; We will here plot the French road network, thanks to Route 500, from IGN:</p>
<pre tabindex="0"><code>roads &lt;- readOGR(dsn=&#34;shp/geofla&#34;, layer=&#34;TRONCON_ROUTE&#34;)
local &lt;- roads[roads$VOCATION==&#34;Liaison locale&#34;,]
principal &lt;- roads[roads$VOCATION==&#34;Liaison principale&#34;,]
regional &lt;- roads[roads$VOCATION==&#34;Liaison r\xe9gionale&#34;,]
highway &lt;- roads[roads$VOCATION==&#34;Type autoroutier&#34;,]
cairo_pdf(&#39;roads.pdf&#39;,width=6,height=4.7)
par(mar=c(0,0,0,0),family=&#34;Myriad Pro&#34;,ps=8)
plot(boundaries, col=&#34;#FFFFFF&#34;)
plot(europe, col=&#34;#F5F5F5&#34;, border=&#34;#AAAAAA&#34;,lwd=1, add=TRUE)
plot(boundaries, col=&#34;#D8D6D4&#34;, lwd=6, add=TRUE)
plot(departements,col=&#34;#FFFFFF&#34;, border=&#34;#FFFFFF&#34;,lwd=.7, add=TRUE)
plot(local, col=&#34;#CBCBCB&#34;, lwd=.1, add=TRUE)
plot(principal, col=&#34;#CCCCCC&#34;, lwd=.3, add=TRUE)
plot(regional, col=&#34;#BBBBBB&#34;, lwd=.5, add=TRUE)
plot(highway, col=&#34;#AAAAAA&#34;, lwd=.7, add=TRUE)
plot(boundaries, col=&#34;#666666&#34;, lwd=1, add=TRUE)
dev.off()
</code></pre><p><img src="routes.webp" alt="French roads network"></p>
<h2 id="blank-world-map">Blank world map</h2>
<h3 id="reading-shapefiles-1">Reading shapefiles</h3>
<p>We will load the following Natural Earth:</p>
<pre tabindex="0"><code># Loading Shapefiles
countries &lt;- readOGR(dsn=&#34;shp/ne/cultural&#34;,layer=&#34;ne_110m_admin_0_countries&#34;)
graticules &lt;- readOGR(dsn=&#34;shp/ne/physical&#34;,layer=&#34;ne_110m_graticules_10&#34;)
bbox &lt;- readOGR(dsn=&#34;shp/ne/physical&#34;,layer=&#34;ne_110m_wgs84_bounding_box&#34;)
</code></pre><h3 id="projection-and-plot-1">Projection and plot</h3>
<p>We will use the Winkel Tripel projection:</p>
<pre tabindex="0"><code># Winkel Tripel projection
countries &lt;- spTransform(countries,CRS(&#34;+proj=wintri&#34;))
bbox &lt;- spTransform(bbox, CRS(&#34;+proj=wintri&#34;))
graticules &lt;- spTransform(graticules, CRS(&#34;+proj=wintri&#34;))
# Ploting world map
pdf(&#39;world.pdf&#39;,width=10,height=6) # PDF export
par(mar=c(0,0,0,0)) # Zero margins
plot(bbox, col=&#34;white&#34;, border=&#34;grey90&#34;,lwd=1)
plot(countries, col=&#34;#E6E6E6&#34;, border=&#34;#AAAAAA&#34;,lwd=1, add=TRUE)
plot(graticules, col=&#34;#CCCCCC33&#34;, lwd=1, add=TRUE)
dev.off() # Saving file
</code></pre><p><img src="monde.webp" alt="Carte du monde projetΓ©e en Winkel Tripel"></p>
<h2 id="visualizing-data-human-development-index">Visualizing data: Human Development Index</h2>
<p>Most frequent usage consists of visualizing data with a color scale. Let&rsquo;s plot the HDI, provided by the United Nations Develment Programme in a CSV file. The procedure is as described above:</p>
<pre tabindex="0"><code># Loading data and merging dataframes
hdi &lt;- read.csv(&#39;csv/hdi.csv&#39;)
countries &lt;- merge(countries, hdi, by.x=&#34;iso_a3&#34;, by.y=&#34;Abbreviation&#34;)
# Converting HDI in numeric
countries$hdi &lt;- as.numeric(levels(countries$X2012.HDI.Value))[countries$X2012.HDI.Value]
# Generating color scale and assigning colors
col &lt;- findColours(classIntervals(countries$hdi, 100, style=&#34;pretty&#34;),
smoothColors(&#34;#ffffe5&#34;,98,&#34;#00441b&#34;))
# Assigning grey to missing data
col[is.na(countries$hdi)] &lt;- &#34;#DDDDDD&#34;
# Generating legend
leg &lt;- findColours(classIntervals(round(countries$hdi,3), 7, style=&#34;pretty&#34;),
smoothColors(&#34;#ffffe5&#34;,5,&#34;#00441b&#34;),
under=&#34;less than&#34;, over=&#34;more than&#34;, between=&#34;β&#34;, cutlabels=FALSE)
# Ploting
cairo_pdf(&#39;hdi.pdf&#39;,width=10,height=6)
par(mar=c(0,0,0,0),family=&#34;Myriad Pro&#34;,ps=8)
plot(bbox, col=&#34;white&#34;, border=&#34;grey90&#34;,lwd=1)
plot(countries, col=col, border=col,lwd=.8, add=TRUE)
plot(graticules,col=&#34;#00000009&#34;,lwd=1, add=TRUE)
legend(-15000000,-3000000,fill=attr(leg, &#34;palette&#34;),
legend= names(attr(leg,&#34;table&#34;)),
title = &#34;HDI in 2012 :&#34;)
dev.off()
</code></pre><p><img src="hdi.webp" alt="Human Development Index (HDI) in 2012"></p>
<h2 id="circles-visualization-most-populated-cities">Circles visualization: most populated cities</h2>
<h3 id="reading-data-1">Reading data</h3>
<p>An other kind of visualization is given by circles. Population of most populated cities is provided by Natural Earth:</p>
<pre tabindex="0"><code># Loading shapefile
cities &lt;- readOGR(dsn=&#34;shp/ne/cultural&#34;, layer=&#34;ne_110m_populated_places&#34;)
cities &lt;- spTransform(cities, CRS(&#34;+proj=wintri&#34;))
</code></pre><h3 id="circle-size">Circle size</h3>
<p>The data shall be proportionate to the circles areas, not the radius; so the radius is the square root of the population:</p>
<pre tabindex="0"><code># Calculating circles radius
cities$radius &lt;- sqrt(cities$POP2015)
cities$radius &lt;- cities$radius/max(cities$radius)*3
</code></pre><h3 id="plot-2">Plot</h3>
<p>We plot the map:</p>
<pre tabindex="0"><code>pdf(&#39;cities.pdf&#39;,width=10,height=6)
par(mar=c(0,0,0,0))
plot(bbox, col=&#34;white&#34;, border=&#34;grey90&#34;, lwd=1)
plot(countries, col=&#34;#E6E6E6&#34;, border=&#34;#AAAAAA&#34;, lwd=1, add=TRUE)
points(cities, col=&#34;#8D111766&#34;, bg=&#34;#8D111766&#34;, lwd=1, pch=21, cex=cities$radius)
plot(graticules, col=&#34;#CCCCCC33&#34;, lwd=1, add=TRUE)
dev.off()
</code></pre><p><img src="villes.webp" alt="Most populated cities in the world"></p>
<h2 id="visualizing-map-data-urban-areas">Visualizing map data: urban areas</h2>
<p>Natural Earth provides urban areas shapefiles, derived from satellite data. Let&rsquo;us map them with night lights colors:</p>
<pre tabindex="0"><code>areas &lt;- readOGR(dsn=&#34;shp/ne/cultural&#34;,layer=&#34;ne_10m_urban_areas&#34;)
areas &lt;- spTransform(areas, CRS(&#34;+proj=wintri&#34;))
pdf(&#39;areas.pdf&#39;,width=10,height=6)
par(mar=c(0,0,0,0))
plot(bbox, col=&#34;#000000&#34;, border=&#34;#000000&#34;,lwd=1)
plot(countries, col=&#34;#000000&#34;, border=&#34;#000000&#34;,lwd=1, add=TRUE)
plot(areas, col=&#34;#FFFFFF&#34;, border=&#34;#FFFFFF66&#34;,lwd=1.5, add=TRUE)
dev.off()
</code></pre><p><img src="urbain.webp" alt="World map (Winkel Tripel projection)"></p>
</description>
</item>
</channel>
</rss>