<rss version="2.0" 
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
>
  <channel>
    <title>Brendan Abolivier</title>
    <link>https://brendan.abolivier.bzh/</link>
    <description>Some guy sharing his knowledge and thoughts with whoever wants to read it.</description>
    <generator>Hugo (gohugo.io)</generator>
    <copyright>(CC-BY-SA) Brendan Abolivier</copyright>
    <lastBuildDate>Mon, 04 May 2026 00:00:00 +0000</lastBuildDate>
    
    <item>
      <title>The quest for equivalent Exchange</title>
      <link>https://brendan.abolivier.bzh/exchange-pt-1/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate>
      <guid>https://brendan.abolivier.bzh/exchange-pt-1/</guid>
      <description>Towards the end of the summer of 2023 my team at MZLA Technologies Corp, the Mozilla Foundation subsidiary that looks after Thunderbird, started discussing a fairly ambitious project: adding support for Microsoft Exchange&#39;s APIs. This would turn into an ambitious project that spanned over 2 years and resulted in the first new protocol to be natively supported by Thunderbird in over 20 years. In the first part of this duology, I&#39;ll go over the project&#39;s big picture, how we defined the project&#39;s direction, and the reasoning behind architectural decisions.</description>
      <content:encoded><![CDATA[<p>Towards the end of the summer of 2023 my team at <a href="https://blog.thunderbird.net/2020/01/thunderbirds-new-home/">MZLA Technologies Corp</a>, the Mozilla Foundation subsidiary that looks after <a href="https://www.thunderbird.net">Thunderbird</a>, started discussing a fairly ambitious project: adding support for Microsoft Exchange&rsquo;s APIs. &ldquo;Ambitious&rdquo;, because the last time native support for a new protocol/platform was added in Thunderbird was&hellip; a while ago. In fact, <a href="https://blog.thunderbird.net/2022/08/thunderbird-time-machine-windows-xp-thunderbird-1-0/">Thunderbird 1.0</a>, which came out towards the end of 2004, already came with most of the email and newsgroups support you know and love, which originated in <a href="https://en.wikipedia.org/wiki/Netscape_Mail_%26_Newsgroups">Netscape Mail</a> 2.0 almost 10 years earlier.</p>
<p>I was lucky to be involved in the project&rsquo;s leadership to varying degrees, continuously up until the <a href="https://blog.thunderbird.net/2025/11/thunderbird-adds-native-microsoft-exchange-email-support/">public release</a> of Exchange support for email last November, and I thought I&rsquo;d reminisce about it here. This blog post overlaps to some extent with <a href="https://fosdem.org/2026/schedule/event/FYVUB8-a_short_story_of_supporting_microsoft_exchange_in_thunderbird/">a talk</a> I gave at FOSDEM 2026 in February, though it&rsquo;s not meant as a textual transcript of it and will go a bit more in depth on some points. It&rsquo;s also much longer, as you can see, because I have ADHD and if there&rsquo;s one thing people with ADHD like to do is talk about anything at length.</p>
<p>In fact, partway through writing this post I decided this blog has enough articles that require a full time job to read, so I decided to split it in two. This one, which you&rsquo;re currently reading, will go over the big picture and explain how we defined the project&rsquo;s direction, and the reasoning behind architectural decisions. The second and final part of this duology should come shortly after this one, and will cover the more practical and technical aspects of this project.</p>
<h1 id="what-is-exchange">What is Exchange?</h1>
<p>Let&rsquo;s start with the basics: what the hell is this &ldquo;Microsoft Exchange&rdquo; thing?</p>
<p>Exchange is the name of the back-end service that supports some of Microsoft&rsquo;s productivity services, such as email, calendar management, and a few others. Essentially, if you&rsquo;ve used Outlook with such services in a Microsoft-heavy environments (like some workplaces or school/universities can be), odds are you&rsquo;ve used Exchange. It powers both Microsoft&rsquo;s SaaS platform (Microsoft 365, which seems to be changing names every other year), as well as on-premise environments through <a href="https://en.wikipedia.org/wiki/Microsoft_Exchange_Server">Microsoft Exchange Server</a>.</p>
<p>Now the next question: why do we even need to support it? In fact, a number of Exchange environments, starting with Microsoft&rsquo;s own free Outlook.com (previously Hotmail) service, can be used over open protocols (at least for email) such as IMAP or SMTP. However, that&rsquo;s far from being the case everywhere, as Exchange particularly thrive in corporate environments that forbid the use of these protocols for &ldquo;security&rdquo; reasons. The only alternative is to use one of the few proprietary APIs designed by Microsoft for interacting with their services (which, thankfully, are publicly documented and available).</p>
<p>Among those, we picked an API called <a href="https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/explore-the-ews-managed-api-ews-and-web-services-in-exchange">EWS</a> (short for &ldquo;Exchange Web Services&rdquo;), which uses XML (SOAP) over HTTPS. This might sound like a weird decision to anyone familiar with Exchange, as even at the time EWS was slowly being deprecated in favour of the newer and shinier <a href="https://learn.microsoft.com/en-us/graph/overview">Microsoft Graph</a> API. In fact, while we we still in the early exploratory phase for this project, Microsoft announced the <a href="https://devblogs.microsoft.com/microsoft365dev/retirement-of-exchange-web-services-in-exchange-online/">upcoming retirement</a> of EWS in their SaaS services, urging applications to migrate to Graph. Despite this, we decided to keep moving ahead with EWS for a few reasons:</p>
<ul>
<li>EWS&rsquo;s retirement date on Microsoft&rsquo;s SaaS services wasn&rsquo;t for another 3 years, and we bet on our ability to land at least email support for EWS with enough time to do the same for Graph before that deadline.</li>
<li>EWS is not being retired on on-premises infrastructure, where Graph is unavailable. We want to support as many environments as possible, so we&rsquo;ll need to support EWS regardless.</li>
<li>EWS is already supported by at least one well-established open-source email application: <a href="https://en.wikipedia.org/wiki/GNOME_Evolution">Evolution</a>, and being able to use it as a reference when Microsoft&rsquo;s documentation was lacking helped us a few times.</li>
<li>EWS is a more appealing API for a native desktop application, specifically regarding notifications. While Graph does support live notifications for e.g. incoming messages, it&rsquo;s heavily based on push notifications, which require the kind of centralised infrastructure a web application would have. On the other hand, EWS supports pull notifications, where the client holds an open connection to the server and receives updates as they becomes available.</li>
</ul>
<p>Note that for convenience, I&rsquo;ll refer to these APIs as &ldquo;protocols&rdquo; from now on. While they&rsquo;re not really at the same layer of the OSI model as, say, IMAP or SMTP, they sit at the same level from an implementation point of view.</p>
<h1 id="open--closed---open">Open + closed = &hellip; open?</h1>
<p>A natural follow-up question to this explanation is: actually, why do we want to support Exchange in Thunderbird? After all, isn&rsquo;t Thunderbird supposed to be a champion of open standards? Why are you trying to taint my open-source with proprietary stuff?</p>
<p>It&rsquo;s true that Thunderbird&rsquo;s main mission is to support and facilitate communication based on open-standards. In fact, its own <a href="https://www.thunderbird.net/en-GB/about/mission-statement/">mission statement</a> says so:</p>
<blockquote>
<p>Thunderbird facilitates cross platform, decentralised, open-standard communication, which puts the user in control of their data and workflow.</p>
</blockquote>
<p>With this in mind, it is fair to question whether supporting a proprietary platform in Thunderbird, and doing so natively (as opposed to building an add-on for it) makes sense.</p>
<p>I think a way to answer this question is to question the often strict dichotomy that exists between open platforms and proprietary ones. I&rsquo;ve sometimes observed open-source communities having an almost visceral rejection of anything that isn&rsquo;t also open-source, acting as if it was unnatural to be using those closed alternatives in the first place. Which I don&rsquo;t believe to be a realistic approach.</p>
<p>In an ideal world, I obviously would like to see every platform and every system being powered by at least a majority of open-source projects with total transparency. In the real world, however, many are limited by constraints they cannot control, which heavily influence the options available to them - for example, most Exchange mailboxes exist in corporate environments, where environments and policies are decided by a specific department, and individual employees outside of that department have little power to change things.</p>
<p>In this sense, a consequence of adopting an inflexible, hard-line stance on the &ldquo;open vs closed&rdquo; debate is that free and open-source software, which at its core is meant to offer users more freedom and choice regarding how they can interface with digital services, ends up leaving the same users with less choice.</p>
<p>Paradoxically, integrating with closed platforms can also help the project fulfil its mission to champion openness. If someone is looking for a solution that helps them use both their work and personal inboxes on the same application, I would argue that allowing them to use an open-source solution such as Thunderbird or Evolution goes further for the project&rsquo;s mission than pushing them towards a closed solution like Outlook. And although it&rsquo;s a very small sample size, I was glad to hear multiple people at FOSDEM tell me they had come back to Thunderbird because they were now able to use it with their Exchange mailbox, which I feel somewhat confirmed this belief of mine.</p>
<p>In the specific case of supporting Exchange APIs, although they&rsquo;re almost never relevant to personal mailboxes, a very significant part of workplaces or schools/universities use Exchange configurations that allows only those APIs. This represents a large enough amount of users that we decided it was worth embarking in this project.</p>
<h1 id="adding-some-new-to-the-old">Adding some new to the old</h1>
<p>Right. Now we&rsquo;ve answered these initial questions, let&rsquo;s try to answer the next: how do we even support a new protocol in Thunderbird? As I mentioned in the introduction, the last time support for a new protocol in Thunderbird was over 30 years ago. This means there aren&rsquo;t a lot of people who took part in that early work and are still involved in the project today. And while we did understand our existing protocol implementations enough to maintain and improve them, our understanding of the overarching code architecture was clearly not good enough.</p>
<p>So let&rsquo;s rephrase that question: how were protocols supported in Thunderbird 20-30 years ago? Well, there&rsquo;s only one way to get to an answer: research it. Open the code, pull each thread one by one and figure out what kind of picture they make together. And <a href="https://source-docs.thunderbird.net/en/latest/backend/email_protocols.html">document</a> as much of it as possible.</p>
<p>But that question lead to another one: do we want to apply the same playbook that was used all that time ago? After all, good development practices, methods and tools have evolved a lot since then, and we decided that this project would be a great opportunity to try to define what a modern protocol implementation looked like in Thunderbird.</p>
<p>This lead, in part, to the introduction of <a href="https://rust-lang.org/">Rust</a> in the code base. This came up for the usual reasons (memory safety, rich ecosystem, decently friendly low-level language, etc.), but also because supporting Rust was made easier from the fact that Thunderbird is built on top of Firefox, and that Firefox has already been shipping with some Rust inside for a while. Due to this configuration, Thunderbird is able to inherit a few components from Firefox, including its build system which already came with Rust compiler support (although we still had to do <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1860654">a</a> <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1864624">few</a> <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1869860">tweaks</a> to make it work for us).</p>
<p>A lot of our code architecture for protocol support relies heavily on C++ inheritance, though, which meant we couldn&rsquo;t use Rust for all of the new code that would be introduced in this project. Rewriting the existing architecture in Rust would have involved either a lot of code duplication, or a very heavy (and risky) refactoring, which would have blown up the scope of an already complex enough project. Ultimately, we decided to use Rust for the new protocol client, and to keep using C++ for the more Thunderbird-specific logic.</p>
<h1 id="fighting-technical-debt-with-intentional-design">Fighting technical debt with intentional design</h1>
<p>The second prong for our approach to building modern protocol support has been being intentional with how we design and architecture new code (at least, as much as we could without throwing away the existing overarching architecture), specifically on the C++ side of things. A portion of Thunderbird&rsquo;s existing C++ code, like its IMAP implementation, is architectured in a way that makes this code pretty difficult to follow or understand, and we certainly don&rsquo;t want to replicate confusing patterns in this new code.</p>
<p>One example of these confusing patterns is the way asynchronicity is handled. A fair amount of asynchronous operations in Thunderbird are performed by giving an asynchronous function a &ldquo;listener&rdquo; object, which is essentially a collection of callbacks. These listeners are designed in interfaces, such that consumers aren&rsquo;t constrained to use a specific implementation, and some of our existing C++ classes implement those listener interfaces <em>on top of</em> implementing their own functional logic. This creates a few issues, for example:</p>
<ul>
<li>For operations that are stateful (i.e. they require data that is specific to the operation to be kept in scope throughout its duration), it means the class will be carrying state that is specific to the longer-lived consumer, but also to a short-lived operation that&rsquo;s being performed on that listener. This makes individual classes very heavy in state and confusing to reason with (because parts of its state end up having different scopes and lifetimes), and it can quite obviously be the source of many a concurrency issue.</li>
<li>On top of this, asynchronous operations implemented this way become very difficult to trace, because the same listener object (which is also its own consumer) might be reused for several kind of operations (or several operations of the same kind), making following the thread of specific operations more difficult than it should be.</li>
</ul>
<p>Let&rsquo;s take the example of the <a href="https://searchfox.org/comm-central/rev/5d544ba521e128e3fbe113541b4bddc904f68bbb/mailnews/imap/src/nsImapMailFolder.h#204"><code>nsImapMailFolder</code></a> class, which represents a mail folder using the IMAP protocol. Managing a mail folder is no simple task, but when we add into that all of the asynchronous listener implementations we end up with a huge monolithic class <a href="https://searchfox.org/comm-central/rev/5d544ba521e128e3fbe113541b4bddc904f68bbb/mailnews/imap/src/nsImapMailFolder.cpp#195-9033">which implementation</a> spans over almost 9000 lines of code. This implementation includes methods such as <a href="https://searchfox.org/comm-central/rev/5d544ba521e128e3fbe113541b4bddc904f68bbb/mailnews/imap/src/nsImapMailFolder.cpp#4892-4903"><code>OnStartRunningUrl</code></a> (which come from <a href="https://searchfox.org/comm-central/rev/5d544ba521e128e3fbe113541b4bddc904f68bbb/mailnews/base/public/nsIUrlListener.idl"><code>nsIUrlListener</code></a>, a listener interface commonly used for all sorts of operations throughout Thunderbird), as well as members such as <code>m_initialized</code> or <code>m_onlineFolderName</code> which refer to the long-lived folder, but also like <code>m_junkMessagesToMarkAsRead</code> or <code>m_curMsgUid</code> which refer to the current, short-lived operation. As you can imagine, this gets unwieldy fairly easily.</p>
<p>Other issues include the use the stateful &ldquo;URL&rdquo; objects to represent remote operations (while URLs are semantically expected to hold information that refers to a resource and how to operate on it, they shouldn&rsquo;t be really expected to, say, be updated with information regarding how that operation is going, or be used to get references to things like event sinks); or the lack of centralised implementations for common local operations (e.g. the local storage management side of operations like message copy needs to be implemented separately for each protocol, despite doing essentially the same thing regardless of the context).</p>
<p>After observing those issues, we decided on a few principles:</p>
<ul>
<li>Reuse as much common code and types as we can. This might sound obvious, but forcing ourselves to e.g. base EWS URLs on a &ldquo;normal&rdquo; and semantically correct URL object interface (which we inherit from Firefox) rather than allowing ourselves to create our own custom &ldquo;EWS URL&rdquo; type helped create constraints that prevented us from accidentally perpetuating the patterns we wanted to avoid.</li>
<li>New classes should do the strict minimum required, and defer to other objects for functionality that wasn&rsquo;t semantically fitting with their model. This means, for example, moving away from a &ldquo;folder&rdquo; mega-class that worked almost entirely on its own, to a smaller class that only does what it needs to do (e.g. orchestrate operations on an email folder) and defers to even smaller classes for e.g. reacting to asynchronous output.</li>
<li>New code should be made as generic as possible. I don&rsquo;t mean this in the syntactic/typing sense, but I&rsquo;m rather referring to generally approaching new code with the idea that if it doesn&rsquo;t need to include logic or a workflow that is specific to a given protocol, then it should live outside of protocol-specific code and be entirely reusable by other protocols that might need similar functionality.</li>
</ul>
<h1 id="thats-all-for-now">That&rsquo;s all for now</h1>
<p>Like I mentioned at the beginning of this post, this is not the end of the story. As always, I would like to thank <a href="https://toot.cat/@CromFr">Thibaut</a> for helping proofread this first part. A second and (for now) final part should appear on this blog fairly soon (hopefully), and it will cover the more technical side of the project - now that we&rsquo;ve defined its direction, it&rsquo;s about time we get our hands dirty!</p>
<p>Stay tuned, and see you again soon!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>What a year, huh? (2023 Recap)</title>
      <link>https://brendan.abolivier.bzh/2023-recap/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Wed, 31 Jan 2024 00:00:00 +0000</pubDate>
      <guid>https://brendan.abolivier.bzh/2023-recap/</guid>
      <description>This time, I&#39;m not writing about something techy (well, mostly), or a project. This time, it&#39;s more of a personal update. 2023 has been a pretty eventful year for me, and even though I haven&#39;t really done that in the past, and I&#39;m not sure how interesting this will be for anyone except myself, I thought it was worth commemorating the milestones that happened in the past year on my own little corner of the World Wide Web.</description>
      <content:encoded><![CDATA[<p>Hey there!</p>
<p>It&rsquo;s been a while since I haven&rsquo;t posted anything on here. <a href="https://brendan.abolivier.bzh/matrix-retention-policies/">A couple of
years</a> actually. This
time, though, I&rsquo;m not writing about something techy (well, mostly), or a
project. This time, it&rsquo;s more of a personal update. 2023 has been a pretty
eventful year for me, and even though I haven&rsquo;t really done that in the past,
and I&rsquo;m not sure how interesting this will be for anyone except myself, I
thought it was worth commemorating the milestones that happened in the past year
on my own little corner of the World Wide Web.</p>
<p>Also, yes, I know, we&rsquo;re already almost a month into 2024. But some say it&rsquo;s
still fine to wish someone a happy new year as long as it&rsquo;s still January, so
surely this is also fine 🙂</p>
<h2 id="a-rough-start">A rough start</h2>
<p>2023 didn&rsquo;t exactly start on a positive note. <a href="https://twitter.com/BrenAbolivier/status/1603432581055258626">Around mid-December
2022</a>, I learned
that I had lost my job at Element following a round of redundancies. Although
I&rsquo;m not going to publicly blame anyone responsible for this decision, it would
be wrong to pretend this did not come as a huge shock for me.</p>
<p>Element had been part of my life for over 4 years (over 5 if you start counting
from when I was still an intern), and had been the only company I worked for
since I decided to emigrate from my hometown in France to the UK. This meant I
needed to rethink my life as a British resident, now that a significant part of
it had been ripped away. Worry also quickly settled in, as even though I left
with a comfortable severance package, money can flow pretty fast in the 2nd most
expensive city in the world.</p>
<p>This meant job hunting could not wait. However, within a few weeks, it quickly
became obvious that what I got myself caught into was one of the most brutal
crisis this industry had seen in a while, with big corporations laying off
employees left and right, rendering the job market overcrowded with more and
more people looking for work, but also more and more companies taking the
cautious approach and leveling down their hiring.</p>
<p>So things went on for a while without much success. Some times it became tricky
to prevent the Baserow table I was tracking my applications into from being
filled with red.</p>
<p><img src="/images/2023-recap/dead_ends.png" alt="A redacted partial screenshot of a Baserow table with all visible rows labeled &ldquo;Dead&rdquo;"></p>
<p>But still, I trudged forward, exploring ways in which I could find my next step.
I did some freelancing to help <a href="https://unwirednetworks.com/en/">Unwired
Networks</a> improve their X509 certificate
verification and handling tooling (while writing <a href="https://github.com/unwired/certsort">open
source</a>!), and even worked a bit for a
credit card company. There have been several crossroads that could have taken me
to a completely different outcome than the one I ended up with (which we&rsquo;ll get
to in a minute), and although I don&rsquo;t really want to share too many details
here, I&rsquo;m immensely grateful to everyone who has followed me throughout that
journey, even if just part of it.</p>
<h2 id="the-big-60">The big 60</h2>
<p>At the same time, my love for cycling was picking up. I&rsquo;ve already shared bits
of it online, and lots of it privately: cycling has been something I&rsquo;ve loved
doing for a very long time now. As I suddenly found myself with some free time
on my hands, and I was coming off a year or two during which I&rsquo;ve slowly but
surely been improving on taking care of myself and getting in a better shape, I
thoroughly enjoyed spending time on my bike.</p>
<p>Towards the start of the year, I finally did something I had been pondering for
a while: signing up for a cycling event.
<a href="https://www.ridelondon.co.uk/">RideLondon</a> had been happening close to me for
quite a while at that point, and I thought it would be a nice challenge to take
on one of their rides, signing up for 60 miles (even though I had initially
settled on 30). This gave me extra motivation to cycle again, and to start doing
longer and longer distances. I spent some amazing weekends cycling through the
British countryside alongside a fellow cyclist friend (thanks a lot, Michael!),
smashing pretty long distances in the process.</p>
<p>The event itself arrived in a timely fashion. Due to some personal events I
won&rsquo;t get into here, the month of May 2023 for me started with one of the worst
mental health breakdowns I had ever experienced. While I spent most of the first
half of the month barely leaving my flat, this upcoming deadling was what I
needed to get out and train.</p>
<p>The ride was honestly one of the best events I&rsquo;ve ever attended. While it was
challenging, I surprised myself with how much fun I was having, crossing
milestones while enjoying parts of the countryside I had never been through.</p>
<p><img src="/images/2023-recap/ridelondon1.jpg" alt="Me cycling on a paved road surrounded by Epping Forest on each side"></p>
<p>Crossing the finish line on Tower Bridge was probably one of the most memorable
moments of my life. It&rsquo;s difficult to put emotions into words, and I&rsquo;m aware
that a lot of people cycle longer distances during those events. But when I was
cycling down Tower Bridge after almost spending close to 5 hours on my bike, I
couldn&rsquo;t help but think back to a time that wasn&rsquo;t even that far away, barely 3
years back, when I was struggling with anxiety, had a very low self-esteem, and
in no way could be motivated to take much care of myself, let alone exercise.
And that same me had just smashed 60 miles, 100 kilometres, of cycling in one
day.</p>
<p><img src="/images/2023-recap/ridelondon2.jpg" alt="Me on a bike, smiling, with London&rsquo;s Tower Bridge behind me"></p>
<p>Spoiler alert: I&rsquo;ve already signed up for RideLondon 2024, for 100 miles this
time. And I&rsquo;m so looking forward to it.</p>
<h2 id="a-new-beginning">A new beginning</h2>
<p>As a number of readers might be aware by this point, I
<a href="https://twitter.com/BrenAbolivier/status/1663853323651350529">joined</a> the
Thunderbird team at Mozilla in June 2023. This came after months of applying to
job after job and getting rejection after rejection, which was slowly eating
away at my confidence and mental health. In total, I applied to 76 positions,
leading to over 50 rejections, and I&rsquo;m not counting applications I never heard
back from (though those statistics are still paling in comparison to the ones my
sister once shared with me about her job hunting in the marketing sector). So
this news felt like a ray of sunlight in the middle of a storm: not only did I
find a role that I was happy and interested in, but it&rsquo;s also to work on
software I&rsquo;ve been using for well over a decade, as part of an organisation I
had been looking up to for so long.</p>
<p>While getting used to Thunderbird was challenging, it was a welcome challenge.
It felt like the first time in a while that I was able to put my skills to use
to not only improve a project that I feel personally attached to, but also have
a positive impact in areas that matter to me. And to be part of a global
community that I share common values and interests with.</p>
<p>A few months in, the focus of my work started shifting from fixing regressions
(which was a great way to get more familiar with the project) to more
feature-oriented work. I helped research existing code architecture, and design
part of the project&rsquo;s future. As the work to integrate Rust into the code base
(led by <a href="https://fosstodon.org/@ikey">Ikey</a>) started to come to a close, and my
own work started to gravitate around it, I got to build on top of it to create
some
<a href="https://source-docs.thunderbird.net/en/latest/rust/index.html">documentation</a>
and <a href="https://searchfox.org/comm-central/source/rust/xpcom_async">code</a>
<a href="https://searchfox.org/comm-central/source/rust/moz_http">infrastructure</a> that
will help Thunderbird developers for year to come. Being able to have this kind
of impact on a project that matters so much to me, within barely more than 6
months on the job, is something I&rsquo;m immensely proud of.</p>
<p>(pssst, if you want to hear more about this, we&rsquo;re giving a
<a href="https://fosdem.org/2024/schedule/event/fosdem-2024-2469-thunderbird-how-to-exchange-rot-for-rust/">talk</a>
on this at FOSDEM this weekend 👀)</p>
<p>This section of the post is probably a bit shorter than the others. To be honest,
I&rsquo;m still struggling to figure out where the last 6 months have gone, because I
don&rsquo;t feel like it&rsquo;s been that long. And it&rsquo;s still just the beginning. Although
I&rsquo;m approaching my professional life with more caution than I used to, thanks to
past experiences, it feels good to be doing something I enjoy again.</p>
<h2 id="to-2024">To 2024</h2>
<p>I think it&rsquo;s fair to say 2023 has included some of the highest highs and some of
the lowest lows of my life (some I haven&rsquo;t included here). If you&rsquo;d asked me a
year from now where I&rsquo;d be now, I very likely wouldn&rsquo;t have been able to guess
where the year took me, and all of the hurdles and joys I encountered along the
way. I want to reiterate how grateful I am to everyone who&rsquo;s been with me
throughout the whole journey, or just part of it.</p>
<p>If 2024 is as eventful as 2023 was, maybe I&rsquo;ll make another one of these in a
year. Though frankly I kinda hope it isn&rsquo;t. But maybe I&rsquo;ll write something up
regardless; who knows 🙂</p>
<p>In the meantime, I hope everyone reading this has a smashing 2024. I&rsquo;m
specifically wishing all the best to everyone I know who&rsquo;s looking for work at
the moment, whether due to layoffs (public and not) or unhappiness with what you
have right now. It&rsquo;s still rough out there, but I know you&rsquo;ll pull through.</p>
<p>See ya!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Implementing support for message retention policies in Matrix</title>
      <link>https://brendan.abolivier.bzh/matrix-retention-policies/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Mon, 11 Jan 2021 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/matrix-retention-policies/</guid>
      <description>As part of my work in the Matrix core team, I got to implement an actively requested feature, i.e. support for message retention policies, in Synapse (the reference Matrix homeserver implementation). In this post, I give you a peek at what the feature does, how it works, and how it&#39;s currently implemented in Synapse.</description>
      <content:encoded><![CDATA[<p>Hello there, long time no see!</p>
<p>As you may know, I&rsquo;m currently working at <a href="https://element.io">Element</a>, as part of the backend team working on <a href="https://matrix.org">Matrix</a>&rsquo;s server-side implementations. The main project involved in this work, at least from my side of things, is <a href="https://github.com/matrix-org/synapse">Synapse</a>, the reference Matrix homeserver implementation. If you don&rsquo;t know what a homeserver is, you may want to check out my post <a href="https://brendan.abolivier.bzh/enter-the-matrix/">Enter the Matrix</a>, in which I give an extensive introduction to Matrix. Parts of this blog post need a basic comprehension of how Matrix works, so if you don&rsquo;t already have that, you&rsquo;ll probably want to give that post an eye before continuing this one.</p>
<p>In this context, in 2019, I got to <a href="https://github.com/matrix-org/synapse/pull/5815">implement</a> in Synapse a feature that had been actively requested by the Matrix community for a while now: message retention policy support. It allows any server or room admin to define a period of time after which a message gets hidden from clients and eventually deleted.</p>
<p>This feature is fairly complex to implement and document, due to different moving parts needing to interact with one another. The <a href="https://github.com/matrix-org/synapse/blob/develop/docs/message_retention_policies.md">current documentation</a> is a good place to start, especially if you&rsquo;re mainly interested in knowing how to configure a retention policy on your server or in your room. But I thought it might be interesting to get a bit deeper into its implementation and explain some design choices and shortcomings.</p>
<p>In other words, the goal of this post isn&rsquo;t to explain how to get set up with this feature, but to be a technical breakdown explaining the internal design of this implementation. For instance, I&rsquo;m not going to dump and explain the necessary bits of configuration right away but rather try to explain is as much detail as I can what they do. If you&rsquo;re a complete stranger to this feature you might want to have a very quick skim through the documentation I&rsquo;ve linked to in the previous paragraph, though I&rsquo;m going to repeat a bunch of what&rsquo;s being said there.</p>
<p>So here I go.</p>
<h1 id="a-quick-look-at-the-spec">A quick look at the spec</h1>
<p>Message retention policies are defined in Matrix in <a href="https://github.com/matrix-org/matrix-doc/blob/matthew/msc1763/proposals/1763-configurable-retention-periods.md">MSC1763</a> (an MSC being a proposal to the Matrix specification, not unlike RFCs), which defines them as state events (of type <code>m.room.retention</code>) sent to the room the administrator (or moderator) wants to regulate. Therefore, a policy is scoped to a room.</p>
<p>The content for this state event looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;max_lifetime&#34;</span><span class="p">:</span> <span class="mi">2419200000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;min_lifetime&#34;</span><span class="p">:</span> <span class="mi">86400000</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This event has two properties: <code>max_lifetime</code> and <code>min_lifetime</code>. Their values are time durations expressed in milliseconds. The combination of these properties define the lifetime of an non-state event after it&rsquo;s been sent to a room:</p>
<ul>
<li><code>max_lifetime</code> defines how long after the message is sent the homeservers participating in the room can keep it around. In the example above (where its value is <code>2419200000</code>), it means a homeserver <strong>must</strong> delete events sent to the room at the latest 28 days after they&rsquo;ve been sent (though we&rsquo;ll see later that, in the current implementation in Synapse, it can vary a bit around that value).</li>
<li><code>min_lifetime</code> defines the minimum amount of time after the event is sent during which homeservers should store the event and not delete it. This is particularly helpful for e.g. governmental organisations that are required (through laws like the Freedom Of Information Act) to keep a record of messages sent, or for moderation purposes. In the example above (where its value is <code>86400000</code>), it means a homeserver <strong>should</strong> store events at least 24h after they&rsquo;ve been sent.</li>
</ul>
<p>In other words, these parameters are limits to the total lifetime of an event. If a message retention policy has no <code>min_lifetime</code> then the homeserver is free to delete events as soon as it wants, and if it&rsquo;s got no <code>max_lifetime</code> then the homeserver is free to never delete any event. It&rsquo;s then up to the homeserver to decide when to delete events using these constraints.</p>
<h1 id="processing-policies">Processing policies</h1>
<p>So let&rsquo;s have a look at how this is implemented in Synapse. Before going any further on the implementation side of things, I need to mention that this implementation is still currently considered experimental, and is disabled by default in any new install. We&rsquo;ll see a bit later in this post how to enable and tweak it using Synapse&rsquo;s configuration file. Currently, the MSC still needs some clarification and discussion, and so future iterations on it might cause the implementation to change. This is why we&rsquo;re not declaring it stable as is and enabling it by default yet. On top of that, its main goal is to perform bulk deletion of data, so we want to make extra sure it&rsquo;s done right before flicking the switch in order to prevent any irreversible breakage.</p>
<p>First, let&rsquo;s see how Synapse keeps track of retention policies for all the rooms it&rsquo;s in. That bit is rather simple: every time a state event is sent to a room with the type <code>m.room.retention</code>, Synapse will <a href="https://github.com/matrix-org/synapse/blob/70259d8c8c0be71d3588a16211ccb42af87235da/synapse/storage/databases/main/events.py#L1270-L1301">insert</a> a row into its <code>room_retention</code> database table. This row will include some data about the policy, including the <code>min_lifetime</code> and <code>max_lifetime</code> properties. Note that both those properties are <code>NULL</code>able, allowing for either (or both) property to be omitted (we&rsquo;ll see later what Synapse does in this case). As far as Synapse is concerned, a room with a retention policy with an empty content (<code>{}</code>) is the same thing as a room with no retention policy.</p>
<p>Now that Synapse knows the retention policy for each room it&rsquo;s in, it can apply it to the events in the room. It&rsquo;s worth noting that a current point of discussion on the MSC, and somewhere the implementation differs from the spec, is that the MSC mentions events should be purged according to the retention policy of the room as it was when the event was sent. Synapse, on the other hand, will purge events based on the retention policy the room currently has, because it creates less technical complications, provides better performances and seems to better fit the expectations of users.</p>
<h1 id="configuring-message-retention-policy-support">Configuring message retention policy support</h1>
<p>Let&rsquo;s take a quick break from the technical breakdown to clarify a thing or two. In the next few sections, I&rsquo;ll take a look at different parts of Synapse&rsquo;s implementation of message retention policy support. I&rsquo;ll also explain how they tie into the feature&rsquo;s configuration.</p>
<p>Message retention policy support can be enabled and tweaked in Synapse&rsquo;s YAML <a href="https://github.com/matrix-org/synapse/blob/70259d8c8c0be71d3588a16211ccb42af87235da/docs/sample_config.yaml">configuration file</a>. All of the configuration related to this feature can be found <a href="https://github.com/matrix-org/synapse/blob/70259d8c8c0be71d3588a16211ccb42af87235da/docs/sample_config.yaml#L369-L436">in the <code>retention</code> section</a>. I&rsquo;m not going to get into too much detail about what the different sub-sections and settings mean and how they&rsquo;re used, as the rest of this post already covers this. One thing I will mention here, however, is that you can enable the feature by setting <code>enable</code> to <code>true</code> in this section.</p>
<p>Note that if this setting is missing or set to <code>false</code>, Synapse will still store new message retention policies. It will not, however, delete any event from the database.</p>
<p>Now let&rsquo;s see how Synapse deletes messages when this feature is enabled.</p>
<h1 id="synapse-and-its-many-jobs">Synapse and its many jobs</h1>
<p>Because it would be too expensive and complex to track the lifetime of each event individually, and set a timer to purge them from the database, Synapse purges events by running regularly scheduled jobs. Doing so also allows merging code paths with another feature, which is the <a href="https://github.com/matrix-org/synapse/blob/master/docs/admin_api/purge_history_api.rst">purge history admin API</a>. The frequency and scope of these jobs are <a href="https://github.com/matrix-org/synapse/blob/70259d8c8c0be71d3588a16211ccb42af87235da/docs/sample_config.yaml#L402-L436">defined in Synapse&rsquo;s configuration</a> as such:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">purge_jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">longest_max_lifetime</span><span class="p">:</span><span class="w"> </span><span class="l">3d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">12h</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">shortest_max_lifetime</span><span class="p">:</span><span class="w"> </span><span class="l">3d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">1d</span><span class="w">
</span></span></span></code></pre></div><p>This example describes two purge jobs. This definition includes a frequency, defined by the required <code>interval</code> setting, which defines the time between two instantiations of a job.</p>
<p>In the example above, Synapse will run a job every 12 hours purging expired events in rooms which retention policy feature a <code>max_lifetime</code> with a value of 3 days or less; as well as another job every day purging expired events in rooms which retention policy feature a <code>max_lifetime</code> with a value of more than 3 days (note that <code>longest_max_lifetime</code> is inclusive but <code>shortest_max_lifetime</code> isn&rsquo;t).</p>
<p>The reason Synapse allows multiple jobs to be defined in the same configuration is that all rooms don&rsquo;t have the same sensitivity with regards to their retention policy. Some might have their policy dictate that no event can live longer than a day, whereas others might only require events to be purged after a year.</p>
<p>Another thing to keep in mind is that running a purge job might be an expensive task to run as it can involve deleting a lot of data, so you don&rsquo;t want to run a job every minute purging all expired events in all rooms.</p>
<p>Defining multiple jobs allows making sure rooms get processed according to the sensitivity of their policy, as well as ensuring the best performance possible. You could see it as sharding (or partitioning) the load of purging history of rooms across all of the jobs based on a room&rsquo;s retention policy. This also allows sufficient flexibility in the configuration.</p>
<p>It&rsquo;s worth noting that both <code>shortest_max_lifetime</code> and <code>longest_max_lifetime</code> are optional here; and here as well lack of one limit simply means there&rsquo;s no limit applied in that direction. For instance, the following example defines a purge job without any limit on the interval of <code>max_lifetime</code> values it handles:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">purge_jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">12h</span><span class="w">
</span></span></span></code></pre></div><p>It is also possible to bind a job to a precise scope by specifying both settings:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">purge_jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">12h</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">shortest_max_lifetime</span><span class="p">:</span><span class="w"> </span><span class="l">6h</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">longest_max_lifetime</span><span class="p">:</span><span class="w"> </span><span class="l">1d</span><span class="w">
</span></span></span></code></pre></div><p>Heads up that it&rsquo;s highly recommended to configure a job with an open limit on each side of the range of <code>max_lifetime</code> values - this can be either a job with no limit (as shown above) or two jobs, each limiting in one direction.</p>
<p>But wait, you&rsquo;ll then say, isn&rsquo;t it bad if Synapse might delete expired messages hours, possibly days, after they&rsquo;ve expired? To which I&rsquo;d answer that yes, probably, however this is mitigated by another feature of this implementation: when a message expires, <a href="https://github.com/matrix-org/synapse/blob/70259d8c8c0be71d3588a16211ccb42af87235da/synapse/visibility.py#L136-L143">Synapse will stop sending it to clients</a>. This means that, even though Synapse might not purge events immediately when they expire, it will prevent clients from seeing it. Note that clients that have already downloaded and stored the event might continue to show it, unless they themselves implement support for message retention policies, and no homeserver can do anything about that.</p>
<p>The same mechanism applies if an expired event is sent to Synapse by another homeserver through federation, for example when <a href="https://matrix.org/docs/spec/server_server/latest#backfilling-and-retrieving-missing-events">backfilling</a>, if the remote server doesn&rsquo;t implement this feature (or doesn&rsquo;t have it enabled). In this case this feature, when enabled, will prevent this event from reaching clients, letting it sit in its database until the next run of a relevant purge job clears it up.</p>
<h1 id="under-the-hood">Under the hood</h1>
<p>Right, now we understand how to configure a purge job, let&rsquo;s see how it actually works. I&rsquo;m not going to go into detail on the specific SQL deletion that happens, the main reason being this code was already there when implementing the feature, as part of the <a href="https://github.com/matrix-org/synapse/blob/master/docs/admin_api/purge_history_api.rst">purge history admin API</a>, and the purge jobs just hook into it.</p>
<p>Quick heads up, in this section I&rsquo;ll be moving from linking to Synapse&rsquo;s code to sharing snippets of the code directly here, because I believe it&rsquo;s nicer to understand what&rsquo;s going on. For reference, all of these snippets will come from <a href="https://github.com/matrix-org/synapse/blob/70259d8c8c0be71d3588a16211ccb42af87235da/synapse/handlers/pagination.py">Synapse&rsquo;s pagination handler</a> and should be located in the top half of the file, if you want to contextualise them.</p>
<p>When Synapse starts, it will start a looping call for each purge job configured:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Run the purge jobs described in the configuration file.</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">job</span> <span class="ow">in</span> <span class="n">hs</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">retention_purge_jobs</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;Setting up purge job with config: </span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">job</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="bp">self</span><span class="o">.</span><span class="n">clock</span><span class="o">.</span><span class="n">looping_call</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">run_as_background_process</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">job</span><span class="p">[</span><span class="s2">&#34;interval&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;purge_history_for_rooms_in_range&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">purge_history_for_rooms_in_range</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">job</span><span class="p">[</span><span class="s2">&#34;shortest_max_lifetime&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="n">job</span><span class="p">[</span><span class="s2">&#34;longest_max_lifetime&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></div><p>If you&rsquo;re running Synapse in a worker setup that isn&rsquo;t configured to run background tasks on the main process, these purge jobs will be scheduled on whichever worker <code>run_background_tasks_on</code> is pointing to in your configuration file.</p>
<p>As expected, we can see that each job is run in a looping call. As its name might suggest, in Synapse, a looping call is a function that is called in an infinite loop (asynchronously) with a given interval between two calls to that function. In this instance, we can see that for each looping call we use the configured interval for the associated purge job configuration. We also provide the function with the purge job&rsquo;s range.</p>
<h2 id="specifying-a-default-retention-policy">Specifying a default retention policy</h2>
<p>Now let&rsquo;s see what a purge job actually does. First it retrieves the rooms it will be purging, and their retention policies, from Synapse&rsquo;s database:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># We want the storage layer to include rooms with no retention policy in its</span>
</span></span><span class="line"><span class="cl"><span class="c1"># return value only if a default retention policy is defined in the server&#39;s</span>
</span></span><span class="line"><span class="cl"><span class="c1"># configuration and that policy&#39;s &#39;max_lifetime&#39; is either lower (or equal) than</span>
</span></span><span class="line"><span class="cl"><span class="c1"># max_ms or higher than min_ms (or both).</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_retention_default_max_lifetime</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">include_null</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">min_ms</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">min_ms</span> <span class="o">&gt;=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_retention_default_max_lifetime</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># The default max_lifetime is lower than (or equal to) min_ms.</span>
</span></span><span class="line"><span class="cl">        <span class="n">include_null</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">max_ms</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">max_ms</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">_retention_default_max_lifetime</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># The default max_lifetime is higher than max_ms.</span>
</span></span><span class="line"><span class="cl">        <span class="n">include_null</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">include_null</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[purge] Running purge job for </span><span class="si">%s</span><span class="s2"> &lt; max_lifetime &lt;= </span><span class="si">%s</span><span class="s2"> (include NULLs = </span><span class="si">%s</span><span class="s2">)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">min_ms</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_ms</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">include_null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rooms</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">get_rooms_for_retention_period_in_range</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">min_ms</span><span class="p">,</span> <span class="n">max_ms</span><span class="p">,</span> <span class="n">include_null</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>The first part of this code doesn&rsquo;t actually do any retrieval from the database, but figures out what to retrieve. More specifically, it figures out whether this purge job needs to process rooms with no retention policy stored as well as rooms which retention policies are within the range of this job. A room with no retention policy will still be stored in the <code>room_retention</code> table, with a <code>NULL</code> retention policy, hence the name of the boolean variable indicating whether we need to retrieve these as well (<code>include_null</code>).</p>
<p>The reason we might want to process these rooms is because it is possible in Synapse to define a default policy for all rooms that don&rsquo;t have one in their state, using the following configuration in the <code>retention</code> section of Synapse&rsquo;s configuration file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">default_policy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">min_lifetime</span><span class="p">:</span><span class="w"> </span><span class="l">1d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">max_lifetime</span><span class="p">:</span><span class="w"> </span><span class="l">1y</span><span class="w">
</span></span></span></code></pre></div><p>This example is equivalent to adding this <code>m.room.retention</code> event into the state of any room that doesn&rsquo;t already specify a retention policy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;min_lifetime&#34;</span><span class="p">:</span> <span class="mi">86400000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;max_lifetime&#34;</span><span class="p">:</span> <span class="mi">31557600000</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>If a room already specifies a retention policy, Synapse will use that policy and not the default one.</p>
<p>Note that there is one difference with actually inserting the policy into the room&rsquo;s state, it&rsquo;s that this default policy will only be applied on your homeserver, so if another homeserver is in the room they won&rsquo;t necessarily apply the same policy. However, as we&rsquo;ve seen before, if another homeserver sends yours events that should be deleted according to your default policy, Synapse will hide it for clients and just wait for the relevant purge job to delete it.</p>
<p>This check is actually quite simple: we only need to process rooms without a retention policy if a default server-wide retention policy has been configured (because it then applies to any room without a policy). On top of that, we check whether this default policy specifies a value for <code>max_lifetime</code> that&rsquo;s within the job&rsquo;s range.</p>
<p>We then call <code>get_rooms_for_retention_period_in_range</code> on Synapse&rsquo;s storage layer, which returns a dictionary associating a room&rsquo;s ID with its retention policy, for example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;!someroom:example.com&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;max_lifetime&#34;</span><span class="p">:</span> <span class="mi">2419200000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;min_lifetime&#34;</span><span class="p">:</span> <span class="mi">86400000</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Once we have these rooms, we iterate over them.</p>
<h2 id="capping-the-policy">Capping the policy</h2>
<p>We first check if there isn&rsquo;t a purge in progress in that room, and if so skip it to prevent any damage due to a conflict:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">if</span> <span class="n">room_id</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_purges_in_progress_by_room</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;[purge] not purging room </span><span class="si">%s</span><span class="s2"> as there&#39;s an ongoing purge running&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34; for this room&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">room_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span></code></pre></div><p>We then proceed to cap the room&rsquo;s retention policy. This is done by another bit of configuration in the <code>retention</code> section of Synapse&rsquo;s configuration file:</p>
<pre tabindex="0"><code>allowed_lifetime_min: 1d
allowed_lifetime_max: 1y
</code></pre><p>The rationale on capping a room&rsquo;s policy is that your homeserver might run under different requirements with regards to data retention than the other homeservers in the room. You might want to make sure you keep messages long enough for e.g. audit or other legal purposes, or you might want to make sure you don&rsquo;t keep them too long so they don&rsquo;t take up too much space on your disk and/or for privacy-related reasons. Whatever your reason is for doing so, Synapse allows you to override a room&rsquo;s retention policy before purging it to ensure it doesn&rsquo;t purge what you want to keep around, or it purges what you don&rsquo;t want around anymore.</p>
<p>Both <code>allowed_lifetime_min</code> and <code>allowed_lifetime_max</code> are optional configuration parameters. They apply to both <code>min_lifetime</code> and <code>max_lifetime</code>, however when running a purge job, we only care about the policy&rsquo;s <code>max_lifetime</code> value, so that&rsquo;s the one Synapse will cap if necessary:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># If max_lifetime is None, it means that the room has no retention policy.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Given we only retrieve such rooms when there&#39;s a default retention policy</span>
</span></span><span class="line"><span class="cl"><span class="c1"># defined in the server&#39;s configuration, we can safely assume that&#39;s the</span>
</span></span><span class="line"><span class="cl"><span class="c1"># case and use it for this room.</span>
</span></span><span class="line"><span class="cl"><span class="n">max_lifetime</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">retention_policy</span><span class="p">[</span><span class="s2">&#34;max_lifetime&#34;</span><span class="p">]</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_retention_default_max_lifetime</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Cap the effective max_lifetime to be within the range allowed in the</span>
</span></span><span class="line"><span class="cl"><span class="c1"># config.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># We do this in two steps:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   1. Make sure it&#39;s higher or equal to the minimum allowed value, and if</span>
</span></span><span class="line"><span class="cl"><span class="c1">#      it&#39;s not replace it with that value. This is because the server</span>
</span></span><span class="line"><span class="cl"><span class="c1">#      operator can be required to not delete information before a given</span>
</span></span><span class="line"><span class="cl"><span class="c1">#      time, e.g. to comply with freedom of information laws.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   2. Make sure the resulting value is lower or equal to the maximum allowed</span>
</span></span><span class="line"><span class="cl"><span class="c1">#      value, and if it&#39;s not replace it with that value. This is because the</span>
</span></span><span class="line"><span class="cl"><span class="c1">#      server operator can be required to delete any data after a specific</span>
</span></span><span class="line"><span class="cl"><span class="c1">#      amount of time.</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_retention_allowed_lifetime_min</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_lifetime</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_retention_allowed_lifetime_min</span><span class="p">,</span> <span class="n">max_lifetime</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_retention_allowed_lifetime_max</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_lifetime</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">max_lifetime</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_retention_allowed_lifetime_max</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;[purge] max_lifetime for room </span><span class="si">%s</span><span class="s2">: </span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">room_id</span><span class="p">,</span> <span class="n">max_lifetime</span><span class="p">)</span>
</span></span></code></pre></div><p>We first figure out what the effective value for <code>max_lifetime</code> is in the room; it&rsquo;s either the value from the room&rsquo;s policy, or from the homeserver&rsquo;s default policy if no specific policy is defined for this room.</p>
<p>Then we:</p>
<ol>
<li>take the maximum value between <code>allowed_lifetime_min</code> and <code>max_lifetime</code>, so we use the effective value if it&rsquo;s within the allowed range, and the minimum allowed value if it&rsquo;s not.</li>
<li>take the minimum value between the result of step 1 and the maximum allowed value, so we use the value from step 1 if it&rsquo;s within the allowed range, and the maximum allowed value if it&rsquo;s not.</li>
</ol>
<p>That way we ensure that, if the effective value of <code>max_lifetime</code> is within the allowed range, it stays the same, otherwise it&rsquo;s changed to the bound it goes over.</p>
<p>Note that a previous implementation of this configuration refused entirely to process any incoming event that was describing a policy that wasn&rsquo;t abiding to this range, this is no longer the case <a href="https://github.com/matrix-org/synapse/pull/8104">as of a few months ago</a>, when it was changed to the implementation I&rsquo;ve just described.</p>
<h2 id="the-purge">The purge</h2>
<p>Now&rsquo;s come the time to get rid of these nasty old events. Let&rsquo;s look at the final preparation before we do that:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Figure out what token we should start purging at.</span>
</span></span><span class="line"><span class="cl"><span class="n">ts</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">clock</span><span class="o">.</span><span class="n">time_msec</span><span class="p">()</span> <span class="o">-</span> <span class="n">max_lifetime</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">stream_ordering</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">find_first_stream_ordering_after_ts</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">get_room_event_before_stream_ordering</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">room_id</span><span class="p">,</span> <span class="n">stream_ordering</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="ow">not</span> <span class="n">r</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;[purge] purging events not possible: No event found &#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;(ts </span><span class="si">%i</span><span class="s2"> =&gt; stream_ordering </span><span class="si">%i</span><span class="s2">)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">ts</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">stream_ordering</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="n">stream</span><span class="p">,</span> <span class="n">topo</span><span class="p">,</span> <span class="n">_event_id</span><span class="p">)</span> <span class="o">=</span> <span class="n">r</span>
</span></span><span class="line"><span class="cl"><span class="n">token</span> <span class="o">=</span> <span class="s2">&#34;t</span><span class="si">%d</span><span class="s2">-</span><span class="si">%d</span><span class="s2">&#34;</span> <span class="o">%</span> <span class="p">(</span><span class="n">topo</span><span class="p">,</span> <span class="n">stream</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">purge_id</span> <span class="o">=</span> <span class="n">random_string</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="bp">self</span><span class="o">.</span><span class="n">_purges_by_id</span><span class="p">[</span><span class="n">purge_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">PurgeStatus</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Starting purging events in room </span><span class="si">%s</span><span class="s2"> (purge_id </span><span class="si">%s</span><span class="s2">)&#34;</span> <span class="o">%</span> <span class="p">(</span><span class="n">room_id</span><span class="p">,</span> <span class="n">purge_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># We want to purge everything, including local events, and to run the purge in</span>
</span></span><span class="line"><span class="cl"><span class="c1"># the background so that it&#39;s not blocking any other operation apart from</span>
</span></span><span class="line"><span class="cl"><span class="c1"># other purges in the same room.</span>
</span></span><span class="line"><span class="cl"><span class="n">run_as_background_process</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;_purge_history&#34;</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_purge_history</span><span class="p">,</span> <span class="n">purge_id</span><span class="p">,</span> <span class="n">room_id</span><span class="p">,</span> <span class="n">token</span><span class="p">,</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>First, we need to figure out the timestamp we need to start purging at, which is just now minus the room&rsquo;s policy&rsquo;s <code>max_lifetime</code>, and convert that into a stream ordering.</p>
<p>Matrix rooms are <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">DAGs</a>, which means it&rsquo;s not always possible to have a straight line from one point of the history to another. To address that, Synapse orders events with their unique index in its streams of incoming events, which is what we call the stream ordering of that event. Retrieving a stream ordering allows us to translate the timestamp into a location in that stream we can then use.</p>
<p>However, here we&rsquo;re retrieving the first stream ordering Synapse can find after the timestamp, but the events stream isn&rsquo;t scoped to the room we want to purge. This means we need to get some data on the closest event <em>in that room</em>, and we do that by calling <code>get_room_event_before_stream_ordering</code>, which will return some metadata on the event sent to that room before the given stream ordering (so the most recent event to purge from that room). This will return, beside the event&rsquo;s ID, its topological and stream ordering.</p>
<p>Now, we already know what a stream ordering is, but what about a topological ordering? Well it&rsquo;s roughly the same thing, except that instead of being the index of the event in Synapse&rsquo;s events stream, it&rsquo;s its index in the room&rsquo;s topology. For example, the first event of the room will have a topological ordering of 1, the next one 2, etc.</p>
<p>The main difference with a stream ordering is that a topological ordering isn&rsquo;t always unique because a DAG can sometimes branch. This is why we&rsquo;re getting both the topological ordering of the event <em>and</em> its stream ordering, so we can tell the purge code exactly what event we want the purge to start at.</p>
<p>From these two integers we create a token, using the format <code>t[topological ordering]-[stream ordering]</code> (starting with <code>t</code> to make it clear which ordering comes first), and we run the <code>_purge_history</code> function into a background process, which is another way of saying we&rsquo;re running that function in a non-blocking way, so we can start process the next room.</p>
<p>Now I&rsquo;m not going to go any further, because as I&rsquo;ve already said the rest of this code was initially introduced when implementing the purge history admin API; and I didn&rsquo;t work much on this code except for making sure it was doing what I would expect it to do.</p>
<p>Though what you&rsquo;ll probably want to know about the code that&rsquo;s actually clearing off events is that it takes some precautions to make sure it doesn&rsquo;t completely break the rooms it&rsquo;s purging, namely:</p>
<ul>
<li>it won&rsquo;t delete state events to prevent the room from getting into a broken state</li>
<li>it won&rsquo;t delete the most recent event in the room; that&rsquo;s, again, because a room&rsquo;s history is a DAG and each event needs to reference previous events (with the exception of <code>m.room.create</code>, which creates the room) - therefore if you don&rsquo;t have any event in the room to reference, nobody will be able to send any new event in that room (or Synapse might try to reference an older state event but then the new event will probably appear out of order on other homeservers)</li>
</ul>
<p>However, despite not being able to delete these events, Synapse will still hide them from clients, which should be enough of a mitigation in most cases.</p>
<h1 id="a-note-on-media">A note on media</h1>
<p>As you might have noticed, this feature only manages the retention of messages, not state events or, a more requested variant, media. Media retention is an entirely different problem (tracked <a href="https://github.com/matrix-org/synapse/issues/6832">here</a>) for a few reasons. For a brief point of context, the way uploading media into a room work in Matrix, is that you first upload your media to the homeserver, then send an event into the room with data on how to reach (and possibly decrypt) the media.</p>
<p>The first issue with this is that in end-to-end encrypted rooms the homeserver won&rsquo;t be able to read the event listing the media&rsquo;s URL and metadata (in fact it&rsquo;s not even capable of distinguishing it from a text message), so it&rsquo;s not always possible to map a media with the room it&rsquo;s been sent to. On top of that, some third-party media stores such as TravisR&rsquo;s <a href="https://github.com/turt2live/matrix-media-repo">matrix-media-repo</a> implement some deduplication logic so the same file might be used in two different rooms, which complicates things even more.</p>
<p>This means a separate feature needs to be implemented for media. The details and design still need to be ironed out, but it&rsquo;s on the team&rsquo;s roadmap. You might notice, however, that while this feature isn&rsquo;t deleting media entirely, it removes references to them from the room, which at least would still prevent members of the room from accessing them easily.</p>
<h1 id="what-a-journey">What a journey</h1>
<p>Message retention policies can be a super useful feature, and some bits can be a bit tricky to understand, or a bit curious in terms of design. So I hope this deep dive into how that feature works and was implemented was helpful. If it&rsquo;s still a hazy and unclear feel free to reach out over <a href="https://matrix.to/#/@brendan:abolivier.bzh">Matrix</a> or <a href="https://twitter.com/BrenAbolivier">Twitter</a>! 🙂</p>
<p>Note that this isn&rsquo;t a technical documentation on how to use the feature, therefore I didn&rsquo;t specifically outline the limitations, important bits of config, etc. related to this feature, but instead spread them through the post. If you just need to make it work and skim across its shortcomings then <a href="https://github.com/matrix-org/synapse/blob/develop/docs/message_retention_policies.md">the documentation</a> is the right place to look.</p>
<p>I sure had fun writing this post, it was nice revisiting one of my first big features in Synapse, and it motivated me to look with fresh new eyes into all of the implementation&rsquo;s details (and even find a few bugs), which was welcome 😀 Huge thanks to <a href="https://github.com/CromFr">Thibaut</a>, <a href="https://github.com/anoadragon453">Andrew</a> and <a href="https://github.com/callahad">Dan</a> for proofreading it!</p>
<p>See you in the next post! 🙂</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Install Party 1.0</title>
      <link>https://brendan.abolivier.bzh/install-party-1.0/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Fri, 01 Nov 2019 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/install-party-1.0/</guid>
      <description>A few weeks ago, I attended Ubucon Europe in Sintra with two of my colleagues from the Matrix core team. One of the workshops we hosted there was about getting the attendees to install their own Matrix homeserver. While trying to figure out how to set it up so that everyone ends up with a working and federating homeserver, we had the idea of an automated tool to create servers dedicated to these kinds of events. This is how my last personal project, Install Party, was born, and it&#39;s now getting its 1.0 release!</description>
      <content:encoded><![CDATA[<p>A few weeks ago, I
<a href="https://twitter.com/BrenAbolivier/status/1182224154784874496">attended</a> <a href="https://sintra2019.ubucon.org/">Ubucon
Europe</a> in Sintra with two of my colleagues from
the <a href="https://matrix.org">Matrix</a> core team (oh, yes, if you didn&rsquo;t know already,
I <a href="https://twitter.com/BrenAbolivier/status/1057656744497811457">joined New
Vector</a> about a
year ago, and I&rsquo;ve been working on Matrix as my full-time job since then). We
had a few chats with very nice people, and also hosted two Matrix-related
workshops.</p>
<p>One of these workshops, which happened on the morning of the last day, was about
installing <a href="https://github.com/matrix-org/synapse">Synapse</a>, the reference
Matrix homeserver implementation. The goal was to give attendees a presentation
about what Matrix is, get them to install their own homeserver, and, if
possible, to get everyone&rsquo;s server to federate with everyone else&rsquo;s.</p>
<p>This is not a trivial thing to do, especially when the technical expertise of
the attendence looks quite diverse. After a quick brainstorm on how to do that,
<a href="https://twitter.com/benparsons">Ben</a> suggested that we give everyone access to
a VPS that is accessible from the Internet, with SSH access and a domain name.</p>
<p>This would make things much easier than trying to get attendees to install a
server on their own machine (no need to setup a custom CA, or a local DNS
server), but I know well enough how making a workshop or a talk rely on Internet
connectivity can really jinx it. The connectivity seemed good enough there
though, so I figured I&rsquo;d give a try at automating the provisioning of such
servers. This evolved into a project I&rsquo;ve kept working on afterwards named
&ldquo;Install Party&rdquo;.</p>
<h2 id="install-party">Install Party</h2>
<p><a href="https://github.com/babolivier/install-party">Install Party</a> is a Python module
that allows users to provision a server by creating an instance (a physical or
virtual machine), attaching a DNS A record to it, and running a script that
installs and configures <a href="https://about.riot.im">Riot</a> and
<a href="https://caddyserver.com/">Caddy</a> on that instance.</p>
<p>This can be done by simply running <code>python -m install_party create -N x</code>, with
the number of servers to create as <code>x</code>:</p>
<pre tabindex="0"><code>$ python install_party create -N 3
INFO - Provisioning server coogl (expected domain name coogl.ubucon.abolivier.bzh)
INFO - Creating instance...
INFO - Waiting for instance to become active...
INFO - Host is active, IPv4 address is 54.38.70.225
INFO - Creating DNS record...
INFO - Created DNS record coogl.ubucon.abolivier.bzh
INFO - Waiting for post-creation script to finish...
INFO - Done!
INFO - Provisioning server czxcx (expected domain name czxcx.ubucon.abolivier.bzh)
INFO - Creating instance...
INFO - Waiting for instance to become active...
INFO - Host is active, IPv4 address is 54.38.70.93
INFO - Creating DNS record...
INFO - Created DNS record czxcx.ubucon.abolivier.bzh
INFO - Waiting for post-creation script to finish...
INFO - Done!
INFO - Provisioning server jswho (expected domain name jswho.ubucon.abolivier.bzh)
INFO - Creating instance...
INFO - Waiting for instance to become active...
INFO - Host is active, IPv4 address is 54.38.71.74
INFO - Creating DNS record...
INFO - Created DNS record jswho.ubucon.abolivier.bzh
INFO - Waiting for post-creation script to finish...
INFO - Done!

All servers have been created:
	- coogl.ubucon.abolivier.bzh
	- czxcx.ubucon.abolivier.bzh
	- jswho.ubucon.abolivier.bzh
$
</code></pre><p>(I&rsquo;ve trimmed the log lines&rsquo; length here, it&rsquo;s usually longer and feature the
date and the name of the module sending the line, but that would have been
unreadable in this post. This will also be the case for other similar sections
of the post.)</p>
<p>The workshop&rsquo;s host can then hand out the domain name attached to a server to
each attendee, who can then log in via SSH to the server and install and
configure a Matrix homeserver (including, if applicable, its built-in ACME
support for automatic provisioning of the certificate needed for federation). As
an example, <a href="https://git.io/JelHi">here</a> are the instructions we got the
attendees to follow during our workshop at Ubucon Europe.</p>
<p><img src="/images/install-party-1.0/domain.jpg" alt="">
<em>One of the domains I handed out during our workshop at Ubucon</em></p>
<p>From there, the attendee can use the instance of Riot to register on their new
homeserver, and federate with every other attendee&rsquo;s homeserver, but also every
other homeserver on the Internet.</p>
<p><img src="/images/install-party-1.0/federating.png" alt="">
<em>The servers federating between themselves but also with some from the wider
Internet</em></p>
<p>Once the workshop is done, the host can then delete every server with <code>python -m install_party delete --all</code>:</p>
<pre tabindex="0"><code>$ python -m install_party delete --all
INFO - Deleting instance for name jswho...
INFO - Deleting domain name for name jswho...
INFO - Deleting instance for name czxcx...
INFO - Deleting domain name for name czxcx...
INFO - Deleting instance for name coogl...
INFO - Deleting domain name for name coogl...
INFO - Applying the instances deletion...
INFO - Applying the DNS changes...
INFO - Done!
$
</code></pre><p>Of course, this deletion mode also has a dry-run mode, which can be turned on
with <code>-d</code>.</p>
<p>They can also delete specific servers with <code>-s foo -s bar</code> (which would only
delete the servers <code>foo</code> and <code>bar</code>), or delete every server except one or more
with <code>-a -e foo -e bar</code> (which would delete every server but <code>foo</code> and <code>bar</code>).
This became very handy when one person arrived late to the workshop, and didn&rsquo;t
get the time to finish it, so I could just give them some extra time to work on
it and exclude their server&rsquo;s domain from the deletion I performed shortly
afterwards.</p>
<p>At any time, the host can also list every server that is still active with
<code>python -m install_party list</code>:</p>
<pre tabindex="0"><code>$ python -m install_party list
+--------+-----------------+------------------+----------+-------+
| Name   | Instance name   | Domain           | Status   | IPv4  |
|--------+-----------------+------------------+----------+-------|
| jswho  | ubucon-jswho    | jswho.ubucon.... | ACTIVE   | ...   |
| czxcx  | ubucon-czxcx    | czxcx.ubucon.... | ACTIVE   | ...   |
| coogl  | ubucon-coogl    | coogl.ubucon.... | ACTIVE   | ...   |
+--------+-----------------+------------------+----------+-------+
$
</code></pre><p>This mode can also detect orphaned domain names (i.e. domain names which target
IP address isn&rsquo;t a known instance) and orphaned instances (i.e. instances that
don&rsquo;t have a domain name targetting their IP address):</p>
<pre tabindex="0"><code>$ python -m install_party list
+--------+-----------------+------------------+----------+-------+
| Name   | Instance name   | Domain           | Status   | IPv4  |
|--------+-----------------+------------------+----------+-------|
| jswho  | ubucon-jswho    | jswho.ubucon.... | ACTIVE   | ...   |
+--------+-----------------+------------------+----------+-------+

ORPHANED INSTANCES
+--------+-----------------+----------+-------------+
| Name   | Instance name   | Status   | IPv4        |
|--------+-----------------+----------+-------------|
| czxcx  | ubucon-czxcx    | ACTIVE   | 54.38.70.93 |
+--------+-----------------+----------+-------------+

ORPHANED DOMAINS
+--------+----------------------------+--------------+
| Name   | Domain                     | Target       |
|--------+----------------------------+--------------|
| coogl  | coogl.ubucon.abolivier.bzh | 54.38.70.225 |
+--------+----------------------------+--------------+
$
</code></pre><h2 id="the-big-10">The big 1.0</h2>
<p>A short while after Ubucon, I finished a basic implementation the three modes
I&rsquo;ve described above (I had already implemented the creation and was halfway
done implementing the listing when the workshop happened), and released
<a href="https://github.com/babolivier/install-party/releases/tag/v0.3.0">v0.3.0</a> with
these.</p>
<p>Since then, I&rsquo;ve been iterating on improving the codebase, adding new features
to the modes, and polishing the whole thing in order to make it easier to use
and contribute to. All of the improvements I&rsquo;ll describe here are documented in
the project&rsquo;s
<a href="https://github.com/babolivier/install-party/blob/v1.0.0/README.md">README</a>.</p>
<p>A major change is that I&rsquo;ve added the ability for users to use their favourite
DNS provider, as well as their favourite instances provider (i.e. the API to use
to create instances), instead of the hardcoded OVH and OpenStack (which are the
ones I personally use). These providers are still available, but users can now
add their own providers by creating a Python class that implements the correct
API, dropping it as a file in the correct location, and start using it right
away.</p>
<p>The creation mode also got two main improvements. The first one is the ability
for multiple instances to be created in the same run with the <code>-N/--number</code>
command-line argument. This is already something I&rsquo;ve described above, but
didn&rsquo;t exist in previous versions of Install Party (indeed, for the Ubucon
workshop, I had to run Install party multiple times in multiple terminals in
order to genenerate the number of instances I was going for).</p>
<p>Another new feature of the creation mode is the ability to provide a
post-install script with the <code>-s/--post-install-script</code>. This script will be
run on the new server(s) after the installation of Riot and Caddy.</p>
<p>The rest of the work was mostly about cleaning up the codebase (e.g. getting rid
of some inconsistencies in the name of some variables of classes), adding some
proper logging, adding docstrings to (almost) every function of the project, and
improving and updating the user-facing documentation in the
<a href="https://github.com/babolivier/install-party/blob/v1.0.0/README.md">README</a>.</p>
<h2 id="thats-all-folks">That&rsquo;s all folks!</h2>
<p>If you&rsquo;re interested in following along further developments of Install Party
(though I expect it to become much calmer now), want to report an issue when
using it, or want to ask a question about it, feel free to join the project&rsquo;s
Matrix room
<a href="https://matrix.to/#/#install-party:abolivier.bzh">#install-party:abolivier.bzh</a>,
or to check out its <a href="https://github.com/babolivier/install-party">Github repo</a>!</p>
<p>When I&rsquo;m writing this post, the video of the workshop Install Party was born for
hasn&rsquo;t been released yet. However, I&rsquo;ve noticed the staff are starting to
publish videos of the event&rsquo;s talks, so I should update this post in a few
days/weeks when the video is released.</p>
<p>I hope you enjoyed reading through this post, see you next time!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Make your own Google Drive&#43;Docs with Nextcloud, Collabora Online and object storage</title>
      <link>https://brendan.abolivier.bzh/your-own-google-drive-docs/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Fri, 13 Jul 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/your-own-google-drive-docs/</guid>
      <description>As someone who values privacy (mine and others&#39;), I usually try to find new ways of getting rid of the now infamous GAFAM and their friends, the biggest of them all being Google. Among every Google service, there&#39;s one that is hugely used among both individuals and organisations, which is Google Docs. Add to that the whole storage service they also provide, and you get Google Drive, the best way to directly feed Google with all your files and data, including personal and administrative documents, music collections, photos from your smartphone... Because I don&#39;t want to share that huge amount of data with Google, I&#39;ve set up my own Google Drive+Docs using mainly self-hosted free software projects, including Nextcloud, Caddy and (Docker-less) Collabora Online.</description>
      <content:encoded><![CDATA[<p>As someone who values privacy (mine and others&rsquo;), I usually try to find new ways of getting rid of the now infamous GAFAM and their friends, the biggest of them all being Google. Among every Google service, there&rsquo;s one that is hugely used among both individuals and organisations, which is Google Docs. Add to that the whole storage service they also provide, and you get Google Drive, the best way to directly feed Google with all your files and data, including personal and administrative documents, music collections, photos from your smartphone&hellip; In other words, a huge volume of data that either is litteraly personal data, or can be used to extract personal data about you without your explicit consent (in Google&rsquo;s case).</p>
<p>In my case, I&rsquo;m not really at ease with the fact that I need to upload administrative documents containing personal data to Google in order to register for my boat driving license, nor do I with the fact that my phone will automatically upload every picture I take to Google&rsquo;s servers so it can be processed and have any data it contains extracted and stored in a database I don&rsquo;t have control over.</p>
<p>Just as many reasons for me to look for a Google Drive-like solution that I could entirely control. Here&rsquo;s what I came up with so far:</p>
<ul>
<li><a href="https://nextcloud.com/">Nexctloud</a> 13 as the whole cloud management solution</li>
<li><a href="https://www.openstack.org/">OpenStack</a>&rsquo;s object storage (Swift) as the scalable storage backend</li>
<li><a href="https://caddyserver.com/">Caddy</a> as the web server</li>
<li><a href="https://www.collaboraoffice.com/code/">Collabora Online</a> (without Docker) as the collaborative office suite</li>
</ul>
<p>So far I got all of that working together on my personal space, so let me walk you through how to do that yourself. Because of the whole stack&rsquo;s current state, it does require some technical skills, though. Sorry about that.</p>
<p><em>(Disclaimer: because I know that some who read this blog might ask the question, let me get things straight first: yup, I&rsquo;m using Nextcloud as my Google Drive replacement, even though I currently work at <a href="https://cozy.io">CozyCloud</a>. Although people can seem to think it might be hypocritical from me to do so, my take on the matter is that Cozy and Nextcloud aren&rsquo;t competitors, and although they have some features in common, one can easily complete the other, as Cozy (which I also use) has features Nexctloud doesn&rsquo;t have and vice-versa. Diversity is great, as people from both <a href="https://twitter.com/cozycloud/status/968763001540145152">CozyCloud</a> and <a href="https://twitter.com/Nextclouders/status/978233603191603200">Nextcloud</a> would tell you. And no, I haven&rsquo;t been asked to write that paragraph 😉)</em></p>
<p><em>Update: right after I published this post, <a href="https://twitter.com/nd4pa">Antoine</a> made an amazing Ansible playbook based on it, which you can check out <a href="https://github.com/nd4pa/homesuite-ansible">right here</a>!</em></p>
<h2 id="lcpp-gnulinux-caddy-postgresql-php">LCPP: (GNU/)Linux-Caddy-PostgreSQL-PHP</h2>
<h3 id="get-the-right-hosting">Get the right hosting</h3>
<p>First things first, because I want to have control over the whole thing, I&rsquo;m self-hosting most of the pieces of software I previously mentioned on a personal VPS. Mine is a <a href="https://www.scaleway.com/pricing/">START 1-S from Scaleway</a>, because, since the storage backend will be remote (I&rsquo;ll come back to that later on), I needed a better bandwidth than the best-effort 100Mbps one I have on my OVH cloud instances (and I know that because I previously tried this setup on one of these, which resulted in poor performances), so mine has a 200Mbps bandwidth (I couldn&rsquo;t find any piece of info about whether that was guaranteed or best-effort, though).</p>
<p>For the record, OVH does offer instances with guaranteed 250Mbps bandwidth, though given their rates it&rsquo;s not something I can afford only for that use, especially given what competitors offer, including Scaleway.</p>
<p>Anyway, try to keep the bandwidth requirement in mind while chosing where you&rsquo;ll host your Nextcloud instance.</p>
<p>Also for the record, the operating system on my VPS at the time of writing is GNU/Linux, more precisely Ubuntu 16.04 LTS, so the processes I&rsquo;ll describe in this blog post might slightly differ if you&rsquo;re running another GNU/Linux distribution or another operating system.</p>
<h3 id="caddy">Caddy</h3>
<p>As I mentioned, I&rsquo;m using <a href="https://caddyserver.com">Caddy</a> as the web server for the whole thing. In case the name doesn&rsquo;t ring a bell, Caddy is a lightweight web server with simple configuration, HTTP/2 as its default, automatic HTTPS through Let&rsquo;s Encrypt, and a lot of available plugins.</p>
<p>One downside of using Caddy is that, unlike popular free software projects out there, it doesn&rsquo;t provide any official packaging (because of complications brought by the plugins system), so installation must be done manually. This is not that hard, though, because the whole process is pretty much straightforward:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Download caddy</span>
</span></span><span class="line"><span class="cl">curl -L <span class="s2">&#34;https://caddyserver.com/download/linux/amd64?license=personal&amp;telemetry=off&#34;</span> &gt; caddy.tar.gz
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Extract the whole thing in a specific directory</span>
</span></span><span class="line"><span class="cl">mkdir caddy
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> caddy
</span></span><span class="line"><span class="cl">tar xvf ../caddy.tar.gz
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install Caddy&#39;s binary and allow it to use the 80 and 443 ports</span>
</span></span><span class="line"><span class="cl">sudo cp ./caddy /usr/local/bin/
</span></span><span class="line"><span class="cl">sudo setcap <span class="nv">cap_net_bind_service</span><span class="o">=</span>+ep /usr/local/bin/caddy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create Caddy&#39;s certificates directory</span>
</span></span><span class="line"><span class="cl">sudo mkdir /etc/ssl/caddy
</span></span><span class="line"><span class="cl">sudo chown -R www-data:www-data /etc/ssl/caddy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create Caddy&#39;s configuration directory</span>
</span></span><span class="line"><span class="cl">sudo mkdir /etc/caddy /etc/caddy/caddy.conf.d
</span></span><span class="line"><span class="cl">sudo <span class="nb">echo</span> <span class="s2">&#34;import caddy.conf.d/*.conf&#34;</span> &gt; /etc/caddy/Caddyfile
</span></span><span class="line"><span class="cl">sudo chown -R www-data:www-data /etc/caddy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create, enable and start Caddy&#39;s systemd service</span>
</span></span><span class="line"><span class="cl">sudo mkdir /usr/lib/systemd/system
</span></span><span class="line"><span class="cl">sudo cp ./init/linux-systemd/caddy.service /usr/lib/systemd/system
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> caddy
</span></span><span class="line"><span class="cl">sudo systemctl start caddy</span></span></code></pre></div>
<p><em>Note: either the <code>curl</code> line or the <code>setcap</code> one (or both of them) might not work because of missing packages depending on your GNU/Linux distribution and your provider&rsquo;s image. On Debian-based distributions, <code>curl</code> can be installed by installing the <code>curl</code> package, and <code>setcap</code> can be installed by installing the <code>libcap2-bin</code> package. If you&rsquo;re running another GNU/Linux distribution, the packages&rsquo; names can differ a bit.</em></p>
<p>Now that you have Caddy installed, let&rsquo;s install another very important component we&rsquo;ll need to run Nextcloud: PHP.</p>
<h3 id="php">PHP</h3>
<p>Because Caddy can only interact with PHP using FastCGI (as far as I&rsquo;m aware of), we&rsquo;ll need to install its <em>FPM</em> (FastCGI Process Manager) version, named <code>php-fpm</code>. Along with that, you&rsquo;ll need a fair amount of PHP extensions Nextcloud depends on. We&rsquo;ll also be using PostgreSQL to manage Nextcloud&rsquo;s database, so we&rsquo;ll also need drivers for that.</p>
<p>On most Debian-based systems, this is done by doing the following:</p>
<pre tabindex="0"><code>sudo apt install php7.0-fpm php7.0-gd php7.0-json php7.0-pgsql php7.0-curl php7.0-mbstring php7.0-intl php7.0-mcrypt php-imagick php7.0-xml php7.0-zip
</code></pre><p>PHP&rsquo;s FPM should now be installed, configured with the right extensions, and running. You can make sure of that by checking whether the file <code>/var/run/php/php7.0-fpm.sock</code> exists.</p>
<p>In order to be sure that all PHP extensions we just installed are loaded, let&rsquo;s restart its FPM:</p>
<pre tabindex="0"><code>sudo systemctl restart php7.0-fpm
</code></pre><h3 id="postgresql">PostgreSQL</h3>
<p>Nextcloud needs a database, so let&rsquo;s install a databases management system! I personally use PostgreSQL because of its performances and ease to use. You can install it by simply running:</p>
<pre tabindex="0"><code>sudo apt install postgresql
</code></pre><p>Now, we need to do some basic configuration within PostgreSQL so Nextcloud has a user and a database to connect to. PostgreSQL provides an interactive shell (<code>/usr/bin/psql</code>), with the user <code>postgres</code> acting as a superadmin by default.</p>
<p>Regarding authentication, and more precisely access to the shell from the local host, PostgreSQL default to using its <code>peer</code> authentication method, which means you can only authenticate and access the shell as a user if you&rsquo;re trying from a system user with the same name. As the <code>postgresql</code> package has already created a <code>postgres</code> system user, all that&rsquo;s left to do is to call the PostgreSQL shell as that user:</p>
<pre tabindex="0"><code>sudo -u postgres psql
</code></pre><p><em>Note: if that doesn&rsquo;t work, it might be because your system user lacks privileges. Try that again as a user with more privileges (and/or root).</em></p>
<p>Now create Nextcloud&rsquo;s user and database from that shell using SQL queries:</p>
<pre tabindex="0"><code>postgres=# CREATE USER nextcloud WITH password &#39;ncpassword&#39;;
CREATE ROLE
postgres=# CREATE DATABASE nextcloud WITH owner &#39;nextcloud&#39;;
CREATE DATABASE
postgres=# \q
</code></pre><p>In PostgreSQL&rsquo;s shell, <code>\q</code> is the exit instruction.</p>
<p>Don&rsquo;t forget to change <code>ncpassword</code> in my example above with a real, strong password for Nextcloud&rsquo;s database user. Also, keep that password somewhere, because you&rsquo;ll need it when installing Nextcloud. And while we&rsquo;re at it&hellip;</p>
<h2 id="installing-nextcloud">Installing Nextcloud</h2>
<p>Because Nextcloud is only a PHP web app, installing it is as simple as downloading a ZIP archive (you might need to run <code>sudo apt install unzip</code> in order to be able to open the archive). The link of the archive to download can be found <a href="https://nextcloud.com/install/#instructions-server">here</a> as the target of the big blue &ldquo;Download Nextcloud&rdquo; button.</p>
<p>As an example, here&rsquo;s how your install should go with Nextcloud 13.0.4, authenticated as root:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Change /srv/http with your web server root (e.g. /var/www)</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /srv/http
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Download Nextcloud&#39;s archive</span>
</span></span><span class="line"><span class="cl">curl -LO https://download.nextcloud.com/server/releases/nextcloud-13.0.4.zip
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Extract the archive&#39;s content</span>
</span></span><span class="line"><span class="cl">unzip nextcloud-13.0.4.zip
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Make the Caddy user owner of the extracted directory</span>
</span></span><span class="line"><span class="cl">chown -R www-data:www-data nextcloud</span></span></code></pre></div>
<p>And here you go, with a brand new Nextcloud install located at <code>/srv/http/nextcloud</code>.</p>
<p>Now let&rsquo;s create Nextcloud&rsquo;s configuration file in Caddy&rsquo;s configuration folder. You must first have a domain, or sub-domain, Nextcloud should be accessible at (i.e. <code>cloud.example.tld</code>). Here&rsquo;s the content of my own <code>/etc/caddy/caddy.conf.d/nextcloud.conf</code> file:</p>
<pre tabindex="0"><code>cloud.example.tld {
	tls letsencrypt@example.tld

	root   /srv/http/nextcloud

	fastcgi / /var/run/php/php7.0-fpm.sock php {
		env PATH /bin
	}

	# checks for images
	rewrite {
		ext .svg .gif .png .html .ttf .woff .ico .jpg .jpeg
		r ^/index.php/(.+)$
		to /{1} /index.php?{1}
	}

	rewrite {
		r ^/index.php/.*$
		to /index.php?{query}
	}

	# client support (e.g. os x calendar / contacts)
	redir /.well-known/carddav /remote.php/carddav 301
	redir /.well-known/caldav /remote.php/caldav 301

	# remove trailing / as it causes errors with php-fpm
	rewrite {
		r ^/remote.php/(webdav|caldav|carddav|dav)(\/?)$
		to /remote.php/{1}
	}

	rewrite {
		r ^/remote.php/(webdav|caldav|carddav|dav)/(.+?)(\/?)$
		to /remote.php/{1}/{2}
	}

	rewrite {
		r ^/public.php/(dav|webdav|caldav|carddav)(\/?)$
		to /public.php/{1}
	}

	rewrite {
		r ^/public.php/(dav|webdav|caldav|carddav)/(.+)(\/?)$
		to /public.php/{1}/{2}
	}

	# .htaccess / data / config / ... shouldn&#39;t be accessible from outside
	status 403 {
		/.htacces
		/data
		/config
		/db_structure
		/.xml
		/README
	}

	header / Strict-Transport-Security &#34;max-age=31536000;&#34;
}
</code></pre><p>This file is directly adapted from one of Caddy&rsquo;s example configuration files, and can be found <a href="https://github.com/caddyserver/examples/blob/master/nextcloud/Caddyfile">right here</a>. I made a few changes, which are the domain I gave Nextcloud on the first line, and the email address Let&rsquo;s Encrypt should send me certificates expiration notices on the second line. I also removed the logging (basically because I don&rsquo;t want it nor need it, since it&rsquo;s my personal instance) and moved the vhost&rsquo;s root to some other place on the disk since I want my web server&rsquo;s root at <code>/srv/http</code> rather than <code>/var/www</code> (but that doesn&rsquo;t really matter).</p>
<p>Now let&rsquo;s reload Caddy&rsquo;s configuration by running <code>sudo systemctl reload caddy</code>. Your Nextcloud should now be accessible from the domain (or sub-domain) you gave it, and opening that in a browser should display a configuration wizard. It can take some time, though, as Caddy needs to talk with Let&rsquo;s Encrypt to generate the required certificates since it&rsquo;s the first time it encounters this domain.</p>
<p><img src="/images/your-own-google-drive-docs/nc-wizard.png" alt="Nextcloud setup wizard"></p>
<p>Within this wizard, you can provide the login and password for your administrator account. Before clicking &ldquo;Finish setup&rdquo;, click &ldquo;Storage &amp; database&rdquo; (if you can&rsquo;t see any other field than the ones requesting the administrator&rsquo;s credentials), because we need to configure Nextcloud&rsquo;s access to PostgreSQL.</p>
<p>Leave the &ldquo;Data folder&rdquo; field as it is, and fill in the user and password of Nextcloud&rsquo;s PostgreSQL user (which, in our example, should be &ldquo;nextcloud&rdquo; and the password you previously set, respectively), and Nextcloud&rsquo;s database (which, in our example, should be &ldquo;nextcloud&rdquo;). Because you might already have a MySQL/MariaDB/SQLite PHP driver installed, Nextcloud might offer you different databases management systems; select &ldquo;PostgreSQL&rdquo;.</p>
<p>Now you can click &ldquo;Finish setup&rdquo;, and here you have a working Nextcloud instance waiting for you!</p>
<p><em>Note that Nextcloud also provides a way to do the whole setup process with a command line interface, as documented <a href="https://docs.nextcloud.com/server/13/admin_manual/installation/command_line_installation.html">here</a>.</em></p>
<p>If you just want a simple Nextcloud instance with local storage, you can stop here. I chose to use OpenStack Swift as Nextcloud&rsquo;s primary storage backend, so if that&rsquo;s what you&rsquo;re looking for, don&rsquo;t upload anything yet and bear with me as I walk you through that setup!</p>
<h2 id="openstack-swift-as-primary-storage-backend">OpenStack Swift as primary storage backend</h2>
<p>As I want to store music and videos in Nextcloud, along with backups from my phone&rsquo;s camera, I might get quite limited with the local disk space on my VPS. As disk space storage can get quite expensive, and is usually really limited on cheap VPSs. A great solution to that problem is cloud-based <a href="https://en.wikipedia.org/wiki/Object_storage">object storage</a>. The main advantage of such a solution is being a very unexpensive storage solution (usually around 0.01€/GB/month) that scales almost infinitely.</p>
<p>There are two main solutions of the sort, as far as I&rsquo;m aware of, which are Amazon S3, and OVH&rsquo;s Public Cloud Storage, which is based on OpenStack Swift. Because I don&rsquo;t want any of my data on Amazon&rsquo;s servers (for the same reason I don&rsquo;t want it on Google&rsquo;s), and because I usually trust OVH with my data (mainly because I know a few people there, thus have a small peek at what internal use is done with that same data), I decided to go with OVH&rsquo;s solution.</p>
<p>Before I go any further, I&rsquo;ll assume you already have an OVH account (which you can create on their website). Direct your browser to OVH&rsquo;s <a href="https://www.ovh.com/manager/cloud">Cloud manager</a>, then order a new &ldquo;Cloud project&rdquo; using the blue &ldquo;Order&rdquo; button on the top left corner of the page. Once your project is created, it should appear under &ldquo;Servers&rdquo; in the navigation bar on the left (which you might need to click on to uncover the list of projects), click on its name, then on the &ldquo;Storage&rdquo; item that just appeared. Click the white &ldquo;Create a container&rdquo; button, which takes you to an interface letting you create an object storage container.</p>
<p><img src="/images/your-own-google-drive-docs/ovh-create-container.png" alt="Container creation interface"></p>
<p>Select the datacentre that best fit your use (i.e. the one located the closest to you). Also make sure your container&rsquo;s type is set to &ldquo;Private&rdquo; so the container can&rsquo;t be accessed without authentication. Name your container and confirm the creation.</p>
<p>Now that you created your container, you&rsquo;ll need Nextcloud to be able to access it. First, you need to retrieve OpenStack credentials for your project. In order to get that, click the &ldquo;OpenStack&rdquo; entry corresponding to your project in the navigation bar on the left of the screen, then &ldquo;Add user&rdquo; and input a description (e.g. &ldquo;nextcloud&rdquo;). Copy the user&rsquo;s password somewhere (because OVH&rsquo;s manager will never displayed it again), then click on the &ldquo;&hellip;&rdquo; button on the right of the user&rsquo;s row, then &ldquo;Downloading an Openstack configuration file&rdquo;, and select the datacentre you chose for your object storage container.</p>
<p><img src="/images/your-own-google-drive-docs/ovh-openstack-user.png" alt="OpenStack user"></p>
<p>This will download a file named <code>openrc.sh</code> containing all remaining pieces of information required for Nextcloud to access your container. Now let&rsquo;s give it these data, by editing Nextcloud&rsquo;s configuration file (<code>/srv/http/nextcloud/config/config.php</code> in my case) and adding this block to the config (make sure to keep the last line (which should just be <code>);</code>) at the very end of the file in order to keep the file&rsquo;s syntax correct):</p>
<pre tabindex="0"><code>&#39;objectstore&#39; =&gt; array(
	&#39;class&#39; =&gt; &#39;OC\\Files\\ObjectStore\\Swift&#39;,
	&#39;arguments&#39; =&gt; array(
		&#39;username&#39; =&gt; &#39;OS_USERNAME&#39;,
		&#39;password&#39; =&gt; &#39;OS_PASSWORD&#39;,
		&#39;bucket&#39; =&gt; &#39;nextcloud&#39;,
		&#39;autocreate&#39; =&gt; false,
		&#39;region&#39; =&gt; &#39;OS_REGION_NAME&#39;,
		&#39;url&#39; =&gt; &#39;https://auth.cloud.ovh.net/v2.0&#39;,
		&#39;tenantName&#39; =&gt; &#39;OS_TENANT_NAME&#39;,
		&#39;serviceName&#39; =&gt; &#39;swift&#39;,
	),
),
</code></pre><p>With:</p>
<ul>
<li><code>OS_USERNAME</code> being the username (or &ldquo;ID&rdquo;, as OVH&rsquo;s manager calls it) of your OpenStack user.</li>
<li><code>OS_PASSWORD</code> being the password of your OpenStack user.</li>
<li><code>OS_REGION_NAME</code> being the datacentre&rsquo;s identifier, which would be <code>GRA3</code> in the example we&rsquo;ve seen before example. This information can be found at the very last line from the <code>openrc.sh</code> file.</li>
<li><code>OS_TENANT_NAME</code> being the name of the OpenStack tenant to use, which can be found in the <code>openrc.sh</code> file on the line starting with <code>export OS_TENANT_NAME</code>.</li>
</ul>
<p>Now you can refresh Nextcloud&rsquo;s tab in your browser, and, if everything went well, it should display Nextcloud&rsquo;s interface. Because the storage backend is now remote, it might be slow to display completely, though, let&rsquo;s see how we can improve performances using caching.</p>
<h2 id="caching-caching-everywhere">Caching, caching everywhere</h2>
<p>All of these solutions work pretty well together, and are part of recommendations made by Nextcloud regarding caching.</p>
<h3 id="zend-opcache">Zend OPCache</h3>
<p>PHP already comes bundled with a cache mechanism, which is a PHP opcache named Zend OPCache. Basically, a PHP opcache stores compiled PHP scripts so they don’t need to be re-compiled every time they are called. To enable it and get it to match Nextcloud&rsquo;s recommendations, uncomment the following lines and ajust the necessary values in yout <code>/etc/php/7.0/fpm/php.ini</code> file in this way:</p>
<pre tabindex="0"><code>opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=1
opcache.save_comments=1
</code></pre><p>Then all you need to do is restart PHP&rsquo;s FPM in order for the changes to be applied:</p>
<pre tabindex="0"><code>sudo systemctl restart php7.0-fpm
</code></pre><h3 id="the-apcu-php-extension">The APCu PHP extension</h3>
<p>Now we need to add some cache for Nextcloud&rsquo;s data. The easiest way to achieve that is by installing the <a href="https://pecl.php.net/package/APCu">APCu</a> PHP extension:</p>
<pre tabindex="0"><code>sudo apt install php-apcu
</code></pre><p>This package automatically installs and configures the extension, so all that&rsquo;s left to do is restart PHP for this extension to be loaded:</p>
<pre tabindex="0"><code>sudo systemctl restart php7.0-fpm
</code></pre><p>Now let&rsquo;s tell Nextcloud to use APCu for local cache by adding this line to its configuration file (<code>/srv/http/nextcloud/config/config.php</code> in my case):</p>
<pre tabindex="0"><code>&#39;memcache.local&#39; =&gt; &#39;\OC\Memcache\APCu&#39;,
</code></pre><p>Again, make sure to keep the file&rsquo;s last line (which should just be <code>);</code>) at the very end in order to keep the file&rsquo;s syntax correct.</p>
<h3 id="redis">Redis</h3>
<p>With its default configuration, Nextcloud can run into some troubles handling file locks. After switching my instance&rsquo;s storage backend to OVH&rsquo;s Public Cloud Storage, I had quite a huge volume of data to re-send from my disk to my instance, which sometimes would result in Nextcloud not being able to upload some files because of (probably) errored file locks.</p>
<p>A single Redis instance can act as a great cache for file locks, and installing it is as simple as running:</p>
<pre tabindex="0"><code>sudo apt install redis-server php-redis
</code></pre><p>Once again, restart PHP&rsquo;s FPM:</p>
<pre tabindex="0"><code>sudo systemctl restart php7.0-fpm
</code></pre><p>Then let&rsquo;s tell Nextcloud to use Redis to cache file locks (and how to reach the Redis instance) by adding this block to Nextcloud&rsquo;s configuration file:</p>
<pre tabindex="0"><code>&#39;memcache.locking&#39; =&gt; &#39;\OC\Memcache\Redis&#39;,
&#39;redis&#39; =&gt; array(
	 &#39;host&#39; =&gt; &#39;localhost&#39;,
	 &#39;port&#39; =&gt; 6379,
),
</code></pre><p>Again, watch out for the file&rsquo;s last line, you know how it goes by now 😄</p>
<p>Refresh Nextcloud&rsquo;s tab in your browser, and it should all run much faster from now on. You can also safely and efficiently start synchronising your data using Nextcloud&rsquo;s or ownCloud&rsquo;s client (since the APIs are the same between both systems).</p>
<h2 id="collabora-online-docker-less">Collabora Online, Docker-less</h2>
<p>Now let&rsquo;s talk about what took me the longest to figure out: Collabora Online (even though once you do, it&rsquo;s pretty simple, so it shouldn&rsquo;t take too long to get you set up with it).</p>
<p><a href="https://www.collaboraoffice.com/code/">Collabora Online</a> is a <a href="https://gerrit.libreoffice.org/gitweb?p=online.git">FLOSS</a> online collaborative office suite based on <a href="https://www.libreoffice.org/">LibreOffice</a>. It makes an amazing replacement to Google Docs&rsquo;s text documents and spreadsheets editors and Nextcloud even provides an integration for it.</p>
<p>One of my biggest troubles, though, was that the current recommended way to install Collabora Online was through Docker. I&rsquo;m personally not a huge fan of Docker, and find it has some awful design flaws when it comes to resources management.</p>
<p>Unfortunately, although it&rsquo;s possible to install Collabora Online from Collabora&rsquo;s Debian/Ubuntu/CentOS/openSUSE repositories, the process of setting it up is barely documented (and even not at all for some parts). After several hours of searching and experimenting with it, I finally managed to get it to work, so here&rsquo;s an attempt at documenting the whole thing.</p>
<p>The first step to take is to install Collabora&rsquo;s repository and the required packages. Please bear in mind that the host is running Ubuntu 16.04, so the process might differ if you&rsquo;re running another GNU/Linux distribution. Instructions for all supported distributions can be found <a href="https://www.collaboraoffice.com/code/#packages_for_linux_x86_64_platform">here</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Install support for HTTPS APT repositories</span>
</span></span><span class="line"><span class="cl">sudo apt install apt-transport-https
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Import Collabora&#39;s signing key</span>
</span></span><span class="line"><span class="cl">sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 0C54D189F4BA284D
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add the URL for the Collabora Online&#39;s repository to /etc/apt/sources.list</span>
</span></span><span class="line"><span class="cl">sudo <span class="nb">echo</span> <span class="s1">&#39;deb https://www.collaboraoffice.com/repos/CollaboraOnline/CODE ./&#39;</span> &gt;&gt; /etc/apt/sources.list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Perform the installation</span>
</span></span><span class="line"><span class="cl">sudo apt update <span class="o">&amp;&amp;</span> sudo apt install loolwsd code-brand</span></span></code></pre></div>
<p>Now edit (as root) the <code>/etc/loolwsd/loolwsd.xml</code> file, such that:</p>
<ul>
<li>in the <code>ssl</code> section, <code>enable</code> should have <code>false</code> as its value, and <code>termination</code> must be set to <code>true</code>. Because we&rsquo;ll serve Collabora Online behind Caddy, acting as a HTTPS reverse proxy, we don&rsquo;t want Collabora Online to serve its content using HTTPS (which might cause some troubles with certificates, as Caddy&rsquo;s way to handle these wouldn&rsquo;t let another program access them), but we want it to tell its clients to use HTTPS URLs instead of HTTP ones, which is exactly what <code>termination</code> does.</li>
<li>in the <code>wopi</code> section (itself being located under the <code>storage</code> section), add a <code>host</code> line containing the domain name you gave Nextcloud, with dots (<code>.</code>) escaped with backslashes (<code>\</code>) because the value is expected to be a regular expression. Make sure the <code>allow</code> attribute is set to <code>true</code>. The <code>storage</code> section contains access control lists (<em>ACLs</em>) to tell Collabora Online where it can access files and where it can&rsquo;t. Because Nextcloud uses <a href="https://wopi.readthedocs.io/en/latest/">WOPI</a> to that end, this line grants access to Nextcloud to provide storage for Collabora Online using the WOPI protocol. As an example, if Nextcloud was served at <code>cloud.example.tld</code>, the line would look like:</li>
</ul>
<pre tabindex="0"><code>&lt;host desc=&#34;Regex pattern of hostname to allow or deny.&#34; allow=&#34;true&#34;&gt;cloud\.example\.tld&lt;/host&gt;
</code></pre><p>Let&rsquo;s restart Collabora Online to let it know about the new configuration:</p>
<pre tabindex="0"><code>sudo systemctl restart loolwsd
</code></pre><p>Now we need to configure Caddy to act as a HTTPS reverse proxy for Collabora Online. Just like Nextcloud, you need a domain (or sub-domain) to give Collabora (here <code>collabora.example.tld</code>). Here&rsquo;s what I have in <code>/etc/caddy/caddy.conf.d/collabora.conf</code>:</p>
<pre tabindex="0"><code>collabora.example.tld {
	tls letsencrypt@example.tld

	proxy /loleaflet http://127.0.0.1:9980 {
		transparent
	}

	proxy /hosting/discovery http://127.0.0.1:9980 {
		transparent
	}

	proxy /lool http://127.0.0.1:9980 {
		transparent
		websocket
	}

	header / {
		Strict-Transport-Security &#34;max-age=31536000;&#34;
		Content-Security-Policy &#34;default-src &#39;none&#39;; frame-src &#39;self&#39; blob:; connect-src &#39;self&#39; wss://cloud.example.tld; script-src &#39;unsafe-inline&#39; &#39;self&#39;; style-src &#39;self&#39; &#39;unsafe-inline&#39;; font-src &#39;self&#39; data:; object-src blob:; img-src &#39;self&#39; data: https://cloud.example.tld:443; frame-ancestors https://cloud.example.tld:443 &#39;self&#39;&#34;
	}
}
</code></pre><p>While the whole file is pretty basic, let&rsquo;s talk about the last instruction, which contains the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy">Content Security Policy</a> HTTP header. Because it&rsquo;s a quite complex header, I don&rsquo;t really know the details of how to tweak it to make it work with Collabora Online, and had neither the time nor the motivation to dive into it. Therefore, the header shown here is a plain copy of the one sent by <a href="https://demo.nextcloud.com">Nextcloud&rsquo;s demo servers</a>, where I replaced every instance of <code>demo.nextcloud.com</code> with Nextcloud&rsquo;s URL (which, in the example shown here, is <code>cloud.example.tld</code>, which you should replace with the domain you gave Nextcloud). I also improved the last part, allowing Collabora to open iframes pointing to itself, which is required for slideshow presentations. It works fine, but I wanted to point out that this part of the configuration isn&rsquo;t my own work.</p>
<p>Now let&rsquo;s reload Caddy to let it know of these changes in its configuration:</p>
<pre tabindex="0"><code>sudo systemctl reload caddy
</code></pre><p>And let&rsquo;s make Nextcloud talk to Collabora Online, by installing the <a href="https://apps.nextcloud.com/apps/richdocuments">Collabora Online Nextcloud app</a>. Documentation on installing Nextcloud apps can be found <a href="https://docs.nextcloud.com/server/13/admin_manual/installation/apps_management_installation.html">here</a>. Then, browse to your Nextcloud settings, and click &ldquo;Collabora Online&rdquo; under the administration settings section. Now all there&rsquo;s left to do is input your Collabora Office instance&rsquo;s URL (which, with the example shown here, would be <code>https://collabora.example.tld</code>), hit &ldquo;Save&rdquo; and here you go! Nextcloud is now correctly hooked up with Collabora Online! 😁</p>
<p>If you want to try it out, you can upload a <code>.doc</code>/<code>.docx</code>/<code>.odt</code>/<code>.ods</code>/etc. file to Nextcloud and open it, or create a new document using the &ldquo;+&rdquo; button at the top of the Files app and open it. Nextcloud should then redirect you to Collabora Online&rsquo;s interface, and you can start editing right away!</p>
<p><img src="/images/your-own-google-drive-docs/collabora.png" alt="Collabora Online web interface&rsquo;s"></p>
<h2 id="conclusion">Conclusion</h2>
<p>Now we have a wonderful self-hosted personal Google Drive/Docs alternative only made of FLOSS projects, which is pretty neat!</p>
<p>However, as you might have noticed, this setup does require quite a high and broad technical knowledge to manage, which is quite sad as it makes the whole thing out of the reach of non-technical people. For some parts, the lack of documentation even makes it out of reach of some people that actually have a technical background, though there are some other ways to end up with such a setup that would require less hassle through inexisting doc (such as using Docker to run Collabora Online with, or using nginx or Apache as the HTTPS reverse proxy).</p>
<p>Because of all that, waving Google &amp; friends goodbye won&rsquo;t be easy, if not unmanagable, for most people in the state things currently are. But to me the future isn&rsquo;t that dark regarding the rise of privacy-respectful alternatives, since we just saw that the tools themselves exist, and what&rsquo;s left to do is find a way to make them more accessible. And although this blogpost focuses entirely on the file hosting and document editing services, there&rsquo;s still a lot of services to build alternatives to, such as <a href="https://joinpeertube.org/en/">PeerTube</a>, an amazing decentralised, federated and peer-to-peer alternative to YouTube, which recently ended a very successful <a href="https://www.kisskissbankbank.com/en/projects/peertube-a-free-and-federated-video-platform">crowdfunding campaign</a> at over 250% of their initial goal.</p>
<p>In my opinion, all that show quite a promising future for the Internet, in which anyone would at some point be able to benefit from amazing services without having to make unfair compromises such as delivering all of its personal data just to send a message to some friends. It does require some work, though, but as members of the French non-profit organisation <a href="https://framasoft.org/">Framasoft</a> would say, the journey is long, but the path is free.</p>
<p>Aaaand that&rsquo;s all for this week! I hope you&rsquo;ve enjoyed reading this post (which is now my longest one as it&rsquo;s slightly longer than <a href="/enter-the-matrix">Enter the Matrix</a>), and if you did, or if you have any kind of feedback regarding it, please feel free to hit me up on <a href="https://twitter.com/BrenAbolivier">Twitter</a>, <a href="https://mastodon.social/@babolivier">Mastodon</a> or <a href="https://matrix.to/#/@brendan:abolivier.bzh">Matrix</a>, I would love to see what you thought of it!</p>
<p>Regarding the rate these posts go out at, it&rsquo;s likely that I won&rsquo;t be able to carry a weekly schedule forever. This small paragraph isn&rsquo;t in any way the announcement of a new rate, but is rather to make it clear that I might skip a week when I deem it necessary. I&rsquo;ll try to keep it as close to weekly as possible, though, and whatever happens, the best way not to miss any post is to subscribe to the <a href="https://brendan.abolivier.bzh/index.xml">RSS feed</a> or follow me <a href="https://twitter.com/BrenAbolivier">on Twitter</a> or <a href="https://mastodon.social/@babolivier">Mastodon</a> (which I&rsquo;ll try to share more stuff on).</p>
<p>Anyway, see you next week (hopefully) for a brand new blog post! 😄</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Random tools #1</title>
      <link>https://brendan.abolivier.bzh/random-tools-1/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Sun, 08 Jul 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/random-tools-1/</guid>
      <description>Over time, I came to encounter a few tools, addressing different use cases and/or issues. Recently, I started listing these tools in order to share some of them with you, whether you already know them or not, in smaller posts like this one, without going too much in depth with them. Welcome to the first episode of &#34;Random tools&#34;!</description>
      <content:encoded><![CDATA[<p>Over time, I came to encounter a few tools, addressing different use cases and/or issues. Recently, I started listing these tools in order to share some of them with you, whether you already know them or not, in smaller posts like this one, without going too much in depth with them.</p>
<p>I&rsquo;m giving posts like this one two objectives: help people discover tools that often help be with small or bigger tasks, sometimes on a daily basis, and allow me to effectively share some of my knowledge with everyone (which is the main goal for this blog) while requiring less work and efforts than the other posts you can usually find around here, which are mainly focus on one specific topic.</p>
<p>So let&rsquo;s get things started with this first episode of &ldquo;Random tools&rdquo;!</p>
<h2 id="tilix">Tilix</h2>
<p>Let&rsquo;s start with one tool that a lot of people already know: <a href="https://gnunn1.github.io/tilix-web/">Tilix</a>. It&rsquo;s a terminal for GNU/Linux systems using GTK+3, which you might also know as its older name: Terminix.</p>
<p>Tilix is currently my main terminal on my computer, and almost the only one I use period (which, given my current job being systems engineer, makes it one of my main work tools). Among the amazing features it offers is the ability to split the screen in panes as much as you want, making it super easy to work with a task requiring data from several hosts at the same time, or checking in real time the effects of a patch on a system while directly applying it without losing sight of each, for example.</p>
<p>Add to that Tilix&rsquo;s &ldquo;Quake mode&rdquo;, allowing you to make Tilix appear on the top of your screen and disappear from it without losing the current session (just like the Quake console minus the animation), and that makes the perfect tool for me to work with, because I never have to waste loads of time trying to bring it my terminal back at my desktop&rsquo;s foreground while jungling with a few other windows.</p>
<p><img src="/images/random-tools-1/tilix.gif" alt="Example GIF"></p>
<p>Keyboard shortcuts for splitting are <code>Ctrl+Alt+R</code> for a vertical split, and <code>Ctrl+Alt+S</code> for a horizontal one. Quake mode can be enabled by editing the keyboard shortcut bound to Tilix in your system settings (or adding one if there&rsquo;s none yet) so it runs <code>tilix --quake</code> (or <code>tilix -q</code>) instead of <code>tilix</code>.</p>
<h2 id="tabsearch">TabSearch</h2>
<p>This tool is a Firefox extension that will help you save a lot of time if you always carry loads of tabs open at once, sometimes even spread through multiple windows. In this kind of configuration, it&rsquo;s usually a frequent waste of time to remeber where a specific tab is located on a tab bar, and what window this tab bar belongs to.</p>
<p>If added to Firefox&rsquo;s top bar, <a href="https://addons.mozilla.org/en-US/firefox/addon/tab_search/">TabSearch</a> will provide you with a small graphical user interface that will allow you to browse through every tab that is open in the current window, open in another window, or has been recently closed. This interface also provides a search feature, and can be toggled by hitting <code>Ctrl+Shift+F</code>. You can then browse through the tabs using the arrow keys or search within them, and hit <code>Enter</code> when you find the tab you want to switch to.</p>
<p><img src="/images/random-tools-1/tabsearch.png" alt="TabSearch&rsquo;s interface"></p>
<p><em>This screenshot isn&rsquo;t mine and comes from <a href="https://addons.mozilla.org/en-US/firefox/addon/tab_search/">the extension&rsquo;s page on AMO</a></em></p>
<p>The extension also provides you with other ways to interact with tabs, which are listed in <a href="https://github.com/reblws/tab-search/#shortcuts">its documentation</a>. It will also add to Firefox&rsquo;s top bar a count of the tabs that are currently opened in the window.</p>
<h2 id="in-the-next-episode">In the next episode</h2>
<p>In order to make these posts quick to make, I&rsquo;ve decided to only cover two tools per episode, so that&rsquo;s the end of this first one! I&rsquo;m not sure about when the next one will get its release, but I&rsquo;ve already the tools to talk about in mind.</p>
<p>If you like that one or want to share some feedback on it with me, feel free to hit me up on <a href="https://twitter.com/BrenAbolivier">Twitter</a>, <a href="https://mastodon.social/@babolivier">Mastodon</a> or <a href="https://matrix.to/#/@brendan:abolivier.bzh">Matrix</a>!</p>
<p>I&rsquo;ll see you before then, next week in fact, for what should be a technical walkthrough. Until then, have fun, and see you next week! 😄</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Making party time</title>
      <link>https://brendan.abolivier.bzh/party-time/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Sun, 01 Jul 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/party-time/</guid>
      <description>A month ago today, the first edition of immersion{s}, a new events brand mainly focused on trance parties in Brest (Brittany), happened. Organising an event isn&#39;t an easy task, and that&#39;s even more relevant when it&#39;s your first one. It took us over 6 months of hard work to make this one happen, and I&#39;m not even counting all the previous failed attemps. During all that time, up to the few hours after the party ended, we&#39;ve learned a lot on a lot of topics we sometimes didn&#39;t even expect to have to deal with, and this new knowledge is what I&#39;d like to share with you this week.</description>
      <content:encoded><![CDATA[<p>A month ago today, the first edition of <em>immersion{s}</em>, a new events brand mainly focused on trance parties in Brest (Brittany), happened. This first edition, as all the next ones will be, are organised by <a href="https://www.trancendances.fr">Trancendances</a>, a French non-profit I co-funded and have been the president of since 2014, focused on promoting trance music all around France.</p>
<p>In the past, we worked on a few events around Paris and promoted others all around the country, but that was the very first event we organised from scratch, as all of the gigs we previously worked on were produced by people outside of our organisation, who sometimes already had some experience with such things, had already handled all the planning and project management.</p>
<p>Organising an event isn&rsquo;t an easy task, and that&rsquo;s even more relevant when it&rsquo;s your first one. It took us over 6 months of hard work to make this one happen, and I&rsquo;m not even counting all the previous failed attemps. During all that time, up to the few hours after the party ended, we&rsquo;ve learned a lot on a lot of topics we sometimes didn&rsquo;t even expect to have to deal with, and this new knowledge is what I&rsquo;d like to share with you this week.</p>
<p><img src="/images/party-time/immersions-wackii.jpg" alt="Alex Wackii playing at immersion{s}"></p>
<p><em><a href="https://www.facebook.com/AlexWackiiOfficial/">Alex Wackii</a> playing at immersion{s} - photo credit: <a href="http://jlartigaud.fr/">Joffrey Lartigaud</a> / <a href="https://www.trancendances.fr/">Trancendances</a></em></p>
<h2 id="your-party-will-be-a-financial-failure">Your party will be a financial failure</h2>
<p>Let&rsquo;s take the bad news in first. If the event you&rsquo;re working on is your first one, you&rsquo;re unlikely to earn a single cent from it. There&rsquo;s one really good reason for that: you don&rsquo;t know what you&rsquo;re doing, and even in the friendliest environment, you&rsquo;re likely to screw up more than once. But that&rsquo;s okay.</p>
<p>In my opinion, the best way to learn how to do something is to do it, not with the expectation to crush it but to learn the processes, the rules to follow, who to speak to, etc. And that&rsquo;s exactly what happened with this event. Financially speaking, it was kind of a disaster, since we only sold enough tickets (including at the door) to cover about 50% of our total expenses.</p>
<p>On the other hand, however, we spent the last months creating our own brand, putting together our own team, learning how this kind of stuff is done, who to get in touch with for each specific need, what questions to ask, etc. We tried some stuff that worked and some that didn&rsquo;t (I&rsquo;ll dive deeper in that later), so that now we know how to do some important tasks right because we got them wrong the last time, or because we got them right on the first try. And we learned all of that stuff so tasks that would take us weeks to work out then will only take us five minutes and an email now.</p>
<p>It can be one hard thing to process, because such an event usually costs a lot of money, representing thousands of euros, and you&rsquo;re not likely to make them back, especially if it&rsquo;s your first shot and you don&rsquo;t really know what you&rsquo;re doing. But it&rsquo;s an important thing to be aware of that as early in the process as possible, because it will allow you to set better, more realistic objectives, which will allow you to look at the upcoming event with a better focus on what actually matters: creating reproducible processes, building a team and learning about the local audience and how to interact with it. In my opinion, it&rsquo;s better to see this not as a financial loss, but rather as an investment on successes that will eventually come in the long run.</p>
<p>There&rsquo;s something you can do to avoid losing too much, though, and that&rsquo;s finding sponsors. It might be quite easy or the hardest thing ever depending on the local ecosystem. Sponsoring basically consists in contacting companies, and offering them to give you money to make your event possible in exchange for communication. Although we didn&rsquo;t have any sponsor on this one (because we messed up somewhere with our planning and reached out too late), we did get the opportunity to get in touch with a few potential sponsors with which we might work on future events.</p>
<p>Quite similar to sponsorships are public subventions, which relate to public services. In fact, we got the chance to work in partnership with the city, which promotes local events organisators. Although it&rsquo;s not money we got from them, they handled the renting of the venue and most of the sound and lighting equipment, which represents a saving we estimate at around 1k€ at least (which is a lot, regarding the event&rsquo;s budget).</p>
<p>Both sponsorships and public subventions happen, and some companies or public services might be more than happy to promote your event. Do not hesitate to reach out to as many people as you can, the venue&rsquo;s city, the state, the government, the EU, a big local company, a bank, a clothing brand, etc.. One of the things I&rsquo;ve learned not only from this event, but from running Trancendances in general, is that a &ldquo;no&rdquo; will very not likely hurt you, and a &ldquo;yes&rdquo; might offer you opportunities you&rsquo;ve never dreamt of, so do not give up on getting in touch with an entity because &ldquo;they&rsquo;re too big and I&rsquo;m too small&rdquo;, &ldquo;they can&rsquo;t interested in this kind of things&rdquo;, or any other reason. And if you can&rsquo;t think of anything, the way to think of potential sponsors or public services that might support your event would be, in my opinion, to have a look at flyers or posters for other similar events, since sponsorship and public subventions usually involve including some logos there.</p>
<h2 id="we-are-human-after-all">We are human after all</h2>
<p>One of the most important part in every project is the people you surround yourself with. Because such a project usually represents a lot of work and efforts, especially if it&rsquo;s your first time, you can&rsquo;t really afford being the only person on board. And for the same reason, you need the people you surround yourself with to be trustworthy, because you can&rsquo;t afford to always be looking over someone else&rsquo;s shoulder.</p>
<p>In the case of <em>immersion{s}</em>, we were actually a small team of two people: Raphaëlle, who manages everything related to events at Trancendances, and myself. Fortunately, Raphaëlle and I know each other since we were children, and have been friends since our teenage years, so we both know pretty well how to trust and work with each other. Working in such conditions makes the experience much better, as none of us would ever question the other&rsquo;s work (once we discussed how we were going to do things), and we had quite some fun working together, making what would have otherwise been an exhaustive, stressful and trying experience much more bearable.</p>
<p>In such an adventure, it&rsquo;s the smallest things that matter, such as being able to tell when the others aren&rsquo;t in the mood to work, are going through a difficult time, etc.. Sometimes, a work meeting would turn into a good time between a couple of friends who had some pressure to release, nibbling pistachios and drinking some tea.</p>
<p>One very important thing that you must never forget is that, in the end, an event is just a group of people, usually not even that much, that get along quite well, and are passionate about something they want to share with the rest of the world. It&rsquo;s not technology or money that drives such a machinery, it&rsquo;s the human that work hard behind the scenes, a lot of times even voluntarily, so everyone have a good time.</p>
<p>As part of the organising team, one of your <em>de facto</em> responsibilities is to manage the other humans you&rsquo;re working with. Always pay attention at them, see how they&rsquo;re handling the work load and pressure, and be able to detect and react to something going wrong. For example, if one of your teammates is having a hard time in their personal life, and starts panicking over what would appear as a silly thing to you, you need to be able to see it and reassure them, to take the necessary actions so they won&rsquo;t be confronted with the struggle they&rsquo;re panicking about, rather than teasing or making fun of them about it. Working under pressure isn&rsquo;t an easy thing, and not everyone reacts the same way in such conditions. Never mock, always empathise.</p>
<p>On the course of the process, we eventually ended up requiring more pairs of hands, especially on the event&rsquo;s night as there are many things to take care of, such as selling tickets, ensuring the artists have all they need, or other specific tasks depending on the venue, partners, etc.. We reached out to a few friends of ours who offered us their help, who are friends we&rsquo;ve known for at least a few years, enough for us to trust them entirely. The result was us being able to carry the event without having to worry a single second about whether someone was doing their job the right way. The carefree feeling it brought was something really amazing, which avoided us ending up having to waste a lot of time looking over everyone&rsquo;s shoulder, and saved us a lot of energy and efforts during the night. For some of these friends, this ponctual help even ended up turning into a longer-term investment as they&rsquo;re joining the permanent team to work with us on future events.</p>
<p>Always trust, always care, and always think bigger; to me, that&rsquo;s how you build a great team.</p>
<h2 id="ask-the-right-questions">Ask the right questions</h2>
<p>The most frustrating part about doing something for the very first time is not knowing what you&rsquo;re doing. You might find yourself a crazy amount of times in a position where you realise there&rsquo;s one piece of information you&rsquo;re missing to complete an important task, or that you&rsquo;ve been assuming something only to find out that you&rsquo;ve wrongly done so, forcing you into improvising while you could have planned the whole thing perfectly if you had only asked.</p>
<p>Situations like these happened quite a lot during the months leading up to <em>immersion{s}</em>, from having to open the ticket shop without knowing the room&rsquo;s capacity, to setting the timetable before being made aware of the hours the venue would allow us to play music at, including planning the setting up of a cloakroom before learning, on the event&rsquo;s day, that the venue already had one they operate themselves, and others situations like those.</p>
<p>One great thing to prevent those from happening is, before you even start planning the event, to sit with your teammate(s) and think about how is everything going to work out, as far as you know, and start listing everything you need to know for every step. Here&rsquo;s a little example of what an extract of such a list might look like:</p>
<ul>
<li>
<p>We want to do a music event at this specific venue</p>
<ul>
<li>
<p>What dates would be great for that?</p>
</li>
<li>
<p>How do you book the venue?</p>
</li>
<li>
<p>What&rsquo;s the venue&rsquo;s capacity?</p>
</li>
<li>
<p>When can we access the venue? When do we need to leave?</p>
</li>
<li>
<p>When is the soonest the music can start? When is the latest it needs to stop?</p>
</li>
<li>
<p>Does the venue deduct a percentage from the ticket sales? If yes, how much?</p>
</li>
<li>
<p>Does the venue give us a percentage from the bar&rsquo;s sales? If yes, how much?</p>
</li>
<li>
<p>Does the venue provide us with the security staff and technicians? If not, who do they use to work with?</p>
</li>
<li>
<p>If they don&rsquo;t provide us with the security staff, what are the security requirements: how many people, with what certifications, etc.?</p>
</li>
<li>
<p>Do they operate a cloakroom? If not, is there somewhere at the venue we can set one up during the event?</p>
</li>
</ul>
</li>
<li>
<p>We want to receive some support from the city</p>
</li>
<li>
<p>Who do we need to speak to?</p>
</li>
<li>
<p>What would they offer us?</p>
</li>
<li>
<p>If they rent the venue for us, is there something we need to do, or do they handle that with the venue directly?</p>
</li>
<li>
<p>If they rent the equipment for us, can they provide us with a list of everything that includes? If not, who do we need to ask that to?</p>
</li>
<li>
<p>If the help doesn&rsquo;t consist in them directly giving us money, how much is the help worth?</p>
</li>
<li>
<p>[&hellip;]</p>
</li>
</ul>
<p>Of course, because &ldquo;as far as you know&rdquo; might not lead you very far, so the list might not be exhaustive, but the goal here is to create at least a base on which you can iterate afterwards. Then, each time you start figuring how something might happen, think of all the pieces of information you need for that and add them to the list.</p>
<p>Not all questions are to ask to the event partners (venue, sponsors, etc.). You also must add to the list all questions you need to ask yourself and your teammates during the process, from &ldquo;in what time frame do we need to have this specific task done&rdquo; to the simple &ldquo;we need a stamp to identify the people who already paid their tickets, where do we find one&rdquo;, including &ldquo;how many posters do we need to order, in what size and for which use&rdquo;, and anything that might be relevant to your project.</p>
<p>Asking questions is the first step into efficient planning, so make sure to make it your very first step into the project, to take the time to make it as exhaustive as possible, and to continuously update it with new questions and answers. The goal here is to have the broadest, most complete picture of your project as soon as possible so you don&rsquo;t move forward blindly and are able to make the best decisions. Knowledge is power, here as much as everywhere else.</p>
<h2 id="the-not-so-retro-spective">The not-so-retro-spective</h2>
<p>On top of answering questions, one very important thing to do is to often, if not continously, ask ones. You need to question everything you&rsquo;ve done as a team, and how it turned out. Was that a good idea to stick half of our very pricey outdoor posters three weeks before the event, not knowing when the monthly cleanup by municipal services would happen, and with other events happening before, which organisers might also want to stick posters for? Probably not. Was it a good idea to pay a security company to provide us with the security staff, which would be more expensive but wouldn&rsquo;t require our team, which has no legal expertise, to find qualified people and write them contracts? Much likely.</p>
<p>Obviously, a retrospective on the whole event can only happen once the event is over, but you also need to make smaller ones, reacting in almost real time to everyone of your choices and their consequences, because a retrospective on the whole project will help you make the next one better, but smaller ones during the process will help you make fewer mistakes between then and the big date.</p>
<p>One very important, and obvious to some, topic these smaller retrospectives must address is your communication campaigns. Alongside planning, communicating efficiently on your event is one of the most important things that can screw up months of work if done wrong. That&rsquo;s why it&rsquo;s important to constantly ask yourself whether you took the right decisions up to that moment, and how to fix things if not. Always ask yourself if your target audience is defined correctly, if your campaigns match such an audience, etc. One of our biggest mistakes might have been not asking ourselves such questions, and wanting to do too much while not knowing what we were doing. This led to a few mistakes, such as investing way too much on online promotion and not enough on local advertisement, messing up our local outdoor display, running short of flyers too early, etc..</p>
<p>All that resulted in a huge loss in money and effectiveness, and while most of my friends were telling me they were always reminded about the upcoming event, most of our local target audience wasn&rsquo;t even aware such an event was happening. This could have been avoided by us asking ourselves more questions such as &ldquo;what audience is the most likely to show up if they&rsquo;re aware there&rsquo;s a party going on that night&rdquo; or &ldquo;how do we interact with them&rdquo;, not stopping at not precise enough answers too wide with the &ldquo;we&rsquo;ll figure this out later&rdquo; mindset, planning every single step of the communication process beforehand, asking ourselves how much we should spend on digital advertisement and what time the promotions should go live, what exact locations should we display physical outdoor advertisements at, how many posters do we need for each location, etc.. These questions might not be obvious on the first time, so it&rsquo;s easy to get it wrong at the beginning, and taking the time as often as possible to ask yourself if you did everything right, taking into account what you&rsquo;ve learned in the process, might help you set things right while there&rsquo;s still time. And don&rsquo;t stop on broad answers thinking the details will figure themselves out later, but rather dig the deepest you can. It requires more work, but in the end you&rsquo;ll have more control over your project and less frustration caused by not knowing what&rsquo;s going on.</p>
<h2 id="towards-the-next-adventure">Towards the next adventure</h2>
<p>Big projects such as organising an event from scratch for the first time are really exciting and interesting, as they teach you a lot about multiple topics. In this case, working on an electronic music party, which can be very briefly summed up as DJs playing DJ sets for a few hours, has involved project management, human management and resources, communication, branding, budgetting and a few other fields that might not look related to that at first sight.</p>
<p>It has been a fascinating adventure, a long, tiring, trying journey that we spent months following, which gave us some amazing results. I&rsquo;ve rarely been more proud of achieving something, and it&rsquo;s been a very fun and rewarding experience which will act as a baseline for future events.</p>
<p>I&rsquo;m really happy to have been able to make this dream come true with a team of amazing people, and I really look forward to the next one, which we&rsquo;re already thinking about.</p>
<p>Before concluding this post, I&rsquo;d like to thank, once again, everyone who worked on this project by my side, along with everyone who helped us make it happen with advices, feedback, support, by sharing the event and spreading the news, or just by being there with us that night, enjoying the moment. It all means so much to me.</p>
<p>I&rsquo;d also like to apologise regarding my absence on this space for the past few weeks. The last couple of weeks before <em>immersion{s}</em> have been a heck of a rush, and I much needed the month of June to recover (not even considering the infection I got in the middle of the month). Now I&rsquo;m back on tracks, and I&rsquo;ll try to keep up with my <a href="/one-post-a-week/">&ldquo;One post a week&rdquo;</a> challenge during the summer.</p>
<p>As always, if you liked this post or want to share some feedback on it with me, feel free to hit me up on <a href="https://twitter.com/BrenAbolivier">Twitter</a>, <a href="https://mastodon.social/@babolivier">Mastodon</a> or <a href="https://matrix.to/#/@brendan:abolivier.bzh">Matrix</a>!</p>
<p>See you next week for a new post!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Manage your passwords with pass</title>
      <link>https://brendan.abolivier.bzh/password-store/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Mon, 21 May 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/password-store/</guid>
      <description>Let&#39;s talk about passwords. Basically, that&#39;s the things you&#39;re supposed to keep different for each account you have on the Internet. Either you don&#39;t do it, do it partially, or have a password manager do it for you. This week, I&#39;m writing about pass, a simple and minimal password manager mainly consisting in a 699-line long bash script, which I&#39;ve been using for some months.</description>
      <content:encoded><![CDATA[<p>Let&rsquo;s talk about passwords. Basically, that&rsquo;s the things you&rsquo;re supposed to keep different for each account you have on the Internet. Either you don&rsquo;t do it, do it partially (like a mix between a leet-speak version of the service&rsquo;s name and a fix part, with an uppercase letter and a character that&rsquo;s neither a letter nor a number at some place, such as <code>mySup3rw3bs!t3MyUsualPassword</code>), or have a password manager do it for you.</p>
<p>I&rsquo;ve had quite a hard time finding a password manager that fits my needs. During the past few years, I&rsquo;ve tried quite a few of them, and eventually stopped using them one after the other. LastPass because of its poor UX on points that mattered to me, and I couldn&rsquo;t feel safe trusting that much into such a centralised and closed service. Keepass because it was a pain to synchronise my database between all devices. Passbolt because it focuses on a team use case and I want something designed for individuals. You name it.</p>
<p>After a while, I started trying to get a description of what I wanted. To me, the ideal password manager must be:</p>
<ul>
<li>free software</li>
<li>security audited</li>
<li>synchronisable across devices</li>
<li>self-hostable</li>
<li>easy to set up</li>
<li>easy/quick to use</li>
</ul>
<p>I realised that was quite an idealistic description, and thought I was done with password managers. To be fair, to this day, I still haven&rsquo;t found one that match all of my criteria, though the one I&rsquo;ll be talking about in this post gets quite close.</p>
<p>Also, let me get things straight first: the last two points in the list above are using the relative definition of &ldquo;easy&rdquo;, i.e. what&rsquo;s easy to set up/use to me, as someone who has some technical knowledge and background. Specifically, the solution I&rsquo;ll be writing about in this post would be labelled as quite painful to use by somebody who isn&rsquo;t used to bash, git et al.</p>
<h2 id="its-all-about-simplicity">It&rsquo;s all about simplicity</h2>
<p><a href="https://www.passwordstore.org/">Pass</a> is a minimal and very simple password manager which consists in a 699 line long bash script (including comments). It stores your password as files in a given directory (the &ldquo;store&rdquo;), and encrypt them using <a href="https://gnupg.org/">GnuPG</a>. That way, you can organise your passwords as you want, in as many sub-directories as you wish, and they will be stored, possibly along with some metadata, in a somewhat-secure fashion.</p>
<p>Notice here that I made a compromise on my criteria of an ideal password manager, here, because, as far as my knowledge goes, pass hasn&rsquo;t got a security audit yet (only GnuPG did). I consider it safe enough for my personal use, though.</p>
<p>Pass also has both CLI and GUI clients for most platforms, including OS X, Android, iOS and Windows, and also some browser extensions, but I&rsquo;ll only cover the basic command-line use of the bash script here. All clients and extensions can be found <a href="https://www.passwordstore.org/#other">here</a>, though.</p>
<h2 id="creating-the-store">Creating the store</h2>
<p>I won&rsquo;t cover installation, which is already covered on <a href="https://www.passwordstore.org/#download">pass&rsquo;s website</a> and should be quite easy on most systems.</p>
<p>You&rsquo;ll also need to generate a GPG key, which is the pass equivalent of the store&rsquo;s master key/passphrase, if you haven&rsquo;t got one, which I also won&rsquo;t cover here since there&rsquo;s already great resources for that on the Internet.</p>
<p>Once pass is installed, let&rsquo;s initialise a store with</p>
<pre tabindex="0"><code>pass init GPG-ID
</code></pre><p>Here, <code>GPG-ID</code> is the identifier of the key you&rsquo;ll use to encrypt your passwords. It can be the key&rsquo;s fingerprint (in the case of my own key, <code>E1D4B7457A829D771FBA8CACE860157274A28D7E</code>) or one of it&rsquo;s associated email addresses (which, in my case, can be <code>hello@brendanabolivier.com</code>).</p>
<p>It will then initialise a store in a directory, which path is <code>~/.password-store</code> and is created if it doesn&rsquo;t exist. This directory is the one in which pass will work in every call you&rsquo;ll make in the future. This value can be overriden by setting the environment variable <code>PASSWORD_STORE_DIR</code>.</p>
<h2 id="adding-an-existing-password">Adding an existing password</h2>
<p>Because you had accounts on the Internet before starting using pass, you might want to store their passwords in your brand new password store.</p>
<p>To insert a password into your password store, just run</p>
<pre tabindex="0"><code>pass insert PASSWORD-NAME
</code></pre><p>Where <code>PASSWORD-NAME</code> is the name you&rsquo;ll give to this entry. If you want to manage your entries with sub-directories, the entry name can also be a relative path to the password store (e.g. <code>pass insert hostProviders/ovh</code> will create an entry in the sub-directory &ldquo;hostProviders&rdquo;). If a sub-directory doesn&rsquo;t exist, pass will create it for you.</p>
<p>It will then prompt you for your password, which you can just paste and validate, and an encrypted copy of it will be stored in the password store. For example, if the entry name is <code>hostProviders/ovh</code>, it will store an encrypted copy of my password in <code>~/.password-store/hostProviders/ovh.gpg</code>.</p>
<p>You might also want to add metadata to your password, such as the account&rsquo;s login, or the service&rsquo;s URL, which some pass clients can use. You can do that by appending the <code>-m</code> flag to your <code>pass insert</code> call (before the <code>PASSWORD-NAME</code>), which allows you to write your entry using more than just one single line and save it using <code>Ctrl+D</code>.</p>
<p>In case of multiline entries, it&rsquo;s usually better to start an entry with the password as the first line&rsquo;s only content, and then add your metadata on the following lines. The reason for that is because, to pass, a non-multiline entry is just a one-line long file with the password as the only content. Having the first line only including the password will help pass handle multiline entries the same way as a single line entry.</p>
<p>In the end, your multiline entry would look like this:</p>
<pre tabindex="0"><code>mySup3rw3bs!t3P4ssw0rd
login: me@me.tld
url: mysuperwebsite.com
</code></pre><p>It might be worth noting that if you come from another password manager, there might be a migration script aiming at migrating all of your entries to pass instead of doing it manually, one at a time. Migration scripts for most password managers can be found <a href="https://www.passwordstore.org/#migration">here</a>.</p>
<h2 id="creating-passwords">Creating passwords</h2>
<p>Of course, one of the good things with having a password manager is having it generate different strong passwords for each service you have an account on. Generating a password with pass is as easy as calling:</p>
<pre tabindex="0"><code>pass generate PASSWORD-NAME
</code></pre><p>As with <code>pass insert</code>, this will create a <code>.gpg</code> file at the desired location, and will this time fill it with a 25-character long password. If you want the password length to be something else than 25, you tell pass by appending the desired length after the <code>PASSWORD-NAME</code>.</p>
<p>Once the password is generated, pass will print it into the terminal, so you can copy it. If you don&rsquo;t want it to appear on your screen, you can also append the <code>-c</code> flag to your call, right before the <code>PASSWORD-NAME</code>. Pass will then copy it into your clipboard, which it will clear after 45 seconds (the delay can be changed by setting the environment variable <code>PASSWORD_STORE_CLIP_TIME</code> to the number of seconds you want).</p>
<p>Another useful trick is appending metadata to the newly generated password, like we&rsquo;ve seen before. It&rsquo;s obviously possible to edit an existing password (using <code>pass edit PASSWORD-NAME</code>, which will open an unencrypted copy of the password entry in vim), but I personally prefer to never have pass printing my password on a screen.</p>
<p>To achieve that, we can first call <code>pass insert -m PASSWORD-NAME</code>, which will prompt for the password and its metadata, leave the first line blank and fill the following one with metadata before hitting <code>Ctrl+D</code>. We can then call <code>pass generate -ci PASSWORD-NAME</code>. Note the <code>-i</code> flag (which stands for &ldquo;in place&rdquo;), which means that the entry we want to generate a password for already exists, in which case pass will replace the entry&rsquo;s first line with the newly generated password, and leave the rest of the file as it was.</p>
<p>You now have your newly generated strong password copied to your clipboard, and the desired metadata in its file.</p>
<h2 id="retrieving-passwords">Retrieving passwords</h2>
<p>It would be quite useless to have all your passwords stored in your store without being able to retrieve them and use them. As everything with pass, this is quite easy:</p>
<pre tabindex="0"><code>pass show PASSWORD-NAME
</code></pre><p>Which you can even shorten as:</p>
<pre tabindex="0"><code>pass PASSWORD-NAME
</code></pre><p>Pass will then print out the corresponding password, along with its metadata (if it has any) in the terminal. If you don&rsquo;t want the password to be printed out, but rather to be copied to your clipboard, just append <code>-c</code> before the <code>PASSWORD-NAME</code>, just like <code>pass generate</code> (and just like <code>pass generate</code>, it will clear the clipboard after 45 seconds (again, this delay can be overriden using the <code>PASSWORD_STORE_CLIP_TIME</code> environment variable)).</p>
<p>You might also prefer not having to fire a terminal and type a command line in order to get a password you&rsquo;ll then copy to the website. In that case, you might be interested in using one of the few browser extensions available, such as <a href="https://gitlab.petton.fr/passwe/passwe-addon">passwe for Firefox and Chrome</a>, <a href="https://github.com/jvenant/passff#readme">PassFF for Firefox</a> or <a href="https://github.com/browserpass/browserpass#readme">Browserpass for Chrome</a>, which you can use to automatically fill in login forms using passwords from your store and their metadata. For what it&rsquo;s worth, I&rsquo;ve been using PassFF for quite a while now, and it works pretty well.</p>
<h2 id="synchronising-passwords">Synchronising passwords</h2>
<p>Because I always have more than one device, one thing I&rsquo;m really looking for in a password manager is its ability to synchronise with other devices easily. This is the reason I stopped using Keepass, because having to manually copy your database across all of your devices each time you add/remove/change an entry was really painful.</p>
<p>Where I become really picky is that I don&rsquo;t want to be stuck with a proprietary service&rsquo;s hosting such as LastPass&rsquo;s or Dashlane&rsquo;s. I want to control where I send my passwords, who can access them, etc.</p>
<p>Once again, pass choses simplicity, by implementing a great compatibility with git, letting it do all of the versionning and networking, which is, obviously, optional.</p>
<p>If you want to synchronise your own password store with a git repository, create an empty one somewhere (I personally did that on one of my own servers, but a GitHub/GitLab/Gitea/etc. repository will, of course, work as well), grab its URL and run</p>
<pre tabindex="0"><code>pass git init
pass git remote add origin REMOTE-URL
</code></pre><p>Where <code>REMOTE-URL</code> is the repo&rsquo;s URL.</p>
<p>This will initialise a local git repository at the root of you password store, and also create a commit containing all your store&rsquo;s content. Note that the <code>pass git</code> commands&rsquo; syntax follow the standard git commands&rsquo;. That is because <code>pass git</code> will actually run every git command you give it in the store, whatever your current working directory is. This means that you can basically use every git command you want, as long as you prefix them with <code>pass</code>, the commands will affect your password store and nothing else.</p>
<p>Now that the git repository is initialised in the password store, each time you&rsquo;ll create, remove or edit a password, pass will automatically create a commit for that, so you only have to run <code>pass git push</code> now and then to synchronise your local password store with your remote copy.</p>
<p>In my case, I like to have a copy of my password store on my phone, and to manage it using <a href="https://github.com/zeapo/Android-Password-Store">the Password Store Android app</a> (available on <a href="https://f-droid.org/repository/browse/?fdid=com.zeapo.pwdstore">F-Droid</a> and <a href="https://play.google.com/store/apps/details?id=com.zeapo.pwdstore">Google Play</a>), to which I just have to give the URL and credentials required to clone the repository, and the GPG key to use when trying to decrypt passwords, and I can instantly use my passwords on my smartphone.</p>
<p>Of course, since pass manages your passwords files and directories, you can have multiple sub-directories in your password store, each one of them having a different git remote. For example, most of my passwords are pushed to a remote repository on a server I own, except for one folder containing internal passwords we use at <a href="https://cozy.io">CozyCloud</a>, which are synchronised with an internal repository we have.</p>
<h2 id="to-infinity-and-beyond">To infinity and beyond</h2>
<p>Of course, I haven&rsquo;t described all the features pass has. This post only describes the few I personally use, along with some setup instructions, and doesn&rsquo;t really cover the various ways in which one can use it. Now it&rsquo;s yours to play with it! 😉</p>
<p>Thanks for reading through this post, and huge thanks to <a href="https://twitter.com/BrenAbolivier/status/995973756240777217">the amazing feedback and attention</a> you gave following my <a href="/enter-the-matrix">latest post on Matrix</a>, that&rsquo;s hugely appreciated. As always, if you wish to chat with me about this post, feel free to hit me up on <a href="https://twitter.com/BrenAbolivier">Twitter</a>, <a href="https://mastodon.social/@babolivier">Mastodon</a> or <a href="https://matrix.to/#/@brendan:abolivier.bzh">Matrix</a>, I&rsquo;d love to hear your thoughts about this one!</p>
<p>Also, the length and complexity of the said latest post brought some fatigue with it, which explains this one&rsquo;s lateness. Taking that into account, and given the fact that I&rsquo;m working really hard on the <a href="http://www.immersions.bzh">Trancendances presents immersion{s}</a> party in Brest that&rsquo;s taking place in less than two weeks, I don&rsquo;t think I&rsquo;ll be publishing any more post in the next couple of weeks (except maybe a very small one on a couple tools I discovered recently, but that&rsquo;s far from sure).</p>
<p>I&rsquo;ll see you after that, most likely in a bit less than three weeks, for a brand new blog post (of which I already know the topic, and it&rsquo;ll be a completely non-tech one, for a change!). See you then!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Enter the Matrix</title>
      <link>https://brendan.abolivier.bzh/enter-the-matrix/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Sun, 13 May 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/enter-the-matrix/</guid>
      <description>Matrix is a protocol for decentralised, federated and secure communications, created and maintained by New Vector, a company split between London, UK and Rennes, France. It&#39;s based on RESTful HTTP/JSON APIs, documented in open specifications, and is designed to be usable for anything that requires real-time-ish communications, from instant messaging to IoT. Let&#39;s see how it works and how to make a basic use of it.</description>
      <content:encoded><![CDATA[<p>As you might know if you&rsquo;ve been following me on <a href="https://twitter.com/BrenAbolivier">Twitter</a> for some time (or if you know me in real life), I&rsquo;m very fond of free software and decentralisation. I love free software because it matches the philosophy I want to live by, and decentralisation because it enlarges a user&rsquo;s freedom and individuality, and I find working on decentralised systems fascinating. Doing so forces one to change their way of designing a system entirely, since most of the Internet now consists of centralised services, which leads people to only learn how to design and engineer these.</p>
<p>Today I want to tell you about one of my favorite decentralised free software projects right now: <a href="https://matrix.org">Matrix</a>. Let&rsquo;s get things straight first, I&rsquo;m talking about neither the science-fiction franchise, nor the nightclub in Berlin. Matrix is a protocol for decentralised, federated and secure communications, created and maintained by New Vector, a company split between London, UK and Rennes, France (which I joined for an internship in London during the last summer). It&rsquo;s based on RESTful HTTP/JSON APIs, documented in <a href="https://matrix.org/docs/spec/">open specifications</a>, and is designed to be usable for anything that requires real-time-ish communications, from instant messaging to IoT. Some people are also experimenting with using Matrix for <a href="https://github.com/lukebarnard1/journal">blogs</a>, RSS reader, and other stuff that&rsquo;s quite far from what you&rsquo;d expect to see with such a project. Despite that, however, it&rsquo;s currently mainly used for instant messaging, especially through the <a href="https://riot.im">Riot</a> client (which is also developed by New Vector).</p>
<p>Matrix also distances itself from the &ldquo;yet another comms thing&rdquo; argument with its philosophy: it&rsquo;s not another standard for communications, but one that aims at binding all communications services together, using bridges, integration et al. For example, at <a href="https://cozy.io">CozyCloud</a>, we have a Matrix room that&rsquo;s bridged to our public IRC channel, meaning that every message sent to the Matrix room will get in the IRC channel as well, and vice-versa. I&rsquo;m even fiddling around in my free time to bridge this room with a channel on our <a href="https://about.mattermost.com/">Mattermost</a> instance, to create a Mattermost&lt;-&gt;Matrix&lt;-&gt;IRC situation and allow the community to interact with the team without members from the latter having to lose time firing up another chat client and looking at it in addition to internal communications.</p>
<p><img src="/images/enter-the-matrix/bridges.jpg" alt=""></p>
<p>There&rsquo;s also been quite some noise around Matrix lately with the French government <a href="https://matrix.org/blog/2018/04/26/matrix-and-riot-confirmed-as-the-basis-for-frances-secure-instant-messenger-app/">announcing its decision to go full Matrix</a> for their internal communications, using a fork of Riot they might also release as free software to the wide world in the future.</p>
<h2 id="under-the-hood">Under the hood</h2>
<p>It&rsquo;s great to introduce the topic, but I guess you were expecting more of a technical and practical post, so let&rsquo;s get into how Matrix works. Quick disclaimer, though: I won&rsquo;t go too much in depth here on how Matrix works (because if I do, the post would be quite too long and I&rsquo;d never get time to even finish it in a week), and will mainly focus on its core principles and how to use it in the most basic way.</p>
<p>As I mentioned before, Matrix is decentralised and federated. The decentralised bit means that you can run a Matrix server on your own server (quite like other services such as Mattermost), and the federated one means that two Matrix servers will be able to talk to one another. This means that, if someone (let&rsquo;s call her Alice) hosts her own Matrix server at <code>matrix.alice.tld</code>, and want to talk to a friend of her (let&rsquo;s call him Bob), who also hosts his own Matrix server at <code>matrix.bob.tld</code>, that&rsquo;s possible and <code>matrix.alice.tld</code> will know how to talk to <code>matrix.bob.tld</code> to forward Alice&rsquo;s message to Bob.</p>
<p><strong>Glossary break:</strong></p>
<ul>
<li>There are a few server types in the Matrix specifications. The homeservers (<em>HS</em>) are the servers that implement the client-server and federation APIs, i.e. the ones that allows actual messages to be sent from Alice to Bob. In my example, in which I was referring to homeservers as &ldquo;Matrix servers&rdquo;, <code>matrix.alice.tld</code> and <code>matrix.bob.tld</code> are homeservers. Among the other server types are the identity servers (<em>IS</em>) that allows one to host third-party identifiers (such as an email address or a phone number) so people can reach them using one of them, and application services (<em>AS</em>) which are mainly used to bridge an existing system to Matrix (but are not limited to that). In this post, I&rsquo;m only going to cover the basic use of homeservers, since knowledge about the other types isn&rsquo;t required to understand the bases of how Matrix works.</li>
<li>In the Matrix spec, both Alice and Bob are identified by a Matrix ID, which takes the form <code>@localpart:homeserver</code>. In our example, their Matrix IDs could respectively be <code>@Alice:matrix.alice.tld</code> and <code>@Bob:matrix.bob.tld</code>. Matrix IDs&rsquo; form actually follows a broader one, taken by any Matrix entity, which is <code>*localpart:homeserver</code>, where <code>*</code> is a &ldquo;sigil&rdquo; character which is used to identify the entity&rsquo;s type. Here, the sigil character <code>@</code> states that the entity is a Matrix ID.</li>
</ul>
<h2 id="three-roomies-on-three-servers">Three roomies on three servers</h2>
<p>Now that we have our two users talking with each other, let&rsquo;s take a look at how third user (let&rsquo;s call him Charlie), also hosting his own homeserver (at <code>matrix.charlie.tld</code>), can chat with both of them. This is done using a room, which can be defined as the Matrix equivalent of an IRC channel. As any entity in Matrix, the room has an ID which takes the general form with the <code>!</code> sigil character. However, although it contains a homerserver&rsquo;s name in its ID, and unlike a user ID, a room isn&rsquo;t bound to any homeserver. Actually, the homeserver in the room ID is the homeserver hosting the user that created the room.</p>
<p>Technically speaking, if Alice wants to send a message in the room where both Bob and Charlie are, she&rsquo;ll ask her homeserver to send a message in that room, which will look into its local database which homeservers are also part of that room (in our example, Bob&rsquo;s and Charlie&rsquo;s), and will send the message to each of them individually (and each of them will display the message to their users in the room, i.e. Bob&rsquo;s server will display it to Bob). Then, each homeserver will keep track of the message in their local database. This means two things:</p>
<ul>
<li>Every homeserver in a room keeps a content of the room&rsquo;s history.</li>
<li>If a homeserver in a room goes down for any reason, even if it&rsquo;s the homeserver which has its name in the room&rsquo;s ID, all of the other homeservers in the room can keep on talking with each other.</li>
</ul>
<p>Broadly speaking, a room can be schematised as follows:</p>
<p><img src="/images/enter-the-matrix/room-schema.png" alt=""></p>
<p>This image is a capture of the interactive explanation on how Matrix works named &ldquo;How does it work?&rdquo; on <a href="https://matrix.org/">Matrix&rsquo;s homepage</a>, which I&rsquo;d really recommand checking out. That&rsquo;s why the Matrix IDs and homeservers&rsquo; names aren&rsquo;t the same as in my example.</p>
<p>For what it&rsquo;s worth, I took a shortcut earlier since, in the Matrix spec, 1-to-1 chats are also rooms. So technically speaking, Alice and Bob were already in a room before Charlie wanted to chat with them.</p>
<p>It might also be worth noting that a room can have an unlimited number of aliases, acting as addresses for the room, which users can use to join it if it&rsquo;s public. Their syntax takes the general form we saw earlier, using <code>#</code> as the sigil character. This way, <code>!wbtZVAjTSFQzROqLrx:matrix.org</code> becomes <code>#cozy:matrix.org</code>, which, let&rsquo;s be honest, is quite easier to read and remember. As with a room&rsquo;s ID, its <code>homeserver</code> part is the homeserver hosting the user who created the alias, which means that I can create <code>#cozycloud:matrix.trancendances.fr</code> if I have enough power level, as I&rsquo;m using this homeserver.</p>
<p>As I quickly hinted at, a room can be either public or private. Public rooms can be joined by anyone knowing one of the room&rsquo;s alias (or getting it from the homeserver&rsquo;s public rooms directory if it&rsquo;s published there), and private rooms work on an invite-only basis. In both cases, if the homeserver doesn&rsquo;t already have a user in the room, it will ask another homeserver to make the join happen (either the homeserver alias which name is in the <code>homeserver</code> part of the alias for a public room, or the homeserver the invite is originating from for a private room).</p>
<h2 id="events-events-everywhere">Events, events everywhere</h2>
<p>Now that we know what a room is, let&rsquo;s talk about what&rsquo;s passing inside of one. Earlier, I&rsquo;ve been talking about messages, which are actually called &ldquo;events&rdquo;. Technically speaking, a Matrix event is a JSON object that&rsquo;s sent in a room and dispatched to all other members of the room. It, of course, has an ID that&rsquo;s generated by the homeserver hosting the user who sent the message, taking the general form we saw earlier and the <code>$</code> sigil character. This JSON has metadata, such as a class name to identify different event types, an author, a creation timestamp, etc. It basically looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;origin_server_ts&#34;</span><span class="p">:</span> <span class="mi">1526072700313</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;sender&#34;</span><span class="p">:</span> <span class="s2">&#34;@Alice:matrix.alice.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;event_id&#34;</span><span class="p">:</span> <span class="s2">&#34;$1526072700393WQoZb:matrix.alice.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;unsigned&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">97</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;transaction_id&#34;</span><span class="p">:</span> <span class="s2">&#34;m1526072700255.17&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;body&#34;</span><span class="p">:</span> <span class="s2">&#34;Hello Bob and Charlie! Welcome to my room :-)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;msgtype&#34;</span><span class="p">:</span> <span class="s2">&#34;m.text&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;m.room.message&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;room_id&#34;</span><span class="p">:</span> <span class="s2">&#34;!TCnDZIwFBeQyBCciFD:matrix.alice.tld&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>The example above is an event sent from Alice to Bob and Charlie in the room they&rsquo;re all in. It&rsquo;s a message, as hinted at by the <code>m.room.message</code> class name in the <code>type</code> property. The <code>content</code> property, which must be an object, contains the event&rsquo;s actual content. In this case, we can see the message is text, and the text itself. This precision is needed because <code>m.room.message</code> can be a text, but also an image, a video, a notice, etc. as mentioned in <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#m-room-message">the spec</a>.</p>
<p>The unsigned property here only means the data in it mustn&rsquo;t be taken into account when computing and verifying the cryptographic signature used by homeserver to pass the event to another homeserver.</p>
<p>The Matrix spec defines three kind of events that can pass through a room:</p>
<ul>
<li>Timeline events, such as messages, which form the room&rsquo;s timeline that&rsquo;s shared between all homeservers in the room.</li>
<li>State events, that contain an additional <code>state_key</code> property, and form the current state of the room. They can describe room creation (<code>m.room.create</code>), topic edition (<code>m.room.topic</code>), join rules (i.e. either invite-only or public, <code>m.room.join_rules</code>), membership update (i.e. join, leave, invite or ban, <code>m.room.member</code> with the Matrix ID of the user whose membership is being updated as the <code>state_key</code>). Just like timeline events, they&rsquo;re part of the room&rsquo;s timeline, but unlike them, the latest event for a <code>{type, state_key}</code> duo is easily retrievable, as well as the room&rsquo;s current state of the room, which is actually a JSON array contaning the latest events for all <code>{type, state_key}</code> duos. The Matrix APIs also allows one to easily retrieve the full state the room was at when a given timeline message was propagated through the room, and each state event refers to its parent.</li>
<li>Euphemeral events, which aren&rsquo;t included in the room&rsquo;s timeline, and are used to propagate information that doesn&rsquo;t last in time, such as typing notification (&quot;[&hellip;] is typing&hellip;&quot;).</li>
</ul>
<p>Now, one of the things I really like about Matrix is that, besides the base event structure, you can technically put whatever you want into an event. There&rsquo;s no constraint on its class name (except it can&rsquo;t start with <code>m.</code>, which is a namespace reserved for events defined in the spec), nor on its content, so you&rsquo;re free to create your own events as you see fit, whether they are timeline events, state events or both (I&rsquo;m not sure about euphemeral events, though). That&rsquo;s how you can create whole systems using only Matrix as the backend.</p>
<p>Matrix events can also be redacted. This is the equivalent of a deletion, except the event isn&rsquo;t actually deleted but stripped from its content so it doesn&rsquo;t mess with the room&rsquo;s timeline. The redacted event is then dispatched to every homeserver in the room so they can redact their local copy of the event as well. Regarding editing an event&rsquo;s content, it&rsquo;s not possible yet, but it&rsquo;s a highly requested feature and should be available in the not so distant future.</p>
<h2 id="a-very-basic-client">A very basic client</h2>
<p>Now I guess you&rsquo;re wondering how you can use Matrix for your project, because learning the core principles is great but that doesn&rsquo;t explain how to use the whole thing.</p>
<p>In the following steps, I&rsquo;ll assume a few things:</p>
<ul>
<li>The homeserver you&rsquo;re working with is <code>matrix.project.tld</code>, and its client-server API is available on port 443 through HTTPS.</li>
<li>Your user is named <code>Alice</code>. Note that you must change this value for real life tests, because the Matrix ID <code>@Alice:matrix.org</code> is already taken.</li>
<li>Your user&rsquo;s password is <code>1L0v3M4tr!x</code>.</li>
</ul>
<p>Note that I&rsquo;ll only cover some basic use of the client-server spec. If you want to go further, you should have a look at the <a href="https://matrix.org/docs/spec/">full spec</a> or ask any question in the <a href="https://matrix.to/#/#matrix-dev:matrix.org">#matrix-dev</a> room. I also won&rsquo;t cover homeserver setup, here (though I might do just that in a future post). My goal here is mainly to give you a look at how the client-server APIs globally works rather tha creating a whole shiny app which would take too long for a single blog post.</p>
<p>It might also be worth noting that each Matrix API endpoint I&rsquo;ll name in the rest of this post is a clickable link to the related section of the Matrix spec, which you can follow if you want more complete documentation on a specific endpoint.</p>
<h3 id="registering">Registering</h3>
<p>Of course, your user doesn&rsquo;t exist yet, so let&rsquo;s register it against the homeserver.</p>
<p>The endpoint for registration is <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-register"><code>/_matrix/client/r0/register</code></a>, which you should request using a <code>POST</code> HTTP request. In our example, the request&rsquo;s full URL is <code>https://matrix.project.tld/_matrix/client/r0/register</code>.</p>
<p>Note that every endpoint in the Matrix spec always starts with <code>/_matrix/</code>.</p>
<p>The request body is a JSON which takes the following form:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;username&#34;</span><span class="p">:</span> <span class="s2">&#34;Alice&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;password&#34;</span><span class="p">:</span> <span class="s2">&#34;1L0v3M4tr!x&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Here, the <code>username</code> and <code>password</code> properties are exactly what you think it is. The Matrix ID generated for a new user contains what&rsquo;s provided in the <code>username</code> property as the <code>localpart</code>.</p>
<p>Fire this request. You&rsquo;ll now get a <code>401</code> status code along with some JSON, which looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;flows&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;stages&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;m.login.dummy&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;stages&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;m.login.email.identity&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;params&#34;</span><span class="p">:</span> <span class="p">{},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;session&#34;</span><span class="p">:</span> <span class="s2">&#34;HrvSksPaKpglatvIqJHVEfkd&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Now, this enpoint uses a part of the spec called the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#user-interactive-authentication-api">User-Interactive Authentication API</a>. This means that authentication can be seen as flows of consecutive stages. That&rsquo;s exactly what we have here: two flows, each containing one stage. This example is a very simple one, but it can get quite more complex, such as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;flows&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;stages&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;m.login.recaptcha&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;stages&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;m.login.email.identity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;m.login.recaptcha&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;params&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;m.login.recaptcha&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;public_key&#34;</span><span class="p">:</span> <span class="s2">&#34;6Le31_kSAAAAAK-54VKccKamtr-MFA_3WS1d_fGV&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;session&#34;</span><span class="p">:</span> <span class="s2">&#34;qxATPqBPdTsaMBmOPkxZngXR&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Here we can see two flows, one with a single stage, the other one with two stages. Note that there&rsquo;s also a parameter in the <code>params</code> object, to be used with the <code>m.login.recaptcha</code> flow.</p>
<p>Because I want to keep it as simple as possible here, let&rsquo;s get back at our initial simple example, and use the first one-stage flow. The only stage in there is <code>m.login.dummy</code>, which describes a stage that will success everytime you send it a correct JSON object.</p>
<p>To register against this stage, we&rsquo;ll only add a few lines to our initial request&rsquo;s JSON:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;auth&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;m.login.dummy&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;session&#34;</span><span class="p">:</span> <span class="s2">&#34;HrvSksPaKpglatvIqJHVEfkd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;username&#34;</span><span class="p">:</span> <span class="s2">&#34;Alice&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;password&#34;</span><span class="p">:</span> <span class="s2">&#34;1L0v3M4tr!x&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Note that the value to the <code>session</code> property in the newly added <code>auth</code> object is the value from <code>session</code> taken from the homeserver&rsquo;s response to our intial request. This <code>auth</code> object will tell the homeserver that this request is a follow-up to the initial request, using the stage <code>m.login.dummy</code>. The homeserver will automatically recognise the flow we&rsquo;re using, and will succeed (because we use <code>m.login.dummy</code>), returning this JSON along with a <code>200</code> status code:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;access_token&#34;</span><span class="p">:</span> <span class="s2">&#34;olic0yeVa1pore2Kie4Wohsh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;device_id&#34;</span><span class="p">:</span> <span class="s2">&#34;FOZLAWNKLD&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;home_server&#34;</span><span class="p">:</span> <span class="s2">&#34;matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;user_id&#34;</span><span class="p">:</span> <span class="s2">&#34;@Alice:matrix.project.tld&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Let&rsquo;s see what we have here:</p>
<ul>
<li>The <code>home_server</code> property contains the address of the homeserver you&rsquo;ve registered on. This can feel like a duplicate, but the Matrix spec allows for a homeserver&rsquo;s name to differ from its address, so here&rsquo;s why it mentions it.</li>
<li>The <code>user_id</code> property contains the newly generated Matrix ID for your user.</li>
<li>The <code>device_id</code> property contains the ID for the device you&rsquo;ve registered with. A device is bound to an access token and E2E encryption keys (which I&rsquo;m not covering in this post).</li>
<li>The <code>access_token</code> property contains the token you&rsquo;ll use to authenticate all your requests to the Matrix client-server APIs. It&rsquo;s usually much longer than the one shown in the example, I&rsquo;ve shortened it for readability&rsquo;s sake.</li>
</ul>
<p>Registering an user instantly logs it in, so you don&rsquo;t have to do it right now. If, for any reason, you get logged out, you can log back in using the endpoint documented <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-login">here</a>.</p>
<h3 id="creating-our-first-room">Creating our first room</h3>
<p>Now that we have an authenticated user on a homeserver, let&rsquo;s create a room. This is done by sending a <code>POST</code> request to the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-createroom"><code>/_matrix/client/r0/createRoom</code></a> endpoint. In our example, the request&rsquo;s full URL is <code>https://matrix.project.tld/_matrix/client/r0/createRoom?access_token=olic0yeVa1pore2Kie4Wohsh</code>. Note the <code>access_token</code> query parameter, which must contain the access token the homeserver previously gave us.</p>
<p>There are a few JSON parameters available which I won&rsquo;t cover here because none of them are required to perform the request. So let&rsquo;s send the request with an empty object (<code>{}</code>) as its body.</p>
<p>Before responding, the homeserver will create the room, fire a few state events in it (such as the initial <code>m.room.create</code> state event or a join event for your user). It should then respond with a <code>200</code> status code and a JSON body looking like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;room_id&#34;</span><span class="p">:</span> <span class="s2">&#34;!RtZiWTovChPysCUIgn:matrix.project.tld&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Here you are, you have created and joined your very first room! As you might have guessed, the value for the <code>room_id</code> property is the ID of the newly created room.</p>
<h3 id="messing-with-the-rooms-state">Messing with the room&rsquo;s state</h3>
<p>Browsing the room&rsquo;s state is completely useless at this stage, but let&rsquo;s do it anyway. Fetching the whole room state, for example, is as easy as a simple <code>GET</code> request on the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-r0-rooms-roomid-state"><code>/_matrix/client/r0/rooms/{roomId}/state</code></a> endpoint, where <code>{roomId}</code> is the room&rsquo;s ID. If you&rsquo;re following these steps using curl requests in bash, you might want to replace the exclamation mark (<code>!</code>) in the room&rsquo;s ID with its URL-encoded variant (<code>%21</code>). Don&rsquo;t forget to append your access token to the full URL as shown above.</p>
<p>The request should return a JSON array containing state events such as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">654742</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;join_rule&#34;</span><span class="p">:</span> <span class="s2">&#34;public&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;event_id&#34;</span><span class="p">:</span> <span class="s2">&#34;$1526078716401exXBQ:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;origin_server_ts&#34;</span><span class="p">:</span> <span class="mi">1526078716874</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;room_id&#34;</span><span class="p">:</span> <span class="s2">&#34;!RtZiWTovChPysCUIgn:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;sender&#34;</span><span class="p">:</span> <span class="s2">&#34;@Alice:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;state_key&#34;</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;m.room.join_rules&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;unsigned&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">654742</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Now let&rsquo;s try to send our own state event in the room, shall we? I order to do that, you&rsquo;ll need to send a <code>PUT</code> request to the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype"><code>/_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}</code></a> endpoint, repacing the room&rsquo;s ID, the event&rsquo;s type and its state key with the right values. Note that if your state key is an empty string, you can just omit it from the URL. Again, don&rsquo;t forget to append your access token!</p>
<p>The body for our request is the event&rsquo;s content object.</p>
<p>Let&rsquo;s create a <code>tld.project.foo</code> event with <code>bar</code> as its state key, and <code>{&quot;baz&quot;: &quot;qux&quot;}</code> as its content. To achieve that, let&rsquo;s send a <code>PUT</code> request to <code>/_matrix/client/r0/rooms/!RtZiWTovChPysCUIgn:matrix.project.tld/state/tld.project.foo/bar?access_token=olic0yeVa1pore2Kie4Wohsh</code> (from which I&rsquo;ve stripped the protocol scheme and FQDN so it doesn&rsquo;t appear too long in the post) with the fillowing content:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;baz&#34;</span><span class="p">:</span> <span class="s2">&#34;qux&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>The homeserver then responds with an object only containing an <code>event_id</code> property, which contains the ID of the newly created state event.</p>
<p>If we retry the request we previously made to retrieve the whole room state, we can now see our event:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">58357</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;baz&#34;</span><span class="p">:</span> <span class="s2">&#34;qux&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;event_id&#34;</span><span class="p">:</span> <span class="s2">&#34;$1526080218403sbpku:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;origin_server_ts&#34;</span><span class="p">:</span> <span class="mi">1526080218639</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;room_id&#34;</span><span class="p">:</span> <span class="s2">&#34;!RtZiWTovChPysCUIgn:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;sender&#34;</span><span class="p">:</span> <span class="s2">&#34;@Alice:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;state_key&#34;</span><span class="p">:</span> <span class="s2">&#34;bar&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;tld.project.foo&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;unsigned&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">58357</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Note that sending an update of a state event is done the same way as sending a new state event with the same class name and the same state key.</p>
<h3 id="sending-actual-messages">Sending actual messages</h3>
<p>Sending timeline events is almost the same thing as sending state events, except it&rsquo;s done through the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid"><code>/_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}</code></a> endpoint, and it uses one parameter we haven&rsquo;t seen yet: the <code>txnId</code>, <em>aka</em> transaction ID. That&rsquo;s simply a unique ID allowing identification for this specific request among all requests for the same access token. You&rsquo;re free to place whatever you want here, as long as you don&rsquo;t use the same value twice with the same access token.</p>
<p>Regarding the request&rsquo;s body, once again, it&rsquo;s the event&rsquo;s content.</p>
<p>Retrieving timeline events, though, is a bit more complicated and is done using a <code>GET</code> request on the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-r0-sync"><code>/_matrix/client/r0/sync</code></a> endpoint. Where it gets tricky is in the fact that this endpoint isn&rsquo;t specific to a room, so it returns every event received in any room you&rsquo;re in, along with some presence event, invites, etc.</p>
<p>Once you&rsquo;ve done such a request (again, with your access token appended to it), you can locate timeline events from your room in the JSON it responds with by looking at the <code>rooms</code> object, which contains an object named <code>join</code> which contains one object for each room you&rsquo;re in. Locate the <code>!RtZiWTovChPysCUIgn:matrix.project.tld</code> room (the one we&rsquo;ve created earlier), and in the corresponding object you&rsquo;ll see the state, timeline and euphemeral events for this room.</p>
<h3 id="inviting-a-folk">Inviting a folk</h3>
<p>So far, Alice has registered on the homeserver and created her room, but she feels quite alone, to be honest. Let&rsquo;s cheer her up by inviting Bob in there.</p>
<p>Inviting someone into a room is also quite simple, and only requires a <code>POST</code> request on the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-rooms-roomid-invite"><code>/_matrix/client/r0/rooms/{roomId}/invite</code></a> endpoint. The request&rsquo;s body must contain the invited Matrix ID as such:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;user_id&#34;</span><span class="p">:</span> <span class="s2">&#34;@Bob:matrix.bob.tld&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Note that the request is the same if Bob has registered on the same server as Alice.</p>
<p>If all went well, the homeserver should respond with a <code>200</code> status code and an empty JSON object (<code>{}</code>) as its body.</p>
<p>In the next request on the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-r0-sync"><code>/_matrix/client/r0/sync</code></a> he&rsquo;ll made, Bob will now see an <code>invite</code> object inside the <code>rooms</code> one contaning the invite Alice sent him, containing a few events including the invite event:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;invite&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;!RtZiWTovChPysCUIgn:matrix.project.tld&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;invite_state&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;events&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;sender&#34;</span><span class="p">:</span> <span class="s2">&#34;@Alice:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;m.room.name&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;state_key&#34;</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;My very cool room&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="p">},</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;sender&#34;</span><span class="p">:</span> <span class="s2">&#34;@Alice:matrix.project.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;m.room.member&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;state_key&#34;</span><span class="p">:</span> <span class="s2">&#34;@Bob:matrix.bob.tld&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&#34;membership&#34;</span><span class="p">:</span> <span class="s2">&#34;invite&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
<p>Now Bob will be able to join the room by sending a simple <code>POST</code> request to the <a href="https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-rooms-roomid-join"><code>/_matrix/client/r0/rooms/{roomId}/join</code></a> endpoint.</p>
<h2 id="alice-meets-bob">Alice meets Bob</h2>
<p>So here we are, with a fresh room where Alice and Bob are able to interact with one another, with everything done using HTTP requests that you could do with your terminal using curl. Of course, you don&rsquo;t always have to do it that manually, and there are Matrix SDKs for various languages and platforms, including <a href="https://matrix.org/docs/projects/sdk/matrix.org-js-sdk.html">JavaScript</a>, <a href="https://matrix.org/docs/projects/sdk/goMatrix.html">Go</a>, <a href="https://matrix.org/docs/projects/sdk/goMatrix.html">Python</a>, <a href="https://matrix.org/docs/projects/sdk/matrix.org-android-sdk.html">Android</a>, <a href="https://matrix.org/docs/projects/sdk/matrix.org-ios-sdk.html">iOS</a>, and a lot more. The full list is available <a href="https://matrix.org/docs/projects/try-matrix-now.html#client-sdks">right here</a>.</p>
<p>If you want to dive a bit deeper into the Matrix APIs, I&rsquo;d advise you to have a look at the <a href="https://matrix.org/docs/spec">spec</a> (even though it still needs a lot of work) and what the community has done with it on the <a href="https://matrix.org/docs/projects/try-matrix-now.html">Try Matrix Now!</a> page on Matrix&rsquo;s website.</p>
<p>I hope you found this journey into Matrix&rsquo;s APIs as interesting as I did when I first heard of the project. Matrix is definitely something I&rsquo;ll keep playing with for a while, and might have some big news related to some Matrix-related projects I&rsquo;m working on to share here in the coming months.</p>
<p>As always, I&rsquo;d like to thank <a href="https://twitter.com/CromFR">Thibaut</a> for proofreading this post and giving me some useful early feedback on it. If you want to share your feedback on this post with me too, don&rsquo;t hesitate to do so, either via <a href="https://twitter.com/BrenAbolivier">Twitter</a> or through Matrix, my own Matrix ID being <a href="https://matrix.to/#/@brendan:abolivier.bzh">@brendan:abolivier.bzh</a>!</p>
<p>See you next week for a new post 🙂</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Centralising logs with rsyslog and parsing them with Graylog extractors</title>
      <link>https://brendan.abolivier.bzh/logs-rsyslog-graylog/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Sat, 05 May 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/logs-rsyslog-graylog/</guid>
      <description>Logs are really useful for a lot of things, from investigating issues to monitoring stuff that can&#39;t be watched efficiently by other monitoring tools. When it comes to storing them, a lot of solutions are available, depending on what you need. At CozyCloud, our main need was to be able to store them somewhere safe, and process them. Let me walk you through how we did it.</description>
      <content:encoded><![CDATA[<p><a href="/zabbix-proxy-encryption">Once</a> <a href="/grafana-dashboards-manager">again</a>, we&rsquo;re up for a monitoring-related post. This time, let&rsquo;s take a look at logs. Logs are really useful for a lot of things, from investigating issues to monitoring stuff that can&rsquo;t be watched efficiently by other monitoring tools (such as detailled traffic stats), and some of us even live in a country where it&rsquo;s illegal to trash logs that were emitted before a given time limit.</p>
<p>When it comes to storing them, a lot of solutions are available, depending on what you need. At <a href="https://cozy.io">CozyCloud</a>, our main need was to be able to store them somewhere safe, preferably outside of our infrastructure.</p>
<h2 id="earth-lend-me-your-logs-says-syslog-dev">Earth, lend me your logs! says syslog-dev</h2>
<p>We started by centralising logs using <a href="https://www.rsyslog.com/">rsyslog</a>, an open logs management system that&rsquo;s described by its creators as a &ldquo;swiss army knife of logging&rdquo;. One of its features I&rsquo;ll be writing the most about in this post is UDP and TCP forwarding. Using that, we (well, my colleagues, since I wasn&rsquo;t there at that time) created a host for each of our environments which task would be to keep a copy of every log emitted from every host and by every application in the given environment.</p>
<p>I&rsquo;ll take a quick break here to explain what I mean by &ldquo;environment&rdquo; in case it&rsquo;s not clear: our infrastructure&rsquo;s architecture is replicated 4 times in 4 different environments, each with a different purpose: <code>dev</code> (dedicated to experimentation and prototyping, aka our playground), <code>int</code> (dedicated to running the developers&rsquo; integration tests, aka their playground), <code>stg</code> (dedicated to battle-testing features before we push them to the production) and <code>prod</code> (I&rsquo;ll let you guess what&rsquo;s its purpose). End of the break.</p>
<p>On each host of the whole infrastructure, we added this line to rsyslog&rsquo;s configuration:</p>
<pre tabindex="0"><code>*.* @CENTRAL_LOG_HOST:514
</code></pre><p>Here, <code>CENTRAL_LOG_HOST</code> is the IP of the host that is centralising the logs for the given environment, in the infrastructure&rsquo;s local private network. What it does is to tell rsyslog to forward every log it gets to the given host using UDP on port 514, which is rsyslog&rsquo;s default port for UDP forwarding.</p>
<p>Then a colleague set up a <a href="https://www.graylog.org/">Graylog</a> instance to try and work out the processing part. He did all the set up and plugged in the <code>dev</code> environment&rsquo;s logs output before getting drowned under a lot of higher-priority tasks, and since I was just finishing setting up a whole monitoring solution we figured I&rsquo;d take over from there.</p>
<h2 id="lets-plug-things">Let&rsquo;s plug things</h2>
<p>Of course, the first thing to do on your own setup is to install and configure Graylog, along with its main dependencies (which are MongoDB and Elasticsearch). The Graylog documentation covers this quite nicely with a <a href="http://docs.graylog.org/en/latest/pages/installation/operating_system_packages.html">general documentation</a> and a few <a href="http://docs.graylog.org/en/latest/pages/installation/operating_system_packages.html#step-by-step-guides">step-by-step guides</a> offering some useful details on installation and configuration. Once your Graylog instance is set up, open your browser on whatever you set as the Web UI&rsquo;s URI. In most cases, it will look like <code>http://YOUR_SERVER:9000</code>.</p>
<p>Once you&rsquo;re authenticated, you&rsquo;ll need to add an input source. Click on &ldquo;Systems&rdquo; in the navigation bar, then &ldquo;Inputs&rdquo; in the dropdown menu that just appeared. You&rsquo;ll then be taken to a page from which you&rsquo;ll be able to configure Graylog&rsquo;s inputs.</p>
<p><img src="/images/logs-rsyslog-graylog/graylog-add-input.png" alt=""></p>
<p>Click on the &ldquo;Select Input&rdquo; dropdown, look for &ldquo;Syslog TCP&rdquo; and click &ldquo;Launch new input&rdquo;. Filling the form that appears then is done accordingly with your needs, however you might want to check &ldquo;Store full message&rdquo; at the very bottom. Graylog understands the Syslog protocol&rsquo;s syntax, and the message it stores is a stripped version of what (r)syslog actually sent. Because you might want to use some of the stripped out parts, it can be wise to tell Graylog to store the full message somewhere before processing it.</p>
<p>You&rsquo;ll then have to configure rsyslog to send the logs it gets to Graylog. Because we centralise all of our logs, we only need to configure one rsyslog daemon, by adding this line to its configuration:</p>
<pre tabindex="0"><code>*.* @@GRAYLOG_HOST:PORT;RSYSLOG_SyslogProtocol23Format
</code></pre><p>Here, the host is your Graylog server&rsquo;s address and the port is the one you previously configured while setting up your Syslog TCP input.</p>
<p>There&rsquo;s two things to notice here. First, there are two <code>@</code> symbols before Graylog&rsquo;s host name, which means the logs are going to be forwarded to Graylog using TCP. We previously saw a forwarding configuration line with a single <code>@</code> sign, which means rsyslog will use UDP. The second thing to notice is the <code>;RSYSLOG_SyslogProtocol23Format</code> part. The semicolon (<code>;</code>) tells rsyslog that this is a parameter defining how to send logs, and <code>RSYSLOG_SyslogProtocol23Format</code> is a built-in parameter telling rsyslog to send logs using the Syslog protocol as defined in <a href="https://tools.ietf.org/html/rfc5424">RFC 5424</a>.</p>
<p>Restart rsyslog to apply the new configuration, and check it works by generating some logs while running</p>
<pre tabindex="0"><code>tcpdump -Xn host GRAYLOG_HOST and port PORT
</code></pre><p>with the same values for <code>GRAYLOG_HOST</code> and <code>PORT</code> as in the bit of configuration below. This <code>tcpdump</code> command line can be called from either the Graylog host or the rsyslog host. If those are the same, remember to add <code>-i lo</code> between <code>tcpdump</code> and <code>-Xn</code> to watch the loopback interface (in this case you can also remove the <code>host GRAYLOG_HOST and</code> part of the command line).</p>
<p>Once you&rsquo;ve created your input, you might want to add <a href="http://docs.graylog.org/en/stable/pages/streams.html">streams</a>. I&rsquo;m not covering this part in this post as I didn&rsquo;t get to play with these, and there&rsquo;s a default stream where all messages go anyway.</p>
<p>Now that logs are coming in, let&rsquo;s process them!</p>
<h2 id="stranger-in-a-strange-land">Stranger in a Strange Land</h2>
<p>There are several ways to configure logs processing in Graylog. One of them is <a href="http://docs.graylog.org/en/stable/pages/pipelines.html">pipelines</a>, which are, as you can guess by the name, processing pipelines you can plug to a stream. I played around with them a bit, but gave them up quite quickly because I couldn&rsquo;t figure out how to make them work properly, and I was getting some weird behaviour with their rules editor.</p>
<p>Another way to process logs is to set up <a href="http://docs.graylog.org/en/stable/pages/extractors.html">extractors</a>. A Graylog extractor is a set of rules which defines how logs coming from a given input will be processed, using one of many possible processing mechanisms, from JSON parsin to plain copy, including splitting, substring, regular expressions or Grok patterns.</p>
<p>Now let&rsquo;s talk about the latter in case it doesn&rsquo;t ring a bell, because I&rsquo;ll be talking a lot about this type of patterns in the rest of the post. Grok patterns are kind of an overlay for regular expressions, addressing the issue of their complexity. I&rsquo;m sure that, just like me, you don&rsquo;t find the thought of parsing 300-character long log entries using a custom format with standard regular expressions very exciting.</p>
<p>Grok patterns take the form of a string (looking like <code>%{PATTERN}</code>) you include in your parsing instruction that will correspond to either a plain regular expression, or a concatenation between other Grok patterns. For example, <code>%{INT}</code>, a common pattern matching any positive or negative integer, corresponds to the regular expression <code>(?:[+-]?(?:[0-9]+))</code>. Another pattern, included in Graylog&rsquo;s base patterns, is <code>%{DATESTAMP}</code> which is defined as <code>%{DATE}[- ]%{TIME}</code>, which is a concatenation between a regular expression and two Grok patterns. These patterns are very useful as they make your parsing instructions way easier to read than if they were only made of common regular expressions.</p>
<p>Graylog, like other pieces of software, allow you to describe a log entry as a concatenation of patterns and regular expressions. For example, here&rsquo;s the line we&rsquo;re using to parse <a href="https://couchdb.apache.org">Apache CouchDB</a>&rsquo;s&rsquo; logs:</p>
<pre tabindex="0"><code>%{DATA} %{NOTSPACE:couchdb_user} %{NOTSPACE:couchdb_method} %{NOTSPACE:couchdb_path} %{NUMBER:couchdb_status_code} %{NOTSPACE} %{NUMBER}
</code></pre><p>Note the colons inside the patterns&rsquo; brackets followed by lower case text: these are named captures, which means that what&rsquo;s captured by the pattern with be labelled with this text. In this case, it will create a new field in the log entry&rsquo;s Elasticsearch document (since Graylog uses Elasticsearch as its storage backend) with this label as the field&rsquo;s name. We can even tell Graylog to ignore all un-named captures when creating an extractor.</p>
<h2 id="dissecting-logs">Dissecting logs</h2>
<p>The easiest way to create a new extractor is to browse to Graylog&rsquo;s search, which can be done by clicking to the related button in the navigation bar. There you&rsquo;ll see a list of all messages sent from your input.</p>
<p><img src="/images/logs-rsyslog-graylog/graylog-search.png" alt=""></p>
<p>Find a log entry you want to be processed, and click on it. If you have more than one input set up, you might want to double check that the entry come from the input you want to plug the extractor on, in order to avoid plugging it to the wrong input. Now locate the field you want to process (here we&rsquo;ll use the <code>full_message</code> field, which is only available if &ldquo;Store full message&rdquo; is checked in the input&rsquo;s configuration). Click on the down arrow icon on its right.</p>
<p><img src="/images/logs-rsyslog-graylog/graylog-message.png" alt=""></p>
<p>A dropdown menu appears, move your cursor over &ldquo;Create extractor for field&hellip;&rdquo;. Because that&rsquo;s close to being the only extractor I got to use while working with Graylog, I&rsquo;ll only cover extractors using Grok patterns here, so select &ldquo;Grok pattern&rdquo;.</p>
<p>Clicking on it will take you to the extractor creation page, using the entry you previously selected as an example to test the extractor against.</p>
<p><img src="/images/logs-rsyslog-graylog/graylog-create-extractor.png" alt=""></p>
<p>You can then enter your Grok pattern in the &ldquo;Grok pattern&rdquo; field. You can even ask Graylog to only extract named captures only by checking the related checkbox.</p>
<p>Now you might think of an issue with this setup: your extractor will be applied against all incoming messages from this input. To tackle that issue, let&rsquo;s look at two points. First, extractors fail silently, meaning that if a log entry doesn&rsquo;t match an extractor&rsquo;s pattern, Graylog will just stop trying this extractor against this specific entry.</p>
<p>Making sure only entries from a specific program and/or host match is the reason we&rsquo;re creating the exporter for the <code>full_message</code> field, since it contains the original host and the program which emitted the entry at the beginning of the message. These pieces of info are, of course, parsed as soon as the log reaches Graylog and saved in appropriate fields, but Graylog doesn&rsquo;t allow an exporter to define execution conditions based on other field&rsquo;s values.</p>
<p>Using values contained in the <code>full_message</code> field, the Grok pattern parsing CouchDB log entries I used as an example above now looks like:</p>
<pre tabindex="0"><code>%{COUCHDBHOST} couchdb %{DATA} %{NOTSPACE:couchdb_user} %{NOTSPACE:couchdb_method} %{NOTSPACE:couchdb_path} %{NUMBER:couchdb_status_code} %{NOTSPACE} %{NUMBER}
</code></pre><p>Now that&rsquo;s a first step, but it still means every log entry will be tested against the pattern, which is a waste of CPU resources. That&rsquo;s where my second point comes in.</p>
<p>Graylog allows you to set some basic conditions that will define whether a log entry must be tested against the pattern. You can check whether the field contains a given string, or matches a given regular expression which can be very basic. I chose the string check because of lack of time, but I&rsquo;d recommand checking against a basic regular expression to better match the log entries you want to target.</p>
<p>One last thing to chose is the &ldquo;Extraction strategy&rdquo;, which I usually set to &ldquo;Copy&rdquo; to better comply with the <em>WORM</em> (Write Once, Read Many) philosophy. You must also set a name to the extractor so you can easily identify it in the list of existing extractors.</p>
<p>Now your extractor should look like this:</p>
<p><img src="/images/logs-rsyslog-graylog/graylog-config-extractor.png" alt=""></p>
<p>All that&rsquo;s left to do is to click &ldquo;Create extractor&rdquo; and that&rsquo;s it! Your extractor is up and running!</p>
<p>You might want to check if it runs correctly by going back to the &ldquo;Search&rdquo; page and selecting a log entry the extractor should target. If the extractor ran correctly, you should see your new fields added to the entry. Note that an extractor only run against entries received after its creation.</p>
<p>If you want to edit an extractor, click on the &ldquo;System&rdquo; link in the navigation bar, the select &ldquo;Inputs&rdquo; in the dropdown menu that appears then. Locate the input your extractor is plugged to, and click on the blue &ldquo;Manage extractors&rdquo; button next to it. You&rsquo;ll then be taken to a list of existing extractors for this input:</p>
<p><img src="/images/logs-rsyslog-graylog/graylog-extractor-list.png" alt=""></p>
<p>Click &ldquo;Edit&rdquo; next to the extractor you want to edit and you&rsquo;ll be taken to a screen very similar to the creation screen, where you&rsquo;ll be able to edit your extractor.</p>
<h2 id="in-the-next-episode">In the next episode</h2>
<p>Now, we have a copy of all of our logs at the same place, and process them at a single location in our infrastructure, which is great but creates a sort-of <em>SPOF</em> (single point of failure). Well, only partial, since the logs are only copied from their original hosts, so if something happen to one of these locations, &ldquo;only&rdquo; the processing can be permanently impacted. Anyway, it doesn&rsquo;t address one of our needs, which is to do all this outside of our infrastructure.</p>
<p>But this is a story for another week, since this post is already quite long. Next time I&rsquo;ll tell you about logs, we&rsquo;ll see how we moved our logs processing and forwarding to a remote service, without losing all the work we did with rsyslog and Graylog. This won&rsquo;t be next week, though, because I already have next week&rsquo;s topic, and it&rsquo;s not even monitoring-related!</p>
<p>Anyway, thanks for bearing with me as I walked you through an interesting (I hope) journey into logs processing. If you&rsquo;re note aware of it, this post was part of my <a href="/one-post-a-week/">One post a week</a> series, in which I challenge myself to write each week a whole blog post in order for me to re-evaluate the knowledge I have and get better at sharing it. If you&rsquo;ve enjoyed it, or if you have any feedback about it, make sure to hit me up on <a href="https://twitter.com/BrenAbolivier">Twitter</a>, I&rsquo;ll be more than happy to discuss it with you 🙂</p>
<p>Thanks to <a href="https://twitter.com/CromFR">Thibaut</a> and <a href="https://twitter.com/SebBLAISOT">Sébastien</a> for giving this post a read before I got to publish it and getting me some nice feedback.</p>
<p>See you next week!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Grafana Dashboards Manager</title>
      <link>https://brendan.abolivier.bzh/grafana-dashboards-manager/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Sat, 28 Apr 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/grafana-dashboards-manager/</guid>
      <description>I&#39;m sort of in charge of all things monitoring currently at CozyCloud. Some of it is done using Zabbix (I already wrote about that), and the other part is pushed to OVH Metrics and visualised through Grafana. When I was alone working on dashboards and graphs, it was all right, but once a colleague came in, we felt the lack of version control would cause great troubles. That&#39;s where the Grafana Dashboards Manager comes in to save the day.</description>
      <content:encoded><![CDATA[<p>At <a href="https://cozy.io/">CozyCloud</a>, most of my work orbites around monitoring and supervision. That&rsquo;s the main reason explaning why I was tasked with dealing with <a href="/zabbix-proxy-encryption/">Zabbix supervision on a remote infrastructure</a> we&rsquo;re setting up, and it also explains why I&rsquo;ll write some more on monitoring solutions in the future.</p>
<p>As you already know, some of it is done using Zabbix, and the rest of it is done using <a href="https://www.ovh.com/fr/data-platforms/metrics/">OVH&rsquo;s Metrics Data Platform</a>, which, once again, I&rsquo;ll write about in a future post. Since OVH hosts a Grafana instance to let their customer visualise their data, we use it to do just that. We actually have one dashboard for each kind of metrics we&rsquo;re sending to the platform, e.g.:</p>
<ul>
<li>a dashboard named &ldquo;Infra&rdquo; to visualise system metrics from each host in our infrastructure</li>
<li>a dashboard named &ldquo;CouchDB&rdquo; to visualise metrics specific to our CouchDB clusters, including nodes status, databases reads/writes, etc.</li>
<li>a dashboard named &ldquo;Cozy Stack&rdquo; to visualise metrics specific to Cozy, CozyCloud&rsquo;s product, including the evolution of the number of created instances, resources usage from the stack, etc.</li>
<li>etc.</li>
</ul>
<p>I created most of these dashboards myself as part of prototyping and deploying the solution we&rsquo;re using to push metrics to OVH&rsquo;s platform (which I won&rsquo;t be describing here as it deserves its own post). In fact, for my first couple of months working on this task, I was the only person creating, modifying or deleting dashboards in our Grafana organisation.</p>
<p>Then <a href="https://twitter.com/nledez">Nicolas</a> started to work with dashboards too, and we stumbled across one big issue: because Grafana doesn&rsquo;t embed a version control system (aka VCS, i.e. what Git, SVN et al. are), it became quite difficult to work on a dashboard: if a colleague modify a dashboard you&rsquo;re currently working on, you can only either overwrite their changes, or give up yours (or merge both manually, which can be really painful).</p>
<p>Another situtation where I disliked the lack of a VCS was when I was editing huge and complex <a href="http://www.warp10.io/reference/">WarpScripts</a>: if you save the dashboard with a faulty script by mistake, you&rsquo;re going to have a very painful time finding it and fixing it. Add to this that the dashboard is actively used by other teams in your company, which adds to pressure you to patch it quickly, and compare that to the easiness of reverting to an older version and investigating calmly.</p>
<p>Considering all the burden this lack could create, I decided to start working on a tool for my team, which I later released as free software as the <a href="https://github.com/babolivier/grafana-dashboard-manager">Grafana Dashboards Manager</a>.</p>
<h2 id="what-is-it">What is it?</h2>
<p>The Grafana Dashboards Manager is a tool written in Go aiming at helping you manage your Grafana dashboards using Git. It takes advantage from the fact that Grafana describes a dashboard as JSON, making it easy to save and edit in a file.</p>
<p>Its goal is to let you retrieve your existing dashboards to a Git repository, and then edit them within your local Git repositrory, so merging two versions of the same dashboard doesn&rsquo;t become a living hell. Once changes have been committed and pushed to the Git repository&rsquo;s <code>master</code> branch the Grafana Dashboards Manager can handle synchronising the changes with your Grafana instance. And since only the <code>master</code> branch is watched, it means that you can take advantage of Git&rsquo;s workflows, such as working on a separate branch, then merging it with the <code>master</code> one, either with a Pull/Merge request or not, and only then will its changes be synchronised with Grafana (if you want them to, of course).</p>
<p>So that&rsquo;s the big picture, now let&rsquo;s look at how it works. It is split in two part: a puller and a pusher. Basically, the whole thing is thought to work like this:</p>
<p><img src="/images/grafana-dashboards-manager/workflow.jpg" alt=""></p>
<p>In this schema, the <strong>puller</strong>, a CLI tool, will fetch changes in the current Grafana dashboards, commit them to a local Git repository, push to a Git remote then exit.</p>
<p>In the meantime, the <strong>pusher</strong> will look for new commits in the repository to retrieve them and push changed files to Grafana as new or changed dashboards. If requested, it will also delete from Grafana all dashboards that were removed from the Git repository. It will, of course, ignore all commits created by the puller.</p>
<p>This check for new commits can be done in two ways: the first one will start a small web server which will only expose a route that can be used to send web hooks. Because we use GitLab internally, which means our dashboards will be versionned there, the dashboards manager currently only supports GitLab webhooks (and that&rsquo;s also the reason the Grafana Dashboards Manager uses Git rather than another VCS). Does this mean you can only use the pusher with GitLab, you may ask? Of course not, I answer! The second available mode allows you to specify any Git repository URL which it will poll at a given frequency. In both mode, it will run as a daemon.</p>
<p>By the way, thanks to the refactoring work required to implement this &ldquo;<code>git pull</code>&rdquo; mode, if you really want to use a GitHub/Bitbucket/etc. webhook, it shouldn&rsquo;t be too hard to add support for that <a href="https://github.com/babolivier/grafana-dashboards-manager/tree/master/src/pusher">in the pusher&rsquo;s code</a>. Any pull request is, of course, more than welcome!</p>
<h2 id="i-dont-want-all-dashboards-to-be-pulled-and-pushed-how-can-i-do-that">I don&rsquo;t want all dashboards to be pulled and pushed, how can I do that?</h2>
<p>The configuration allows you to <a href="https://github.com/babolivier/grafana-dashboards-manager/blob/v1.0.0/config.example.yaml#L11">mention a prefix that defines ignored dashboards</a>. If a dashboard&rsquo;s slug starts with this prefix, it will be ignored by both the puller and the pusher.</p>
<p>Let&rsquo;s say you want to edit a complex dashboard, which JSON representation is thousands of lines long, so you want to edit it using Grafana&rsquo;s GUI, using this setting you can change it&rsquo;s name in the JSON file (which is at the end of the file) so it starts with the given prefix, <a href="http://docs.grafana.org/reference/export_import/#importing-a-dashboard">import it</a>, and you won&rsquo;t be bothering by the puller committing your WIP changes or the pusher overwriting them.</p>
<p>It&rsquo;s worth keeping in mind that this &ldquo;ignore prefix&rdquo; will be replaced with a regular expression in a future release.</p>
<h2 id="what-if-i-just-want-a-back-up-tool">What if I just want a back-up tool?</h2>
<p>The reason the Grafana Dashboards Manager is split in two parts is because each is independant from the other. If you want it to work only one way, that&rsquo;s possible. If you want to use it to only upload JSON descriptions of your dashboards to Grafan, that&rsquo;s possible. If you want to use it to only back-up your dashboards and push them to a Git repository, that&rsquo;s possible. Just run the appropriate binary with the appropriate configuration.</p>
<h2 id="wait-and-if-i-dont-want-to-use-git-at-all">Wait, and if I don&rsquo;t want to use Git at all?</h2>
<p>Of course, if you don&rsquo;t want to get a Git repository involved, the pusher won&rsquo;t work, since its main feature is to interact with a one.</p>
<p>But if you just want to back-up your dashboards on your disk, well, that&rsquo;s also possible! The puller has a second mode that only writes files to disk, which is called <a href="https://github.com/babolivier/grafana-dashboards-manager/blob/v1.0.0/config.example.yaml#L37-L48">the &ldquo;simple sync&rdquo; mode</a>, and allows you to back-up your dashboards as JSON files on your disk.</p>
<h2 id="im-sold-how-do-i-get-it">I&rsquo;m sold! How do I get it?</h2>
<p>The whole thing is <a href="https://github.com/babolivier/grafana-dashboards-manager">available on GitHub</a> as free software (AGPLv3-licensed), with instructions on how to build it, configure it and run it. If you want to skip the &ldquo;building&rdquo; part, <a href="https://github.com/babolivier/grafana-dashboards-manager/releases/tag/v1.0.0">here</a> are some built linux-amd64 binaries. All that&rsquo;s left for you is to download them, create a configuration file from <a href="https://github.com/babolivier/grafana-dashboards-manager/blob/v1.0.0/config.example.yaml">the existing example</a> and run the puller, the pusher or both in the configuration you want.</p>
<p>Thanks a lot to <a href="https://twitter.com/nledez">Nicolas</a> who gave me the idea to work on this tool, and to <a href="https://twitter.com/GillesBIANNIC">Gilles</a> who gave me a lot of amazing feedback on it 🙂 And as with the latest post, thanks also to <a href="https://twitter.com/CromFR">Thibaut</a> for his early feedback on this post.</p>
<p>See you <a href="/one-post-a-week/">next week</a> for a new post, and in the meantime feel free to <a href="https://twitter.com/BrenAbolivier">tweet me</a> some feedback about this one!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Zabbix supervision on a remote infrastructure with proxy and PSK-based encryption</title>
      <link>https://brendan.abolivier.bzh/zabbix-proxy-encryption/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Fri, 20 Apr 2018 00:00:00 +0200</pubDate>
      <guid>https://brendan.abolivier.bzh/zabbix-proxy-encryption/</guid>
      <description>At CozyCloud, we recently had to set up Zabbix supervision on a new infrastructure which could only speak to our Zabbix server over the Internet. As a result, we had to install a Zabbix proxy on the new infrastructure and configure it to use PSK-based encryption when talking to the server. Bear with me as I explain to you the steps we followed.</description>
      <content:encoded><![CDATA[<p>All of <a href="https://cozy.io/">CozyCloud</a>&rsquo;s production and development infrastructure is hosted in <a href="https://ovh.com">OVH</a>&rsquo;s datacenters. We monitor this infrastructure in two ways: by sending data points on various metrics to <a href="https://www.ovh.com/fr/data-platforms/metrics/">OVH&rsquo;s Metrics Data Platform</a> (I&rsquo;ll write about that in a future post), and also by using a self-hosted <a href="https://www.zabbix.com/">Zabbix</a> server.</p>
<p>All of our OVH hosts are connected to a virtual local network (vRack) that cannot be accessed from the outside world, so on-host Zabbix agents use it to send their unencrypted data to the Zabbix server, which is also connected to this local network. It was a very simple setup, which looked like this:</p>
<p><img src="/images/zabbix-proxy-encryption/zabbix-agent-server-main.jpg" alt=""></p>
<h2 id="a-new-challenger-approaches">A new challenger approaches</h2>
<p>Recently, we&rsquo;ve been tasked with the setup of a new production infrastructure on another hosting provider. The question of how we were going to set up Zabbix&rsquo;s monitoring in this new environment came up quickly. We decided not to set up another Zabbix server on the new hosting provider&rsquo;s infrastructure as it would make things painful to set up and we&rsquo;d have two places to watch instead of only one. So we decided that all Zabbix agents monitoring host on the remote infrastructure would send their data to the Zabbix server we already had set up on OVH&rsquo;s infrastructure.</p>
<p>Now, this brought up an issue that needed solving before we could do anything: there&rsquo;s no private local network linking the two hosting providers, so the traffic between the two goes through the Internet with neither encryption nor checksum. Luckily, Zabbix provides an encryption feature, and a proxy software which forwards data from agents to a server, so we decided that we would set up a Zabbix proxy on the remote infrastructure and would turn encryption on between the proxy and the Zabbix server. The resulting setup would look like this:</p>
<p><img src="/images/zabbix-proxy-encryption/zabbix-agent-server-proxy-main-remote.jpg" alt=""></p>
<h2 id="lets-encrypt-stuff">Let&rsquo;s encrypt stuff</h2>
<p>Let&rsquo;s have a look at how we&rsquo;ll encrypt the traffic between the proxy and the server. Zabbix actually provides three modes to describe encryption for incoming or outgoing connections:</p>
<ul>
<li>unencrypted: the data is sent in plain text over the Internet (aka what we don&rsquo;t want).</li>
<li>PSK (aka <em>Pre-Shared Key</em>): an encryption key that must be shared between the proxy and the server and is used to encrypt and decrypt the data.</li>
<li>Certificate-based: a PEM certificate signed by a certification authority (either public or in-house) must be generated; the CA&rsquo;s certificate must be provided to the Zabbix server and is used to validate the certificates used by the proxy.</li>
</ul>
<p>Because it was simpler to set up, we went with the PSK option. However, our Zabbix server was built and installed from the sources, with the <code>--with-openssl</code> option, and <a href="https://www.zabbix.com/documentation/3.0/manual/encryption#compiling_zabbix_with_encryption_support">Zabbix&rsquo;s doc on encryption</a> states the following:</p>
<blockquote>
<p>If you plan to use pre-shared keys (PSK) consider using GnuTLS or mbed TLS libraries in Zabbix components using PSKs. GnuTLS and mbed TLS libraries support PSK ciphersuites with <a href="https://en.wikipedia.org/wiki/Forward_secrecy#Perfect_forward_secrecy_.28PFS.29">Perfect Forward Secrecy</a>. OpenSSL library (versions 1.0.1, 1.0.2c) does support PSKs but available PSK ciphersuites do not provide Perfect Forward Secrecy.</p>
</blockquote>
<p>And since we had to update the server anyway, one of my colleague thought he would create an unofficial package (for internal use) from the sources. Why not use the official Debian packages, you ask? Because the packages coming from the official Debian repos are outdated, and we couldn&rsquo;t find whether the packages coming from Zabbix&rsquo;s official repos were built using OpenSSL or GnuTLS. This way, we were sure to use the latest Zabbix version with the best encryption settings.</p>
<p>I&rsquo;m explaining this because it means we&rsquo;re not using the official packages, which means that, although the setup process should be roughly the same, some steps may differ from the official from-packages install.</p>
<p>At this point, we have our internal packages of the Zabbix server, proxy and agent, and I was tasked to set up the whole thing on the remote infrastructure.</p>
<h2 id="the-proxy-a-walkthrough">The proxy: a walkthrough</h2>
<p>I&rsquo;ll begin with the assumption that you already have a running Zabbix server somewhere on the Internet.</p>
<p>First, you need to install the Zabbix proxy. This should be as simple as running</p>
<pre tabindex="0"><code>sudo apt install zabbix-proxy-BACKEND
</code></pre><p>but can be a bit more complicated if you&rsquo;re installing the proxy from the sources. Either way, <a href="https://www.zabbix.com/documentation/3.0/manual/installation">it&rsquo;s all documented</a>.</p>
<p>In my case, once I created the proxy&rsquo;s PostgreSQL user and database, I also had to manually load the database schema into PostgreSQL, or else the proxy wouldn&rsquo;t start. If that&rsquo;s your case, find the <code>schema.sql</code> or <code>schema.sql.gz</code> file installed on the proxy&rsquo;s host by the sources or the package, un-compress it using <code>gunzip</code> if necessary, then enter the PostgreSQL shell (<code>psql -U PROXY USER -d PROXY DATABASE</code>), and run <code>\i /path/to/schema.sql</code>. This will do all the necessary operations to make the database usable by the proxy.</p>
<p>Now let&rsquo;s configure the proxy. The configuration file we use, located at <code>/etc/zabbix/zabbix_proxy.conf</code> looks like this:</p>
<pre tabindex="0"><code># Proxy operating mode.
# 0 - proxy in the active mode
# 1 - proxy in the passive mode
ProxyMode=0

# IP address (or hostname) of Zabbix server.
Server=ZABBIX SERVER IP/HOSTNAME

# Unique, case sensitive Proxy name.
Hostname=zabbix-proxy

# Log file name
LogFile=/var/log/zabbix-proxy/zabbix_proxy.log

# Database name.
DBName=POSTGRES DB NAME

# Database user.
DBUser=POSTGRES USER

# Database password.
DBPassword=POSTGRES PASSWORD

# How often proxy retrieves configuration data from Zabbix Server in seconds.
# For a proxy in the passive mode this parameter will be ignored.
# The default is 3600, which is an hour. We don&#39;t want to wait up to an hour
# for a new host to start being supervised.
ConfigFrequency=300

# How long we wait for agent, SNMP device or external check (in seconds).
Timeout=4

# How long a database query may take before being logged (in milliseconds).
# Only works if DebugLevel set to 3 or 4.
LogSlowQueries=3000

# How the proxy should connect to Zabbix server, aka the encryption mode we want
# to use.
TLSConnect=psk

# Unique, case sensitive string used to identify the pre-shared key.
TLSPSKIdentity=psk_remote
# Full pathname of a file containing the pre-shared key.
TLSPSKFile=/etc/zabbix/zabbix_proxy.psk
</code></pre><p>Some values have been censored because they contain sensible data (such as secrets or passwords).</p>
<p>Let&rsquo;s give a closer look at some parts of this file.</p>
<pre tabindex="0"><code># Proxy operating mode.
# 0 - proxy in the active mode
# 1 - proxy in the passive mode
ProxyMode=0
</code></pre><p>This means that the proxy runs in the active mode, and will fetch by itself its configuration on the server. This mainly means we don&rsquo;t have to restart the proxy each time we add a host.</p>
<pre tabindex="0"><code># IP address (or hostname) of Zabbix server.
Server=ZABBIX SERVER IP/HOSTNAME

# Unique, case sensitive Proxy name.
Hostname=zabbix-proxy
</code></pre><p>This part tells the proxy what server it should contact and what name must it give to be recognised as itself. The first parameter would be ignored if we were running in passive mode.</p>
<pre tabindex="0"><code># Database name.
DBName=POSTGRES DB NAME

# Database user.
DBUser=POSTGRES USER

# Database password.
DBPassword=POSTGRES PASSWORD
</code></pre><p>This part tells the proxy how to connect to its database. In this case we&rsquo;re using PostgreSQL.</p>
<pre tabindex="0"><code># How the proxy should connect to Zabbix server, aka the encryption mode we want
# to use.
TLSConnect=psk

# Unique, case sensitive string used to identify the pre-shared key.
TLSPSKIdentity=psk_remote
# Full pathname of a file containing the pre-shared key.
TLSPSKFile=/etc/zabbix/zabbix_proxy.psk
</code></pre><p>Now here&rsquo;s the interesting part: the part where we set up encryption for outgoing connections. We don&rsquo;t set up any encryption for incoming connections, because we&rsquo;re running our proxy in the active mode, which means a connection between the server and the proxy will always come from the proxy to the server.</p>
<p>The first parameter is <code>TLSConnect</code>, which tells the proxy what mode it should use to connect to the server. It can either be <code>unencrypted</code>, <code>psk</code> or <code>cert</code>.</p>
<p>Once we&rsquo;ve told our proxy we want to talk with the server, there are two parameters we must define:</p>
<ul>
<li><code>TLSPSKIdentity</code>: the &ldquo;identity&rdquo; of the pre-shared key, aka a non-secret string identifier. You can basically input whatever you want here.</li>
<li><code>TLSPSKFile</code>: the file containing your secret pre-shared key.</li>
</ul>
<p><a href="https://www.zabbix.com/documentation/3.0/manual/encryption/using_pre_shared_keys#generating_psk">Zabbix&rsquo;s documentation</a> provides two ways to generate the PSK, which is basically a random 32-byte long string, using either OpenSSL or GnuTLS. I used GnuTLS, which looked like this:</p>
<pre tabindex="0"><code>$ psktool -u psk_identity -p database.psk -s 32
Generating a random key for user &#39;psk_identity&#39;
Key stored to database.psk

$ cat database.psk
psk_identity:9b8eafedfaae00cece62e85d5f4792c7d9c9bcc851b23216a1d300311cc4f7cb
</code></pre><p>Let&rsquo;s just clarify a point here: the key isn&rsquo;t the one we&rsquo;re using. The code block above is just an exact copy from Zabbix&rsquo;s documentation.</p>
<p>Now that we have generated our <code>database.psk</code> file, we&rsquo;ll need to transform it a bit so Zabbix can read it, by removing the identity and the colon, leaving only the key in the file. Using the file generated in the previous example, it should now look like this:</p>
<pre tabindex="0"><code>$ cat database.psk
9b8eafedfaae00cece62e85d5f4792c7d9c9bcc851b23216a1d300311cc4f7cb
</code></pre><p>You may of course rename the file and move it on the proxy&rsquo;s host. The next step is to re-open the proxy configuration file, copy the <code>.psk</code> file&rsquo;s absolute path as the value for the <code>TLSPSKFile</code> parameter, restart the proxy and voilà! The proxy should now be able to talk to the server! Or at least try to, because the server doesn&rsquo;t know our proxy. Let&rsquo;s see how we can fix this.</p>
<h2 id="server-meets-proxy">Server meets proxy</h2>
<p>Now you&rsquo;ll need to log into your Zabbix server&rsquo;s web interface (as an administrator), and click on the &ldquo;Proxies&rdquo; sub-menu from the &ldquo;Administration&rdquo; menu. From there, click &ldquo;Create proxy&rdquo;.</p>
<p><img src="/images/zabbix-proxy-encryption/zabbix-proxy-creation.png" alt=""></p>
<p>Fill in your proxy&rsquo;s name, but don&rsquo;t click &ldquo;Add&rdquo; yet. Also, make sure the name is exactly the same as the <code>Hostname</code> you specified in the proxy&rsquo;s configuration (it&rsquo;s case-sensitive).</p>
<p><img src="/images/zabbix-proxy-encryption/zabbix-proxy-name.png" alt=""></p>
<p>Then click &ldquo;Encryption&rdquo; (at the top of the gray block, next to &ldquo;Proxy&rdquo;), uncheck &ldquo;No encryption&rdquo;, check &ldquo;PSK&rdquo;, fill in the PSK&rsquo;s identity (again, this needs to be exactly the same as the value you set to <code>TLSPSKIdentity</code>, and is case-sensitive), and the PSK (which is the content of the <code>.psk</code> file we generated just before).</p>
<p><img src="/images/zabbix-proxy-encryption/zabbix-proxy-encryption.png" alt=""></p>
<p>Now you can click &ldquo;Add&rdquo;, and voilà! Your server now knows your proxy and will be happy to talk to it, using the PSK to encrypt all communications.</p>
<h2 id="a-few-words-on-the-agents">A few words on the agents</h2>
<p>Now this whole setup won&rsquo;t disturb on-host agents that much. They talk to a proxy the same way they talk to a server. However, you&rsquo;ll need to make them talk to the proxy, and this is done in two parts:</p>
<ul>
<li>In the agent&rsquo;s configuration file, set the <code>Server</code> parameter to the proxy&rsquo;s address, not the Zabbix server&rsquo;s.</li>
<li>In the server&rsquo;s web interface, when creating the host, make sure to select the proxy in the &ldquo;Monitored by proxy&rdquo; dropdown at the bottom of the main view:</li>
</ul>
<p><img src="/images/zabbix-proxy-encryption/zabbix-agent-proxy.png" alt=""></p>
<p>There&rsquo;s one special case, though, it&rsquo;s the agent that&rsquo;s on the proxy&rsquo;s host. If you use it with the same configuration than the other agents in your remote infrastructure, it will make that the proxy forward its own monitoring data, which is not good if you want to be able to investigate incidents efficiently (and can lead to countless issues). So I&rsquo;d advise to make it talk (in an encrypted fashion) directly to the Zabbix server. The agent&rsquo;s configuration is almost exactly the same than the proxy&rsquo;s, in fact we can even use the same encryption key. At CozyCloud, we only append these lines to the proxy&rsquo;s agent configuration:</p>
<pre tabindex="0"><code>TLSConnect=psk
TLSAccept=psk
TLSPSKIdentity=psk_remote
TLSPSKFile=/etc/zabbix/zabbix_proxy.psk
</code></pre><p>Also don&rsquo;t forget to change the agent&rsquo;s <code>Server</code> configuration parameter to replace it with your server&rsquo;s public address instead of the proxy&rsquo;s internal address.</p>
<h2 id="and-voilà">And voilà!</h2>
<p>There you go, the whole thing is set up and ready to work! You can make sure encryption is turned on using <code>tcpdump</code> like this:</p>
<pre tabindex="0"><code>$ tcpdump -X -i eth0 dst host ZABBIX SERVER IP/HOSTNAME and dst port 10051
</code></pre><p>Make sure this command line is run from the proxy&rsquo;s host. You may want to change the interface (here <code>eth0</code>) and the port the Zabbix server listens to (here <code>10051</code>) accordingly with your own setup.</p>
<p>If encryption is indeed turned on, all of the translated content sent from the proxy to the server (the right part of the output) must be un-understandable gibberish.</p>
<p>If no traffic goes between your proxy and your server (i.e. if <code>tcpdump</code> shows nothing), you might want to update the firewall rules on your Zabbix server&rsquo;s host to allow incoming connection on port 10051 (or any other port you might have configured the server to listen to).</p>
<p>If you were not aware of it, this blog post was the first episode of my <a href="https://brendan.abolivier.bzh/one-post-a-week/">One post a week</a> series, in which I&rsquo;m trying to keep up with writing a blog post a week to help me get better at sharing my knowledge. If you have any feedback on this post, make sure to hit me up on <a href="https://twitter.com/BrenAbolivier">Twitter</a>, I&rsquo;ll be more than happy to discuss it with you 🙂</p>
<p>I&rsquo;d also like to thank <a href="https://twitter.com/nledez">Nicolas</a> who spent so much time helping me with this setup and explaining so much things on Zabbix to me, along with <a href="https://twitter.com/CromFR">Thibaut</a> and <a href="https://twitter.com/SebBLAISOT">Sébastien</a> for their early feedback on this post, which helped me make it even better.</p>
<p>See you next week for a new post!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>One post a week</title>
      <link>https://brendan.abolivier.bzh/one-post-a-week/</link>
      <author>Brendan Abolivier</author>
      <pubDate>Sat, 14 Apr 2018 00:00:00 +0000</pubDate>
      <guid>https://brendan.abolivier.bzh/one-post-a-week/</guid>
      <description>When I was at BreizhCamp, a 3-day long tech conference in the West of France that happened a couple of weeks ago, I attended a talk that gave me the idea to share each week on this blog some new stuff I learned on the way.</description>
      <content:encoded><![CDATA[<p>My name is Brendan Abolivier. I&rsquo;m a young guy from <a href="https://www.openstreetmap.org/relation/1076124">Brest, France</a> working as a junior system administrator at <a href="https://cozy.io/">CozyCloud</a>, a small French company working on an open personal cloud platform aiming at giving people ownership on their personal data back.</p>
<p>When I was at <a href="https://www.breizhcamp.org/">BreizhCamp</a>, a 3-day long tech conference in the West of France that happened a couple of weeks ago, I attended a talk called &ldquo;Teaching is learning: become a better dev by sharing your knowledge&rdquo;. During this talk, the speaker, Céline Martinet Sanchez, spoke about her journey in software development and how she used knowledge that was shared by others and slowly became the one to share her own knowledge with random people on Internet forums. The full 28-min long talk is available <a href="https://www.youtube.com/watch?v=TmcxPGtBpiU">right here</a>.</p>
<p>In the &ldquo;sharing&rdquo; part of the talk, she described the different ways in which you can share knowledge with other people (forum posts, blog posts, talks, etc.), and remarked that we usually refrain from sharing such knowledge. We sometimes use excuses such as &ldquo;I&rsquo;m not good at explaining&rdquo; or &ldquo;I don&rsquo;t have anything interesting to share with people&rdquo;. She actually listed most of the excuses she used to either hear or say herself, and explained how most of them were just that, excuses with no real base. She explained that you won&rsquo;t get better at explaining stuff by not doing anything about it, and that most of the time you actually have something interesting to share (you must have learned something at work this week, or while talking with friends or colleagues, that helped you in your projects), but you usually consider it as not interesting enough to share it with the rest of the world.</p>
<p>While listening to her speaking, I noticed that, most of the time, when I was considering going to a conference, I always had a small moment when I was undecided about how to attend (speaker? attendee? volunteer?), and always quickly rejected the speaker option because I thought I had nothing worth sharing. Same goes with writing blog posts. Most of the excuses she listed during her talk were excuses I heard coming from myself, and it made be think that maybe I devalue what I know too much, and maybe what I learn each day/week/month is worth sharing with the rest of the world. This thought became even more realistic as I got to speak with Céline Martinet Sanchez later that day, when she told me she was actually pushed by her colleagues towards doing a talk, went through this whole thought process and came up with an amazing talk that really stand out to me.</p>
<p>Realising all of this, I thought it would be a great exercise to finally make use of this blog I set up without a real goal a few months ago, and, each week, share something I learned at work or while working on personal projects, or just something I have in mind and want to share on this space. The posts can be tutorials, feedbacks, or even reflections on non-technical parts of stuff I work on. Some week there might even be nothing because I won&rsquo;t write random stuff if I have nothing to talk (even though it&rsquo;s very unlikely).</p>
<p>I hope you&rsquo;ll hang here with me, and I&rsquo;ll see you next week for the first post from this series!</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
